TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Contrat de citations : le modèle ne peut citer que les balises [Sx] réellement fournies.2// Toute balise inconnue est neutralisée et comptée. Les balises valides sont résolues en3// objets cliquables transmis au client.45import type { ContextBlock, RetrievedChunk } from "./search.ts";67export type ResolvedCitation = {8 tag: string; // S19 index: number; // 110 courseCode: string | null;11 docTitle: string;12 filename: string;13 path: string;14 refLabel: string; // « Séance 4 — Diapositive 18 »15 refType: string;16 refNumber: number | null;17 title: string;18 excerpt: string;19 chunkId: number;20 documentId: number;21};2223export function resolveCitations(text: string, context: ContextBlock): {24 cleaned: string;25 citations: ResolvedCitation[];26 invalidCount: number;27} {28 const valid = new Map(context.sources.map((s) => [s.tag, s.chunk]));29 const used = new Map<string, RetrievedChunk>();30 let invalidCount = 0;3132 // Normalise [S1, S3] → [S1][S3], puis vérifie chaque balise.33 let cleaned = text.replace(/\[(S\d+(?:\s*,\s*S\d+)+)\]/g, (_m, group: string) =>34 group35 .split(/\s*,\s*/)36 .map((t: string) => `[${t}]`)37 .join("")38 );39 cleaned = cleaned.replace(/\[S(\d+)\]/g, (m, n: string) => {40 const tag = `S${n}`;41 const chunk = valid.get(tag);42 if (!chunk) {43 invalidCount++;44 return "";45 }46 if (!used.has(tag)) used.set(tag, chunk);47 return m;48 });4950 const citations: ResolvedCitation[] = [...used.entries()].map(([tag, c]) => ({51 tag,52 index: parseInt(tag.slice(1), 10),53 courseCode: c.course_code,54 docTitle: c.doc_title,55 filename: c.filename,56 path: c.doc_path,57 refLabel: c.ref_label,58 refType: c.ref_type,59 refNumber: c.ref_number,60 title: c.title,61 excerpt: c.content.slice(0, 700),62 chunkId: c.id,63 documentId: c.document_id,64 }));6566 return { cleaned, citations, invalidCount };67}68