From ca971dda2959bb7b0190b9afe23d1ce9aca415c6 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Fri, 19 Dec 2025 00:47:20 +1100 Subject: [PATCH] fix: improve AI Bridge request logs UI/UX (#21252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #21052 and is related to #21218 This pull request implements a bunch of changes the team requested to improve the overall user experience when navigating to the Request Logs page. Since these are mostly UI changes, screenshots do a much better job of showing what's different than me trying to explain everything in words, so I've included them below. > [!WARNING] > More globally we've changed how we're rendering ``, therefore we've renamed it to ``. This will effect multiple layouts however its a global improvement we're intentionally looking to make. ### Preview ![ai-bridge-improvements](https://github.com/user-attachments/assets/ce92852e-d0fc-4268-b52b-9aefd431033b) ### Changes 1. Date formatting set to a standardised way including the month as a `MMM` (i.e `Dec`) 2. Names and avatars are now rendered larger, furthermore they render the `.name` field falling back on `.username`. 3. Font-size increased from `text-xs` to `text-sm` to increase overall legibility globally. 4. Initiator is now rendered inside of a `` and gives longer names a chance to render. 5. Models are now rendered in a `` alongside their icons. 6. Input/Output tokens are in a shared ``-group alongside truncation (when necessary). 7. Badge from `5` is rendered with the proper name being visible on hover (for use if truncated). 8. Move globally the `Showing X of X` counts to the bottom of the tables (effects other components outside of AI Bridge). 9. Icons are now included within the dropdown for the `Provider` filter. 10. Truncation of elements is now handled so larger prompts don't cause the table to require further scrolling. 11. New merged Input/Output `
` element with the badge | Position | Pull-request | | -------- | ------------ | | ✅ | [fix: improve AI Bridge request logs UI/UX](https://github.com/coder/coder/pull/21252) | | | [feat: add AI Bridge request logs model filter](https://github.com/coder/coder/pull/21259) | | | [chore!: promote AIBridge from `ExperimentalHandler`](https://github.com/coder/coder/pull/21278) | | | [feat: implement request log collapsing prompt (``)](https://github.com/coder/coder/pull/21313) | --------- Co-authored-by: ケイラ --- ...inationHeader.tsx => PaginationAmount.tsx} | 3 +- .../PaginationWidget/PaginationContainer.tsx | 42 ++-- .../RequestLogsPage/AIBridgeProviderIcon.tsx | 36 ++++ .../RequestLogsPage/RequestLogsPageView.tsx | 7 +- .../RequestLogsRow/RequestLogsRow.tsx | 203 ++++++++++++++---- .../filter/RequestLogsFilter.tsx | 2 +- .../RequestLogsPage/filter/filter.tsx | 7 + .../WorkspacesPage/WorkspacesPageView.tsx | 5 +- site/static/icon/claude.svg | 5 +- 9 files changed, 233 insertions(+), 77 deletions(-) rename site/src/components/PaginationWidget/{PaginationHeader.tsx => PaginationAmount.tsx} (94%) create mode 100644 site/src/pages/AIBridgePage/RequestLogsPage/AIBridgeProviderIcon.tsx diff --git a/site/src/components/PaginationWidget/PaginationHeader.tsx b/site/src/components/PaginationWidget/PaginationAmount.tsx similarity index 94% rename from site/src/components/PaginationWidget/PaginationHeader.tsx rename to site/src/components/PaginationWidget/PaginationAmount.tsx index bb0f8f6925..204825cfb7 100644 --- a/site/src/components/PaginationWidget/PaginationHeader.tsx +++ b/site/src/components/PaginationWidget/PaginationAmount.tsx @@ -13,7 +13,7 @@ type PaginationHeaderProps = { className?: string; }; -export const PaginationHeader: FC = ({ +export const PaginationAmount: FC = ({ paginationUnitLabel, limit, totalRecords, @@ -30,7 +30,6 @@ export const PaginationHeader: FC = ({ alignItems: "center", margin: 0, fontSize: "13px", - paddingBottom: "8px", color: theme.palette.text.secondary, height: "36px", // The size of a small button "& strong": { diff --git a/site/src/components/PaginationWidget/PaginationContainer.tsx b/site/src/components/PaginationWidget/PaginationContainer.tsx index c480b41e8a..b4c9b53a65 100644 --- a/site/src/components/PaginationWidget/PaginationContainer.tsx +++ b/site/src/components/PaginationWidget/PaginationContainer.tsx @@ -1,6 +1,6 @@ import type { PaginationResultInfo } from "hooks/usePaginatedQuery"; import type { FC, HTMLAttributes } from "react"; -import { PaginationHeader } from "./PaginationHeader"; +import { PaginationAmount } from "./PaginationAmount"; import { PaginationWidgetBase } from "./PaginationWidgetBase"; export type PaginationResult = PaginationResultInfo & { @@ -19,35 +19,27 @@ export const PaginationContainer: FC = ({ ...delegatedProps }) => { return ( - <> - + {children} + + -
- {children} - - {query.isSuccess && ( - - )} -
- + {query.isSuccess && ( + + )} + ); }; diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/AIBridgeProviderIcon.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/AIBridgeProviderIcon.tsx new file mode 100644 index 0000000000..538942c8a2 --- /dev/null +++ b/site/src/pages/AIBridgePage/RequestLogsPage/AIBridgeProviderIcon.tsx @@ -0,0 +1,36 @@ +import type { AIBridgeInterception } from "api/typesGenerated"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { CircleQuestionMarkIcon } from "lucide-react"; +import { cn } from "utils/cn"; + +export const AIBridgeProviderIcon = ({ + provider, + ...props +}: { + provider: AIBridgeInterception["provider"]; +} & React.ComponentProps<"svg">) => { + const iconClassName = "flex-shrink-0"; + switch (provider) { + case "openai": + return ( + + ); + case "anthropic": + return ( + + ); + default: + return ( + + ); + } +}; diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx index fbac613fb2..2ee5e6d9c7 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx @@ -51,13 +51,14 @@ export const RequestLogsPageView: FC = ({ query={interceptionsQuery} paginationUnitLabel="interceptions" > - +
- + Timestamp - User + Initiator Prompt Tokens + Model Tool Calls diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx index 510023412d..129c2f0402 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx @@ -1,5 +1,6 @@ import type { AIBridgeInterception } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; +import { Badge } from "components/Badge/Badge"; import { TableCell, TableRow } from "components/Table/Table"; import { Tooltip, @@ -16,11 +17,22 @@ import { import { type FC, Fragment, useState } from "react"; import { cn } from "utils/cn"; import { humanDuration } from "utils/time"; +import { AIBridgeProviderIcon } from "../AIBridgeProviderIcon"; type RequestLogsRowProps = { interception: AIBridgeInterception; }; +const customisedDateLocale: Intl.DateTimeFormatOptions = { + second: "2-digit", + minute: "2-digit", + hour: "2-digit", + day: "numeric", + // Show the month as a short name + month: "short", + year: "numeric", +}; + type TokenUsageMetadataMerged = | null | Record @@ -132,10 +144,11 @@ export const RequestLogsRow: FC = ({ interception }) => { return ( <> setIsOpen(!isOpen)} + hover > - +
= ({ interception }) => { )} ({isOpen ? "Hide" : "Show more"}) - {new Date(interception.started_at).toLocaleString()} + {new Date(interception.started_at).toLocaleString( + undefined, + customisedDateLocale, + )}
- -
- -
{interception.initiator.username}
+ +
+
+ +
+ {interception.initiator.name ?? interception.initiator.username} +
+
- {firstPrompt?.prompt} - -
- + + {/* + This is ensuring that the prompt is truncated and won't escape its bounding + container with an `absolute`. + + Alternatively we could use a `table-fixed` table, but that would break worse + on mobile with the `min-w-0` column required. + + This is a bit of a hack, but it works. + */} +
+
+ {firstPrompt?.prompt} +
+
+
+ +
+ -
- -
{inputTokens}
-
+ + + + {inputTokens} + +
- Input Tokens + {inputTokens} Input Tokens
- + -
- -
{outputTokens}
-
+ + + + {outputTokens} + +
- Output Tokens + {outputTokens} Output Tokens
- {toolCalls} + + + + +
+ +
+ +
+ + {interception.model} + +
+
+
+ {interception.model} +
+
+
+ {toolCalls} {isOpen && ( -
+
@@ -205,14 +268,20 @@ export const RequestLogsRow: FC = ({ interception }) => {
Start Time:
- {new Date(interception.started_at).toLocaleString()} + {new Date(interception.started_at).toLocaleString( + undefined, + customisedDateLocale, + )}
{interception.ended_at && ( <>
End Time:
- {new Date(interception.ended_at).toLocaleString()} + {new Date(interception.ended_at).toLocaleString( + undefined, + customisedDateLocale, + )}
)} @@ -227,30 +296,84 @@ export const RequestLogsRow: FC = ({ interception }) => { )}
Initiator:
-
- {interception.initiator.username} +
+ + + {interception.initiator.name ?? + interception.initiator.username} +
Model:
-
{interception.model}
- -
Input Tokens:
-
{inputTokens}
- -
Output Tokens:
-
{outputTokens}
+
+ +
+ +
+ + {interception.model} + +
+
Tool Calls:
{interception.tool_usages.length}
+ +
Input/Output Tokens:
+
+
+ + + + + + + {inputTokens} + + + + + {inputTokens} Input Tokens + + + + + + + + + + {outputTokens} + + + + + {outputTokens} Output Tokens + + + +
+
{interception.user_prompts.length > 0 && (
Prompts
{interception.user_prompts.map((prompt) => ( diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/filter/RequestLogsFilter.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/filter/RequestLogsFilter.tsx index afc5c969fb..34efa48a38 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/filter/RequestLogsFilter.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/filter/RequestLogsFilter.tsx @@ -35,7 +35,7 @@ export const RequestLogsFilter: FC = ({ error={error} options={ <> - + } diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/filter/filter.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/filter/filter.tsx index 3184bbe441..f1a75a4346 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/filter/filter.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/filter/filter.tsx @@ -7,15 +7,22 @@ import { type SelectFilterOption, } from "components/Filter/SelectFilter"; import type { FC } from "react"; +import { AIBridgeProviderIcon } from "../AIBridgeProviderIcon"; const AIBRIDGE_PROVIDERS: SelectFilterOption[] = [ { label: "OpenAI", value: "openai", + startIcon: ( + + ), }, { label: "Anthropic", value: "anthropic", + startIcon: ( + + ), }, ]; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 9c515e76ff..b52a26ade5 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -12,7 +12,7 @@ import { import { EmptyState } from "components/EmptyState/EmptyState"; import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; -import { PaginationHeader } from "components/PaginationWidget/PaginationHeader"; +import { PaginationAmount } from "components/PaginationWidget/PaginationAmount"; import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; @@ -196,12 +196,11 @@ export const WorkspacesPageView: FC = ({ ) : ( !pageNumberIsInvalid && ( - ) )} diff --git a/site/static/icon/claude.svg b/site/static/icon/claude.svg index 998fb0d52f..654fa09145 100644 --- a/site/static/icon/claude.svg +++ b/site/static/icon/claude.svg @@ -1,4 +1,3 @@ - - - + +