From 6a4c3a161282febc2bc49b93760931675050322a Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Fri, 15 Nov 2024 17:31:57 -0600 Subject: [PATCH] feat: improve thread breadcrumbs --- ui/admin/app/components/chat/ChatContext.tsx | 6 +- ui/admin/app/components/header/HeaderNav.tsx | 44 +++++++++--- ui/admin/app/lib/service/routeService.ts | 13 ++-- ui/admin/app/routes/_auth.agents._index.tsx | 1 + ui/admin/app/routes/_auth.threads.tsx | 70 ++++++++++--------- ui/admin/app/routes/_auth.users.tsx | 1 + .../app/routes/_auth.workflows._index.tsx | 1 + 7 files changed, 84 insertions(+), 52 deletions(-) diff --git a/ui/admin/app/components/chat/ChatContext.tsx b/ui/admin/app/components/chat/ChatContext.tsx index 67d8c0bf..71a1d948 100644 --- a/ui/admin/app/components/chat/ChatContext.tsx +++ b/ui/admin/app/components/chat/ChatContext.tsx @@ -22,7 +22,7 @@ interface ChatContextType { mode: Mode; processUserMessage: (text: string, sender: "user" | "agent") => void; id: string; - threadId: string | undefined; + threadId: Nullish; invoke: (prompt?: string) => void; readOnly?: boolean; isRunning: boolean; @@ -42,7 +42,7 @@ export function ChatProvider({ children: ReactNode; mode?: Mode; id: string; - threadId?: string; + threadId?: Nullish; onCreateThreadId?: (threadId: string) => void; readOnly?: boolean; }) { @@ -116,7 +116,7 @@ export function useChat() { return context; } -function useMessageSource(threadId?: string) { +function useMessageSource(threadId?: Nullish) { const [messages, setMessages] = useState([]); const [isRunning, setIsRunning] = useState(false); diff --git a/ui/admin/app/components/header/HeaderNav.tsx b/ui/admin/app/components/header/HeaderNav.tsx index 60320e05..c4a6d74c 100644 --- a/ui/admin/app/components/header/HeaderNav.tsx +++ b/ui/admin/app/components/header/HeaderNav.tsx @@ -60,7 +60,8 @@ function RouteBreadcrumbs() { - {routeInfo?.path === "/agents/:agent" ? ( + + {routeInfo?.path === "/agents/:agent" && ( <> @@ -76,18 +77,34 @@ function RouteBreadcrumbs() { - ) : ( - routeInfo?.path === "/agents" && ( - - Agents - - ) )} - {routeInfo?.path === "/threads" && ( + + {routeInfo?.path === "/agents" && ( - Threads + Agents )} + + {routeInfo?.path === "/threads" && ( + <> + {routeInfo.query?.from && ( + <> + + + {renderThreadFrom(routeInfo.query.from)} + + + + + + )} + + + Threads + + + )} + {routeInfo?.path === "/thread/:id" && ( <> @@ -144,6 +161,15 @@ function RouteBreadcrumbs() { ); } +const renderThreadFrom = (from: "agents" | "workflows" | "users") => { + if (from === "agents") return Agents; + + if (from === "workflows") + return Workflows; + + if (from === "users") return Users; +}; + const AgentName = ({ agentId }: { agentId: string }) => { const { data: agent } = useSWR( AgentService.getAgentById.key(agentId), diff --git a/ui/admin/app/lib/service/routeService.ts b/ui/admin/app/lib/service/routeService.ts index 62a18bee..4db430d2 100644 --- a/ui/admin/app/lib/service/routeService.ts +++ b/ui/admin/app/lib/service/routeService.ts @@ -4,13 +4,14 @@ import { ZodNull, ZodSchema, ZodType, z } from "zod"; const QuerySchemas = { agentSchema: z.object({ - threadId: z.string().optional(), - from: z.string().optional(), + threadId: z.string().nullish(), + from: z.string().nullish(), }), threadsListSchema: z.object({ - agentId: z.string().optional(), - userId: z.string().optional(), - workflowId: z.string().optional(), + agentId: z.string().nullish(), + userId: z.string().nullish(), + workflowId: z.string().nullish(), + from: z.enum(["workflows", "agents", "users"]).nullish().catch(null), }), } as const; @@ -117,7 +118,7 @@ type PathInfo = ReturnType< typeof $params >; -type RouteInfo = { +export type RouteInfo = { path: T; query: QueryInfo | null; pathParams: T extends keyof RoutesWithParams ? PathInfo : unknown; diff --git a/ui/admin/app/routes/_auth.agents._index.tsx b/ui/admin/app/routes/_auth.agents._index.tsx index d0fcd0df..9cb8ff70 100644 --- a/ui/admin/app/routes/_auth.agents._index.tsx +++ b/ui/admin/app/routes/_auth.agents._index.tsx @@ -127,6 +127,7 @@ export default function Agents() { diff --git a/ui/admin/app/routes/_auth.threads.tsx b/ui/admin/app/routes/_auth.threads.tsx index ee5271a9..dddcdc8c 100644 --- a/ui/admin/app/routes/_auth.threads.tsx +++ b/ui/admin/app/routes/_auth.threads.tsx @@ -8,7 +8,7 @@ import { } from "@remix-run/react"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { PuzzleIcon, Trash, XIcon } from "lucide-react"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { $path } from "remix-routes"; import useSWR, { preload } from "swr"; @@ -278,43 +278,45 @@ function ThreadFilters({ agentMap: Record; workflowMap: Record; }) { - const [searchParams, setSearchParams] = useSearchParams(); - - const removeParam = useCallback( - (key: string) => - setSearchParams((prev) => { - const params = new URLSearchParams(prev); - params.delete(key); - return params; - }), - [setSearchParams] - ); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const filters = useMemo(() => { - const threadFilters = { - agentId: (value: string) => agentMap[value]?.name ?? value, - userId: (value: string) => userMap[value]?.email ?? "-", - workflowId: (value: string) => workflowMap[value]?.name ?? value, + const query = + RouteService.getQueryParams("/threads", searchParams.toString()) ?? + {}; + const { from: _, ...filters } = query; + + const updateFilters = (param: keyof typeof filters) => { + // note(ryanhopperlowe) this is a hack because setting a param to null/undefined + // appends "null" to the query string. + const newQuery = structuredClone(query); + delete newQuery[param]; + return navigate($path("/threads", newQuery)); }; - const labels = { - agentId: "Agent", - userId: "User", - workflowId: "Workflow", - }; - - const query = RouteService.getQueryParams( - "/threads", - searchParams.toString() - ); - - return Object.entries(query ?? {}).map(([key, value]) => ({ - key, - label: labels[key as keyof SearchParams], - value: threadFilters[key as keyof SearchParams](value), - onRemove: () => removeParam(key), - })); - }, [agentMap, removeParam, searchParams, userMap, workflowMap]); + return [ + filters.agentId && { + key: "agentId", + label: "Agent", + value: agentMap[filters.agentId]?.name ?? filters.agentId, + onRemove: () => updateFilters("agentId"), + }, + filters.userId && { + key: "userId", + label: "User", + value: userMap[filters.userId]?.email ?? filters.userId, + onRemove: () => updateFilters("userId"), + }, + filters.workflowId && { + key: "workflowId", + label: "Workflow", + value: + workflowMap[filters.workflowId]?.name ?? filters.workflowId, + onRemove: () => updateFilters("workflowId"), + }, + ].filter((x) => !!x); + }, [agentMap, navigate, searchParams, userMap, workflowMap]); return (
diff --git a/ui/admin/app/routes/_auth.users.tsx b/ui/admin/app/routes/_auth.users.tsx index 66921c27..a54bb8cf 100644 --- a/ui/admin/app/routes/_auth.users.tsx +++ b/ui/admin/app/routes/_auth.users.tsx @@ -79,6 +79,7 @@ export default function Users() { diff --git a/ui/admin/app/routes/_auth.workflows._index.tsx b/ui/admin/app/routes/_auth.workflows._index.tsx index b0ef3967..453493de 100644 --- a/ui/admin/app/routes/_auth.workflows._index.tsx +++ b/ui/admin/app/routes/_auth.workflows._index.tsx @@ -118,6 +118,7 @@ export default function Workflows() {