mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
fix(site): sessions list and timeline polish (#23885)
- Prompt table was collapsing and sizing improperly, fixed - Make pretty much everything `text-sm` and `font-normal` - Add model filter - Back button on session threads page now navigates back instead of going straight to `/aibridge/sessions` --------- Co-authored-by: Jake Howell <jake@hwll.me>
This commit is contained in:
co-authored by
Jake Howell
parent
b86161e0a6
commit
1d16ff1ca6
@@ -20,6 +20,7 @@ const defaultFilterProps = getDefaultFilterProps<
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,6 +50,7 @@ export const WithQuery: Story = {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
used: true,
|
||||
}),
|
||||
@@ -62,6 +64,7 @@ export const Loading: Story = {
|
||||
user: { ...MockMenu, isInitializing: true },
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
ClientFilter,
|
||||
type ClientFilterMenu,
|
||||
} from "../RequestLogsPage/RequestLogsFilter/ClientFilter";
|
||||
import {
|
||||
ModelFilter,
|
||||
type ModelFilterMenu,
|
||||
} from "../RequestLogsPage/RequestLogsFilter/ModelFilter";
|
||||
import {
|
||||
ProviderFilter,
|
||||
type ProviderFilterMenu,
|
||||
@@ -21,6 +25,7 @@ interface ListSessionsFilterProps {
|
||||
user: UserFilterMenu;
|
||||
provider: ProviderFilterMenu;
|
||||
client: ClientFilterMenu;
|
||||
model: ModelFilterMenu;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +55,7 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
|
||||
<UserMenu menu={menus.user} placeholder="All users" />
|
||||
<ProviderFilter menu={menus.provider} />
|
||||
<ClientFilter menu={menus.client} />
|
||||
<ModelFilter menu={menus.model} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RequirePermission } from "#/modules/permissions/RequirePermission";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { getAIBridgePermissions } from "../getAIBridgePermissions";
|
||||
import { useClientFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ClientFilter";
|
||||
import { useModelFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ModelFilter";
|
||||
import { useProviderFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ProviderFilter";
|
||||
import { ListSessionsPageView } from "./ListSessionsPageView";
|
||||
|
||||
@@ -64,6 +65,15 @@ const AISessionListPage: FC = () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const modelMenu = useModelFilterMenu({
|
||||
value: filter.values.model,
|
||||
onChange: (option) =>
|
||||
filter.update({
|
||||
...filter.values,
|
||||
model: option?.value,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<RequirePermission isFeatureVisible={hasPermission}>
|
||||
<title>{pageTitle("Sessions", "AI Bridge")}</title>
|
||||
@@ -85,6 +95,7 @@ const AISessionListPage: FC = () => {
|
||||
user: userMenu,
|
||||
provider: providerMenu,
|
||||
client: clientMenu,
|
||||
model: modelMenu,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -43,11 +43,13 @@ const ThreadTooltip: FC<PropsWithChildren> = ({ children }) => (
|
||||
<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
|
||||
side="top"
|
||||
align="end"
|
||||
className="max-w-xs text-sm font-normal"
|
||||
>
|
||||
A thread is a multi-part interaction between human and agent involving
|
||||
an initial human prompt and a subsequent agentic loop.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -78,9 +80,9 @@ export const ListSessionsPageView: FC<ListSessionsPageViewProps> = ({
|
||||
<ListSessionsFilter {...filterProps} />
|
||||
|
||||
<PaginationContainer query={sessionsQuery} paginationUnitLabel="sessions">
|
||||
<Table className="text-sm">
|
||||
<Table className="text-sm font-normal">
|
||||
<TableHeader>
|
||||
<TableRow className="text-xs">
|
||||
<TableRow>
|
||||
<TableHead>Last Prompt</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
|
||||
@@ -33,14 +33,15 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<TableCell className="max-w-32 flex-1 overflow-auto">
|
||||
<TableCell className="max-w-32 flex-1 overflow-auto font-normal">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className="truncate">{session.last_prompt}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64">
|
||||
{session.last_prompt}
|
||||
<TooltipContent className="max-w-64" side="top" align="start">
|
||||
<div className="font-bold">Last prompt</div>
|
||||
<div>{session.last_prompt}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -54,7 +55,7 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
size="lg"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="font-medium truncate min-w-0 flex-1 overflow-hidden">
|
||||
<div className="font-normal truncate min-w-0 flex-1 overflow-hidden">
|
||||
{session.initiator.name ?? session.initiator.username}
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +104,7 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
{session.threads}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="w-48 whitespace-nowrap">
|
||||
<TableCell className="w-48 whitespace-nowrap font-normal">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{formatDateTime(
|
||||
|
||||
@@ -45,28 +45,30 @@ export const SessionSummaryTable = ({
|
||||
return (
|
||||
<dl
|
||||
className={cn(
|
||||
"text-xs text-content-secondary m-0",
|
||||
"grid grid-cols-[auto_1fr] gap-y-0.5 [&_dd]:ml-0 [&_dd]:text-content-primary",
|
||||
"text-sm text-content-secondary m-0 whitespace-nowrap",
|
||||
"grid grid-cols-[auto_1fr] gap-y-2 [&_dd]:ml-0 [&_dd]:text-content-primary",
|
||||
"[&_dd]:h-6 [&_dd]:flex [&_dd]:min-w-0 [&_dd]:items-center [&_dd]:justify-end",
|
||||
"[&_dt]:h-6 [&_dt]:inline-flex [&_dt]:items-center [&_dt]:font-normal",
|
||||
)}
|
||||
>
|
||||
<dt>Session ID</dt>
|
||||
<dd className="font-mono min-w-0" title={sessionId}>
|
||||
<dd className="text-xs font-mono min-w-0" title={sessionId}>
|
||||
<span className="truncate w-full text-right">{sessionId}</span>
|
||||
</dd>
|
||||
|
||||
<dt>Start time</dt>
|
||||
<dd className="font-mono" title={formatDateTime(startTime)}>
|
||||
<dd className="text-xs font-mono" title={formatDateTime(startTime)}>
|
||||
{formatDateTime(startTime)}
|
||||
</dd>
|
||||
|
||||
<dt>End time</dt>
|
||||
<dd className="font-mono">{endTime ? formatDateTime(endTime) : "—"}</dd>
|
||||
<dd className="text-xs font-mono">
|
||||
{endTime ? formatDateTime(endTime) : "—"}
|
||||
</dd>
|
||||
|
||||
<dt>Duration</dt>
|
||||
<dd
|
||||
className="font-mono"
|
||||
className="text-xs font-mono"
|
||||
title={durationInMs !== undefined ? `${durationInMs} ms` : undefined}
|
||||
>
|
||||
{durationInMs !== undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import { useInfiniteQuery } from "react-query";
|
||||
import { useParams } from "react-router";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { infiniteSessionThreads } from "#/api/queries/aiBridge";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
@@ -12,6 +12,7 @@ import { SessionThreadsPageView } from "./SessionThreadsPageView";
|
||||
const SessionThreadsPage: FC = () => {
|
||||
const { permissions } = useAuthenticated();
|
||||
const { entitlements } = useDashboard();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions(
|
||||
entitlements,
|
||||
@@ -44,6 +45,7 @@ const SessionThreadsPage: FC = () => {
|
||||
onFetchNextPage={sessionQuery.fetchNextPage}
|
||||
isAISessionsEnabled={isEnabled}
|
||||
isAISessionsEntitled={isEntitled}
|
||||
onBackClicked={() => navigate(-1)}
|
||||
/>
|
||||
</RequirePermission>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ArrowLeftIcon, InfoIcon } from "lucide-react";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import type {
|
||||
AIBridgeSessionThreadsResponse,
|
||||
AIBridgeThread,
|
||||
@@ -27,7 +26,7 @@ const SessionSummaryTooltip: FC<PropsWithChildren> = ({ children }) => (
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="max-w-xs flex flex-col gap-1 text-sm p-3"
|
||||
className="max-w-xs flex flex-col gap-1 text-sm font-normal p-3"
|
||||
>
|
||||
<p className="m-0 leading-snug">
|
||||
A session is a set of threads or interceptions logically grouped by a
|
||||
@@ -47,6 +46,7 @@ interface SessionThreadsPageViewProps {
|
||||
onFetchNextPage: () => void;
|
||||
isAISessionsEnabled: boolean;
|
||||
isAISessionsEntitled: boolean;
|
||||
onBackClicked: () => void;
|
||||
}
|
||||
|
||||
export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
|
||||
@@ -58,6 +58,7 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
|
||||
onFetchNextPage,
|
||||
isAISessionsEnabled,
|
||||
isAISessionsEntitled,
|
||||
onBackClicked,
|
||||
}) => {
|
||||
if (!isAISessionsEntitled) {
|
||||
return <PaywallAIGovernance />;
|
||||
@@ -81,19 +82,20 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
|
||||
variant="outline"
|
||||
size="lg"
|
||||
title="Back to AI Bridge sessions list"
|
||||
onClick={onBackClicked}
|
||||
>
|
||||
<RouterLink to="/aibridge/sessions">
|
||||
<span>
|
||||
<ArrowLeftIcon />
|
||||
Back
|
||||
</RouterLink>
|
||||
</span>
|
||||
</Button>
|
||||
</nav>
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-6">
|
||||
<aside className="md:w-64 md:shrink-0 px-3 py-2.5 border border-solid rounded-md flex flex-col gap-1">
|
||||
<aside className="md:w-80 md:shrink-0 px-3 py-2.5 border border-solid rounded-md flex flex-col gap-1">
|
||||
<h2 className="text-sm font-semibold flex items-center m-0">
|
||||
Session summary
|
||||
<SessionSummaryTooltip>
|
||||
<InfoIcon className="ml-2 text-content-secondary size-icon-sm" />
|
||||
<InfoIcon className="ml-2 text-content-secondary size-icon-xs" />
|
||||
</SessionSummaryTooltip>
|
||||
</h2>
|
||||
{loading && <Loader className="my-4" />}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const AgenticLoopTable: FC<AgenticLoopTableProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm text-content-secondary flex flex-col gap-1",
|
||||
"text-sm text-content-secondary font-normal flex flex-col gap-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const PromptTable: FC<PromptTableProps> = ({
|
||||
return (
|
||||
<dl
|
||||
className={cn(
|
||||
"text-sm text-content-secondary m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 items-center",
|
||||
"text-sm text-content-secondary font-normal m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 items-center",
|
||||
"[&_dt]:whitespace-nowrap py-1",
|
||||
"[&_dt]:pr-4 [&_dt]:flex [&_dt]:items-center [&_dt]:h-6",
|
||||
"[&_dd]:m-0 [&_dd]:min-w-0 [&_dd]:h-6",
|
||||
|
||||
+17
-14
@@ -160,7 +160,10 @@ const ThinkingBlock: FC<ThinkingBlockProps> = ({ text }) => (
|
||||
<LoaderIcon className="size-icon-xs text-content-secondary" />
|
||||
<span className="font-mono ml-2 text-xs">Thinking...</span>
|
||||
</div>
|
||||
<ExpandableText text={text} className="text-sm text-pretty m-0" />
|
||||
<ExpandableText
|
||||
text={text}
|
||||
className="text-sm text-pretty font-normal m-0"
|
||||
/>
|
||||
</BracketConnector>
|
||||
);
|
||||
|
||||
@@ -191,7 +194,7 @@ const ToolCallBlock: FC<ToolCallBlockProps> = ({
|
||||
<BracketConnector contentClassName="mt-2 mr-4 border border-solid rounded-md overflow-x-auto">
|
||||
<div className="flex items-center">
|
||||
<CollapseButton isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
|
||||
<span className="text-sm">Tool call</span>
|
||||
<span className="text-sm font-normal">Tool call</span>
|
||||
<Badge size="xs" className="font-mono ml-1">
|
||||
{tool}
|
||||
</Badge>
|
||||
@@ -236,13 +239,13 @@ const AgenticLoopCompletedBlock: FC<AgenticLoopCompletedBlockProps> = ({
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<CollapseButton isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
|
||||
<span className="text-sm">Agentic loop completed</span>
|
||||
<span className="text-sm font-normal">Agentic loop completed</span>
|
||||
</CollapseButton>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="mb-4 ml-3 mr-4 flex flex-col gap-2 lg:w-1/2 text-sm text-content-secondary">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">In / out tokens</span>
|
||||
<span className="font-normal">In / out tokens</span>
|
||||
<TokenBadges
|
||||
inputTokens={inputTokens}
|
||||
outputTokens={outputTokens}
|
||||
@@ -302,9 +305,9 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border border-solid rounded-md flex flex-col lg:flex-row gap-6 p-2">
|
||||
<div className="border border-solid rounded-md flex flex-col items-start w-full lg:w-auto lg:flex-row gap-6 p-2">
|
||||
{/* left column: avatar and username */}
|
||||
<div className="flex flex-row items-items-start gap-1">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<Avatar
|
||||
src={initiator.avatar_url}
|
||||
fallback={initiator.name ?? initiator.username}
|
||||
@@ -323,7 +326,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
|
||||
<div className="text-sm text-content-secondary font-normal my-1">
|
||||
Prompt
|
||||
</div>
|
||||
<p className="text-sm text-content-secondary bg-surface-secondary leading-relaxed rounded-md p-3 overflow-auto m-0 text-pretty">
|
||||
<p className="text-sm text-content-secondary font-normal bg-surface-secondary leading-relaxed rounded-md p-3 overflow-auto m-0 text-pretty">
|
||||
{thread.prompt}
|
||||
</p>
|
||||
</>
|
||||
@@ -332,7 +335,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
|
||||
|
||||
{/* right column: details */}
|
||||
<PromptTable
|
||||
className="lg:max-w-64 flex-shrink-0"
|
||||
className="lg:max-w-64 flex-shrink-0 w-full lg:w-auto"
|
||||
timestamp={new Date(thread.started_at)}
|
||||
model={thread.model}
|
||||
inputTokens={thread.token_usage.input_tokens}
|
||||
@@ -352,7 +355,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
|
||||
isOpen={agenticLoopOpen}
|
||||
onClick={() => setAgenticLoopOpen(!agenticLoopOpen)}
|
||||
>
|
||||
<span className="text-sm">Agentic loop</span>
|
||||
<span className="text-sm font-normal">Agentic loop</span>
|
||||
</CollapseButton>
|
||||
</div>
|
||||
|
||||
@@ -440,7 +443,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="row-start-1 col-start-4 col-span-2 flex items-center h-10">
|
||||
<span className="text-content-secondary ml-4 py-1 text-sm">
|
||||
<span className="text-content-secondary font-normal ml-4 py-1 text-sm">
|
||||
Session started
|
||||
</span>
|
||||
</div>
|
||||
@@ -456,7 +459,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
|
||||
</div>
|
||||
|
||||
{/* row 3/4: AI Governance tooltip */}
|
||||
<div className="row-start-3 col-start-5 row-span-2 flex items-center text-sm text-content-secondary px-2 pt-1">
|
||||
<div className="row-start-3 col-start-5 row-span-2 flex items-center text-sm text-content-secondary font-normal px-2 pt-1">
|
||||
AI Governance
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -464,11 +467,11 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
|
||||
<InfoIcon className="size-icon-sm p-0.5 ml-1" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="max-w-64 text-sm"
|
||||
className="max-w-80 text-sm font-normal"
|
||||
align="end"
|
||||
side="top"
|
||||
>
|
||||
<div className="text-content-secondary font-medium mb-1">
|
||||
<div className="text-content-secondary mb-1">
|
||||
Controls and logs AI tooling so AI use stays secure,
|
||||
compliant, and visible.
|
||||
</div>
|
||||
@@ -554,7 +557,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="row-start-8 col-start-4 flex items-center">
|
||||
<span className="text-content-success ml-4 text-sm py-1">
|
||||
<span className="text-content-success font-normal ml-4 text-sm py-1">
|
||||
Session completed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ToolCallTable: FC<ToolCallTableProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"flex flex-col gap-2 text-sm text-content-secondary",
|
||||
"flex flex-col gap-2 text-sm text-content-secondary font-normal",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between whitespace-nowrap">
|
||||
|
||||
@@ -55,8 +55,8 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="text-xs text-content-secondary">Input</div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
<div className="text-sm text-content-secondary">Input</div>
|
||||
<div className="text-sm text-content-secondary">
|
||||
{inputTokens}
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,8 +70,8 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="text-xs text-content-secondary">Output</div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
<div className="text-sm text-content-secondary">Output</div>
|
||||
<div className="text-sm text-content-secondary">
|
||||
{outputTokens}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ export const TokenBadges: FC<TokenBadgesProps> = ({
|
||||
<div className="text-content-primary text-sm mt-4">
|
||||
Token usage metadata
|
||||
</div>
|
||||
<pre className="mt-2 p-4 bg-surface-secondary rounded text-xs overflow-x-auto">
|
||||
<pre className="mt-2 mb-1 p-4 bg-surface-secondary rounded overflow-x-auto">
|
||||
<JsonPrettyPrinter input={JSON.stringify(tokenUsageMetadata)} />
|
||||
</pre>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user