mirror of
https://github.com/willchen96/mike.git
synced 2026-07-24 23:41:04 +02:00
206 lines
7.6 KiB
TypeScript
206 lines
7.6 KiB
TypeScript
import { Loader2, Scale } from "lucide-react";
|
|
import { FileTypeIcon } from "../../shared/FileTypeIcon";
|
|
import { displayCitationQuote, formatCitationPage } from "../../shared/types";
|
|
import type { Citation } from "../../shared/types";
|
|
import { RESPONSE_GLASS_ANNOTATION, RESPONSE_GLASS_SURFACE } from "./messageStyles";
|
|
|
|
type CitationSourceRow = {
|
|
key: string;
|
|
label: string;
|
|
source: Citation;
|
|
entries: { annotation: Citation; index: number }[];
|
|
};
|
|
|
|
function citationSourceKey(annotation: Citation): string {
|
|
if (annotation.kind === "case") {
|
|
return `case:${annotation.cluster_id}`;
|
|
}
|
|
return `document:${annotation.document_id}`;
|
|
}
|
|
|
|
function citationSourceLabel(annotation: Citation): string {
|
|
if (annotation.kind === "case") {
|
|
const caseName = annotation.case_name?.trim();
|
|
const citation = annotation.citation?.trim();
|
|
if (caseName && citation) return `${caseName}, ${citation}`;
|
|
return caseName || citation || `Case ${annotation.cluster_id}`;
|
|
}
|
|
return annotation.filename;
|
|
}
|
|
|
|
export function citationTooltip(annotation: Citation): string {
|
|
const locator = formatCitationPage(annotation);
|
|
const quote = displayCitationQuote(annotation);
|
|
return locator ? `${locator}: "${quote}"` : `"${quote}"`;
|
|
}
|
|
|
|
function CitationSourceIcon({
|
|
annotation,
|
|
}: {
|
|
annotation: Citation;
|
|
}) {
|
|
if (annotation.kind === "case") {
|
|
return <Scale className="h-3.5 w-3.5 text-slate-600" />;
|
|
}
|
|
return (
|
|
<FileTypeIcon fileType={annotation.filename} className="h-3.5 w-3.5" />
|
|
);
|
|
}
|
|
|
|
function buildCitationSourceRows(
|
|
citations: Citation[],
|
|
): CitationSourceRow[] {
|
|
const rows = new Map<string, CitationSourceRow>();
|
|
citations.forEach((annotation, index) => {
|
|
const key = citationSourceKey(annotation);
|
|
const existing = rows.get(key);
|
|
if (existing) {
|
|
existing.entries.push({ annotation, index });
|
|
return;
|
|
}
|
|
rows.set(key, {
|
|
key,
|
|
label: citationSourceLabel(annotation),
|
|
source: annotation,
|
|
entries: [{ annotation, index }],
|
|
});
|
|
});
|
|
return Array.from(rows.values());
|
|
}
|
|
|
|
function escapeHtmlText(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function ensureTerminalPeriod(value: string): string {
|
|
return /[.!?]$/.test(value.trim()) ? value.trim() : `${value.trim()}.`;
|
|
}
|
|
|
|
export function buildCitationAppendix(citations: Citation[]) {
|
|
if (citations.length === 0) return { html: "", text: "" };
|
|
let previousSourceKey: string | null = null;
|
|
const entries = citations.map((annotation) => {
|
|
const sourceKey = citationSourceKey(annotation);
|
|
const label =
|
|
sourceKey === previousSourceKey
|
|
? "Id."
|
|
: citationSourceLabel(annotation);
|
|
previousSourceKey = sourceKey;
|
|
return {
|
|
number: annotation.ref,
|
|
label,
|
|
quote: displayCitationQuote(annotation).trim(),
|
|
};
|
|
});
|
|
const textLines = [
|
|
"",
|
|
"Citations",
|
|
...entries.map((entry) => {
|
|
const quote = entry.quote ? ` "${entry.quote}"` : "";
|
|
return `${entry.number} ${ensureTerminalPeriod(entry.label)}${quote}`;
|
|
}),
|
|
];
|
|
const html = [
|
|
`<section class="copied-citations">`,
|
|
`<h3>Citations</h3>`,
|
|
...entries.map((entry) => {
|
|
const label = escapeHtmlText(ensureTerminalPeriod(entry.label));
|
|
const quote = entry.quote
|
|
? ` "${escapeHtmlText(entry.quote)}"`
|
|
: "";
|
|
return `<p><sup>${entry.number}</sup> ${label}${quote}</p>`;
|
|
}),
|
|
`</section>`,
|
|
].join("");
|
|
return { html, text: textLines.join("\n") };
|
|
}
|
|
|
|
export function CitationsBlock({
|
|
citations,
|
|
onCitationClick,
|
|
onOpenSource,
|
|
canOpenSource,
|
|
showWhenEmpty = false,
|
|
isLoading = false,
|
|
}: {
|
|
citations: Citation[];
|
|
onCitationClick?: (citation: Citation) => void;
|
|
onOpenSource?: (citation: Citation) => void;
|
|
canOpenSource?: (citation: Citation) => boolean;
|
|
showWhenEmpty?: boolean;
|
|
isLoading?: boolean;
|
|
}) {
|
|
const rows = buildCitationSourceRows(citations);
|
|
if (rows.length === 0 && !showWhenEmpty) return null;
|
|
|
|
return (
|
|
<div className="mt-2 mb-3">
|
|
<div className={`overflow-hidden ${RESPONSE_GLASS_SURFACE}`}>
|
|
<div className="flex items-center justify-between gap-3 bg-white/25 px-3 py-2">
|
|
<h3 className="text-base font-serif text-gray-900">
|
|
Citations
|
|
</h3>
|
|
{isLoading && (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
{rows.map((row) => {
|
|
const sourceIsClickable =
|
|
!!onOpenSource &&
|
|
(canOpenSource?.(row.source) ?? true);
|
|
return (
|
|
<div
|
|
key={row.key}
|
|
className="flex items-center gap-3 px-3 py-3"
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenSource?.(row.source)}
|
|
disabled={!sourceIsClickable}
|
|
className="flex min-w-0 flex-1 items-center gap-2 rounded-lg text-left text-sm font-serif text-gray-700 transition-colors enabled:hover:text-gray-950 disabled:cursor-default"
|
|
>
|
|
<CitationSourceIcon
|
|
annotation={row.source}
|
|
/>
|
|
<span className="truncate">
|
|
{row.label}
|
|
</span>
|
|
</button>
|
|
<div className="flex shrink-0 flex-wrap justify-end gap-1">
|
|
{row.entries.map(
|
|
({ annotation, index }) => (
|
|
<button
|
|
key={`${row.key}:${index}`}
|
|
type="button"
|
|
onClick={() =>
|
|
onCitationClick?.(
|
|
annotation,
|
|
)
|
|
}
|
|
className={
|
|
RESPONSE_GLASS_ANNOTATION
|
|
}
|
|
title={citationTooltip(
|
|
annotation,
|
|
)}
|
|
>
|
|
{annotation.ref}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|