Files
AI-VideoAssistant/web/components/debug-transcript/message-utils.ts
2026-03-13 07:11:48 +08:00

638 lines
14 KiB
TypeScript

import type {
DebugTranscriptRow,
DebugTranscriptTextRole,
DebugTranscriptTextRow,
DebugTranscriptToolRow,
DebugTranscriptToolStatus,
} from './types';
let rowCounter = 0;
const createRowId = (prefix: string) => `${prefix}_${Date.now()}_${++rowCounter}`;
const isTextRow = (
row: DebugTranscriptRow | undefined | null
): row is DebugTranscriptTextRow => row?.kind === 'text';
const isToolRow = (
row: DebugTranscriptRow | undefined | null
): row is DebugTranscriptToolRow => row?.kind === 'tool';
const findRowIndexById = (rows: DebugTranscriptRow[], rowId?: string | null) =>
rowId ? rows.findIndex((row) => row.id === rowId) : -1;
const findAssistantRowIndexByResponseId = (rows: DebugTranscriptRow[], responseId?: string) => {
if (!responseId) return -1;
return rows.findIndex(
(row) => isTextRow(row) && row.role === 'assistant' && row.responseId === responseId
);
};
const findLastAssistantCandidateIndex = (
rows: DebugTranscriptRow[],
responseId?: string
) => {
for (let i = rows.length - 1; i >= 0; i -= 1) {
const row = rows[i];
if (isTextRow(row) && row.role === 'assistant') {
if (responseId && row.responseId && row.responseId !== responseId) {
break;
}
return i;
}
if (isTextRow(row) && row.role === 'user') {
break;
}
}
return -1;
};
const createTextRow = ({
role,
text,
turnId,
utteranceId,
responseId,
ttfbMs,
isStreaming,
}: {
role: DebugTranscriptTextRole;
text: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
ttfbMs?: number;
isStreaming?: boolean;
}): DebugTranscriptTextRow => ({
kind: 'text',
id: createRowId(role),
role,
text,
...(turnId ? { turnId } : {}),
...(utteranceId ? { utteranceId } : {}),
...(responseId ? { responseId } : {}),
...(typeof ttfbMs === 'number' ? { ttfbMs } : {}),
...(typeof isStreaming === 'boolean' ? { isStreaming } : {}),
});
const createToolRow = ({
toolCallId,
toolName,
toolDisplayName,
executor,
turnId,
utteranceId,
responseId,
source,
status,
args,
result,
error,
rawCall,
rawResult,
}: {
toolCallId: string;
toolName: string;
toolDisplayName: string;
executor: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
source?: string;
status: DebugTranscriptToolStatus;
args?: unknown;
result?: unknown;
error?: unknown;
rawCall?: unknown;
rawResult?: unknown;
}): DebugTranscriptToolRow => ({
kind: 'tool',
id: toolCallId || createRowId('tool'),
toolCallId,
toolName,
toolDisplayName,
executor,
...(turnId ? { turnId } : {}),
...(utteranceId ? { utteranceId } : {}),
...(responseId ? { responseId } : {}),
...(source ? { source } : {}),
status,
...(args !== undefined ? { args } : {}),
...(result !== undefined ? { result } : {}),
...(error !== undefined ? { error } : {}),
...(rawCall !== undefined ? { rawCall } : {}),
...(rawResult !== undefined ? { rawResult } : {}),
});
export const resetTranscriptRows = (): DebugTranscriptRow[] => [];
export const appendTextRow = (
rows: DebugTranscriptRow[],
{
role,
text,
turnId,
utteranceId,
responseId,
ttfbMs,
isStreaming,
}: {
role: DebugTranscriptTextRole;
text: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
ttfbMs?: number;
isStreaming?: boolean;
}
): DebugTranscriptRow[] => [
...rows,
createTextRow({ role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming }),
];
export const appendNoticeRow = (rows: DebugTranscriptRow[], text: string) =>
appendTextRow(rows, { role: 'notice', text, isStreaming: false });
export const updateUserDraftRow = (
rows: DebugTranscriptRow[],
{
draftRowId,
text,
turnId,
utteranceId,
}: {
draftRowId?: string | null;
text: string;
turnId?: string;
utteranceId?: string;
}
): { rows: DebugTranscriptRow[]; draftRowId: string } => {
const rowIndex = findRowIndexById(rows, draftRowId);
if (rowIndex !== -1) {
const row = rows[rowIndex];
if (isTextRow(row) && row.role === 'user') {
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
text,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
isStreaming: true,
};
return { rows: nextRows, draftRowId: row.id };
}
}
const nextRow = createTextRow({
role: 'user',
text,
turnId,
utteranceId,
isStreaming: true,
});
return {
rows: [...rows, nextRow],
draftRowId: nextRow.id,
};
};
export const finalizeUserDraftRow = (
rows: DebugTranscriptRow[],
{
draftRowId,
text,
turnId,
utteranceId,
}: {
draftRowId?: string | null;
text: string;
turnId?: string;
utteranceId?: string;
}
): { rows: DebugTranscriptRow[]; draftRowId: null } => {
const rowIndex = findRowIndexById(rows, draftRowId);
if (rowIndex !== -1) {
const row = rows[rowIndex];
if (isTextRow(row) && row.role === 'user') {
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
text: text || row.text,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
isStreaming: false,
};
return { rows: nextRows, draftRowId: null };
}
}
if (!text) {
return { rows, draftRowId: null };
}
return {
rows: appendTextRow(rows, {
role: 'user',
text,
turnId,
utteranceId,
isStreaming: false,
}),
draftRowId: null,
};
};
export const updateAssistantDeltaRow = (
rows: DebugTranscriptRow[],
{
draftRowId,
delta,
turnId,
utteranceId,
responseId,
ttfbMs,
}: {
draftRowId?: string | null;
delta: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
ttfbMs?: number;
}
): { rows: DebugTranscriptRow[]; draftRowId: string } => {
let rowIndex = findRowIndexById(rows, draftRowId);
if (
rowIndex !== -1 &&
(!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant')
) {
rowIndex = -1;
}
if (rowIndex === -1) {
rowIndex = findAssistantRowIndexByResponseId(rows, responseId);
}
if (rowIndex === -1) {
rowIndex = findLastAssistantCandidateIndex(rows, responseId);
}
if (rowIndex === -1) {
const lastRow = rows[rows.length - 1];
if (
isTextRow(lastRow) &&
lastRow.role === 'assistant' &&
lastRow.text === delta &&
lastRow.responseId === responseId
) {
return {
rows,
draftRowId: lastRow.id,
};
}
const nextRow = createTextRow({
role: 'assistant',
text: delta,
turnId,
utteranceId,
responseId,
ttfbMs,
isStreaming: true,
});
return {
rows: [...rows, nextRow],
draftRowId: nextRow.id,
};
}
const row = rows[rowIndex];
if (!isTextRow(row) || row.role !== 'assistant') {
return {
rows,
draftRowId: draftRowId || createRowId('assistant'),
};
}
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
text: row.text + delta,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
responseId: row.responseId || responseId,
ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs,
isStreaming: true,
};
return {
rows: nextRows,
draftRowId: row.id,
};
};
export const finalizeAssistantTextRow = (
rows: DebugTranscriptRow[],
{
draftRowId,
text,
turnId,
utteranceId,
responseId,
ttfbMs,
}: {
draftRowId?: string | null;
text: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
ttfbMs?: number;
}
): { rows: DebugTranscriptRow[]; draftRowId: null } => {
let rowIndex = findRowIndexById(rows, draftRowId);
if (
rowIndex !== -1 &&
(!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant')
) {
rowIndex = -1;
}
if (rowIndex === -1) {
rowIndex = findAssistantRowIndexByResponseId(rows, responseId);
}
if (rowIndex === -1) {
rowIndex = findLastAssistantCandidateIndex(rows, responseId);
}
if (rowIndex !== -1) {
const row = rows[rowIndex];
if (isTextRow(row) && row.role === 'assistant') {
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
text: text || row.text,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
responseId: row.responseId || responseId,
ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs,
isStreaming: false,
};
return {
rows: nextRows,
draftRowId: null,
};
}
}
if (!text) {
return { rows, draftRowId: null };
}
const lastRow = rows[rows.length - 1];
if (
isTextRow(lastRow) &&
lastRow.role === 'assistant' &&
(!responseId || !lastRow.responseId || lastRow.responseId === responseId)
) {
if (lastRow.text === text) {
return { rows, draftRowId: null };
}
if (text.startsWith(lastRow.text) || lastRow.text.startsWith(text)) {
const nextRows = [...rows];
nextRows[nextRows.length - 1] = {
...lastRow,
text,
turnId: lastRow.turnId || turnId,
utteranceId: lastRow.utteranceId || utteranceId,
responseId: lastRow.responseId || responseId,
ttfbMs: typeof lastRow.ttfbMs === 'number' ? lastRow.ttfbMs : ttfbMs,
isStreaming: false,
};
return {
rows: nextRows,
draftRowId: null,
};
}
}
return {
rows: appendTextRow(rows, {
role: 'assistant',
text,
turnId,
utteranceId,
responseId,
ttfbMs,
isStreaming: false,
}),
draftRowId: null,
};
};
export const attachAssistantTtfb = (
rows: DebugTranscriptRow[],
{
responseId,
ttfbMs,
}: {
responseId?: string;
ttfbMs: number;
}
) => {
const rowIndex =
findAssistantRowIndexByResponseId(rows, responseId) !== -1
? findAssistantRowIndexByResponseId(rows, responseId)
: findLastAssistantCandidateIndex(rows, responseId);
if (rowIndex === -1) {
return rows;
}
const row = rows[rowIndex];
if (!isTextRow(row) || row.role !== 'assistant') {
return rows;
}
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
ttfbMs,
};
return nextRows;
};
export const trimInterruptedResponseRows = (
rows: DebugTranscriptRow[],
responseId?: string
) => {
if (!responseId) return rows;
return rows.filter((row) => row.responseId !== responseId);
};
export const upsertToolCallRow = (
rows: DebugTranscriptRow[],
{
toolCallId,
toolName,
toolDisplayName,
executor,
turnId,
utteranceId,
responseId,
args,
rawCall,
}: {
toolCallId: string;
toolName: string;
toolDisplayName: string;
executor: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
args?: unknown;
rawCall?: unknown;
}
) => {
const rowIndex = rows.findIndex(
(row) => isToolRow(row) && row.toolCallId === toolCallId
);
if (rowIndex === -1) {
return [
...rows,
createToolRow({
toolCallId,
toolName,
toolDisplayName,
executor,
turnId,
utteranceId,
responseId,
status: 'pending',
args,
rawCall,
}),
];
}
const row = rows[rowIndex];
if (!isToolRow(row)) {
return rows;
}
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
toolName,
toolDisplayName,
executor,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
responseId: row.responseId || responseId,
status: 'pending',
args: args !== undefined ? args : row.args,
rawCall: rawCall !== undefined ? rawCall : row.rawCall,
};
return nextRows;
};
export const normalizeToolStatus = (
statusCode?: number,
statusMessage?: string
): DebugTranscriptToolStatus => {
if (statusCode === 504 || String(statusMessage || '').toLowerCase().includes('timeout')) {
return 'timeout';
}
if (typeof statusCode === 'number' && statusCode >= 200 && statusCode < 300) {
return 'success';
}
return 'error';
};
export const resolveToolResultRow = (
rows: DebugTranscriptRow[],
{
toolCallId,
toolName,
toolDisplayName,
executor,
turnId,
utteranceId,
responseId,
source,
status,
args,
result,
error,
rawCall,
rawResult,
}: {
toolCallId: string;
toolName: string;
toolDisplayName: string;
executor?: string;
turnId?: string;
utteranceId?: string;
responseId?: string;
source?: string;
status: DebugTranscriptToolStatus;
args?: unknown;
result?: unknown;
error?: unknown;
rawCall?: unknown;
rawResult?: unknown;
}
) => {
const rowIndex = rows.findIndex(
(row) => isToolRow(row) && row.toolCallId === toolCallId
);
if (rowIndex === -1) {
return [
...rows,
createToolRow({
toolCallId,
toolName,
toolDisplayName,
executor: executor || 'server',
turnId,
utteranceId,
responseId,
source,
status,
args,
result,
error,
rawCall,
rawResult,
}),
];
}
const row = rows[rowIndex];
if (!isToolRow(row)) {
return rows;
}
const nextRows = [...rows];
nextRows[rowIndex] = {
...row,
toolName: toolName || row.toolName,
toolDisplayName: toolDisplayName || row.toolDisplayName,
executor: executor || row.executor,
turnId: row.turnId || turnId,
utteranceId: row.utteranceId || utteranceId,
responseId: row.responseId || responseId,
source: source || row.source,
status,
args: args !== undefined ? args : row.args,
result: result !== undefined ? result : row.result,
error,
rawCall: rawCall !== undefined ? rawCall : row.rawCall,
rawResult: rawResult !== undefined ? rawResult : row.rawResult,
};
return nextRows;
};