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" }],
+};