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..47f255d2 100644 --- a/ui/admin/app/components/agent/Agent.tsx +++ b/ui/admin/app/components/agent/Agent.tsx @@ -1,12 +1,15 @@ -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"; import { cn } from "~/lib/utils"; import { TypographyP } from "~/components/Typography"; +import { AdvancedForm } from "~/components/agent/AdvancedForm"; import { AgentProvider, useAgent } from "~/components/agent/AgentContext"; import { AgentForm } from "~/components/agent/AgentForm"; +import { PastThreads } from "~/components/agent/PastThreads"; +import { ToolForm } from "~/components/agent/ToolForm"; import { AgentKnowledgePanel } from "~/components/knowledge"; import { Accordion, @@ -18,12 +21,10 @@ import { Button } from "~/components/ui/button"; import { ScrollArea } from "~/components/ui/scroll-area"; import { useDebounce } from "~/hooks/useDebounce"; -import { ToolForm } from "./ToolForm"; - type AgentProps = { agent: AgentType; className?: string; - onRefresh?: () => void; + onRefresh?: (threadId: string | null) => void; }; export function Agent(props: AgentProps) { @@ -52,6 +53,13 @@ function AgentContent({ className, onRefresh }: AgentProps) { const debouncedSetAgentInfo = useDebounce(partialSetAgent, 1000); + const handleThreadSelect = useCallback( + (threadId: string) => { + onRefresh?.(threadId); + }, + [onRefresh] + ); + return (
@@ -62,15 +70,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 +148,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..d9ad593e 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,21 @@ 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..e960c76a --- /dev/null +++ b/ui/admin/app/components/agent/PastThreads.tsx @@ -0,0 +1,123 @@ +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 { TypographyP } from "~/components/Typography"; +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"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip"; + +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, + mutate, + } = useSWR(ThreadsService.getThreadsByAgent.key(agentId), () => + ThreadsService.getThreadsByAgent(agentId) + ); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (newOpen) { + mutate(); + } + }; + + const handleThreadSelect = (threadId: string) => { + onThreadSelect(threadId); + setOpen(false); + }; + + return ( + + + + + + + + + Switch threads + + + + + 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..581ba3bb 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..47d0ef6b 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"; @@ -33,6 +34,7 @@ export function ThreadMeta({ files, className, }: ThreadMetaProps) { + const from = $path("/thread/:id", { id: thread.id }); return ( @@ -61,7 +63,11 @@ export function ThreadMeta({ asChild > diff --git a/ui/admin/app/components/ui/input.tsx b/ui/admin/app/components/ui/input.tsx index 4db24b77..2612d11e 100644 --- a/ui/admin/app/components/ui/input.tsx +++ b/ui/admin/app/components/ui/input.tsx @@ -1,18 +1,33 @@ +import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; import { cn } from "~/lib/utils"; -export type InputProps = React.InputHTMLAttributes; +const inputVariants = cva( + "flex h-9 w-full rounded-md px-3 bg-transparent border border-input text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + variant: { + default: "", + ghost: "shadow-none cursor-pointer hover:border-primary px-0 mb-0 font-bold outline-none border-transparent focus:border-primary", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface InputProps + extends React.InputHTMLAttributes, + VariantProps {} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { + ({ className, variant, type, ...props }, ref) => { return ( @@ -21,4 +36,4 @@ const Input = React.forwardRef( ); Input.displayName = "Input"; -export { Input }; +export { Input, inputVariants }; diff --git a/ui/admin/app/lib/service/routeQueryParams.ts b/ui/admin/app/lib/service/routeQueryParams.ts index 7cda4273..7103137e 100644 --- a/ui/admin/app/lib/service/routeQueryParams.ts +++ b/ui/admin/app/lib/service/routeQueryParams.ts @@ -8,4 +8,8 @@ export const QueryParamSchemas = { agentId: z.string().optional(), workflowId: z.string().optional(), }), + Agents: z.object({ + threadId: z.string().optional(), + 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..63d959f4 100644 --- a/ui/admin/app/routes/_auth.agents.$agent.tsx +++ b/ui/admin/app/routes/_auth.agents.$agent.tsx @@ -1,39 +1,23 @@ -import { ArrowLeftIcon } from "@radix-ui/react-icons"; import { ClientLoaderFunctionArgs, - Link, redirect, useLoaderData, useNavigate, } from "@remix-run/react"; import { useCallback } from "react"; import { $params, $path } from "remix-routes"; -import { z } from "zod"; import { AgentService } from "~/lib/service/api/agentService"; +import { QueryParamSchemas } from "~/lib/service/routeQueryParams"; 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(), - from: z.string().optional(), -}); - -export type SearchParams = z.infer; export const clientLoader = async ({ params, @@ -41,7 +25,7 @@ export const clientLoader = async ({ }: ClientLoaderFunctionArgs) => { const { agent: agentId } = $params("/agents/:agent", params); const { threadId, from } = - parseQueryParams(request.url, paramSchema).data || {}; + parseQueryParams(request.url, QueryParamSchemas.Agents).data || {}; if (!agentId) { throw redirect("/agents"); @@ -57,7 +41,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,27 +69,7 @@ export default function ChatAgent() { className="flex-auto" > - - - - Go Back - - - updateThreadId(null)} - /> + diff --git a/ui/admin/app/routes/_auth.agents._index.tsx b/ui/admin/app/routes/_auth.agents._index.tsx index 18edf61d..590b7851 100644 --- a/ui/admin/app/routes/_auth.agents._index.tsx +++ b/ui/admin/app/routes/_auth.agents._index.tsx @@ -9,7 +9,6 @@ import useSWR, { preload } from "swr"; 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"; @@ -74,7 +73,7 @@ export default function Threads() { onClick={() => { AgentService.createAgent({ agent: { - name: generateRandomName(), + name: "New Agent", } as Agent, }).then((agent) => { navigate(