UNPKG

@kaifronsdal/transcript-viewer

Version:

A web-based viewer for AI conversation transcripts with rollback support

305 lines (263 loc) 10.4 kB
import type { Message, MessageWithUIProps, TranscriptEvent, Edit, ToolCreationEvent, Events } from './types'; export interface ConversationColumn { id: string; title: string; messages: MessageWithUIProps[]; editNumber: number; } export interface SharedMessage { messageIndex: number; editNumber: number; isShared: boolean; } // Extract all unique views from transcript events export function extractAvailableViews(events: Events[]): string[] { console.log('🔍 [DEBUG] extractAvailableViews called with', events.length, 'events'); const viewSet = new Set<string>(); // Filter for TranscriptEvent types const transcriptEvents = events.filter((event): event is TranscriptEvent => event.type === 'transcript_event'); console.log('📋 [DEBUG] Found', transcriptEvents.length, 'transcript events'); for (const event of transcriptEvents) { if (Array.isArray(event.view)) { event.view.forEach(view => viewSet.add(view)); } else { viewSet.add(event.view); } } const result = Array.from(viewSet).sort(); console.log('✅ [DEBUG] extractAvailableViews returning:', result); return result; } // Check if an event applies to a specific view function eventAppliesToView(event: TranscriptEvent, targetView: string): boolean { if (Array.isArray(event.view)) { return event.view.includes(targetView); } return event.view === targetView; } // Parse events from the new transcript format with careful rollback handling export function parseTranscriptEvents(events: Events[], view: string, showApiFailures: boolean = false): ConversationColumn[] { console.log('🔄 [DEBUG] parseTranscriptEvents called:', { eventsLength: events.length, view, showApiFailures }); const columns: ConversationColumn[] = []; let currentMessages: MessageWithUIProps[] = []; let editNumber = 0; // Track the current column we're building let currentColumnMessages: MessageWithUIProps[] = []; // Filter for TranscriptEvent types const transcriptEvents = events.filter((event): event is TranscriptEvent => event.type === 'transcript_event'); console.log('📋 [DEBUG] Filtered transcript events:', transcriptEvents.length); // Filter events for the specified view const relevantEvents = transcriptEvents.filter(event => eventAppliesToView(event, view)); console.log('🎯 [DEBUG] Relevant events for view', view + ':', relevantEvents.length); for (let i = 0; i < relevantEvents.length; i++) { const event = relevantEvents[i]; editNumber++; if (event.edit.operation === 'add' && event.edit.message) { const message = event.edit.message; // Skip API failures if not showing them if (message.type === 'api_failure' && !showApiFailures) { continue; } // Mark message as shared/not shared and add view information const messageWithSharedFlag: MessageWithUIProps = { ...message, isShared: false, // Will be updated if this message becomes shared viewSource: event.view, // Add the view information from the event eventId: event.id // Track the event ID that created this message }; currentMessages.push(messageWithSharedFlag); currentColumnMessages.push(messageWithSharedFlag); } else if (event.edit.operation === 'rollback' && (event.edit.to_id || event.edit.count)) { // Create a column with messages up to this point if (currentColumnMessages.length > 0) { columns.push({ id: `column-${columns.length}`, title: columns.length === 0 ? 'Original' : `Branch ${columns.length}`, messages: [...currentColumnMessages], editNumber: editNumber - 1 }); } // Determine rollback target let rollbackIndex = currentMessages.length; if (event.edit.to_id) { // Find the index of the message with the specified ID const targetIndex = currentMessages.findIndex(msg => msg.id === event.edit.to_id); if (targetIndex !== -1) { // Rollback to just after this message (keep the message with to_id) rollbackIndex = targetIndex + 1; } } else if (event.edit.count) { // Fallback to count-based rollback const rollbackCount = Math.min(event.edit.count, currentMessages.length); rollbackIndex = currentMessages.length - rollbackCount; } // Rollback by keeping only messages up to the rollback index currentMessages = currentMessages.slice(0, rollbackIndex); // Start a new column with ALL the remaining messages, marked as shared if (currentMessages.length > 0) { currentColumnMessages = currentMessages.map(msg => ({ ...msg, isShared: true })); } else { currentColumnMessages = []; } } else if (event.edit.operation === 'reset') { // Create a column with messages up to this point if (currentColumnMessages.length > 0) { columns.push({ id: `column-${columns.length}`, title: columns.length === 0 ? 'Original' : `Branch ${columns.length}`, messages: [...currentColumnMessages], editNumber: editNumber - 1 }); } // Reset to new messages const newMessages = event.edit.new_messages || []; const messagesWithFlags: MessageWithUIProps[] = newMessages.map(msg => ({ ...msg, isShared: false, viewSource: event.view, eventId: event.id // Track the event ID for reset messages too })); currentMessages = messagesWithFlags; currentColumnMessages = [...currentMessages]; } } // Add the final column if it has content if (currentColumnMessages.length > 0) { console.log('➕ [DEBUG] Adding final column with', currentColumnMessages.length, 'messages'); columns.push({ id: `column-${columns.length}`, title: columns.length === 0 ? 'Original' : `Branch ${columns.length}`, messages: [...currentColumnMessages], editNumber: editNumber }); } console.log('✅ [DEBUG] parseTranscriptEvents completed, returning', columns.length, 'columns'); return columns; } // // Legacy function for backward compatibility // export function parseTranscriptEdits(edits: any[]): ConversationColumn[] { // const columns: ConversationColumn[] = []; // let currentMessages: MessageWithUIProps[] = []; // let editNumber = 0; // // Track the current column we're building // let currentColumnMessages: MessageWithUIProps[] = []; // for (let i = 0; i < edits.length; i++) { // const edit = edits[i]; // editNumber++; // if (edit.operation === 'add' && edit.message) { // // Add message to current conversation // const messageId = `edit-${editNumber}`; // const message: MessageWithUIProps = { // type: edit.message.type as any, // content: edit.message.content, // id: messageId, // name: edit.message.name || 'unknown', // Use name field from v3 // metadata: {}, // tool_calls: (edit.message as any).tool_calls || undefined, // tool_call_id: (edit.message as any).tool_call_id || undefined, // status: (edit.message as any).status || undefined, // isShared: false // Will be updated if this message becomes shared // }; // currentMessages.push(message); // currentColumnMessages.push(message); // } else if (edit.operation === 'rollback' && edit.count) { // // Create a column with messages up to this point // if (currentColumnMessages.length > 0) { // columns.push({ // id: `column-${columns.length}`, // title: columns.length === 0 ? 'Original' : `Branch ${columns.length}`, // messages: [...currentColumnMessages], // editNumber: editNumber - 1 // }); // } // // Rollback by removing the last N messages // currentMessages = currentMessages.slice(0, -edit.count); // // Start a new column with ALL the remaining messages, marked as shared // if (currentMessages.length > 0) { // currentColumnMessages = currentMessages.map(msg => ({ // ...msg, // isShared: true // })); // } else { // currentColumnMessages = []; // } // } // } // // Add the final column if it has content // if (currentColumnMessages.length > 0) { // columns.push({ // id: `column-${columns.length}`, // title: columns.length === 0 ? 'Original' : `Branch ${columns.length}`, // messages: [...currentColumnMessages], // editNumber: editNumber // }); // } // return columns; // } export function getMessageTypeColor(type: string): string { switch (type.toLowerCase()) { case 'system': return 'bg-blue-50 border-blue-200 text-gray-900 dark:text-gray-100'; case 'user': return 'bg-white border-gray-200 text-gray-900 dark:text-gray-100'; case 'assistant': return 'bg-green-50 border-green-200 text-gray-900 dark:text-gray-100'; case 'tool': return 'bg-yellow-50 border-yellow-200 text-gray-900 dark:text-gray-100'; case 'api_failure': return 'bg-red-50 border-red-200 text-gray-900 dark:text-gray-100'; default: return 'bg-gray-50 border-gray-200 text-gray-900 dark:text-gray-100'; } } export function getMessageBackgroundColor(type: string): string { switch (type.toLowerCase()) { case 'system': return 'bg-blue-50 dark:bg-blue-950/30'; case 'user': return 'bg-purple-50 dark:bg-purple-950/30'; case 'assistant': return 'bg-green-50 dark:bg-green-950/30'; case 'tool': return 'bg-orange-50 dark:bg-orange-950/30'; case 'api_failure': return 'bg-red-50 dark:bg-red-950/30'; default: return 'bg-gray-50 dark:bg-gray-800'; } } export function getMessageBorderColor(type: string): string { switch (type.toLowerCase()) { case 'system': return 'border-blue-200 dark:border-blue-700 border-l-blue-500 dark:border-l-blue-400'; case 'user': return 'border-purple-200 dark:border-purple-700 border-l-purple-500 dark:border-l-purple-400'; case 'assistant': return 'border-green-200 dark:border-green-700 border-l-green-500 dark:border-l-green-400'; case 'tool': return 'border-orange-200 dark:border-orange-700 border-l-orange-500 dark:border-l-orange-400'; case 'api_failure': return 'border-red-200 dark:border-red-700 border-l-red-500 dark:border-l-red-400'; default: return 'border-gray-200 dark:border-gray-700 border-l-gray-500 dark:border-l-gray-400'; } } export function getMessageTypeBadgeColor(type: string): string { switch (type.toLowerCase()) { case 'system': return 'badge-info'; case 'user': return 'badge-primary'; case 'assistant': return 'badge-secondary'; case 'tool': return 'badge-accent'; case 'api_failure': return 'badge-error'; default: return 'badge-neutral'; } }