UNPKG

@kaifronsdal/transcript-viewer

Version:

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

192 lines (172 loc) 6.24 kB
import type { ColumnDef } from '@tanstack/svelte-table'; import type { TableRow } from '$lib/shared/types'; import { extractFirstSentence } from '$lib/shared/utils/transcript-utils'; /** * Calculate optimal width for ID column based on content and nesting depth */ function calculateIdColumnWidth(data: TableRow[]): number { let maxWidth = 100; // Minimum width const baseIndentation = 12; // Base padding const depthIndentation = 20; // Per-depth indentation const charWidth = 8; // Approximate character width in pixels for font-mono const padding = 24; // Left + right padding (12px each) function measureContent(rows: TableRow[], depth: number = 0): void { for (const row of rows) { let contentLength = 0; if (row.type === 'folder') { // For folders, use the folder name contentLength = (row.name || 'Folder').length; } else { // For transcripts, use the formatted ID (#123) const idPart = row.id ? String(row.id).split('_').pop() : ''; contentLength = idPart ? `#${idPart}`.length : 0; } // Calculate total width needed: content + indentation + padding const indentationWidth = baseIndentation + (depth * depthIndentation); const totalWidth = (contentLength * charWidth) + indentationWidth + padding; maxWidth = Math.max(maxWidth, totalWidth); // Recursively check subRows if (row.subRows && row.subRows.length > 0) { measureContent(row.subRows, depth + 1); } } } measureContent(data); // Cap the maximum width to prevent extremely wide columns const finalWidth = Math.min(maxWidth, 400); console.log('📏 [DEBUG] ID column width calculation:', { calculatedWidth: maxWidth, finalWidth, dataLength: data.length }); return finalWidth; } // Use the unified TableRow type directly (no more TreeTableRow duplication) export function createColumns(scoreTypes: string[], data: TableRow[] = [], scoreDescriptions: Record<string, string> = {}): ColumnDef<TableRow>[] { // Calculate dynamic width for ID column const idColumnWidth = calculateIdColumnWidth(data); const baseColumns: ColumnDef<TableRow>[] = [ { id: 'id', accessorKey: 'id', header: 'ID', cell: ({ row, getValue }) => { const value = getValue(); const isFolder = row.original.type === 'folder'; if (isFolder) { return row.original.name || 'Folder'; } return value ? `#${String(value).split('_').pop()}` : ''; }, enableSorting: true, enableResizing: true, size: idColumnWidth, minSize: 100, }, { id: 'model', accessorKey: 'model', header: 'Model', cell: ({ row, getValue }) => { if (row.original.type === 'folder') return ''; return getValue(); }, enableSorting: true, enableResizing: true, size: 150, minSize: 70, }, { id: 'tags', accessorKey: 'tags', header: 'Tags', cell: ({ row }) => { if (row.original.type === 'folder') return ''; const tags = (row.original.tags || []) as string[]; return tags.join(', '); }, filterFn: (row, _columnId, value) => { // Always keep folders visible so tree structure persists if (row.original.type === 'folder') return true; const selected: string[] = Array.isArray(value) ? value : []; if (!selected || selected.length === 0) return true; const rowTags: string[] = Array.isArray((row.original as any).tags) ? (row.original as any).tags : []; // OR semantics: show if row has at least one of the selected tags return selected.some((t) => rowTags.includes(t)); }, enableSorting: false, enableResizing: true, size: 220, minSize: 120, }, { id: 'summary', accessorKey: 'summary', header: 'Summary', cell: ({ row, getValue }) => { if (row.original.type === 'folder') return ''; const summary = getValue() as string; return summary ? (summary.length > 150 ? summary.slice(0, 150) + '...' : summary) : ''; }, enableSorting: false, enableResizing: true, size: 400, minSize: 120, } ]; // Add score columns dynamically const scoreColumns: ColumnDef<TableRow>[] = scoreTypes.map(scoreType => { const formattedHeader = scoreType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const description = scoreDescriptions[scoreType]; const tooltipText = description ? extractFirstSentence(description) : undefined; return { id: `score_${scoreType}`, accessorFn: (row) => row.scores?.[scoreType], header: formattedHeader, meta: { tooltip: tooltipText // Store tooltip in meta for custom rendering }, cell: ({ row, getValue }) => { if (row.original.type === 'folder') return ''; const score = getValue() as number; return score !== undefined ? `${score}/10` : ''; }, enableSorting: true, enableResizing: true, size: 120, minSize: 50, }; }); return [...baseColumns, ...scoreColumns]; } export function getDefaultColumnVisibility(scoreTypes: string[]) { const visibility: Record<string, boolean> = { id: true, model: true, tags: true, split: false, concerningScore: true, summary: true, judgeSummary: false, }; // Hide most score columns by default to avoid overwhelming UI scoreTypes.forEach(scoreType => { visibility[`score_${scoreType}`] = false; }); return visibility; } export function getAllColumnInfo(scoreTypes: string[]) { return [ { id: 'id', header: 'ID' }, { id: 'model', header: 'Model' }, { id: 'tags', header: 'Tags' }, { id: 'split', header: 'Split' }, { id: 'concerningScore', header: 'Concerning Score' }, { id: 'summary', header: 'Summary' }, { id: 'judgeSummary', header: 'Judge Summary' }, ...scoreTypes.map(scoreType => ({ id: `score_${scoreType}`, header: scoreType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) })) ]; }