Spaces:
Sleeping
Sleeping
Download ui/src/components/NotePanel.tsx from charlesyapai/hcc-meat-coder: direct link, hf CLI and curl.
- Browser
- Download file 4.26 kB
-
https://huggingface.co/spaces/charlesyapai/hcc-meat-coder/resolve/main/ui/src/components/NotePanel.tsx
- Command line
-
hf download hf://spaces/charlesyapai/hcc-meat-coder/ui/src/components/NotePanel.tsx
-
curl -L -o NotePanel.tsx https://huggingface.co/spaces/charlesyapai/hcc-meat-coder/resolve/main/ui/src/components/NotePanel.tsx
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> | |
| ); | |
| } | |