mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add page for ai-bridge interception logs (#20331)
Relates #20287 This pull-request introduces a basic routing for `AI Governance`'s Request Logs feature. Currently we're just pulling back the basics from the database and rendering it into the table. Nothing exciting. The idea is to extend further upon the `/aigovernance` route so it has been appropriately wrapped with a `<AIGovernanceLayout />` to introduce a navigation later.
This commit is contained in:
@@ -2716,6 +2716,16 @@ class ExperimentalApiMethods {
|
||||
setTimeout(() => res(), 500);
|
||||
});
|
||||
};
|
||||
|
||||
getAIBridgeInterceptions = async (options: SearchParamOptions) => {
|
||||
const url = getURLWithSearchParams(
|
||||
"/api/experimental/aibridge/interceptions",
|
||||
options,
|
||||
);
|
||||
const response =
|
||||
await this.axios.get<TypesGen.AIBridgeListInterceptionsResponse>(url);
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
// This is a hard coded CSRF token/cookie pair for local development. In prod,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { API } from "api/api";
|
||||
import type { AIBridgeListInterceptionsResponse } from "api/typesGenerated";
|
||||
import { useFilterParamsKey } from "components/Filter/Filter";
|
||||
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
|
||||
|
||||
export const paginatedInterceptions = (
|
||||
searchParams: URLSearchParams,
|
||||
): UsePaginatedQueryOptions<AIBridgeListInterceptionsResponse, string> => {
|
||||
return {
|
||||
queryPayload: () => searchParams.get(useFilterParamsKey) ?? "",
|
||||
queryKey: ({ payload, pageNumber }) => {
|
||||
return ["aiBridgeInterceptions", payload, pageNumber] as const;
|
||||
},
|
||||
queryFn: ({ limit, offset, payload }) =>
|
||||
API.experimental.getAIBridgeInterceptions({
|
||||
offset,
|
||||
limit,
|
||||
q: payload,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -18,6 +18,7 @@ interface DeploymentDropdownProps {
|
||||
canViewAuditLog: boolean;
|
||||
canViewConnectionLog: boolean;
|
||||
canViewHealth: boolean;
|
||||
canViewAIGovernance: boolean;
|
||||
}
|
||||
|
||||
export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
|
||||
@@ -26,6 +27,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
|
||||
canViewAuditLog,
|
||||
canViewConnectionLog,
|
||||
canViewHealth,
|
||||
canViewAIGovernance,
|
||||
}) => {
|
||||
if (
|
||||
!canViewAuditLog &&
|
||||
@@ -56,6 +58,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewHealth={canViewHealth}
|
||||
canViewAIGovernance={canViewAIGovernance}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -68,6 +71,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
|
||||
canViewAuditLog,
|
||||
canViewHealth,
|
||||
canViewConnectionLog,
|
||||
canViewAIGovernance,
|
||||
}) => {
|
||||
return (
|
||||
<nav>
|
||||
@@ -111,6 +115,17 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
)}
|
||||
{canViewAIGovernance && (
|
||||
<PopoverClose asChild>
|
||||
<MenuItem
|
||||
component={NavLink}
|
||||
to="/aigovernance"
|
||||
css={styles.menuItem}
|
||||
>
|
||||
AI Governance
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
)}
|
||||
{canViewHealth && (
|
||||
<PopoverClose asChild>
|
||||
<MenuItem component={NavLink} to="/health" css={styles.menuItem}>
|
||||
|
||||
@@ -25,6 +25,8 @@ export const Navbar: FC = () => {
|
||||
featureVisibility.audit_log && permissions.viewAnyAuditLog;
|
||||
const canViewConnectionLog =
|
||||
featureVisibility.connection_log && permissions.viewAnyConnectionLog;
|
||||
const canViewAIGovernance =
|
||||
featureVisibility.aibridge && permissions.viewAnyAIBridgeInterception;
|
||||
|
||||
const uniqueLinks = new Map<string, LinkConfig>();
|
||||
for (const link of appearance.support_links ?? []) {
|
||||
@@ -44,6 +46,7 @@ export const Navbar: FC = () => {
|
||||
canViewHealth={canViewHealth}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewAIGovernance={canViewAIGovernance}
|
||||
proxyContextValue={proxyContextValue}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ interface NavbarViewProps {
|
||||
canViewAuditLog: boolean;
|
||||
canViewConnectionLog: boolean;
|
||||
canViewHealth: boolean;
|
||||
canViewAIGovernance: boolean;
|
||||
proxyContextValue?: ProxyContextValue;
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
canViewHealth,
|
||||
canViewAuditLog,
|
||||
canViewConnectionLog,
|
||||
canViewAIGovernance,
|
||||
proxyContextValue,
|
||||
}) => {
|
||||
const webPush = useWebpushNotifications();
|
||||
@@ -95,6 +97,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
canViewDeployment={canViewDeployment}
|
||||
canViewHealth={canViewHealth}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewAIGovernance={canViewAIGovernance}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -169,6 +169,13 @@ export const permissionChecks = {
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
viewAnyAIBridgeInterception: {
|
||||
object: {
|
||||
resource_type: "aibridge_interception",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
} as const satisfies Record<string, AuthorizationCheck>;
|
||||
|
||||
export const canViewDeploymentSettings = (
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipContent,
|
||||
HelpTooltipIconTrigger,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import type { FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
export const AIGovernanceHelpTooltip: FC = () => {
|
||||
return (
|
||||
<HelpTooltip>
|
||||
<HelpTooltipIconTrigger />
|
||||
|
||||
<HelpTooltipContent>
|
||||
<HelpTooltipTitle>What is AI Governance?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
AI Governance is a proxy that unifies and audits LLM usage across your
|
||||
organization.
|
||||
</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href={docs("/ai-coder/ai-bridge")}>
|
||||
What we track
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltipContent>
|
||||
</HelpTooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
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";
|
||||
import { AIGovernanceHelpTooltip } from "./AIGovernanceHelpTooltip";
|
||||
|
||||
const AIGovernanceLayout: FC<PropsWithChildren> = () => {
|
||||
return (
|
||||
<Margins className="pb-12">
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>AI Governance</span>
|
||||
<AIGovernanceHelpTooltip />
|
||||
</div>
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
Manage usage for your organization.
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
<Outlet />
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIGovernanceLayout;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { paginatedInterceptions } from "api/queries/aiBridge";
|
||||
import { useFilter } from "components/Filter/Filter";
|
||||
import { useUserFilterMenu } from "components/Filter/UserFilter";
|
||||
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
|
||||
import type { FC } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { useProviderFilterMenu } from "./filter/filter";
|
||||
import { RequestLogsPageView } from "./RequestLogsPageView";
|
||||
|
||||
const RequestLogsPage: FC = () => {
|
||||
const feats = useFeatureVisibility();
|
||||
const isRequestLogsVisible = Boolean(feats.aibridge);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const interceptionsQuery = usePaginatedQuery(
|
||||
paginatedInterceptions(searchParams),
|
||||
);
|
||||
const filter = useFilter({
|
||||
searchParams,
|
||||
onSearchParamsChange: setSearchParams,
|
||||
onUpdate: interceptionsQuery.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 (
|
||||
<>
|
||||
<title>{pageTitle("Request Logs", "AI Governance")}</title>
|
||||
|
||||
<RequestLogsPageView
|
||||
isLoading={interceptionsQuery.isLoading}
|
||||
isRequestLogsVisible={isRequestLogsVisible}
|
||||
interceptions={interceptionsQuery.data?.results}
|
||||
interceptionsQuery={interceptionsQuery}
|
||||
filterProps={{
|
||||
filter,
|
||||
error: interceptionsQuery.error,
|
||||
menus: {
|
||||
user: userMenu,
|
||||
provider: providerMenu,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestLogsPage;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { MockInterception } 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 { RequestLogsPageView } from "./RequestLogsPageView";
|
||||
|
||||
type FilterProps = ComponentProps<typeof RequestLogsPageView>["filterProps"];
|
||||
|
||||
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||
query: "owner:me",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
},
|
||||
});
|
||||
|
||||
const interceptions = [MockInterception, MockInterception, MockInterception];
|
||||
|
||||
const meta: Meta<typeof RequestLogsPageView> = {
|
||||
title: "pages/AIGovernancePage/RequestLogsPageView",
|
||||
component: RequestLogsPageView,
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RequestLogsPageView>;
|
||||
|
||||
export const Paywall: Story = {
|
||||
args: {
|
||||
isRequestLogsVisible: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loaded: Story = {
|
||||
args: {
|
||||
isRequestLogsVisible: true,
|
||||
interceptions,
|
||||
filterProps: {
|
||||
...defaultFilterProps,
|
||||
},
|
||||
interceptionsQuery: mockSuccessResult,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
isRequestLogsVisible: true,
|
||||
interceptions: [],
|
||||
filterProps: {
|
||||
...defaultFilterProps,
|
||||
},
|
||||
interceptionsQuery: mockSuccessResult,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
isRequestLogsVisible: true,
|
||||
interceptions: [],
|
||||
filterProps: {
|
||||
...defaultFilterProps,
|
||||
},
|
||||
interceptionsQuery: mockInitialRenderResult,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { AIBridgeInterception } from "api/typesGenerated";
|
||||
import {
|
||||
PaginationContainer,
|
||||
type PaginationResult,
|
||||
} from "components/PaginationWidget/PaginationContainer";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "components/Table/Table";
|
||||
import { TableEmpty } from "components/TableEmpty/TableEmpty";
|
||||
import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
import { RequestLogsFilter } from "./filter/RequestLogsFilter";
|
||||
import { RequestLogsRow } from "./RequestLogsRow/RequestLogsRow";
|
||||
|
||||
interface RequestLogsPageViewProps {
|
||||
isLoading: boolean;
|
||||
isRequestLogsVisible: boolean;
|
||||
interceptions?: readonly AIBridgeInterception[];
|
||||
interceptionsQuery: PaginationResult;
|
||||
filterProps: ComponentProps<typeof RequestLogsFilter>;
|
||||
}
|
||||
|
||||
export const RequestLogsPageView: FC<RequestLogsPageViewProps> = ({
|
||||
isLoading,
|
||||
isRequestLogsVisible,
|
||||
interceptions,
|
||||
interceptionsQuery,
|
||||
filterProps,
|
||||
}) => {
|
||||
if (!isRequestLogsVisible) {
|
||||
return (
|
||||
<Paywall
|
||||
message="AI Governance"
|
||||
description="AI Governance allows you to monitor and manage AI requests. You need an Premium license to use this feature."
|
||||
documentationLink={docs("/ai-coder/ai-bridge")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RequestLogsFilter {...filterProps} />
|
||||
|
||||
<PaginationContainer
|
||||
query={interceptionsQuery}
|
||||
paginationUnitLabel="interceptions"
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Prompt</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
<TableHead>Tool Calls</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableLoader />
|
||||
) : interceptions?.length === 0 ? (
|
||||
<TableEmpty message={"No request logs available"} />
|
||||
) : (
|
||||
interceptions?.map((interception) => (
|
||||
<RequestLogsRow
|
||||
interception={interception}
|
||||
key={interception.id}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PaginationContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { MockInterception } from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Table, TableBody } from "components/Table/Table";
|
||||
import { RequestLogsRow } from "./RequestLogsRow";
|
||||
|
||||
const meta: Meta<typeof RequestLogsRow> = {
|
||||
title: "pages/AIGovernancePage/RequestLogsRow",
|
||||
component: RequestLogsRow,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<Story />
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RequestLogsRow>;
|
||||
|
||||
export const Close: Story = {
|
||||
args: {
|
||||
interception: MockInterception,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
import type { AIBridgeInterception } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { TableCell, TableRow } from "components/Table/Table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, Fragment, useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
type RequestLogsRowProps = {
|
||||
interception: AIBridgeInterception;
|
||||
};
|
||||
|
||||
export const RequestLogsRow: FC<RequestLogsRowProps> = ({ interception }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [firstPrompt] = interception.user_prompts;
|
||||
|
||||
const inputTokens = interception.token_usages.reduce(
|
||||
(acc, tokenUsage) => acc + tokenUsage.input_tokens,
|
||||
0,
|
||||
);
|
||||
const outputTokens = interception.token_usages.reduce(
|
||||
(acc, tokenUsage) => acc + tokenUsage.output_tokens,
|
||||
0,
|
||||
);
|
||||
const toolCalls = interception.tool_usages.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
className="select-none cursor-pointer hover:bg-surface-secondary"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<TableCell>
|
||||
<div
|
||||
className={cn([
|
||||
"flex items-center gap-2",
|
||||
isOpen && "text-content-primary",
|
||||
])}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDownIcon className="size-icon-xs" />
|
||||
) : (
|
||||
<ChevronRightIcon className="size-icon-xs" />
|
||||
)}
|
||||
<span className="sr-only">({isOpen ? "Hide" : "Show more"})</span>
|
||||
{new Date(interception.started_at).toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar
|
||||
fallback={interception.initiator.username}
|
||||
src={interception.initiator.avatar_url}
|
||||
/>
|
||||
<div className="font-medium">{interception.initiator.username}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{firstPrompt?.prompt}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-4">
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowDownIcon className="size-icon-xs" />
|
||||
<div>{inputTokens}</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Input Tokens</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowUpIcon className="size-icon-xs" />
|
||||
<div>{outputTokens}</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Output Tokens</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{toolCalls}</TableCell>
|
||||
</TableRow>
|
||||
{isOpen && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999} className="p-4 border-t-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<dl
|
||||
className={cn([
|
||||
"text-xs text-content-secondary",
|
||||
"m-0 grid grid-cols-[auto_1fr] gap-x-4 items-center",
|
||||
"[&_dd]:text-content-primary [&_dd]:font-mono [&_dd]:leading-[22px] [&_dt]:font-medium",
|
||||
])}
|
||||
>
|
||||
<dt>Request ID:</dt>
|
||||
<dd data-chromatic="ignore">{interception.id}</dd>
|
||||
|
||||
<dt>Start Time:</dt>
|
||||
<dd data-chromatic="ignore">
|
||||
{new Date(interception.started_at).toLocaleString()}
|
||||
</dd>
|
||||
|
||||
{interception.ended_at && (
|
||||
<>
|
||||
<dt>End Time:</dt>
|
||||
<dd data-chromatic="ignore">
|
||||
{new Date(interception.ended_at).toLocaleString()}
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
<dt>Initiator:</dt>
|
||||
<dd data-chromatic="ignore">
|
||||
{interception.initiator.username}
|
||||
</dd>
|
||||
|
||||
<dt>Model:</dt>
|
||||
<dd data-chromatic="ignore">{interception.model}</dd>
|
||||
|
||||
<dt>Input Tokens:</dt>
|
||||
<dd data-chromatic="ignore">{inputTokens}</dd>
|
||||
|
||||
<dt>Output Tokens:</dt>
|
||||
<dd data-chromatic="ignore">{outputTokens}</dd>
|
||||
|
||||
<dt>Tool Calls:</dt>
|
||||
<dd data-chromatic="ignore">
|
||||
{interception.tool_usages.length}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
{interception.user_prompts.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>Prompts</div>
|
||||
<div
|
||||
className="bg-surface-secondary rounded-md p-4"
|
||||
data-chromatic="ignore"
|
||||
>
|
||||
{interception.user_prompts.map((prompt) => (
|
||||
<Fragment key={prompt.id}>{prompt.prompt}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{interception.tool_usages.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>Tool Usages</div>
|
||||
<div
|
||||
className="bg-surface-secondary rounded-md p-4"
|
||||
data-chromatic="ignore"
|
||||
>
|
||||
{interception.tool_usages.map((toolUsage) => {
|
||||
return (
|
||||
<dl
|
||||
key={toolUsage.id}
|
||||
className={cn([
|
||||
"text-xs text-content-secondary",
|
||||
"m-0 grid grid-cols-[auto_1fr] gap-x-4 items-center",
|
||||
"[&_dt]:text-content-primary [&_dd]:font-mono [&_dt]:leading-[22px] [&_dt]:font-medium",
|
||||
])}
|
||||
>
|
||||
<dt>{toolUsage.tool}</dt>
|
||||
<dd>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{toolUsage.input}</div>
|
||||
{toolUsage.invocation_error && (
|
||||
<div className="text-content-destructive">
|
||||
{toolUsage.invocation_error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter";
|
||||
import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
|
||||
import type { FC } from "react";
|
||||
import { ProviderFilter, type ProviderFilterMenu } from "./filter";
|
||||
|
||||
interface RequestLogsFilterProps {
|
||||
filter: ReturnType<typeof useFilter>;
|
||||
error?: unknown;
|
||||
menus: {
|
||||
user: UserFilterMenu;
|
||||
provider: ProviderFilterMenu;
|
||||
};
|
||||
}
|
||||
|
||||
export const RequestLogsFilter: FC<RequestLogsFilterProps> = ({
|
||||
filter,
|
||||
error,
|
||||
menus,
|
||||
}) => {
|
||||
return (
|
||||
<Filter
|
||||
filter={filter}
|
||||
optionsSkeleton={<MenuSkeleton />}
|
||||
isLoading={menus.user.isInitializing}
|
||||
presets={[
|
||||
{
|
||||
name: "All requests",
|
||||
query: "",
|
||||
},
|
||||
{
|
||||
name: "My requests",
|
||||
query: "initiator:me",
|
||||
},
|
||||
]}
|
||||
error={error}
|
||||
options={
|
||||
<>
|
||||
<UserMenu menu={menus.user} />
|
||||
<ProviderFilter menu={menus.provider} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
type UseFilterMenuOptions,
|
||||
useFilterMenu,
|
||||
} from "components/Filter/menu";
|
||||
import {
|
||||
SelectFilter,
|
||||
type SelectFilterOption,
|
||||
} from "components/Filter/SelectFilter";
|
||||
import type { FC } from "react";
|
||||
|
||||
const AIBRIDGE_PROVIDERS: SelectFilterOption[] = [
|
||||
{
|
||||
label: "OpenAI",
|
||||
value: "openai",
|
||||
},
|
||||
{
|
||||
label: "Anthropic",
|
||||
value: "anthropic",
|
||||
},
|
||||
];
|
||||
|
||||
export const useProviderFilterMenu = ({
|
||||
value,
|
||||
onChange,
|
||||
enabled,
|
||||
}: Pick<UseFilterMenuOptions, "value" | "onChange" | "enabled">) => {
|
||||
return useFilterMenu({
|
||||
id: "provider",
|
||||
getSelectedOption: async () =>
|
||||
AIBRIDGE_PROVIDERS.find((option) => option.value === value) ?? null,
|
||||
getOptions: async () => {
|
||||
return AIBRIDGE_PROVIDERS;
|
||||
},
|
||||
value,
|
||||
onChange,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export type ProviderFilterMenu = ReturnType<typeof useProviderFilterMenu>;
|
||||
|
||||
interface ProviderFilterProps {
|
||||
menu: ProviderFilterMenu;
|
||||
}
|
||||
|
||||
export const ProviderFilter: FC<ProviderFilterProps> = ({ menu }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label={"Select provider"}
|
||||
placeholder={"All providers"}
|
||||
emptyText="No providers found"
|
||||
options={menu.searchOptions}
|
||||
onSelect={(option) => menu.selectOption(option)}
|
||||
selectedOption={menu.selectedOption ?? undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -334,6 +334,12 @@ const ProvisionerJobsPage = lazy(
|
||||
);
|
||||
const TasksPage = lazy(() => import("./pages/TasksPage/TasksPage"));
|
||||
const TaskPage = lazy(() => import("./pages/TaskPage/TaskPage"));
|
||||
const AIGovernanceLayout = lazy(
|
||||
() => import("./pages/AIGovernancePage/AIGovernanceLayout"),
|
||||
);
|
||||
const AIGovernanceRequestLogsPage = lazy(
|
||||
() => import("./pages/AIGovernancePage/RequestLogsPage/RequestLogsPage"),
|
||||
);
|
||||
|
||||
const RoutesWithSuspense = () => {
|
||||
return (
|
||||
@@ -557,6 +563,14 @@ export const router = createBrowserRouter(
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="/aigovernance" element={<AIGovernanceLayout />}>
|
||||
<Route index element={<Navigate to="request-logs" replace />} />
|
||||
<Route
|
||||
path="request-logs"
|
||||
element={<AIGovernanceRequestLogsPage />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/health" element={<HealthLayout />}>
|
||||
<Route index element={<Navigate to="access-url" replace />} />
|
||||
<Route path="access-url" element={<AccessURLPage />} />
|
||||
|
||||
@@ -3101,6 +3101,7 @@ export const MockPermissions: Permissions = {
|
||||
editAnySettings: true,
|
||||
viewAnyIdpSyncSettings: true,
|
||||
viewAnyMembers: true,
|
||||
viewAnyAIBridgeInterception: true,
|
||||
};
|
||||
|
||||
export const MockNoPermissions: Permissions = {
|
||||
@@ -3129,6 +3130,7 @@ export const MockNoPermissions: Permissions = {
|
||||
editAnySettings: false,
|
||||
viewAnyIdpSyncSettings: false,
|
||||
viewAnyMembers: false,
|
||||
viewAnyAIBridgeInterception: true,
|
||||
};
|
||||
|
||||
export const MockOrganizationPermissions: OrganizationPermissions = {
|
||||
@@ -5051,3 +5053,31 @@ export const MockTasks = [
|
||||
},
|
||||
},
|
||||
] satisfies TypesGen.Task[];
|
||||
|
||||
export const MockInterception: TypesGen.AIBridgeInterception = {
|
||||
id: "5c1da48a-9eb0-440e-9c82-5bc5692a603d",
|
||||
initiator: {
|
||||
id: "1ebb7622-e6ea-45b4-b244-dda30afc7238",
|
||||
username: "testuser",
|
||||
avatar_url: "https://example.com/avatar.png",
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
started_at: "2022-05-17T17:39:01.382927298Z",
|
||||
ended_at: "2022-05-17T17:39:01.382927298Z",
|
||||
token_usages: [
|
||||
{
|
||||
id: "32e7fd17-24be-46b9-b867-2f0adfd42aff",
|
||||
interception_id: "5c1da48a-9eb0-440e-9c82-5bc5692a603d",
|
||||
provider_response_id: "res_1234567890",
|
||||
input_tokens: 5,
|
||||
output_tokens: 1,
|
||||
metadata: {},
|
||||
created_at: "2022-05-17T17:39:01.382927298Z",
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
user_prompts: [],
|
||||
tool_usages: [],
|
||||
api_key_id: "5c1da48a-9eb0-440e-9c82-5bc5692a603d",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user