Skip to content

Commit 1bf5e2f

Browse files
committed
fix: address PR review feedback for transcript conversation UI
- Gate route prefetch on TracingService feature support - Remove unnecessary useMemo on transcript list (data changes per refresh) - Replace custom empty state with shared Empty compound component - Extract formatProtoTimestamp, formatProtoTime, formatProtoDuration, formatTokenCount utilities to keep JSX dumb - Replace all inline proto timestamp/duration/token formatting with utils
1 parent 6f7914b commit 1bf5e2f

4 files changed

Lines changed: 78 additions & 43 deletions

File tree

frontend/src/components/pages/agents/details/ai-agent-transcripts-tab.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
*/
1111

1212
import { getRouteApi, useNavigate } from '@tanstack/react-router';
13+
import {
14+
formatProtoDuration,
15+
formatProtoTimestamp,
16+
formatTokenCount,
17+
} from 'components/pages/transcripts/utils/transcript-formatters';
1318
import { Button } from 'components/redpanda-ui/components/button';
1419
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'components/redpanda-ui/components/card';
20+
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from 'components/redpanda-ui/components/empty';
1521
import { Input } from 'components/redpanda-ui/components/input';
1622
import {
1723
Select,
@@ -22,14 +28,12 @@ import {
2228
} from 'components/redpanda-ui/components/select';
2329
import { Spinner } from 'components/redpanda-ui/components/spinner';
2430
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'components/redpanda-ui/components/table';
25-
import { AlertCircle, Inbox, RefreshCw, Search } from 'lucide-react';
31+
import { AlertCircle, RefreshCw, Search } from 'lucide-react';
2632
import { TranscriptStatus } from 'protogen/redpanda/api/dataplane/v1alpha3/transcript_pb';
2733
import { type ChangeEvent, useMemo, useState } from 'react';
2834
import { useListTranscriptsQuery } from 'react-query/api/transcript';
2935

3036
import { ConversationStatusBadge } from './conversation-status-badge';
31-
import { durationMs, timestampDate } from '@bufbuild/protobuf/wkt';
32-
import { formatDuration as formatDurationMs, formatTimestamp as formatTimestampMs } from 'components/pages/transcripts/utils/transcript-formatters';
3337

3438
const routeApi = getRouteApi('/agents/$id/');
3539

@@ -61,7 +65,7 @@ export const AIAgentTranscriptsTab = () => {
6165

6266
const { data, isLoading, isFetching, error, dataUpdatedAt, refetch } = useListTranscriptsQuery({ agentId: id });
6367

64-
const transcripts = useMemo(() => data?.transcripts ?? [], [data?.transcripts]);
68+
const transcripts = data?.transcripts ?? [];
6569

6670
const filteredTranscripts = useMemo(() => {
6771
const query = searchQuery.toLowerCase();
@@ -136,12 +140,16 @@ export const AIAgentTranscriptsTab = () => {
136140
</CardHeader>
137141
<CardContent className="p-0">
138142
{filteredTranscripts.length === 0 ? (
139-
<div className="flex flex-col items-center justify-center gap-3 py-12">
140-
<Inbox className="h-12 w-12 text-muted-foreground/40" strokeWidth={1.5} />
141-
<p className="text-muted-foreground text-sm">
142-
{transcripts.length > 0 ? 'No transcripts match your search criteria' : 'No transcripts yet'}
143-
</p>
144-
</div>
143+
<Empty>
144+
<EmptyHeader>
145+
<EmptyTitle>{transcripts.length > 0 ? 'No matching transcripts' : 'No transcripts yet'}</EmptyTitle>
146+
<EmptyDescription>
147+
{transcripts.length > 0
148+
? 'No transcripts match your search criteria'
149+
: 'Transcripts will appear here once this agent processes conversations'}
150+
</EmptyDescription>
151+
</EmptyHeader>
152+
</Empty>
145153
) : (
146154
<Table variant="simple">
147155
<TableHeader>
@@ -170,17 +178,15 @@ export const AIAgentTranscriptsTab = () => {
170178
{transcript.conversationId}
171179
</TableCell>
172180
<TableCell className="text-muted-foreground text-sm">
173-
{transcript.startTime ? formatTimestampMs(timestampDate(transcript.startTime).getTime()) : '—'}
181+
{formatProtoTimestamp(transcript.startTime)}
174182
</TableCell>
175-
<TableCell className="font-mono text-sm">{transcript.duration ? formatDurationMs(durationMs(transcript.duration)) : '—'}</TableCell>
183+
<TableCell className="font-mono text-sm">{formatProtoDuration(transcript.duration)}</TableCell>
176184
<TableCell className="text-center">{transcript.turnCount}</TableCell>
177185
<TableCell>
178186
<ConversationStatusBadge status={transcript.status} />
179187
</TableCell>
180188
<TableCell className="text-right font-mono text-sm">
181-
{transcript.usage?.totalTokens != null
182-
? Number(transcript.usage.totalTokens).toLocaleString()
183-
: '—'}
189+
{formatTokenCount(transcript.usage?.totalTokens)}
184190
</TableCell>
185191
</TableRow>
186192
))}

frontend/src/components/pages/agents/details/conversation-detail-page.tsx

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

12-
import { durationMs, timestampDate } from '@bufbuild/protobuf/wkt';
12+
import { durationMs } from '@bufbuild/protobuf/wkt';
1313
import { getRouteApi, useNavigate } from '@tanstack/react-router';
14+
import {
15+
formatProtoDuration,
16+
formatProtoTime,
17+
formatProtoTimestamp,
18+
formatTokenCount,
19+
} from 'components/pages/transcripts/utils/transcript-formatters';
1420
import { Button } from 'components/redpanda-ui/components/button';
1521
import { Card, CardContent } from 'components/redpanda-ui/components/card';
1622
import { Spinner } from 'components/redpanda-ui/components/spinner';
@@ -41,7 +47,6 @@ import { useGetTranscriptQuery } from 'react-query/api/transcript';
4147
import { uiState } from 'state/ui-state';
4248

4349
import { ConversationStatusBadge } from './conversation-status-badge';
44-
import { formatDuration as formatDurationMs, formatTimestamp as formatTimestampMs } from 'components/pages/transcripts/utils/transcript-formatters';
4550

4651
// -- Helpers --
4752

@@ -249,9 +254,7 @@ const ThreeColumnView = ({ interactions, systemPrompt }: { systemPrompt: string;
249254
const llmCalls = interaction.agentResponses.filter((r) => r.role === TranscriptTurnRole.ASSISTANT).length;
250255
const toolCallCount = allToolCalls.length;
251256

252-
const userTimestamp = interaction.userInput?.timestamp
253-
? new Date(Number(interaction.userInput.timestamp.seconds) * 1000).toLocaleTimeString()
254-
: '—';
257+
const userTimestamp = formatProtoTime(interaction.userInput?.timestamp);
255258

256259
return (
257260
<Card
@@ -273,15 +276,13 @@ const ThreeColumnView = ({ interactions, systemPrompt }: { systemPrompt: string;
273276
{!isReconstructed && (
274277
<div className="flex items-center gap-1 text-muted-foreground text-xs">
275278
<Zap className="size-3" />
276-
<span className="font-mono">{totalTokens.toLocaleString()}</span>
279+
<span className="font-mono">{formatTokenCount(totalTokens)}</span>
277280
</div>
278281
)}
279282
{!isReconstructed && (
280283
<div className="flex items-center gap-1 text-muted-foreground text-xs">
281284
<Clock className="size-3" />
282-
<span className="font-mono">
283-
{endToEndMs > 0 ? `${(endToEndMs / 1000).toFixed(1)}s` : '—'}
284-
</span>
285+
<span className="font-mono">{endToEndMs > 0 ? `${(endToEndMs / 1000).toFixed(1)}s` : '—'}</span>
285286
</div>
286287
)}
287288
</div>
@@ -354,9 +355,7 @@ const ThreeColumnView = ({ interactions, systemPrompt }: { systemPrompt: string;
354355

355356
<div className="flex items-center justify-between">
356357
<span className="text-muted-foreground">Latency</span>
357-
<span className="font-mono">
358-
{endToEndMs > 0 ? `${(endToEndMs / 1000).toFixed(1)}s` : '—'}
359-
</span>
358+
<span className="font-mono">{endToEndMs > 0 ? `${(endToEndMs / 1000).toFixed(1)}s` : '—'}</span>
360359
</div>
361360

362361
<div className="flex items-center justify-between">
@@ -369,7 +368,7 @@ const ThreeColumnView = ({ interactions, systemPrompt }: { systemPrompt: string;
369368
<div className="flex items-center justify-between">
370369
<span className="text-muted-foreground">Tokens</span>
371370
<span className="font-mono">
372-
{totalInputTokens.toLocaleString()} in / {totalOutputTokens.toLocaleString()} out
371+
{formatTokenCount(totalInputTokens)} in / {formatTokenCount(totalOutputTokens)} out
373372
</span>
374373
</div>
375374

@@ -473,17 +472,11 @@ const ChatView = ({ systemPrompt, turns }: { systemPrompt: string; turns: Transc
473472
>
474473
<p className="whitespace-pre-wrap text-sm leading-relaxed">{turn.content}</p>
475474
<div className="mt-2 flex items-center gap-3 text-muted-foreground text-xs">
476-
{!turn.isReconstructed && (
477-
<span>
478-
{turn.timestamp
479-
? new Date(Number(turn.timestamp.seconds) * 1000).toLocaleTimeString()
480-
: ''}
481-
</span>
482-
)}
483-
{!isUser && !turn.isReconstructed && Boolean(turn.latency) && (
475+
{!turn.isReconstructed && <span>{formatProtoTime(turn.timestamp)}</span>}
476+
{!(isUser || turn.isReconstructed) && Boolean(turn.latency) && (
484477
<span className="flex items-center gap-1">
485478
<Clock className="size-3" />
486-
{formatDurationMs(durationMs(turn.latency!))}
479+
{formatProtoDuration(turn.latency)}
487480
</span>
488481
)}
489482
</div>
@@ -567,10 +560,10 @@ export const ConversationDetailPage = () => {
567560
{summary ? <ConversationStatusBadge status={summary.status} /> : null}
568561
</div>
569562
<div className="mt-1 flex items-center gap-4 text-muted-foreground text-xs">
570-
<span>{summary?.startTime ? formatTimestampMs(timestampDate(summary.startTime).getTime()) : '—'}</span>
563+
<span>{formatProtoTimestamp(summary?.startTime)}</span>
571564
<span className="flex items-center gap-1">
572565
<Clock className="size-3" />
573-
{summary?.duration ? formatDurationMs(durationMs(summary.duration)) : '—'}
566+
{formatProtoDuration(summary?.duration)}
574567
</span>
575568
<span>{summary?.turnCount ?? 0} turns</span>
576569
</div>
@@ -582,9 +575,7 @@ export const ConversationDetailPage = () => {
582575
{summary?.usage ? (
583576
<div className="flex items-center gap-1 rounded-md bg-muted px-2 py-1">
584577
<Zap className="size-3 text-muted-foreground" />
585-
<span className="font-medium font-mono text-xs">
586-
{Number(summary.usage.totalTokens).toLocaleString()}
587-
</span>
578+
<span className="font-medium font-mono text-xs">{formatTokenCount(summary.usage.totalTokens)}</span>
588579
</div>
589580
) : null}
590581

frontend/src/components/pages/transcripts/utils/transcript-formatters.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

12+
import type { Duration, Timestamp } from '@bufbuild/protobuf/wkt';
13+
import { durationMs, timestampDate } from '@bufbuild/protobuf/wkt';
1214
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
1315
import { getTextPreview, truncateWithEllipsis } from 'utils/string';
1416

@@ -93,3 +95,35 @@ export const formatJsonContent = (content: string, truncate = false): string =>
9395
return truncate ? truncateContent(content) : content;
9496
}
9597
};
98+
99+
/** Formats a protobuf Timestamp as relative time (e.g. "5 minutes ago"), or '—' if absent. */
100+
export const formatProtoTimestamp = (ts?: Timestamp): string => {
101+
if (!ts) {
102+
return '—';
103+
}
104+
return formatTimestamp(timestampDate(ts).getTime());
105+
};
106+
107+
/** Formats a protobuf Timestamp as a local time string (e.g. "2:30:45 PM"), or '—' if absent. */
108+
export const formatProtoTime = (ts?: Timestamp): string => {
109+
if (!ts) {
110+
return '—';
111+
}
112+
return timestampDate(ts).toLocaleTimeString();
113+
};
114+
115+
/** Formats a protobuf Duration to a human-readable string (e.g. "1.50s"), or '—' if absent. */
116+
export const formatProtoDuration = (d?: Duration): string => {
117+
if (!d) {
118+
return '—';
119+
}
120+
return formatDuration(durationMs(d));
121+
};
122+
123+
/** Formats a token count with thousands separators (e.g. "1,234"), or '—' if absent. */
124+
export const formatTokenCount = (tokens?: bigint | number | null): string => {
125+
if (tokens === null || tokens === undefined) {
126+
return '—';
127+
}
128+
return Number(tokens).toLocaleString();
129+
};

frontend/src/routes/agents/$id/transcripts/$conversationId.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@ import { createFileRoute } from '@tanstack/react-router';
1515
import { ConversationDetailPage } from 'components/pages/agents/details/conversation-detail-page';
1616
import { GetTranscriptRequestSchema } from 'protogen/redpanda/api/dataplane/v1alpha3/transcript_pb';
1717
import { getTranscript } from 'protogen/redpanda/api/dataplane/v1alpha3/transcript-TranscriptService_connectquery';
18+
import { useSupportedFeaturesStore } from 'state/supported-features';
1819

1920
export const Route = createFileRoute('/agents/$id/transcripts/$conversationId')({
2021
staticData: {
2122
title: 'Conversation',
2223
},
2324
loader: ({ context: { queryClient, dataplaneTransport }, params: { id, conversationId } }) => {
25+
if (!useSupportedFeaturesStore.getState().tracingService) {
26+
return;
27+
}
2428
// Prefetch without blocking — component handles loading/error states
2529
queryClient.prefetchQuery(
2630
createQueryOptions(getTranscript, create(GetTranscriptRequestSchema, { agentId: id, conversationId }), {
2731
transport: dataplaneTransport,
28-
}),
32+
})
2933
);
3034
},
3135
component: ConversationDetailPage,

0 commit comments

Comments
 (0)