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