| import type { CitationView } from "@/lib/api"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const CITATION_RE = /\[\s*([A-Za-z][A-Za-z .'-]{1,48}?)\s*,\s*(?:Article|Art\.?)\s*\(?\s*(\d{1,3})\s*\)?\s*\]/gi; |
|
|
| export type Segment = |
| | { kind: "text"; value: string } |
| | { kind: "citation"; value: string; lawLabel: string; articleNo: number; citation: CitationView | null }; |
|
|
| export function segmentAnswer(text: string, citations: CitationView[]): Segment[] { |
| const byKey = new Map<string, CitationView>(); |
| for (const citation of citations) { |
| byKey.set(`${normalise(citation.law_label)}|${citation.article_no}`, citation); |
| } |
|
|
| const segments: Segment[] = []; |
| let cursor = 0; |
| CITATION_RE.lastIndex = 0; |
| let match = CITATION_RE.exec(text); |
| while (match !== null) { |
| const [raw, lawLabel = "", articleRaw = "0"] = match; |
| if (match.index > cursor) { |
| segments.push({ kind: "text", value: text.slice(cursor, match.index) }); |
| } |
| const articleNo = Number.parseInt(articleRaw, 10); |
| segments.push({ |
| kind: "citation", |
| value: raw, |
| lawLabel: lawLabel.trim(), |
| articleNo, |
| citation: byKey.get(`${normalise(lawLabel)}|${articleNo}`) ?? null, |
| }); |
| cursor = match.index + raw.length; |
| match = CITATION_RE.exec(text); |
| } |
| if (cursor < text.length) { |
| segments.push({ kind: "text", value: text.slice(cursor) }); |
| } |
| return segments; |
| } |
|
|
| function normalise(label: string): string { |
| return label.toLowerCase().replace(/[^a-z0-9]+/g, ""); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function highlightRanges(passage: string, answer: string): [number, number][] { |
| const answerWords = new Set( |
| answer.toLowerCase().match(/[a-z0-9]{4,}/g) ?? [], |
| ); |
| if (answerWords.size === 0) return []; |
|
|
| const ranges: [number, number][] = []; |
| const sentenceRe = /[^.;:]+[.;:]?/g; |
| let match = sentenceRe.exec(passage); |
| while (match !== null) { |
| const sentence = match[0]; |
| const words = sentence.toLowerCase().match(/[a-z0-9]{4,}/g) ?? []; |
| if (words.length >= 4) { |
| const hits = words.filter((word) => answerWords.has(word)).length; |
| if (hits / words.length >= 0.6) { |
| ranges.push([match.index, match.index + sentence.length]); |
| } |
| } |
| match = sentenceRe.exec(passage); |
| } |
| return mergeAdjacent(ranges); |
| } |
|
|
| function mergeAdjacent(ranges: [number, number][]): [number, number][] { |
| if (ranges.length === 0) return []; |
| const merged: [number, number][] = [ranges[0]!]; |
| for (const range of ranges.slice(1)) { |
| const last = merged[merged.length - 1]!; |
| if (range[0] - last[1] <= 2) last[1] = range[1]; |
| else merged.push(range); |
| } |
| return merged; |
| } |
|
|