124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { MessageSquare } from 'lucide-react';
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
import TranscriptAssistantBlock from './TranscriptAssistantBlock';
|
|
import TranscriptItem from './TranscriptItem';
|
|
import type { DebugTranscriptRow, DebugTranscriptTextRow, DebugTranscriptToolRow } from './types';
|
|
|
|
type AssistantRenderBlock = {
|
|
kind: 'assistant-block';
|
|
id: string;
|
|
message?: DebugTranscriptTextRow;
|
|
tools: DebugTranscriptToolRow[];
|
|
};
|
|
|
|
type TranscriptRenderItem =
|
|
| { kind: 'row'; id: string; row: DebugTranscriptRow }
|
|
| AssistantRenderBlock;
|
|
|
|
const getCorrelationKey = (row: Pick<DebugTranscriptRow, 'turnId' | 'utteranceId' | 'responseId'>) => {
|
|
if (row.responseId) return `response:${row.responseId}`;
|
|
if (row.turnId && row.utteranceId) return `turn:${row.turnId}:utterance:${row.utteranceId}`;
|
|
if (row.turnId) return `turn:${row.turnId}`;
|
|
if (row.utteranceId) return `utterance:${row.utteranceId}`;
|
|
return '';
|
|
};
|
|
|
|
const buildRenderItems = (messages: DebugTranscriptRow[]): TranscriptRenderItem[] => {
|
|
const items: TranscriptRenderItem[] = [];
|
|
const assistantBlocks = new Map<string, AssistantRenderBlock>();
|
|
|
|
messages.forEach((row) => {
|
|
if (row.kind === 'text' && row.role === 'assistant') {
|
|
const correlationKey = getCorrelationKey(row);
|
|
if (!correlationKey) {
|
|
items.push({ kind: 'row', id: row.id, row });
|
|
return;
|
|
}
|
|
|
|
const existingBlock = assistantBlocks.get(correlationKey);
|
|
if (existingBlock) {
|
|
existingBlock.message = row;
|
|
return;
|
|
}
|
|
|
|
const block: AssistantRenderBlock = {
|
|
kind: 'assistant-block',
|
|
id: `assistant-block:${correlationKey}`,
|
|
message: row,
|
|
tools: [],
|
|
};
|
|
assistantBlocks.set(correlationKey, block);
|
|
items.push(block);
|
|
return;
|
|
}
|
|
|
|
if (row.kind === 'tool') {
|
|
const correlationKey = getCorrelationKey(row);
|
|
if (!correlationKey) {
|
|
items.push({ kind: 'row', id: row.id, row });
|
|
return;
|
|
}
|
|
|
|
const existingBlock = assistantBlocks.get(correlationKey);
|
|
if (existingBlock) {
|
|
existingBlock.tools.push(row);
|
|
return;
|
|
}
|
|
|
|
const block: AssistantRenderBlock = {
|
|
kind: 'assistant-block',
|
|
id: `assistant-block:${correlationKey}`,
|
|
tools: [row],
|
|
};
|
|
assistantBlocks.set(correlationKey, block);
|
|
items.push(block);
|
|
return;
|
|
}
|
|
|
|
items.push({ kind: 'row', id: row.id, row });
|
|
});
|
|
|
|
return items;
|
|
};
|
|
|
|
const TranscriptList: React.FC<{
|
|
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
messages: DebugTranscriptRow[];
|
|
isLoading: boolean;
|
|
className?: string;
|
|
}> = ({ scrollRef, messages, isLoading, className = '' }) => {
|
|
const renderItems = useMemo(() => buildRenderItems(messages), [messages]);
|
|
|
|
return (
|
|
<div
|
|
ref={scrollRef}
|
|
className={cn(
|
|
'flex-1 overflow-y-auto overflow-x-hidden rounded-md border border-white/5 bg-black/20 p-2 min-h-0 custom-scrollbar',
|
|
className
|
|
)}
|
|
>
|
|
{messages.length === 0 && !isLoading ? (
|
|
<div className="flex h-full flex-col items-center justify-center space-y-3 text-muted-foreground/60">
|
|
<MessageSquare className="h-8 w-8 opacity-20" />
|
|
<p className="text-xs">鏆傛棤瀵硅瘽璁板綍</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 pb-4">
|
|
{renderItems.map((item) =>
|
|
item.kind === 'assistant-block' ? (
|
|
<TranscriptAssistantBlock key={item.id} message={item.message} tools={item.tools} />
|
|
) : (
|
|
<TranscriptItem key={item.id} row={item.row} />
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(TranscriptList);
|