hcc-meat-coder / ui /src /components /NotePanel.tsx
chraos's picture
Claude Opus 4.7 (1M context)
0.6.4: /api/status diagnostic + StatusBanner + remove unused NotePanel prop
d0ec498
Raw History Blame Contribute Delete
4.26 kB
import { useMemo } from "react";
import type { GalleryItem, HCCCandidate, MEATEvidence } from "../lib/types";
import { cn } from "../lib/cn";
interface Props {
noteText: string;
onChange: (text: string) => void;
gallery: GalleryItem[];
onPickGalleryItem: (item: GalleryItem) => void;
onCode: () => void;
loading: boolean;
selectedCandidate: HCCCandidate | null;
}
export function NotePanel(props: Props) {
const { noteText, onChange, gallery, onPickGalleryItem, onCode, loading, selectedCandidate } =
props;
const highlights = useMemo(() => buildHighlights(selectedCandidate, noteText), [
selectedCandidate,
noteText,
]);
return (
<div className="flex h-full flex-col overflow-hidden rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between border-b border-slate-200 px-3 py-2 dark:border-slate-800">
<h2 className="text-sm font-medium text-slate-900 dark:text-slate-100">Clinical note</h2>
<div className="flex items-center gap-2">
<GalleryDropdown gallery={gallery} onPick={onPickGalleryItem} />
<button
onClick={onCode}
disabled={loading}
className={cn(
"rounded px-3 py-1 text-sm text-white",
loading
? "bg-slate-400 dark:bg-slate-600"
: "bg-indigo-600 hover:bg-indigo-700",
)}
>
{loading ? "Coding…" : "Code note"}
</button>
</div>
</div>
<div className="grid flex-1 grid-cols-1 gap-2 overflow-hidden p-3 lg:grid-cols-2">
<textarea
value={noteText}
onChange={(e) => onChange(e.target.value)}
className="h-full min-h-[320px] w-full resize-none rounded border border-slate-200 bg-slate-50 p-2 font-mono text-xs leading-relaxed text-slate-900 outline-indigo-400 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-100"
spellCheck={false}
/>
<div className="note-preview h-full overflow-auto whitespace-pre-wrap rounded border border-slate-200 bg-white p-2 font-mono text-xs leading-relaxed text-slate-800 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200">
{highlights}
</div>
</div>
</div>
);
}
function buildHighlights(
candidate: HCCCandidate | null,
text: string,
): React.ReactNode {
if (!candidate)
return <span className="text-slate-400 dark:text-slate-500">Preview</span>;
const spans: { start: number; end: number; cat: string; ev: MEATEvidence }[] = [];
for (const ev of candidate.meat_evidence) {
if (!ev.verified) continue;
spans.push({ start: ev.start, end: ev.end, cat: ev.category, ev });
}
spans.sort((a, b) => a.start - b.start);
const out: React.ReactNode[] = [];
let cursor = 0;
spans.forEach((s, i) => {
if (s.start < cursor) return; // overlap; skip
if (s.start > cursor)
out.push(<span key={`t-${i}-${cursor}`}>{text.slice(cursor, s.start)}</span>);
out.push(
<span
key={`h-${i}`}
className="meat-highlight"
data-cat={s.cat}
title={`${s.cat} (conf ${s.ev.extractor_confidence.toFixed(2)})`}
>
{text.slice(s.start, s.end)}
</span>,
);
cursor = s.end;
});
if (cursor < text.length) out.push(<span key={`t-end-${cursor}`}>{text.slice(cursor)}</span>);
return <>{out}</>;
}
function GalleryDropdown({
gallery,
onPick,
}: {
gallery: GalleryItem[];
onPick: (i: GalleryItem) => void;
}) {
if (!gallery.length)
return <span className="text-xs text-slate-400 dark:text-slate-500">(gallery loading…)</span>;
return (
<select
className="rounded border border-slate-200 bg-white px-2 py-1 text-sm text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
defaultValue=""
onChange={(e) => {
const pick = gallery.find((g) => g.case_id === e.target.value);
if (pick) onPick(pick);
}}
>
<option value="" disabled>
Load example…
</option>
{gallery.map((g) => (
<option key={g.case_id} value={g.case_id}>
{g.name}
</option>
))}
</select>
);
}