@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
305 lines (263 loc) • 10.4 kB
text/typescript
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';
}
}