@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
148 lines (129 loc) • 4.01 kB
text/typescript
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import transcriptSchema from './transcript-schema.json';
// Create AJV instance with proper configuration
const ajv = new Ajv({
allErrors: true, // Collect all errors, not just the first one
verbose: true, // Include schema and data information in errors
strict: false // Allow unknown keywords (for Pydantic-generated schemas)
});
// Add standard formats (including date-time)
addFormats(ajv);
// Add a more lenient date-time format that accepts various formats
ajv.addFormat('date-time', {
type: 'string',
validate: function(dateTimeString: string) {
// Accept ISO 8601 format
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(dateTimeString)) {
return !isNaN(Date.parse(dateTimeString));
}
// Accept space-separated format (common in transcript files)
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/.test(dateTimeString)) {
return !isNaN(Date.parse(dateTimeString.replace(' ', 'T')));
}
return false;
}
});
// Compile the transcript schema
const validateTranscript = ajv.compile(transcriptSchema);
export interface ValidationError {
message: string;
path: string;
value: any;
schema: any;
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
data?: any;
}
/**
* Validate transcript data against the JSON schema
*/
export function validateTranscriptData(data: any): ValidationResult {
const valid = validateTranscript(data);
if (valid) {
return {
valid: true,
errors: [],
data
};
}
const errors: ValidationError[] = (validateTranscript.errors || []).map(error => ({
message: error.message || 'Unknown validation error',
path: error.instancePath || error.schemaPath || 'unknown',
value: error.data,
schema: error.schema
}));
return {
valid: false,
errors,
data
};
}
/**
* Format validation errors into a human-readable message
*/
export function formatValidationErrors(errors: ValidationError[]): string {
if (errors.length === 0) return 'No validation errors';
const errorMessages = errors.map(error => {
const path = error.path || 'root';
return `• ${path}: ${error.message}`;
});
return `Schema validation failed:\n${errorMessages.join('\n')}`;
}
/**
* Validate and parse transcript file content
*/
export function validateAndParseTranscript(content: string, filename?: string): ValidationResult {
try {
// First, try to parse as JSON
const data = JSON.parse(content);
// Then validate against schema
const result = validateTranscriptData(data);
if (!result.valid) {
console.error(`Transcript validation failed${filename ? ` for ${filename}` : ''}: ${result.errors.length} validation errors. ${result.errors.map(error => error.message).join('\n')}`);
}
return result;
} catch (parseError) {
return {
valid: false,
errors: [{
message: `JSON parsing failed: ${parseError instanceof Error ? parseError.message : 'Unknown parse error'}`,
path: 'root',
value: content,
schema: null
}],
data: null
};
}
}
/**
* Check if data looks like a transcript (basic structure check)
*/
export function isTranscriptLike(data: any): boolean {
return (
data &&
typeof data === 'object' &&
data.metadata &&
Array.isArray(data.events)
);
}
/**
* Extract basic info from potentially invalid transcript data
*/
export function extractTranscriptInfo(data: any): {
hasMetadata: boolean;
hasEvents: boolean;
eventCount: number;
version: string | null;
sessionId: string | null;
} {
return {
hasMetadata: !!(data && data.metadata),
hasEvents: !!(data && Array.isArray(data.events)),
eventCount: (data && Array.isArray(data.events)) ? data.events.length : 0,
version: data?.metadata?.version || null,
sessionId: data?.metadata?.session_id || null
};
}