diff --git a/ui/admin/app/components/header/HeaderNav.tsx b/ui/admin/app/components/header/HeaderNav.tsx index bb046ebd..39d27c6a 100644 --- a/ui/admin/app/components/header/HeaderNav.tsx +++ b/ui/admin/app/components/header/HeaderNav.tsx @@ -1,11 +1,8 @@ -import { Link } from "@remix-run/react"; +import { Link, UIMatch, useLocation, useMatches } from "@remix-run/react"; +import React from "react"; import { $path } from "remix-routes"; -import useSWR from "swr"; -import { AgentService } from "~/lib/service/api/agentService"; -import { ThreadsService } from "~/lib/service/api/threadsService"; -import { WebhookApiService } from "~/lib/service/api/webhookApiService"; -import { WorkflowService } from "~/lib/service/api/workflowService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { cn } from "~/lib/utils"; import { DarkModeToggle } from "~/components/DarkModeToggle"; @@ -19,7 +16,6 @@ import { } from "~/components/ui/breadcrumb"; import { SidebarTrigger } from "~/components/ui/sidebar"; import { UserMenu } from "~/components/user/UserMenu"; -import { useUnknownPathParams } from "~/hooks/useRouteInfo"; export function HeaderNav() { const headerHeight = "h-[60px]"; @@ -36,7 +32,7 @@ export function HeaderNav() {
- +
@@ -49,8 +45,39 @@ export function HeaderNav() { ); } -function RouteBreadcrumbs() { - const routeInfo = useUnknownPathParams(); +function RouteBreadcrumbHandles() { + const matches = useMatches() as UIMatch[]; + const location = useLocation(); + const filtered = matches.filter((match) => match.handle?.breadcrumb); + + const renderItem = ( + match: UIMatch, + isLeaf: boolean + ) => { + if (!match.handle?.breadcrumb) return; + + return match.handle.breadcrumb(location).map((item, i, arr) => { + const withHref = isLeaf && i === arr.length - 1; + + return ( + + + + + {withHref ? ( + {item.content} + ) : ( + + + {item.content} + + + )} + + + ); + }); + }; return ( @@ -60,196 +87,11 @@ function RouteBreadcrumbs() { Home - - - {routeInfo?.path === "/agents/:agent" && ( - <> - - - Agents - - - - - - - - - - )} - - {routeInfo?.path === "/agents" && ( - - Agents - - )} - - {routeInfo?.path === "/threads" && ( - <> - {routeInfo.query?.from && ( - <> - - - {renderThreadFrom(routeInfo.query.from)} - - - - - - )} - - - Threads - - - )} - - {routeInfo?.path === "/threads/:id" && ( - <> - - - Threads - - - - - - - - - - )} - - {routeInfo?.path === "/workflows/:workflow" && ( - <> - - - Workflows - - - - - - - - - - )} - - {routeInfo?.path === "/webhooks" && ( - - Webhooks - - )} - {routeInfo?.path === "/webhooks/create" && ( - <> - - - Webhooks - - - - - Create Webhook - - - )} - - {routeInfo?.path === "/webhooks/:webhook" && ( - <> - - - Webhooks - - - - - - - - - - )} - - {routeInfo?.path === "/tools" && ( - - Tools - - )} - {routeInfo?.path === "/users" && ( - - Users - - )} - {routeInfo?.path === "/oauth-apps" && ( - - OAuth Apps - - )} - {routeInfo?.path === "/model-providers" && ( - - Model Providers - + {filtered.map((match, i, arr) => + renderItem(match, i === arr.length - 1) )} ); } - -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), - ({ agentId }) => AgentService.getAgentById(agentId) - ); - - return <>{agent?.name || "New Agent"}; -}; - -const WorkflowName = ({ workflowId }: { workflowId: string }) => { - const { data: workflow } = useSWR( - WorkflowService.getWorkflowById.key(workflowId), - ({ workflowId }) => WorkflowService.getWorkflowById(workflowId) - ); - - return <>{workflow?.name || "New Workflow"}; -}; - -const ThreadName = ({ threadId }: { threadId: string }) => { - const { data: thread } = useSWR( - ThreadsService.getThreadById.key(threadId), - ({ threadId }) => ThreadsService.getThreadById(threadId) - ); - - return <>{thread?.description || threadId}; -}; - -const WebhookName = ({ webhookId }: { webhookId: string }) => { - const { data } = useSWR( - WebhookApiService.getWebhookById.key(webhookId), - ({ id }) => WebhookApiService.getWebhookById(id) - ); - - return <>{data?.name || webhookId}; -}; diff --git a/ui/admin/app/lib/service/routeHandles.ts b/ui/admin/app/lib/service/routeHandles.ts new file mode 100644 index 00000000..65a255ed --- /dev/null +++ b/ui/admin/app/lib/service/routeHandles.ts @@ -0,0 +1,13 @@ +type BreadcrumbItem = { + content?: React.ReactNode; + href?: string; +}; + +type BreadcrumbProps = { + pathname: string; + search: string; +}; + +export type RouteHandle = { + breadcrumb?: (props: BreadcrumbProps) => BreadcrumbItem[]; +}; diff --git a/ui/admin/app/lib/service/routeService.ts b/ui/admin/app/lib/service/routeService.ts index 17540e13..5a283e66 100644 --- a/ui/admin/app/lib/service/routeService.ts +++ b/ui/admin/app/lib/service/routeService.ts @@ -200,4 +200,5 @@ export const RouteService = { getUnknownRouteInfo, getRouteInfo, getQueryParams, + getPathParams: $params, }; diff --git a/ui/admin/app/routes/_auth.agents.$agent.tsx b/ui/admin/app/routes/_auth.agents.$agent.tsx index 22d14732..bab33b66 100644 --- a/ui/admin/app/routes/_auth.agents.$agent.tsx +++ b/ui/admin/app/routes/_auth.agents.$agent.tsx @@ -2,14 +2,16 @@ import { ClientLoaderFunctionArgs, redirect, useLoaderData, + useMatch, useNavigate, } from "@remix-run/react"; import { useCallback } from "react"; import { $path } from "remix-routes"; -import { preload } from "swr"; +import useSWR, { preload } from "swr"; import { AgentService } from "~/lib/service/api/agentService"; import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { noop } from "~/lib/utils"; @@ -92,3 +94,18 @@ export default function ChatAgent() {
); } + +const AgentBreadcrumb = () => { + const match = useMatch("/agents/:agent"); + + const { data: agent } = useSWR( + AgentService.getAgentById.key(match?.params.agent), + ({ agentId }) => AgentService.getAgentById(agentId) + ); + + return <>{agent?.name || "New Agent"}; +}; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: }], +}; diff --git a/ui/admin/app/routes/_auth.agents.tsx b/ui/admin/app/routes/_auth.agents.tsx new file mode 100644 index 00000000..89e1cbb2 --- /dev/null +++ b/ui/admin/app/routes/_auth.agents.tsx @@ -0,0 +1,5 @@ +import { RouteHandle } from "~/lib/service/routeHandles"; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Agents" }], +}; diff --git a/ui/admin/app/routes/_auth.model-providers.tsx b/ui/admin/app/routes/_auth.model-providers.tsx index e77cf901..9b661ed2 100644 --- a/ui/admin/app/routes/_auth.model-providers.tsx +++ b/ui/admin/app/routes/_auth.model-providers.tsx @@ -4,6 +4,7 @@ import { ModelProvider } from "~/lib/model/modelProviders"; import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService"; import { ModelApiService } from "~/lib/service/api/modelApiService"; import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { TypographyH2 } from "~/components/Typography"; import { WarningAlert } from "~/components/composed/WarningAlert"; @@ -90,3 +91,7 @@ export default function ModelProviders() {
); } + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Model Providers" }], +}; diff --git a/ui/admin/app/routes/_auth.oauth-apps.tsx b/ui/admin/app/routes/_auth.oauth-apps.tsx index 3c9e4ca5..0c9148ef 100644 --- a/ui/admin/app/routes/_auth.oauth-apps.tsx +++ b/ui/admin/app/routes/_auth.oauth-apps.tsx @@ -1,6 +1,7 @@ import { preload } from "swr"; import { OauthAppService } from "~/lib/service/api/oauthAppService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { TypographyH2 } from "~/components/Typography"; import { OAuthAppList } from "~/components/oauth-apps/OAuthAppList"; @@ -33,3 +34,7 @@ export default function OauthApps() { ); } + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "OAuth Apps" }], +}; diff --git a/ui/admin/app/routes/_auth.threads.$id.tsx b/ui/admin/app/routes/_auth.threads.$id.tsx index 26a50a88..c60b8837 100644 --- a/ui/admin/app/routes/_auth.threads.$id.tsx +++ b/ui/admin/app/routes/_auth.threads.$id.tsx @@ -3,12 +3,14 @@ import { Link, redirect, useLoaderData, + useMatch, } from "@remix-run/react"; import { ArrowLeftIcon } from "lucide-react"; import { AgentService } from "~/lib/service/api/agentService"; import { ThreadsService } from "~/lib/service/api/threadsService"; import { WorkflowService } from "~/lib/service/api/workflowService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteService } from "~/lib/service/routeService"; import { noop } from "~/lib/utils"; @@ -118,3 +120,9 @@ export default function ChatAgent() { ); } + +const ThreadBreadcrumb = () => useMatch("/threads/:id")?.params.id; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: }], +}; diff --git a/ui/admin/app/routes/_auth.threads._index.tsx b/ui/admin/app/routes/_auth.threads._index.tsx index a96e9eac..d27eacfe 100644 --- a/ui/admin/app/routes/_auth.threads._index.tsx +++ b/ui/admin/app/routes/_auth.threads._index.tsx @@ -20,6 +20,7 @@ import { AgentService } from "~/lib/service/api/agentService"; import { ThreadsService } from "~/lib/service/api/threadsService"; import { UserService } from "~/lib/service/api/userService"; import { WorkflowService } from "~/lib/service/api/workflowService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { timeSince } from "~/lib/utils"; @@ -330,3 +331,30 @@ function ThreadFilters({ } const columnHelper = createColumnHelper(); + +const getFromBreadcrumb = (search: string) => { + const { from } = RouteService.getQueryParams("/threads", search) || {}; + + if (from === "agents") + return { + content: "Agents", + href: $path("/agents"), + }; + + if (from === "users") + return { + content: "Users", + href: $path("/users"), + }; + + if (from === "workflows") + return { + content: "Workflows", + href: $path("/workflows"), + }; +}; + +export const handle: RouteHandle = { + breadcrumb: ({ search }) => + [getFromBreadcrumb(search), { content: "Threads" }].filter((x) => !!x), +}; diff --git a/ui/admin/app/routes/_auth.tools._index.tsx b/ui/admin/app/routes/_auth.tools._index.tsx index e5145428..17eab295 100644 --- a/ui/admin/app/routes/_auth.tools._index.tsx +++ b/ui/admin/app/routes/_auth.tools._index.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import useSWR, { preload } from "swr"; import { ToolReferenceService } from "~/lib/service/api/toolreferenceService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { TypographyH2 } from "~/components/Typography"; import { ErrorDialog } from "~/components/composed/ErrorDialog"; @@ -113,3 +114,7 @@ export default function Tools() { ); } + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Tools" }], +}; diff --git a/ui/admin/app/routes/_auth.users.tsx b/ui/admin/app/routes/_auth.users.tsx index 2e090c03..163ed023 100644 --- a/ui/admin/app/routes/_auth.users.tsx +++ b/ui/admin/app/routes/_auth.users.tsx @@ -7,6 +7,7 @@ import { Thread } from "~/lib/model/threads"; import { User, roleToString } from "~/lib/model/users"; import { ThreadsService } from "~/lib/service/api/threadsService"; import { UserService } from "~/lib/service/api/userService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { pluralize, timeSince } from "~/lib/utils"; import { TypographyH2, TypographyP } from "~/components/Typography"; @@ -112,3 +113,7 @@ export default function Users() { } const columnHelper = createColumnHelper(); + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Users" }], +}; diff --git a/ui/admin/app/routes/_auth.webhooks.$webhook.tsx b/ui/admin/app/routes/_auth.webhooks.$webhook.tsx index 691d82ad..1fff34b3 100644 --- a/ui/admin/app/routes/_auth.webhooks.$webhook.tsx +++ b/ui/admin/app/routes/_auth.webhooks.$webhook.tsx @@ -1,7 +1,12 @@ -import { ClientLoaderFunctionArgs, useLoaderData } from "@remix-run/react"; +import { + ClientLoaderFunctionArgs, + useLoaderData, + useMatch, +} from "@remix-run/react"; import useSWR, { preload } from "swr"; import { WebhookApiService } from "~/lib/service/api/webhookApiService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteService } from "~/lib/service/routeService"; import { WebhookForm } from "~/components/webhooks/WebhookForm"; @@ -34,3 +39,18 @@ export default function Webhook() { return ; } + +const WebhookBreadcrumb = () => { + const match = useMatch("/webhooks/:webhook"); + + const { data: webhook } = useSWR( + WebhookApiService.getWebhookById.key(match?.params.webhook || ""), + ({ id }) => WebhookApiService.getWebhookById(id) + ); + + return webhook?.name || webhook?.id || "Edit"; +}; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: }], +}; diff --git a/ui/admin/app/routes/_auth.webhooks.create.tsx b/ui/admin/app/routes/_auth.webhooks.create.tsx index 07b2dd4d..5e28499a 100644 --- a/ui/admin/app/routes/_auth.webhooks.create.tsx +++ b/ui/admin/app/routes/_auth.webhooks.create.tsx @@ -1,5 +1,11 @@ +import { RouteHandle } from "~/lib/service/routeHandles"; + import { WebhookForm } from "~/components/webhooks/WebhookForm"; export default function CreateWebhookPage() { return ; } + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Create" }], +}; diff --git a/ui/admin/app/routes/_auth.webhooks.tsx b/ui/admin/app/routes/_auth.webhooks.tsx new file mode 100644 index 00000000..bfa74ff2 --- /dev/null +++ b/ui/admin/app/routes/_auth.webhooks.tsx @@ -0,0 +1,5 @@ +import { RouteHandle } from "~/lib/service/routeHandles"; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Webhooks" }], +}; diff --git a/ui/admin/app/routes/_auth.workflows.$workflow.tsx b/ui/admin/app/routes/_auth.workflows.$workflow.tsx index f66784ed..a94959b3 100644 --- a/ui/admin/app/routes/_auth.workflows.$workflow.tsx +++ b/ui/admin/app/routes/_auth.workflows.$workflow.tsx @@ -2,13 +2,15 @@ import { ClientLoaderFunctionArgs, redirect, useLoaderData, + useMatch, useNavigate, } from "@remix-run/react"; import { useCallback } from "react"; import { $path } from "remix-routes"; -import { preload } from "swr"; +import useSWR, { preload } from "swr"; import { WorkflowService } from "~/lib/service/api/workflowService"; +import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { Chat } from "~/components/chat"; @@ -88,3 +90,18 @@ export default function ChatAgent() { ); } + +const WorkflowBreadcrumb = () => { + const match = useMatch("/workflows/:workflow"); + + const { data: workflow } = useSWR( + WorkflowService.getWorkflowById.key(match?.params.workflow || ""), + ({ workflowId }) => WorkflowService.getWorkflowById(workflowId) + ); + + return workflow?.name; +}; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: }], +}; diff --git a/ui/admin/app/routes/_auth.workflows.tsx b/ui/admin/app/routes/_auth.workflows.tsx new file mode 100644 index 00000000..3167a008 --- /dev/null +++ b/ui/admin/app/routes/_auth.workflows.tsx @@ -0,0 +1,5 @@ +import { RouteHandle } from "~/lib/service/routeHandles"; + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Workflows" }], +};