File size: 4,258 Bytes
c6cfe5f
d0ec498
c6cfe5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ce893e
 
 
c6cfe5f
 
 
 
 
 
 
9ce893e
 
 
c6cfe5f
 
 
 
 
 
 
 
 
 
 
9ce893e
c6cfe5f
 
9ce893e
c6cfe5f
 
 
 
 
 
 
 
 
 
 
9ce893e
 
c6cfe5f
 
 
 
 
 
 
 
 
 
 
9ce893e
 
c6cfe5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ce893e
c6cfe5f
 
9ce893e
c6cfe5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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>
  );
}