diff --git a/ui/admin/app/components/Typography.tsx b/ui/admin/app/components/Typography.tsx index 34a9613a..dc94e683 100644 --- a/ui/admin/app/components/Typography.tsx +++ b/ui/admin/app/components/Typography.tsx @@ -1,114 +1,183 @@ -import { ReactNode } from "react"; +import React, { ReactNode } from "react"; import { cn } from "~/lib/utils"; -interface TypographyProps { +type TypographyElement = keyof JSX.IntrinsicElements; + +type TypographyProps = { children: ReactNode; className?: string; -} +} & React.JSX.IntrinsicElements[T]; -export function TypographyH1({ children, className }: TypographyProps) { +export function TypographyH1({ + children, + className, + ...props +}: TypographyProps<"h1">) { return (

{children}

); } -export function TypographyH2({ children, className }: TypographyProps) { +export function TypographyH2({ + children, + className, + ...props +}: TypographyProps<"h2">) { return (

{children}

); } -export function TypographyH3({ children, className }: TypographyProps) { +export function TypographyH3({ + children, + className, + ...props +}: TypographyProps<"h3">) { return (

{children}

); } -export function TypographyH4({ children, className }: TypographyProps) { +export function TypographyH4({ + children, + className, + ...props +}: TypographyProps<"h4">) { return (

{children}

); } -export function TypographyP({ children, className }: TypographyProps) { - return

{children}

; +export function TypographyP({ + children, + className, + ...props +}: TypographyProps<"p">) { + return ( +

+ {children} +

+ ); } -export function TypographyBlockquote({ children, className }: TypographyProps) { +export function TypographyBlockquote({ + children, + className, + ...props +}: TypographyProps<"blockquote">) { return ( -
+
{children}
); } -export function TypographyInlineCode({ children, className }: TypographyProps) { +export function TypographyInlineCode({ + children, + className, + ...props +}: TypographyProps<"code">) { return ( {children} ); } -export function TypographyLead({ children, className }: TypographyProps) { +export function TypographyLead({ + children, + className, + ...props +}: TypographyProps<"p">) { return ( -

+

{children}

); } -export function TypographyLarge({ children, className }: TypographyProps) { +export function TypographyLarge({ + children, + className, + ...props +}: TypographyProps<"div">) { return ( -
{children}
+
+ {children} +
); } -export function TypographySmall({ children, className }: TypographyProps) { +export function TypographySmall({ + children, + className, + ...props +}: TypographyProps<"small">) { return ( - + {children} ); } -export function TypographyMuted({ children, className }: TypographyProps) { +export function TypographyMuted({ + children, + className, + ...props +}: TypographyProps<"p">) { return ( -

+

{children}

); diff --git a/ui/admin/app/components/agent/AdvancedForm.tsx b/ui/admin/app/components/agent/AdvancedForm.tsx new file mode 100644 index 00000000..e0f6e1a8 --- /dev/null +++ b/ui/admin/app/components/agent/AdvancedForm.tsx @@ -0,0 +1,65 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Agent } from "~/lib/model/agents"; + +import { ControlledTextarea } from "~/components/form/controlledInputs"; +import { Form } from "~/components/ui/form"; + +const formSchema = z.object({ + prompt: z.string().optional(), +}); + +export type AdvancedFormValues = z.infer; + +type AdvancedFormProps = { + agent: Agent; + onSubmit?: (values: AdvancedFormValues) => void; + onChange?: (values: AdvancedFormValues) => void; +}; + +export function AdvancedForm({ agent, onSubmit, onChange }: AdvancedFormProps) { + const form = useForm({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + prompt: agent.prompt || "", + }, + }); + + useEffect(() => { + if (agent) form.reset({ prompt: agent.prompt || "" }); + }, [agent, form]); + + useEffect(() => { + return form.watch((values) => { + if (!onChange) return; + + const { data, success } = formSchema.safeParse(values); + + if (!success) return; + + onChange(data); + }).unsubscribe; + }, [onChange, form]); + + const handleSubmit = form.handleSubmit((values: AdvancedFormValues) => + onSubmit?.({ ...values }) + ); + + return ( +
+ + + + + ); +} diff --git a/ui/admin/app/components/agent/Agent.tsx b/ui/admin/app/components/agent/Agent.tsx index bffffb5b..e258b49b 100644 --- a/ui/admin/app/components/agent/Agent.tsx +++ b/ui/admin/app/components/agent/Agent.tsx @@ -1,4 +1,4 @@ -import { LibraryIcon, RotateCcw, WrenchIcon } from "lucide-react"; +import { LibraryIcon, PlusIcon, SettingsIcon, WrenchIcon } from "lucide-react"; import { useCallback, useState } from "react"; import { Agent as AgentType } from "~/lib/model/agents"; @@ -18,12 +18,14 @@ import { Button } from "~/components/ui/button"; import { ScrollArea } from "~/components/ui/scroll-area"; import { useDebounce } from "~/hooks/useDebounce"; +import { AdvancedForm } from "./AdvancedForm"; +import { PastThreads } from "./PastThreads"; import { ToolForm } from "./ToolForm"; type AgentProps = { agent: AgentType; className?: string; - onRefresh?: () => void; + onRefresh?: (threadId: string | null) => void; }; export function Agent(props: AgentProps) { @@ -52,6 +54,13 @@ function AgentContent({ className, onRefresh }: AgentProps) { const debouncedSetAgentInfo = useDebounce(partialSetAgent, 1000); + const handleThreadSelect = useCallback( + (threadId: string) => { + onRefresh?.(threadId); + }, + [onRefresh] + ); + return (
@@ -62,15 +71,27 @@ function AgentContent({ className, onRefresh }: AgentProps) { />
- - - - - + + + + + Tools +

+ Add tools the allow the agent to perform useful + actions such as searching the web, reading + files, or interacting with other systems. +

- - - - + + + + Knowledge - + +

+ Provide knowledge to the agent in the form of + files, website, or external links in order to + give it context about various topics. +

+ + + + + + Advanced + + + + + +
@@ -102,13 +149,22 @@ function AgentContent({ className, onRefresh }: AgentProps) {
)} - +
+ + +
); diff --git a/ui/admin/app/components/agent/AgentForm.tsx b/ui/admin/app/components/agent/AgentForm.tsx index 8f55c539..8387a16c 100644 --- a/ui/admin/app/components/agent/AgentForm.tsx +++ b/ui/admin/app/components/agent/AgentForm.tsx @@ -5,12 +5,10 @@ import { z } from "zod"; import { Agent } from "~/lib/model/agents"; -import { - ControlledInput, - ControlledTextarea, -} from "~/components/form/controlledInputs"; import { Form } from "~/components/ui/form"; +import { ControlledInput } from "../form/controlledInputs"; + const formSchema = z.object({ name: z.string().min(1, { message: "Name is required.", @@ -60,28 +58,19 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) { return (
- + - - - - diff --git a/ui/admin/app/components/agent/PastThreads.tsx b/ui/admin/app/components/agent/PastThreads.tsx new file mode 100644 index 00000000..b4bdb19e --- /dev/null +++ b/ui/admin/app/components/agent/PastThreads.tsx @@ -0,0 +1,103 @@ +import { ChevronUpIcon } from "lucide-react"; +import React, { useState } from "react"; +import useSWR from "swr"; + +import { Thread } from "~/lib/model/threads"; +import { ThreadsService } from "~/lib/service/api/threadsService"; + +import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; +import { Button } from "~/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "~/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; + +interface PastThreadsProps { + agentId: string; + onThreadSelect: (threadId: string) => void; +} + +export const PastThreads: React.FC = ({ + agentId, + onThreadSelect, +}) => { + const [open, setOpen] = useState(false); + const { + data: threads, + error, + isLoading, + } = useSWR(ThreadsService.getThreadsByAgent.key(agentId), () => + ThreadsService.getThreadsByAgent(agentId) + ); + + if (error) { + console.error("Error fetching threads:", error); + } + + const handleThreadSelect = (threadId: string) => { + onThreadSelect(threadId); + setOpen(false); + }; + + return ( + + + + + + + + + No threads found. + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ Failed to load threads +
+ ) : threads && threads.length > 0 ? ( + + {threads.map((thread: Thread) => ( + + handleThreadSelect(thread.id) + } + className="cursor-pointer" + > +
+

+ Thread + + {thread.id} + +

+

+ {new Date( + thread.created + ).toLocaleString()} +

+
+
+ ))} +
+ ) : null} +
+
+
+
+ ); +}; diff --git a/ui/admin/app/components/agent/ToolForm.tsx b/ui/admin/app/components/agent/ToolForm.tsx index 7b2d54e7..33f308a4 100644 --- a/ui/admin/app/components/agent/ToolForm.tsx +++ b/ui/admin/app/components/agent/ToolForm.tsx @@ -71,27 +71,6 @@ export function ToolForm({ return (
-
- - - - - - - - -
))} +
+ + + + + + + + +
)} diff --git a/ui/admin/app/components/chat/NoMessages.tsx b/ui/admin/app/components/chat/NoMessages.tsx index dba93548..9d55cf24 100644 --- a/ui/admin/app/components/chat/NoMessages.tsx +++ b/ui/admin/app/components/chat/NoMessages.tsx @@ -1,3 +1,5 @@ +import { BrainCircuit, Handshake, Rocket } from "lucide-react"; + import { useChat } from "~/components/chat/ChatContext"; import { Button } from "~/components/ui/button"; @@ -19,19 +21,26 @@ export function NoMessages() { variant="secondary" onClick={() => handleAddMessage("Hello, how are you?")} > - 👋 Greeting + + Greeting diff --git a/ui/admin/app/components/form/controlledInputs.tsx b/ui/admin/app/components/form/controlledInputs.tsx index 8ac45be9..8fc5e128 100644 --- a/ui/admin/app/components/form/controlledInputs.tsx +++ b/ui/admin/app/components/form/controlledInputs.tsx @@ -103,7 +103,7 @@ export function ControlledTextarea< {label && {label}} {description && ( - + {description} )} diff --git a/ui/admin/app/components/header/HeaderNav.tsx b/ui/admin/app/components/header/HeaderNav.tsx index b9567407..a46c6099 100644 --- a/ui/admin/app/components/header/HeaderNav.tsx +++ b/ui/admin/app/components/header/HeaderNav.tsx @@ -1,5 +1,5 @@ -import { useLocation, useParams } from "@remix-run/react"; -import { MenuIcon } from "lucide-react"; +import { Link, useLocation, useParams } from "@remix-run/react"; +import { ArrowLeftIcon, MenuIcon } from "lucide-react"; import { $params, $path } from "remix-routes"; import useSWR from "swr"; @@ -98,6 +98,10 @@ function getHeaderContent(route: string) { } const AgentEditContent = () => { + const { from } = + parseQueryParams(window.location.href, QueryParamSchemas.Agents).data || + {}; + const params = useParams(); const { agent: agentId } = $params("/agents/:agent", params); @@ -106,7 +110,18 @@ const AgentEditContent = () => { ({ agentId }) => AgentService.getAgentById(agentId) ); - return <>{agent?.name || "New Agent"}; + return ( +
+ {from && ( + + )} + {agent?.name || "New Agent"} +
+ ); }; const ThreadsContent = () => { diff --git a/ui/admin/app/components/thread/ThreadMeta.tsx b/ui/admin/app/components/thread/ThreadMeta.tsx index 6d055a69..a2b25f8f 100644 --- a/ui/admin/app/components/thread/ThreadMeta.tsx +++ b/ui/admin/app/components/thread/ThreadMeta.tsx @@ -1,5 +1,6 @@ import { Link } from "@remix-run/react"; import { EditIcon, FileIcon, FilesIcon } from "lucide-react"; +import { $path } from "remix-routes"; import { Agent } from "~/lib/model/agents"; import { KnowledgeFile } from "~/lib/model/knowledge"; @@ -61,7 +62,20 @@ export function ThreadMeta({ asChild > diff --git a/ui/admin/app/lib/service/routeQueryParams.ts b/ui/admin/app/lib/service/routeQueryParams.ts index 7cda4273..5fd02c86 100644 --- a/ui/admin/app/lib/service/routeQueryParams.ts +++ b/ui/admin/app/lib/service/routeQueryParams.ts @@ -8,4 +8,7 @@ export const QueryParamSchemas = { agentId: z.string().optional(), workflowId: z.string().optional(), }), + Agents: z.object({ + from: z.string().optional(), + }), }; diff --git a/ui/admin/app/routes/_auth.agents.$agent.tsx b/ui/admin/app/routes/_auth.agents.$agent.tsx index 2701cba4..60fb41d1 100644 --- a/ui/admin/app/routes/_auth.agents.$agent.tsx +++ b/ui/admin/app/routes/_auth.agents.$agent.tsx @@ -1,7 +1,5 @@ -import { ArrowLeftIcon } from "@radix-ui/react-icons"; import { ClientLoaderFunctionArgs, - Link, redirect, useLoaderData, useNavigate, @@ -15,18 +13,11 @@ import { noop, parseQueryParams } from "~/lib/utils"; import { Agent } from "~/components/agent"; import { Chat, ChatProvider } from "~/components/chat"; -import { Button } from "~/components/ui/button"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/ui/resizable"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip"; const paramSchema = z.object({ threadId: z.string().optional(), @@ -57,7 +48,7 @@ export const clientLoader = async ({ }; export default function ChatAgent() { - const { agent, threadId, from } = useLoaderData(); + const { agent, threadId } = useLoaderData(); const navigate = useNavigate(); const updateThreadId = useCallback( @@ -85,26 +76,11 @@ export default function ChatAgent() { className="flex-auto" > - - - - Go Back - - updateThreadId(null)} + onRefresh={(threadId: string | null) => + updateThreadId(threadId) + } /> diff --git a/ui/admin/app/routes/_auth.agents._index.tsx b/ui/admin/app/routes/_auth.agents._index.tsx index 18edf61d..f7fea7d8 100644 --- a/ui/admin/app/routes/_auth.agents._index.tsx +++ b/ui/admin/app/routes/_auth.agents._index.tsx @@ -5,11 +5,11 @@ import { SquarePen, Trash } from "lucide-react"; import { useMemo } from "react"; import { $path } from "remix-routes"; import useSWR, { preload } from "swr"; +import { z } from "zod"; import { Agent } from "~/lib/model/agents"; import { AgentService } from "~/lib/service/api/agentService"; import { ThreadsService } from "~/lib/service/api/threadsService"; -import { generateRandomName } from "~/lib/service/nameGenerator"; import { timeSince } from "~/lib/utils"; import { TypographyP } from "~/components/Typography"; @@ -23,6 +23,10 @@ import { } from "~/components/ui/tooltip"; import { useAsync } from "~/hooks/useAsync"; +export const agentEditParamSchema = z.object({ + from: z.string().optional(), +}); + export async function clientLoader() { await Promise.all([ preload(AgentService.getAgents.key(), AgentService.getAgents), @@ -74,7 +78,7 @@ export default function Threads() { onClick={() => { AgentService.createAgent({ agent: { - name: generateRandomName(), + name: "New Agent", } as Agent, }).then((agent) => { navigate(