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