fix(site): session threads feedback (#23945)

This commit is contained in:
Jeremy Ruppel
2026-04-02 08:51:31 -04:00
committed by GitHub
parent ed5c06f039
commit ca3ae3643d
9 changed files with 186 additions and 78 deletions
@@ -1,6 +1,6 @@
import type { FC } from "react";
import { useInfiniteQuery } from "react-query";
import { useNavigate, useParams } from "react-router";
import { useLocation, useNavigate, useParams } from "react-router";
import { infiniteSessionThreads } from "#/api/queries/aiBridge";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
@@ -13,6 +13,7 @@ const SessionThreadsPage: FC = () => {
const { permissions } = useAuthenticated();
const { entitlements } = useDashboard();
const navigate = useNavigate();
const location = useLocation();
const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions(
entitlements,
@@ -45,7 +46,17 @@ const SessionThreadsPage: FC = () => {
onFetchNextPage={sessionQuery.fetchNextPage}
isAISessionsEnabled={isEnabled}
isAISessionsEntitled={isEntitled}
onBackClicked={() => navigate(-1)}
onBackClicked={() => {
// location.key is "default" when the user navigated directly to
// this page (e.g. by refreshing or opening in a new tab). if there
// is a previous page in the history stack, navigate back. otherwise,
// navigate to the sessions list page without params
if (location.key === "default") {
navigate("/aibridge/sessions");
} else {
navigate(-1);
}
}}
/>
</RequirePermission>
);
@@ -16,6 +16,7 @@ import {
import { AIBridgeSetupAlert } from "../AIBridgeSetupAlert";
import { SessionSummaryTable } from "./SessionSummaryTable";
import { SessionTimeline } from "./SessionTimeline/SessionTimeline";
import { SessionTimelineSkeleton } from "./SessionTimeline/SessionTimelineSkeleton";
const SessionSummaryTooltip: FC<PropsWithChildren> = ({ children }) => (
<TooltipProvider>
@@ -118,7 +119,7 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
)}
</aside>
<main className="flex-1 min-w-0">
{session && (
{session ? (
<SessionTimeline
initiator={session.initiator}
threads={threads}
@@ -126,6 +127,8 @@ export const SessionThreadsPageView: FC<SessionThreadsPageViewProps> = ({
isFetchingNextPage={isFetchingNextPage}
onFetchNextPage={onFetchNextPage}
/>
) : (
loading && <SessionTimelineSkeleton />
)}
</main>
</div>
@@ -13,8 +13,6 @@ export const Short: Story = {
args: {
duration: 4_200,
toolCalls: 3,
inputTokens: 1234,
outputTokens: 567,
},
};
@@ -22,8 +20,6 @@ export const Long: Story = {
args: {
duration: 125_000,
toolCalls: 42,
inputTokens: 150_000,
outputTokens: 12_000,
},
};
@@ -31,8 +27,6 @@ export const SingleToolCall: Story = {
args: {
duration: 980,
toolCalls: 1,
inputTokens: 320,
outputTokens: 88,
},
};
@@ -40,7 +34,5 @@ export const NoToolCalls: Story = {
args: {
duration: 500,
toolCalls: 0,
inputTokens: 100,
outputTokens: 50,
},
};
@@ -1,21 +1,16 @@
import type { FC } from "react";
import { cn } from "#/utils/cn";
import { TokenBadges } from "../../TokenBadges";
import { roundDurationDisplay } from "../../utils";
interface AgenticLoopTableProps {
duration: number; // in seconds
toolCalls: number;
inputTokens: number;
outputTokens: number;
className?: string;
}
export const AgenticLoopTable: FC<AgenticLoopTableProps> = ({
duration,
toolCalls,
inputTokens,
outputTokens,
className,
}) => {
return (
@@ -25,10 +20,6 @@ export const AgenticLoopTable: FC<AgenticLoopTableProps> = ({
className,
)}
>
<div className="flex items-center justify-between h-6">
<span className="pr-4">In / out tokens</span>
<TokenBadges inputTokens={inputTokens} outputTokens={outputTokens} />
</div>
<div className="flex items-center justify-between h-6">
<span className="pr-4">Tool calls</span>
<span>{toolCalls}</span>
@@ -43,7 +43,7 @@ export const PromptTable: FC<PromptTableProps> = ({
className="text-right flex items-center justify-end"
title={formatDate(timestamp)}
>
<span className="block font-mono whitespace-nowrap truncate">
<span className="block font-mono text-xs whitespace-nowrap truncate">
{formatDate(timestamp)}
</span>
</dd>
@@ -30,14 +30,17 @@ import { AgenticLoopTable } from "./AgenticLoopTable";
import { PromptTable } from "./PromptTable";
import { ToolCallTable } from "./ToolCallTable";
const EXPANDABLE_COLLAPSE_HEIGHT = 50;
interface ExpandableTextProps {
maxHeight: number;
text: string;
className?: string;
}
const ExpandableText: FC<ExpandableTextProps> = ({ text, className }) => {
const ExpandableText: FC<ExpandableTextProps> = ({
maxHeight,
text,
className,
}) => {
const contentRef = useRef<HTMLParagraphElement>(null);
const [isExpandable, setIsExpandable] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
@@ -45,8 +48,8 @@ const ExpandableText: FC<ExpandableTextProps> = ({ text, className }) => {
useEffect(() => {
const el = contentRef.current;
if (!el) return;
setIsExpandable(el.scrollHeight > EXPANDABLE_COLLAPSE_HEIGHT);
}, []);
setIsExpandable(el.scrollHeight > maxHeight);
}, [maxHeight]);
return (
<div className="relative">
@@ -55,11 +58,11 @@ const ExpandableText: FC<ExpandableTextProps> = ({ text, className }) => {
style={
isExpandable && !isExpanded
? {
maxHeight: EXPANDABLE_COLLAPSE_HEIGHT,
maxHeight,
}
: undefined
}
className={cn(className, "overflow-hidden", isExpanded && "pb-9")}
className={cn(className, "overflow-scroll", isExpanded && "pb-9")}
>
{text}
</p>
@@ -161,6 +164,7 @@ const ThinkingBlock: FC<ThinkingBlockProps> = ({ text }) => (
<span className="font-mono ml-2 text-xs">Thinking...</span>
</div>
<ExpandableText
maxHeight={50}
text={text}
className="text-sm text-pretty font-normal m-0"
/>
@@ -210,8 +214,11 @@ const ToolCallBlock: FC<ToolCallBlockProps> = ({
outputTokens={outputTokens}
tokenUsageMetadata={tokenUsageMetadata}
/>
<pre className="bg-surface-secondary rounded-md m-4 p-4 text-sm font-mono text-content-primary overflow-x-auto m-0">
{tool} <JsonPrettyPrinter input={input} />
<pre className="flex gap-4 bg-surface-secondary rounded-md m-4 p-4 text-sm font-mono text-content-primary overflow-x-auto m-0">
<span>{tool}</span>
<span>
<JsonPrettyPrinter input={input} />
</span>
</pre>
</>
)}
@@ -298,7 +305,9 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
new Date(thread.ended_at ?? Date.now()).getTime() -
new Date(thread.started_at).getTime();
const toolCalls = thread.agentic_actions?.reduce(
const hasAgenticLoop = thread.agentic_actions.length > 0;
const toolCalls = thread.agentic_actions.reduce(
(count, action) => count + action.tool_calls.length,
0,
);
@@ -320,7 +329,7 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
</div>
{/* center column: prompt */}
<div className="flex-grow flex flex-col gap-1">
<div className="flex flex-col gap-1 mb-2 min-w-0 flex-1 w-full">
{thread.prompt && (
<>
<div className="text-sm text-content-secondary font-normal my-1 flex items-center gap-1">
@@ -352,9 +361,11 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
</Tooltip>
</TooltipProvider>
</div>
<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>
<ExpandableText
maxHeight={200}
text={thread.prompt}
className="text-sm text-content-secondary font-normal bg-surface-secondary leading-relaxed rounded-md p-3 m-0 text-pretty"
/>
</>
)}
</div>
@@ -369,50 +380,54 @@ const ThreadItem: FC<ThreadItemProps> = ({ thread, initiator }) => {
/>
</div>
<BracketConnector
firstRowHeight="60px"
contentClassName="border border-dashed rounded-md my-4"
>
{/* Agentic loop */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between">
<div>
<CollapseButton
isOpen={agenticLoopOpen}
onClick={() => setAgenticLoopOpen(!agenticLoopOpen)}
>
<span className="text-sm font-normal">Agentic loop</span>
</CollapseButton>
</div>
<AgenticLoopTable
className="lg:max-w-64 flex-1 my-3 mx-2"
duration={durationInMs}
toolCalls={toolCalls}
inputTokens={thread.token_usage.input_tokens}
outputTokens={thread.token_usage.output_tokens}
/>
</div>
{agenticLoopOpen && (
<>
{/* the little top rounded line above the thinking block */}
<div className="border-0 border-t border-r border-solid rounded-tr-lg w-[calc(1rem+1px)] h-[20px]">
{/* we need the 1px extra to line up with the left border on the other lines */}
{hasAgenticLoop ? (
<BracketConnector
firstRowHeight="60px"
contentClassName="border border-dashed rounded-md my-4"
>
{/* Agentic loop */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between">
<div>
<CollapseButton
isOpen={agenticLoopOpen}
onClick={() => setAgenticLoopOpen(!agenticLoopOpen)}
>
<span className="text-sm font-normal">Agentic loop</span>
</CollapseButton>
</div>
{/* Agentic actions */}
{thread.agentic_actions?.map((action, i) => (
<AgenticActionItem key={`${thread.id}-${i}`} action={action} />
))}
{/* Agentic loop completed block */}
<AgenticLoopCompletedBlock
inputTokens={thread.token_usage.input_tokens}
outputTokens={thread.token_usage.output_tokens}
<AgenticLoopTable
className="lg:max-w-64 flex-1 my-3 mx-2"
duration={durationInMs}
toolCalls={toolCalls}
/>
</>
)}
</BracketConnector>
</div>
{agenticLoopOpen && (
<>
{/* the little top rounded line above the thinking block */}
<div className="border-0 border-t border-r border-solid rounded-tr-lg w-[calc(1rem+1px)] h-[20px]">
{/* we need the 1px extra to line up with the left border on the other lines */}
</div>
{/* Agentic actions */}
{thread.agentic_actions?.map((action, i) => (
<AgenticActionItem key={`${thread.id}-${i}`} action={action} />
))}
{/* Agentic loop completed block */}
<AgenticLoopCompletedBlock
inputTokens={thread.token_usage.input_tokens}
outputTokens={thread.token_usage.output_tokens}
/>
</>
)}
</BracketConnector>
) : (
// if no agentic loop, we need a little spacing element to create
// the visual gap between threads
<div className="h-4" />
)}
</>
);
};
@@ -524,7 +539,7 @@ export const SessionTimeline: FC<SessionTimelineProps> = ({
<div className="row-start-4 col-start-3 border-0 border-l border-solid">
{/* vertical line */}
</div>
<div className="row-start-4 col-start-4 border-0 border-t border-dashed">
<div className="row-start-4 col-start-4 border-0 border-t border-dashed border-surface-green">
{/* horizontal border */}
</div>
<div className="row-start-4 col-start-6 border-0 border-r border-t border-dashed border-surface-green rounded-tr-lg size-4">
@@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { SessionTimelineSkeleton } from "./SessionTimelineSkeleton";
const meta: Meta<typeof SessionTimelineSkeleton> = {
title: "pages/AIBridgePage/SessionTimeline/SessionTimelineSkeleton",
component: SessionTimelineSkeleton,
};
export default meta;
type Story = StoryObj<typeof SessionTimelineSkeleton>;
export const Default: Story = {};
@@ -0,0 +1,84 @@
import type { FC } from "react";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { StatusIndicatorDot } from "#/components/StatusIndicator/StatusIndicator";
export const SessionTimelineSkeleton: FC = () => {
return (
<div className="relative">
<div className="grid grid-cols-[16px_1rem_1px_1fr_auto_16px]">
{/* row 1: session start */}
<div className="row-start-1 col-start-2 relative h-10 py-1">
<StatusIndicatorDot
variant="inactive"
className="absolute right-0 translate-x-1/2 translate-y-1/2"
/>
</div>
<div className="row-start-1 col-start-4 col-span-2 flex items-center h-10">
<span className="text-content-secondary font-normal ml-4 py-1 text-sm">
Session started
</span>
</div>
{/* row 2: vertical line */}
<div className="row-start-2 col-start-3 border-0 border-l border-solid" />
{/* row 3: space above timeline border */}
<div className="row-start-3 col-start-3 border-0 border-l border-t border-solid h-6" />
{/* row 4: top border */}
<div className="row-start-4 col-start-1 border-0 border-l border-t border-dashed border-surface-green rounded-tl-lg size-4" />
<div className="row-start-4 col-start-2 border-0 border-t border-dashed border-surface-green" />
<div className="row-start-4 col-start-3 border-0 border-l border-solid" />
<div className="row-start-4 col-start-4 border-0 border-t border-dashed border-surface-green" />
<div className="row-start-4 col-start-6 border-0 border-r border-t border-dashed border-surface-green rounded-tr-lg size-4" />
{/* row 5: skeleton thread cards */}
<div className="row-start-5 col-start-1 border-0 border-l border-dashed border-surface-green" />
<div className="row-start-5 col-start-2 col-span-4 flex flex-col gap-4 py-2">
{[0, 1, 2].map((i) => (
<div
key={i}
className="border border-solid rounded-md flex flex-col lg:flex-row gap-6 p-2"
>
{/* avatar + username */}
<div className="flex flex-row items-center gap-2">
<Skeleton className="size-6 rounded-full flex-shrink-0" />
<Skeleton className="h-4 w-20" />
</div>
{/* prompt */}
<div className="flex-grow flex flex-col gap-2">
<Skeleton className="h-3 w-12" />
<Skeleton className="h-16 w-full" />
</div>
{/* right-column details */}
<div className="flex flex-col gap-2 lg:w-64 flex-shrink-0">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-4/5" />
<Skeleton className="h-3 w-3/5" />
</div>
</div>
))}
</div>
<div className="row-start-5 col-start-6 border-0 border-r border-dashed border-surface-green" />
{/* row 6: bottom border */}
<div className="row-start-6 col-start-1 border-0 border-l border-b border-dashed border-surface-green rounded-bl-lg size-4" />
<div className="row-start-6 col-start-2 border-0 border-b border-dashed border-surface-green" />
<div className="row-start-6 col-start-3 border-0 border-l border-solid" />
<div className="row-start-6 col-start-4 col-span-2 border-0 border-b border-dashed border-surface-green" />
<div className="row-start-6 col-start-6 border-0 border-r border-b border-dashed border-surface-green rounded-br-lg size-4" />
{/* row 7: space below timeline border */}
<div className="row-start-7 col-start-3 border-0 border-l border-t border-solid h-4" />
{/* row 8: session end placeholder */}
<div className="row-start-8 col-start-2 relative">
<Skeleton className="size-2 rounded-full absolute right-0 translate-x-1/2 translate-y-1/2" />
</div>
<div className="row-start-8 col-start-4 flex items-center">
<Skeleton className="h-4 w-32 ml-4" />
</div>
</div>
</div>
);
};
@@ -39,7 +39,7 @@ export const ToolCallTable: FC<ToolCallTableProps> = ({
<div className="flex items-center justify-between">
<span className="pr-4">Started at</span>
<span
className="font-mono whitespace-nowrap truncate"
className="font-mono text-xs whitespace-nowrap truncate"
title={formatDate(timestamp)}
>
{formatDate(timestamp)}