UNPKG

@kaifronsdal/transcript-viewer

Version:

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

169 lines (149 loc) 6.69 kB
import type { Transcript, TranscriptDisplay, TranscriptDisplayFull, TranscriptDisplayMeta } from '$lib/shared/types'; /** * Extract system prompt from transcript events */ export function extractSystemPrompt(transcript: Transcript): string | undefined { // Check if events exist and is iterable if (!transcript?.events || !Array.isArray(transcript.events)) { console.warn('extractSystemPrompt: transcript.events is not a valid array:', transcript?.events); return undefined; } for (const event of transcript.events) { if (event.type === 'transcript_event') { if (event.edit.operation === 'add' && event.edit.message?.role === 'system') { const content = event.edit.message.content; return typeof content === 'string' ? content : JSON.stringify(content); } } } return undefined; } /** * Extract target model from transcript metadata or system prompt content */ export function extractTargetModel(transcript: Transcript): string { // First try the metadata field if (transcript.metadata.target_model && transcript.metadata.target_model !== 'unknown') { return transcript.metadata.target_model; } // If metadata is unknown, try to extract from system prompt content const systemPrompt = extractSystemPrompt(transcript); if (systemPrompt) { // Look for pattern like "The target AI is a `anthropic:claude-3-5-sonnet-latest` model" const match = systemPrompt.match(/The target AI is a `([^`]+)` model/); if (match && match[1]) { return match[1]; } } // Fallback to unknown return 'unknown'; } /** * Extract model name from target model string */ export function extractModelName(targetModel: string): string { if (!targetModel || targetModel === 'unknown') { return 'unknown'; } // Extract just the model name from patterns like "anthropic:claude-3-5-sonnet-latest" const parts = targetModel.split(':'); if (parts.length > 1) { return parts[1]; } return targetModel; } // Note: extractBehaviorDir and extractTranscriptNumber functions were removed // since they required Node.js path module and weren't being used anywhere /** * Extract the first sentence from a description text */ export function extractFirstSentence(text: string): string { if (!text) return ''; // Split on sentence endings, but be careful with abbreviations // Look for periods, exclamation marks, or question marks followed by space or end of string const sentenceEndings = /[.!?](?:\s|$)/; const match = text.match(sentenceEndings); if (match) { const endIndex = match.index! + match[0].length - (match[0].endsWith(' ') ? 1 : 0); return text.substring(0, endIndex).trim(); } // If no sentence ending found, return the whole text (up to a reasonable limit) return text.length > 150 ? text.substring(0, 150) + '...' : text; } /** * Collect all score descriptions from transcripts and return a map of score name to description * For the home page, this allows us to show tooltips even when we don't have the full transcript data */ export function collectScoreDescriptions(transcripts: { scoreDescriptions?: Record<string, string> }[]): Record<string, string> { const allDescriptions: Record<string, string> = {}; for (const transcript of transcripts) { if (transcript.scoreDescriptions) { for (const [scoreName, description] of Object.entries(transcript.scoreDescriptions)) { // Only add if we don't already have a description for this score if (!allDescriptions[scoreName] && description) { allDescriptions[scoreName] = description; } } } } return allDescriptions; } /** * Create display data from transcript (full version with all data) */ export function createTranscriptDisplay( transcript: Transcript, filePath: string ): TranscriptDisplayFull { const targetModel = extractTargetModel(transcript); // Extract behavior directory (parent folder name) - client-safe version const pathParts = filePath.split('/'); const behaviorDir = pathParts.length > 1 ? pathParts[pathParts.length - 2] : ''; // Extract transcript number/name (filename without extension) - client-safe version const fileName = pathParts[pathParts.length - 1]; const transcriptNumber = fileName.endsWith('.json') ? fileName.slice(0, -5) : fileName; return { id: transcript.metadata.transcript_id || transcriptNumber, model: extractModelName(targetModel), split: behaviorDir, concerningScore: transcript.metadata.judge_output?.scores?.concerning || 0, summary: transcript.metadata.judge_output?.summary || transcript.metadata.description || 'No summary available', scores: transcript.metadata.judge_output?.scores || {}, scoreDescriptions: transcript.metadata.judge_output?.score_descriptions, judgeSummary: transcript.metadata.judge_output?.summary || 'No judgment summary available', justification: transcript.metadata.judge_output?.justification || 'No justification available', tags: transcript.metadata.tags || [], systemPrompt: extractSystemPrompt(transcript), transcript: transcript, _filePath: filePath }; } /** * Extract just metadata needed for display (for metadata-only operations) */ export function extractTranscriptMetadata( transcript: Transcript, filePath: string ): TranscriptDisplayMeta { const targetModel = extractTargetModel(transcript); // Extract behavior directory (parent folder name) - client-safe version const pathParts = filePath.split('/'); const behaviorDir = pathParts.length > 1 ? pathParts[pathParts.length - 2] : ''; // Extract transcript number/name (filename without extension) - client-safe version const fileName = pathParts[pathParts.length - 1]; const transcriptNumber = fileName.endsWith('.json') ? fileName.slice(0, -5) : fileName; return { id: transcript.metadata.transcript_id || transcriptNumber, model: extractModelName(targetModel), split: behaviorDir, concerningScore: transcript.metadata.judge_output?.scores?.concerning || 0, summary: transcript.metadata.judge_output?.summary || transcript.metadata.description || 'No summary available', scores: transcript.metadata.judge_output?.scores || {}, scoreDescriptions: transcript.metadata.judge_output?.score_descriptions, judgeSummary: transcript.metadata.judge_output?.summary || 'No judgment summary available', justification: transcript.metadata.judge_output?.justification || 'No justification available', tags: transcript.metadata.tags || [], systemPrompt: undefined, // Don't extract system prompt for metadata-only _filePath: filePath }; }