UNPKG

@kaifronsdal/transcript-viewer

Version:

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

196 lines (171 loc) 5.38 kB
import { compileExpression } from 'filtrex'; import type { TranscriptDisplay } from './types'; /** * Compile and evaluate filter expression safely */ export function createFilterFunction(expression: string): (transcript: TranscriptDisplay) => boolean { // Return true for empty expressions (no filtering) if (!expression || !expression.trim()) { return () => true; } try { // Add custom functions for common string operations const options = { extraFunctions: { startsWith: (str: string, prefix: string) => str?.startsWith(prefix) || false, endsWith: (str: string, suffix: string) => str?.endsWith(suffix) || false, contains: (str: string, substring: string) => str?.includes(substring) || false, toLowerCase: (str: string) => str?.toLowerCase() || '', toUpperCase: (str: string) => str?.toUpperCase() || '', } }; // Compile the expression const compiledExpression = compileExpression(expression, options); return (transcript: TranscriptDisplay) => { try { // Flatten the transcript object to make fields accessible const context = { // Basic fields id: transcript.id, model: transcript.model, split: transcript.split, concerningScore: transcript.concerningScore, summary: transcript.summary, judgeSummary: transcript.judgeSummary, justification: transcript.justification, // Individual score fields (scores.scoreA becomes scoreA) ...Object.fromEntries( Object.entries(transcript.scores || {}).map(([key, value]) => [key, value]) ), // Also keep scores object for scores.scoreA syntax scores: transcript.scores || {} }; return Boolean(compiledExpression(context)); } catch (error) { // If evaluation fails for this transcript, exclude it console.warn('Filter evaluation failed for transcript:', transcript.id, error); return false; } }; } catch (error) { // If compilation fails, return a function that shows all transcripts console.warn('Filter compilation failed:', error); return () => true; } } /** * Get validation errors for a filter expression */ export function validateFilterExpression(expression: string): string | null { if (!expression || !expression.trim()) { return null; // Empty expressions are valid } try { // Try to compile with some dummy custom functions const options = { extraFunctions: { startsWith: () => true, endsWith: () => true, contains: () => true, toLowerCase: () => '', toUpperCase: () => '', } }; compileExpression(expression, options); return null; // No errors } catch (error) { return error instanceof Error ? error.message : 'Invalid expression'; } } /** * Get example filter expressions for help text */ export function getFilterExamples(): string[] { return [ 'scoreA < 5', 'scoreA < 5 and scoreB > 3', '1 < scoreA < 4', 'scoreA in (1, 3, 5)', 'split == "scheming"', 'split in ("scheming", "power_seeking")', 'startsWith(model, "gpt")', 'contains(summary, "refusal")', 'concerningScore > 7 and scoreA < 3', '(scoreA == 1 or scoreA == 3) and scoreB < 6' ]; } /** * Get all available field names for autocomplete */ export function getAvailableFields(scoreTypes: string[]): string[] { const baseFields = [ 'id', 'model', 'split', 'concerningScore', 'summary', 'judgeSummary' ]; const customFunctions = [ 'startsWith', 'endsWith', 'contains', 'toLowerCase', 'toUpperCase' ]; return [ ...baseFields, ...scoreTypes, ...customFunctions ]; } /** * Get current word being typed and its position */ export function getCurrentWord(text: string, cursorPosition: number): { word: string, startPos: number, endPos: number } { // Find word boundaries (letters, numbers, underscores) const wordPattern = /[a-zA-Z_][a-zA-Z0-9_]*/g; let match; while ((match = wordPattern.exec(text)) !== null) { const startPos = match.index; const endPos = match.index + match[0].length; if (cursorPosition >= startPos && cursorPosition <= endPos) { return { word: match[0], startPos, endPos }; } } // If cursor is not in a word, find the start of a potential new word const beforeCursor = text.slice(0, cursorPosition); const wordStart = beforeCursor.search(/[a-zA-Z_][a-zA-Z0-9_]*$/); if (wordStart !== -1) { const partialWord = beforeCursor.slice(wordStart); return { word: partialWord, startPos: wordStart, endPos: cursorPosition }; } return { word: '', startPos: cursorPosition, endPos: cursorPosition }; } /** * Get autocomplete suggestions for current word */ export function getAutocompleteSuggestions(currentWord: string, availableFields: string[]): string[] { if (!currentWord) return []; const lowerCurrentWord = currentWord.toLowerCase(); return availableFields .filter(field => field.toLowerCase().includes(lowerCurrentWord)) .sort((a, b) => { // Prioritize exact matches at the start const aStartsWithCurrent = a.toLowerCase().startsWith(lowerCurrentWord); const bStartsWithCurrent = b.toLowerCase().startsWith(lowerCurrentWord); if (aStartsWithCurrent && !bStartsWithCurrent) return -1; if (!aStartsWithCurrent && bStartsWithCurrent) return 1; // Then sort alphabetically return a.localeCompare(b); }) .slice(0, 10); // Limit to 10 suggestions }