import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import rehypeHighlight from "rehype-highlight"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import rehypeStringify from "rehype-stringify"; import { classifyForExport, formatDuration, groupObservationsByTurn, statusIcon } from "./ai-export-content"; import type { IAiSessionExporter, TAiExportBundle, TAiExportContext } from "./ai-export-types"; // Same extension the frontend's Markdown.tsx makes to rehype-sanitize's default (github-flavored) // schema: keep fenced-code language classes and GFM task-list checkboxes, block everything else // rehype-sanitize already blocks by default (script tags, event handlers, javascript: URLs). const SANITIZE_SCHEMA = { ...defaultSchema, tagNames: [...(defaultSchema.tagNames ?? []), "input"], attributes: { ...defaultSchema.attributes, code: [...(defaultSchema.attributes?.code ?? []), "className"], span: [...(defaultSchema.attributes?.span ?? []), "className"], input: ["type", "checked", "disabled"], }, }; const processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeHighlight).use(rehypeSanitize, SANITIZE_SCHEMA).use(rehypeStringify); function escapeHtml(text: string): string { return text.replace(/&/g, "&").replace(//g, ">"); } function renderMarkdown(text: string): string { if (!text.trim()) return ""; try { return String(processor.processSync(text)); } catch { return `
${escapeHtml(text)}`;
}
}
function labelOutcome(outcome: TAiExportBundle["analysis"]["outcome"]): string {
const labels: Record${escapeHtml(observation.input)}` : "") +
(observation.output ? `${escapeHtml(observation.output)}` : "") +
`