mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
feat(site): add AI sessions list page (#23388)
<!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> Adds the AI Bridge sessions list page.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { API } from "#/api/api";
|
||||
import type { AIBridgeListInterceptionsResponse } from "#/api/typesGenerated";
|
||||
import type {
|
||||
AIBridgeListInterceptionsResponse,
|
||||
AIBridgeListSessionsResponse,
|
||||
} from "#/api/typesGenerated";
|
||||
import { useFilterParamsKey } from "#/components/Filter/Filter";
|
||||
import type { UsePaginatedQueryOptions } from "#/hooks/usePaginatedQuery";
|
||||
|
||||
@@ -20,3 +23,21 @@ export const paginatedInterceptions = (
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const paginatedSessions = (
|
||||
searchParams: URLSearchParams,
|
||||
): UsePaginatedQueryOptions<AIBridgeListSessionsResponse, string> => {
|
||||
return {
|
||||
searchParams,
|
||||
queryPayload: () => searchParams.get(useFilterParamsKey) ?? "",
|
||||
queryKey: ({ payload, pageNumber }) => {
|
||||
return ["aiBridgeSessions", payload, pageNumber] as const;
|
||||
},
|
||||
queryFn: ({ offset, limit, payload }) =>
|
||||
API.getAIBridgeSessionList({
|
||||
offset,
|
||||
limit,
|
||||
q: payload,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderSubtitle,
|
||||
PageHeaderTitle,
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
const AIBridgeSessionsLayout: FC<PropsWithChildren> = () => {
|
||||
return (
|
||||
<Margins className="pb-12">
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>AI Sessions</span>
|
||||
</div>
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
Centralized auditing for LLM usage across your organization.{" "}
|
||||
<Link
|
||||
href="https://coder.com/docs/ai-coder/ai-governance"
|
||||
className="ml-auto"
|
||||
target="_blank"
|
||||
>
|
||||
More about AI Governance
|
||||
</Link>
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
<Outlet />
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIBridgeSessionsLayout;
|
||||
@@ -21,38 +21,29 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
|
||||
menus,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-end">
|
||||
<span className="mr-2 text-content-secondary">Organization:</span>
|
||||
{/* TODO: add organization filter */}
|
||||
{/* <OrganizationAutocomplete */}
|
||||
{/* onChange={(org) => console.info("Selected organization", org)} */}
|
||||
{/* /> */}
|
||||
</div>
|
||||
<Filter
|
||||
filter={filter}
|
||||
optionsSkeleton={<MenuSkeleton />}
|
||||
isLoading={menus.user.isInitializing}
|
||||
presets={[
|
||||
{
|
||||
name: "All sessions",
|
||||
query: "",
|
||||
},
|
||||
{
|
||||
name: "My sessions",
|
||||
query: "initiator:me",
|
||||
},
|
||||
]}
|
||||
error={error}
|
||||
options={
|
||||
<>
|
||||
<UserMenu menu={menus.user} placeholder="All users" />
|
||||
<ProviderFilter menu={menus.provider} />
|
||||
{/* TODO: add client filter */}
|
||||
{/* <ClientFilter menu={menus.client} /> */}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<Filter
|
||||
filter={filter}
|
||||
optionsSkeleton={<MenuSkeleton />}
|
||||
isLoading={menus.user.isInitializing}
|
||||
presets={[
|
||||
{
|
||||
name: "All sessions",
|
||||
query: "",
|
||||
},
|
||||
{
|
||||
name: "My sessions",
|
||||
query: "initiator:me",
|
||||
},
|
||||
]}
|
||||
error={error}
|
||||
options={
|
||||
<>
|
||||
<UserMenu menu={menus.user} placeholder="All users" />
|
||||
<ProviderFilter menu={menus.provider} />
|
||||
{/* TODO: add client filter */}
|
||||
{/* <ClientFilter menu={menus.client} /> */}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { paginatedSessions } from "api/queries/aiBridge";
|
||||
import { useFilter } from "components/Filter/Filter";
|
||||
import { useUserFilterMenu } from "components/Filter/UserFilter";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { RequirePermission } from "modules/permissions/RequirePermission";
|
||||
import type { FC } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { useProviderFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ProviderFilter";
|
||||
import { ListSessionsPageView } from "./ListSessionsPageView";
|
||||
|
||||
const AISessionListPage: FC = () => {
|
||||
const { permissions } = useAuthenticated();
|
||||
const { entitlements } = useDashboard();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Users are allowed to view their own request logs via the API,
|
||||
// but this page is only visible if the feature is enabled and the user
|
||||
// has the `viewAnyAIBridgeInterception` permission.
|
||||
// (as its defined in the Admin settings dropdown).
|
||||
const isEntitled =
|
||||
entitlements.features.aibridge.entitlement === "entitled" ||
|
||||
entitlements.features.aibridge.entitlement === "grace_period";
|
||||
const isEnabled = entitlements.features.aibridge.enabled;
|
||||
const hasPermission = permissions.viewAnyAIBridgeInterception;
|
||||
const canViewSessions = isEntitled && hasPermission;
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const sessionsQuery = usePaginatedQuery({
|
||||
...paginatedSessions(searchParams),
|
||||
enabled: canViewSessions,
|
||||
});
|
||||
const filter = useFilter({
|
||||
searchParams,
|
||||
onSearchParamsChange: setSearchParams,
|
||||
onUpdate: sessionsQuery.goToFirstPage,
|
||||
});
|
||||
|
||||
const userMenu = useUserFilterMenu({
|
||||
value: filter.values.initiator,
|
||||
onChange: (option) =>
|
||||
filter.update({
|
||||
...filter.values,
|
||||
initiator: option?.value,
|
||||
}),
|
||||
});
|
||||
|
||||
const providerMenu = useProviderFilterMenu({
|
||||
value: filter.values.provider,
|
||||
onChange: (option) =>
|
||||
filter.update({
|
||||
...filter.values,
|
||||
provider: option?.value,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<RequirePermission isFeatureVisible={hasPermission}>
|
||||
<title>{pageTitle("Sessions", "AI Bridge")}</title>
|
||||
|
||||
<ListSessionsPageView
|
||||
isLoading={sessionsQuery.isLoading}
|
||||
isAISessionsEntitled={isEntitled}
|
||||
isAISessionsEnabled={isEnabled}
|
||||
sessions={sessionsQuery.data?.sessions}
|
||||
sessionsQuery={sessionsQuery}
|
||||
onSessionRowClick={(sessionId) =>
|
||||
navigate(`/aibridge/sessions/${sessionId}`)
|
||||
}
|
||||
filterProps={{
|
||||
filter,
|
||||
error: sessionsQuery.error,
|
||||
menus: {
|
||||
user: userMenu,
|
||||
provider: providerMenu,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</RequirePermission>
|
||||
);
|
||||
};
|
||||
|
||||
export default AISessionListPage;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { MockSession } from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import {
|
||||
getDefaultFilterProps,
|
||||
MockMenu,
|
||||
} from "components/Filter/storyHelpers";
|
||||
import {
|
||||
mockInitialRenderResult,
|
||||
mockSuccessResult,
|
||||
} from "components/PaginationWidget/PaginationContainer.mocks";
|
||||
import type { ComponentProps } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
import { ListSessionsPageView } from "./ListSessionsPageView";
|
||||
|
||||
type FilterProps = ComponentProps<typeof ListSessionsPageView>["filterProps"];
|
||||
|
||||
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||
query: "owner:me",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
});
|
||||
|
||||
const meta: Meta<typeof ListSessionsPageView> = {
|
||||
title: "pages/AIBridgePage/ListSessionsPageView",
|
||||
component: ListSessionsPageView,
|
||||
args: {
|
||||
isLoading: false,
|
||||
isAISessionsEntitled: true,
|
||||
isAISessionsEnabled: true,
|
||||
filterProps: defaultFilterProps,
|
||||
sessionsQuery: mockSuccessResult,
|
||||
onSessionRowClick: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ListSessionsPageView>;
|
||||
|
||||
export const Paywall: Story = {
|
||||
args: {
|
||||
isAISessionsEntitled: false,
|
||||
isAISessionsEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const NotEnabled: Story = {
|
||||
args: {
|
||||
isAISessionsEntitled: true,
|
||||
isAISessionsEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
sessions: undefined,
|
||||
sessionsQuery: mockInitialRenderResult,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
sessions: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const Loaded: Story = {
|
||||
args: {
|
||||
sessions: [MockSession],
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleSessions: Story = {
|
||||
args: {
|
||||
sessions: Array.from({ length: 5 }, (_, i) => ({
|
||||
...MockSession,
|
||||
id: `session-${i}`,
|
||||
threads: i + 1,
|
||||
last_prompt: [
|
||||
"But *can* I really fix it?",
|
||||
"Can you refactor the entire authentication module to use JWT tokens instead of session cookies?",
|
||||
"What's the best way to handle errors in Go?",
|
||||
"Help me write a Terraform module for a Kubernetes cluster.",
|
||||
"Explain how the agentic loop works in this codebase.",
|
||||
][i],
|
||||
token_usage_summary: {
|
||||
input_tokens: 1000 * (i + 1),
|
||||
output_tokens: 300 * (i + 1),
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { AIBridgeSession } from "api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } from "components/Alert/Alert";
|
||||
import { Link } from "components/Link/Link";
|
||||
import {
|
||||
PaginationContainer,
|
||||
type PaginationResult,
|
||||
} from "components/PaginationWidget/PaginationContainer";
|
||||
import { PaywallAIGovernance } from "components/Paywall/PaywallAIGovernance";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "components/Table/Table";
|
||||
import { TableEmpty } from "components/TableEmpty/TableEmpty";
|
||||
import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import type { ComponentProps, FC, PropsWithChildren } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
import { DATE_FORMAT, formatDateTime } from "utils/time";
|
||||
import { ListSessionsFilter } from "./ListSessionsFilter";
|
||||
import { ListSessionsRow } from "./ListSessionsRow";
|
||||
|
||||
interface ListSessionsPageViewProps {
|
||||
isLoading: boolean;
|
||||
isAISessionsEntitled: boolean;
|
||||
isAISessionsEnabled: boolean;
|
||||
sessions?: readonly AIBridgeSession[];
|
||||
sessionsQuery: PaginationResult;
|
||||
filterProps: ComponentProps<typeof ListSessionsFilter>;
|
||||
onSessionRowClick?: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
const ThreadTooltip: FC<PropsWithChildren> = ({ children }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex-shrink-0 flex items-center">{children}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="end" className="max-w-xs">
|
||||
<p className="text-sm">
|
||||
A thread is a multi-part interaction between human and agent involving
|
||||
an initial human prompt and a subsequent agentic loop.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
export const ListSessionsPageView: FC<ListSessionsPageViewProps> = ({
|
||||
isLoading,
|
||||
isAISessionsEntitled,
|
||||
isAISessionsEnabled,
|
||||
sessions,
|
||||
sessionsQuery,
|
||||
filterProps,
|
||||
onSessionRowClick,
|
||||
}) => {
|
||||
if (!isAISessionsEntitled) {
|
||||
return <PaywallAIGovernance />;
|
||||
}
|
||||
|
||||
if (!isAISessionsEnabled) {
|
||||
return (
|
||||
<Alert className="mb-12" severity="warning" prominent>
|
||||
<AlertTitle>
|
||||
AI Bridge is included in your license, but not set up yet.
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
You have access to AI Governance, but it still needs to be setup.
|
||||
Check out the{" "}
|
||||
<Link href={docs("/ai-coder/ai-bridge")} target="_blank">
|
||||
AI Bridge
|
||||
</Link>{" "}
|
||||
documentation to get started.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const utcOffset = formatDateTime(new Date(), DATE_FORMAT.UTC_OFFSET);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListSessionsFilter {...filterProps} />
|
||||
|
||||
<PaginationContainer query={sessionsQuery} paginationUnitLabel="sessions">
|
||||
<Table className="text-sm">
|
||||
<TableHeader>
|
||||
<TableRow className="text-xs">
|
||||
<TableHead>Last Prompt</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>In/Out Tokens</TableHead>
|
||||
<TableHead className="flex items-center gap-1">
|
||||
Threads
|
||||
<ThreadTooltip>
|
||||
<InfoIcon className="size-icon-xs" />
|
||||
</ThreadTooltip>
|
||||
</TableHead>
|
||||
<TableHead>Timestamp [UTC{utcOffset}]</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableLoader />
|
||||
) : sessions?.length === 0 ? (
|
||||
<TableEmpty message="No session logs available" />
|
||||
) : (
|
||||
sessions?.map((session) => (
|
||||
<ListSessionsRow
|
||||
session={session}
|
||||
key={session.id}
|
||||
onClick={() => onSessionRowClick?.(session.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PaginationContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MockSession } from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Table, TableBody } from "components/Table/Table";
|
||||
import { fn } from "storybook/test";
|
||||
import { ListSessionsRow } from "./ListSessionsRow";
|
||||
|
||||
const meta: Meta<typeof ListSessionsRow> = {
|
||||
title: "pages/AIBridgePage/ListSessionsRow",
|
||||
component: ListSessionsRow,
|
||||
args: {
|
||||
onClick: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<Story />
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ListSessionsRow>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
session: MockSession,
|
||||
},
|
||||
};
|
||||
|
||||
export const NullClient: Story = {
|
||||
args: {
|
||||
session: { ...MockSession, client: null },
|
||||
},
|
||||
};
|
||||
|
||||
export const NoInitiatorName: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
initiator: { ...MockSession.initiator, name: "" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongPrompt: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
last_prompt:
|
||||
"Can you refactor the entire authentication module to use JWT tokens instead of session cookies, and also update all the tests, documentation, and CI pipelines while you're at it?",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoPrompt: Story = {
|
||||
args: {
|
||||
session: { ...MockSession, last_prompt: undefined },
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyThreads: Story = {
|
||||
args: {
|
||||
session: { ...MockSession, threads: 128 },
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeTokenCounts: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
token_usage_summary: {
|
||||
input_tokens: 198_000,
|
||||
output_tokens: 32_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { AIBridgeSession } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { TableCell, TableRow } from "components/Table/Table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { ArrowDownIcon, ArrowUpIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { AIBridgeClientIcon } from "pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon";
|
||||
import { AIBridgeProviderIcon } from "pages/AIBridgePage/RequestLogsPage/icons/AIBridgeProviderIcon";
|
||||
import type { FC } from "react";
|
||||
import { DATE_FORMAT, formatDateTime } from "utils/time";
|
||||
import {
|
||||
getProviderDisplayName,
|
||||
getProviderIconName,
|
||||
roundTokenDisplay,
|
||||
} from "../utils";
|
||||
|
||||
type ListSessionsRowProps = {
|
||||
session: AIBridgeSession;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
session,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<TableCell className="max-w-32 flex-1 overflow-auto">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className="truncate">{session.last_prompt}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64">
|
||||
{session.last_prompt}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<TableCell className="w-48 max-w-48">
|
||||
<div className="w-full min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Avatar
|
||||
fallback={session.initiator.username}
|
||||
src={session.initiator.avatar_url}
|
||||
size="lg"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="font-medium truncate min-w-0 flex-1 overflow-hidden">
|
||||
{session.initiator.name ?? session.initiator.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-40 max-w-40">
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="gap-1.5 max-w-full">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<AIBridgeProviderIcon
|
||||
provider={getProviderIconName(session.providers[0])}
|
||||
className="size-icon-xs"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate min-w-0">
|
||||
{getProviderDisplayName(session.providers[0])}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{getProviderDisplayName(session.providers[0])}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-40 max-w-40">
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="gap-1.5 max-w-full">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<AIBridgeClientIcon
|
||||
client={session.client}
|
||||
className="size-icon-xs"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate min-w-0">
|
||||
{session.client ?? "Unknown"}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{session.client}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-32">
|
||||
<div className="flex items-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="gap-0 rounded-e-none">
|
||||
<ArrowDownIcon className="size-icon-lg flex-shrink-0" />
|
||||
<span className="truncate min-w-0 w-full">
|
||||
{roundTokenDisplay(
|
||||
session.token_usage_summary.input_tokens,
|
||||
)}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{session.token_usage_summary.input_tokens} Input Tokens
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="gap-0 bg-surface-tertiary rounded-s-none">
|
||||
<ArrowUpIcon className="size-icon-lg flex-shrink-0" />
|
||||
<span className="truncate min-w-0 w-full">
|
||||
{roundTokenDisplay(
|
||||
session.token_usage_summary.output_tokens,
|
||||
)}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{session.token_usage_summary.output_tokens} Output Tokens
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-32">
|
||||
<Badge className="bg-surface-secondary align-end">
|
||||
{session.threads}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="w-48 whitespace-nowrap">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{formatDateTime(
|
||||
new Date(session.started_at),
|
||||
DATE_FORMAT.FULL_DATETIME,
|
||||
)}
|
||||
</span>
|
||||
<ChevronRightIcon className="ml-4" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -374,6 +374,14 @@ const AIBridgeRequestLogsPage = lazy(
|
||||
() => import("./pages/AIBridgePage/RequestLogsPage/RequestLogsPage"),
|
||||
);
|
||||
|
||||
const AIBridgeSessionsLayout = lazy(
|
||||
() => import("./pages/AIBridgePage/AIBridgeSessionsLayout"),
|
||||
);
|
||||
|
||||
const AIBridgeListSessionsPage = lazy(
|
||||
() => import("./pages/AIBridgePage/ListSessionsPage/ListSessionsPage"),
|
||||
);
|
||||
|
||||
const GlobalLayout = () => {
|
||||
return (
|
||||
<Suspense fallback={<Loader fullscreen />}>
|
||||
@@ -609,6 +617,10 @@ export const router = createBrowserRouter(
|
||||
<Route path="request-logs" element={<AIBridgeRequestLogsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/aibridge/sessions" element={<AIBridgeSessionsLayout />}>
|
||||
<Route index element={<AIBridgeListSessionsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/health" element={<HealthLayout />}>
|
||||
<Route index element={<Navigate to="access-url" replace />} />
|
||||
<Route path="access-url" element={<AccessURLPage />} />
|
||||
|
||||
Reference in New Issue
Block a user