pih-appointment-widget 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -70
- package/babel.config.js +5 -5
- package/dist/App.js +10 -9
- package/dist/components/AppointmentPage.js +87 -62
- package/dist/components/ICD10Assistant.js +43 -46
- package/dist/doctor-appointments-widget.umd.js +115 -119
- package/dist/doctor-appointments-widget.umd.min.js +1 -1
- package/dist/hooks/useClipboard.js +3 -3
- package/dist/pih-appointment-widget.umd.js +185 -191
- package/dist/pih-appointment-widget.umd.min.js +1 -1
- package/dist/services/appointmentService.js +20 -20
- package/dist/services/httpService.js +14 -18
- package/dist/services/icdService.js +21 -23
- package/package.json +67 -67
- package/public/index.html +43 -43
- package/public/manifest.json +25 -25
- package/public/robots.txt +3 -3
- package/rollup.config.js +43 -43
- package/src/App.js +50 -50
- package/src/Example.js +14 -14
- package/src/assets/icons/icdIcons.js +23 -23
- package/src/components/AppointmentPage.js +2502 -2498
- package/src/components/ICD10Assistant.jsx +923 -923
- package/src/constants/apiConfig.js +29 -29
- package/src/hooks/useClipboard.js +35 -35
- package/src/index.js +6 -6
- package/src/services/appointmentService.js +92 -92
- package/src/services/httpService.js +103 -103
- package/src/services/icdService.js +76 -76
|
@@ -1,923 +1,923 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ICD10Assistant.jsx
|
|
3
|
-
* AI-powered ICD-10 coding assistant panel.
|
|
4
|
-
* Floats as a pill button; expands into a full panel on click.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
7
|
-
import { getICDSuggestions, ICD_MODE } from "../services/icdService";
|
|
8
|
-
import { useClipboard } from "../hooks/useClipboard";
|
|
9
|
-
import { IMG, ICON } from "../assets/icons/icdIcons";
|
|
10
|
-
|
|
11
|
-
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
12
|
-
const MAX_HISTORY = 3;
|
|
13
|
-
const FONT = '"Nunito", sans-serif';
|
|
14
|
-
|
|
15
|
-
// ─── Colour tokens ────────────────────────────────────────────────────────────
|
|
16
|
-
const C = {
|
|
17
|
-
primary: "#4C4DDC",
|
|
18
|
-
primaryDark: "#3A3BBD",
|
|
19
|
-
primaryLight: "#E8EEF4",
|
|
20
|
-
teal: "#1CC3CE",
|
|
21
|
-
tealLight: "#E0F8F9",
|
|
22
|
-
success: "#16a34a",
|
|
23
|
-
successLight: "#dcfce7",
|
|
24
|
-
error: "#dc2626",
|
|
25
|
-
errorLight: "#fee2e2",
|
|
26
|
-
warn: "#d97706",
|
|
27
|
-
warnLight: "#fef3c7",
|
|
28
|
-
text: "#1a1a1a",
|
|
29
|
-
muted: "#666",
|
|
30
|
-
border: "#E5E5E5",
|
|
31
|
-
bg: "#F5F5F7",
|
|
32
|
-
white: "#FFFFFF",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
36
|
-
function Badge({ children, color = C.primary, bg = C.primaryLight }) {
|
|
37
|
-
return (
|
|
38
|
-
<span
|
|
39
|
-
style={{
|
|
40
|
-
display: "inline-flex",
|
|
41
|
-
alignItems: "center",
|
|
42
|
-
padding: "2px 10px",
|
|
43
|
-
borderRadius: "999px",
|
|
44
|
-
fontSize: "12px",
|
|
45
|
-
fontWeight: 700,
|
|
46
|
-
color,
|
|
47
|
-
background: bg,
|
|
48
|
-
letterSpacing: "0.5px",
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{children}
|
|
52
|
-
</span>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function Spinner() {
|
|
57
|
-
return (
|
|
58
|
-
<>
|
|
59
|
-
<style>{`@keyframes icd-spin{to{transform:rotate(360deg)}}`}</style>
|
|
60
|
-
<div
|
|
61
|
-
style={{
|
|
62
|
-
width: 20,
|
|
63
|
-
height: 20,
|
|
64
|
-
border: "2.5px solid #d1d5db",
|
|
65
|
-
borderTop: `2.5px solid ${C.primary}`,
|
|
66
|
-
borderRadius: "50%",
|
|
67
|
-
animation: "icd-spin 0.8s linear infinite",
|
|
68
|
-
flexShrink: 0,
|
|
69
|
-
}}
|
|
70
|
-
/>
|
|
71
|
-
</>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function CopyButton({ text, label = "Copy", copyKey, copiedKey, copy }) {
|
|
76
|
-
const copied = copiedKey === copyKey;
|
|
77
|
-
return (
|
|
78
|
-
<button
|
|
79
|
-
onClick={() => copy(text, copyKey)}
|
|
80
|
-
title={copied ? "Copied!" : `Copy ${label}`}
|
|
81
|
-
style={{
|
|
82
|
-
display: "inline-flex",
|
|
83
|
-
alignItems: "center",
|
|
84
|
-
gap: 4,
|
|
85
|
-
padding: "3px 10px",
|
|
86
|
-
fontSize: 11,
|
|
87
|
-
fontWeight: 600,
|
|
88
|
-
fontFamily: FONT,
|
|
89
|
-
border: `1px solid ${copied ? C.success : C.border}`,
|
|
90
|
-
borderRadius: 5,
|
|
91
|
-
background: copied ? C.successLight : C.white,
|
|
92
|
-
color: copied ? C.success : C.muted,
|
|
93
|
-
cursor: "pointer",
|
|
94
|
-
transition: "all 0.15s ease",
|
|
95
|
-
whiteSpace: "nowrap",
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
{copied ? "✓ Copied" : `⎘ ${label}`}
|
|
99
|
-
</button>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── Result Card ──────────────────────────────────────────────────────────────
|
|
104
|
-
function ResultCard({ match, index, copy, copiedKey }) {
|
|
105
|
-
const cardCopyKey = `card-${index}`;
|
|
106
|
-
const fullText = `${match.code} — ${match.description}${match.reason ? `\nNote: ${match.reason}` : ""}`;
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div
|
|
110
|
-
style={{
|
|
111
|
-
border: `1px solid ${C.border}`,
|
|
112
|
-
borderRadius: 10,
|
|
113
|
-
padding: "14px 16px",
|
|
114
|
-
background: C.white,
|
|
115
|
-
display: "flex",
|
|
116
|
-
flexDirection: "column",
|
|
117
|
-
gap: 6,
|
|
118
|
-
boxShadow: "0 1px 4px rgba(0,0,0,0.06)",
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 8 }}>
|
|
122
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
123
|
-
<Badge color={C.primary} bg={C.primaryLight}>
|
|
124
|
-
{match.code}
|
|
125
|
-
</Badge>
|
|
126
|
-
<span style={{ fontSize: 13, fontWeight: 600, color: C.text, lineHeight: 1.4 }}>
|
|
127
|
-
{match.description}
|
|
128
|
-
</span>
|
|
129
|
-
</div>
|
|
130
|
-
<CopyButton
|
|
131
|
-
text={fullText}
|
|
132
|
-
label={match.code}
|
|
133
|
-
copyKey={cardCopyKey}
|
|
134
|
-
copiedKey={copiedKey}
|
|
135
|
-
copy={copy}
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
{match.reason && (
|
|
139
|
-
<p style={{ margin: 0, fontSize: 12, color: C.muted, lineHeight: 1.5, paddingLeft: 2 }}>
|
|
140
|
-
{ICON.reason} {match.reason}
|
|
141
|
-
</p>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ─── History Chip ─────────────────────────────────────────────────────────────
|
|
148
|
-
function HistoryChip({ item, onClick }) {
|
|
149
|
-
return (
|
|
150
|
-
<button
|
|
151
|
-
onClick={() => onClick(item.query)}
|
|
152
|
-
title={`Re-run: ${item.query}`}
|
|
153
|
-
style={{
|
|
154
|
-
padding: "4px 10px",
|
|
155
|
-
border: `1px solid ${C.border}`,
|
|
156
|
-
borderRadius: 999,
|
|
157
|
-
fontSize: 11,
|
|
158
|
-
fontFamily: FONT,
|
|
159
|
-
color: C.muted,
|
|
160
|
-
background: C.white,
|
|
161
|
-
cursor: "pointer",
|
|
162
|
-
maxWidth: 180,
|
|
163
|
-
overflow: "hidden",
|
|
164
|
-
textOverflow: "ellipsis",
|
|
165
|
-
whiteSpace: "nowrap",
|
|
166
|
-
transition: "border-color 0.15s",
|
|
167
|
-
}}
|
|
168
|
-
onMouseEnter={(e) => (e.currentTarget.style.borderColor = C.primary)}
|
|
169
|
-
onMouseLeave={(e) => (e.currentTarget.style.borderColor = C.border)}
|
|
170
|
-
>
|
|
171
|
-
{ICON.history} {item.query}
|
|
172
|
-
</button>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ─── Mode Toggle (tab bar) ──────────────────────────────────────────────────
|
|
177
|
-
function ModeToggle({ mode, onChange }) {
|
|
178
|
-
const tab = (id, label, icon) => (
|
|
179
|
-
<button
|
|
180
|
-
onClick={() => onChange(id)}
|
|
181
|
-
style={{
|
|
182
|
-
flex: 1,
|
|
183
|
-
padding: "6px 0",
|
|
184
|
-
border: "none",
|
|
185
|
-
borderRadius: 6,
|
|
186
|
-
fontSize: 12,
|
|
187
|
-
fontWeight: 700,
|
|
188
|
-
fontFamily: FONT,
|
|
189
|
-
cursor: "pointer",
|
|
190
|
-
transition: "all 0.15s",
|
|
191
|
-
background: mode === id ? C.white : "transparent",
|
|
192
|
-
color: mode === id ? C.primary : "rgba(255,255,255,0.7)",
|
|
193
|
-
boxShadow: mode === id ? "0 1px 4px rgba(0,0,0,0.15)" : "none",
|
|
194
|
-
display: "flex",
|
|
195
|
-
alignItems: "center",
|
|
196
|
-
justifyContent: "center",
|
|
197
|
-
gap: 5,
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
200
|
-
<span>{icon}</span> {label}
|
|
201
|
-
</button>
|
|
202
|
-
);
|
|
203
|
-
return (
|
|
204
|
-
<div
|
|
205
|
-
style={{
|
|
206
|
-
display: "flex",
|
|
207
|
-
background: "rgba(255,255,255,0.15)",
|
|
208
|
-
borderRadius: 8,
|
|
209
|
-
padding: 3,
|
|
210
|
-
gap: 2,
|
|
211
|
-
marginTop: 10,
|
|
212
|
-
}}
|
|
213
|
-
>
|
|
214
|
-
{tab("nlm", "Quick Lookup", "")}
|
|
215
|
-
{tab("ai", "AI Suggest", "")}
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ─── NLM Lookup Panel ────────────────────────────────────────────────────────
|
|
221
|
-
// Uses the free NLM Clinical Tables API — no API key, no credits needed.
|
|
222
|
-
function NLMLookupPanel({ panelRef, onClose, mode, onModeChange }) {
|
|
223
|
-
const [query, setQuery] = useState("");
|
|
224
|
-
const [results, setResults] = useState([]);
|
|
225
|
-
const [status, setStatus] = useState("");
|
|
226
|
-
const [copied, setCopied] = useState("");
|
|
227
|
-
const debounceRef = useRef(null);
|
|
228
|
-
const inputRef = useRef(null);
|
|
229
|
-
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
setTimeout(() => inputRef.current?.focus(), 80);
|
|
232
|
-
}, []);
|
|
233
|
-
|
|
234
|
-
const search = useCallback(async (term) => {
|
|
235
|
-
if (term.length < 2) {
|
|
236
|
-
setResults([]);
|
|
237
|
-
setStatus("");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
setStatus("Searching…");
|
|
241
|
-
try {
|
|
242
|
-
const data = await getICDSuggestions(term, ICD_MODE.ICD_ONLY);
|
|
243
|
-
const codes = (data.matches || []).map((m) => [m.code, m.description]);
|
|
244
|
-
setResults(codes);
|
|
245
|
-
setStatus(
|
|
246
|
-
codes.length === 0
|
|
247
|
-
? "No matching codes found."
|
|
248
|
-
: `Showing ${codes.length} result${codes.length !== 1 ? "s" : ""}`
|
|
249
|
-
);
|
|
250
|
-
} catch (err) {
|
|
251
|
-
setStatus(err.message || "Could not fetch results. Check your connection.");
|
|
252
|
-
setResults([]);
|
|
253
|
-
}
|
|
254
|
-
}, []);
|
|
255
|
-
|
|
256
|
-
const handleChange = (e) => {
|
|
257
|
-
const val = e.target.value;
|
|
258
|
-
setQuery(val);
|
|
259
|
-
clearTimeout(debounceRef.current);
|
|
260
|
-
debounceRef.current = setTimeout(() => search(val.trim()), 300);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const copyCode = (code) => {
|
|
264
|
-
navigator.clipboard.writeText(code).then(() => {
|
|
265
|
-
setCopied(code);
|
|
266
|
-
setTimeout(() => setCopied(""), 2000);
|
|
267
|
-
});
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<div
|
|
272
|
-
ref={panelRef}
|
|
273
|
-
style={{
|
|
274
|
-
position: "fixed",
|
|
275
|
-
bottom: 28,
|
|
276
|
-
right: 28,
|
|
277
|
-
zIndex: 99999,
|
|
278
|
-
width: "min(480px, calc(100vw - 32px))",
|
|
279
|
-
maxHeight: "calc(100vh - 56px)",
|
|
280
|
-
display: "flex",
|
|
281
|
-
flexDirection: "column",
|
|
282
|
-
background: C.white,
|
|
283
|
-
borderRadius: 16,
|
|
284
|
-
boxShadow: "0 12px 48px rgba(0,0,0,0.18)",
|
|
285
|
-
fontFamily: FONT,
|
|
286
|
-
overflow: "hidden",
|
|
287
|
-
animation: "icd-slide-in 0.2s ease",
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
<style>{`@keyframes icd-slide-in{from{opacity:0;transform:translateY(20px) scale(0.97)}to{opacity:1;transform:translateY(0) scale(1)}}`}</style>
|
|
291
|
-
|
|
292
|
-
{/* Header */}
|
|
293
|
-
<div style={{ background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`, padding: "14px 18px", flexShrink: 0 }}>
|
|
294
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
295
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
296
|
-
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
|
297
|
-
<div>
|
|
298
|
-
<div style={{ color: C.white, fontWeight: 700, fontSize: 15, lineHeight: 1.2 }}>ICD-10 Coding Assistant</div>
|
|
299
|
-
<div style={{ color: "rgba(255,255,255,0.75)", fontSize: 11 }}>Quick ICD-10 lookup</div>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
<button onClick={onClose} style={{ background: "rgba(255,255,255,0.2)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: C.white, cursor: "pointer", fontSize: 18, fontWeight: "bold", width: 32, height: 32, display: "flex", alignItems: "center", justifyContent: "center" }}>×</button>
|
|
303
|
-
</div>
|
|
304
|
-
<ModeToggle mode={mode} onChange={onModeChange} />
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{/* Body */}
|
|
308
|
-
<div style={{ flex: 1, overflowY: "auto", padding: "16px 18px", display: "flex", flexDirection: "column", gap: 10 }}>
|
|
309
|
-
<input
|
|
310
|
-
ref={inputRef}
|
|
311
|
-
type="text"
|
|
312
|
-
value={query}
|
|
313
|
-
onChange={handleChange}
|
|
314
|
-
placeholder="Type a diagnosis, e.g. diabetes, hypertension…"
|
|
315
|
-
style={{
|
|
316
|
-
width: "100%",
|
|
317
|
-
padding: "10px 14px",
|
|
318
|
-
fontSize: 14,
|
|
319
|
-
fontFamily: FONT,
|
|
320
|
-
border: `1.5px solid ${C.border}`,
|
|
321
|
-
borderRadius: 8,
|
|
322
|
-
outline: "none",
|
|
323
|
-
boxSizing: "border-box",
|
|
324
|
-
transition: "border-color 0.15s",
|
|
325
|
-
}}
|
|
326
|
-
onFocus={(e) => (e.target.style.borderColor = C.primary)}
|
|
327
|
-
onBlur={(e) => (e.target.style.borderColor = C.border)}
|
|
328
|
-
/>
|
|
329
|
-
|
|
330
|
-
{/* Persistent note */}
|
|
331
|
-
<p style={{ margin: 0, fontSize: 11, color: "#aaa", lineHeight: 1.6, borderLeft: `3px solid ${C.border}`, paddingLeft: 8 }}>
|
|
332
|
-
<strong>Note:</strong> Use short, specific keywords for better results, or try{" "}
|
|
333
|
-
<button
|
|
334
|
-
onClick={() => onModeChange("ai")}
|
|
335
|
-
style={{
|
|
336
|
-
background: "none",
|
|
337
|
-
border: "none",
|
|
338
|
-
padding: 0,
|
|
339
|
-
color: C.primary,
|
|
340
|
-
fontWeight: 700,
|
|
341
|
-
fontSize: 11,
|
|
342
|
-
cursor: "pointer",
|
|
343
|
-
textDecoration: "underline",
|
|
344
|
-
fontFamily: FONT,
|
|
345
|
-
}}
|
|
346
|
-
>
|
|
347
|
-
AI Suggest
|
|
348
|
-
</button>{" "}
|
|
349
|
-
for broader symptom-based recommendations.
|
|
350
|
-
</p>
|
|
351
|
-
|
|
352
|
-
{/* Status — only show while searching or when results found */}
|
|
353
|
-
{status && results.length > 0 && (
|
|
354
|
-
<p style={{ margin: 0, fontSize: 12, color: C.muted }}>{status}</p>
|
|
355
|
-
)}
|
|
356
|
-
|
|
357
|
-
{/* No results state */}
|
|
358
|
-
{status === "No matching codes found." && results.length === 0 && (
|
|
359
|
-
<div
|
|
360
|
-
style={{
|
|
361
|
-
background: C.warnLight,
|
|
362
|
-
border: `1px solid #fcd34d`,
|
|
363
|
-
borderRadius: 10,
|
|
364
|
-
padding: "14px 16px",
|
|
365
|
-
display: "flex",
|
|
366
|
-
gap: 10,
|
|
367
|
-
alignItems: "flex-start",
|
|
368
|
-
}}
|
|
369
|
-
>
|
|
370
|
-
<span style={{ fontSize: 18, flexShrink: 0 }}>🔍</span>
|
|
371
|
-
<div>
|
|
372
|
-
<p style={{ margin: "0 0 4px", fontSize: 13, fontWeight: 700, color: C.warn }}>
|
|
373
|
-
No results found
|
|
374
|
-
</p>
|
|
375
|
-
<p style={{ margin: 0, fontSize: 12, color: C.warn, lineHeight: 1.6 }}>
|
|
376
|
-
We couldn't find a match. Try using more accurate keywords or switch to{" "}
|
|
377
|
-
<button
|
|
378
|
-
onClick={() => onModeChange("ai")}
|
|
379
|
-
style={{
|
|
380
|
-
background: "none",
|
|
381
|
-
border: "none",
|
|
382
|
-
padding: 0,
|
|
383
|
-
color: C.primary,
|
|
384
|
-
fontWeight: 700,
|
|
385
|
-
fontSize: 12,
|
|
386
|
-
cursor: "pointer",
|
|
387
|
-
textDecoration: "underline",
|
|
388
|
-
fontFamily: FONT,
|
|
389
|
-
}}
|
|
390
|
-
>
|
|
391
|
-
AI Suggest
|
|
392
|
-
</button>{" "}
|
|
393
|
-
for better results.
|
|
394
|
-
</p>
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
)}
|
|
398
|
-
|
|
399
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
400
|
-
{results.map(([code, name]) => (
|
|
401
|
-
<div
|
|
402
|
-
key={code}
|
|
403
|
-
onClick={() => copyCode(code)}
|
|
404
|
-
style={{
|
|
405
|
-
display: "flex",
|
|
406
|
-
alignItems: "center",
|
|
407
|
-
gap: 12,
|
|
408
|
-
padding: "10px 14px",
|
|
409
|
-
border: `1px solid ${C.border}`,
|
|
410
|
-
borderRadius: 8,
|
|
411
|
-
cursor: "pointer",
|
|
412
|
-
background: C.white,
|
|
413
|
-
transition: "background 0.1s",
|
|
414
|
-
}}
|
|
415
|
-
onMouseEnter={(e) => (e.currentTarget.style.background = C.bg)}
|
|
416
|
-
onMouseLeave={(e) => (e.currentTarget.style.background = C.white)}
|
|
417
|
-
>
|
|
418
|
-
<span style={{ fontFamily: "monospace", fontSize: 12, background: C.primaryLight, color: C.primary, padding: "3px 8px", borderRadius: 6, whiteSpace: "nowrap", fontWeight: 700 }}>
|
|
419
|
-
{code}
|
|
420
|
-
</span>
|
|
421
|
-
<span style={{ fontSize: 13, flex: 1, color: C.text }}>{name}</span>
|
|
422
|
-
<span style={{ fontSize: 11, color: copied === code ? C.success : C.muted, fontWeight: copied === code ? 700 : 400 }}>
|
|
423
|
-
{copied === code ? "✓ copied" : "copy"}
|
|
424
|
-
</span>
|
|
425
|
-
</div>
|
|
426
|
-
))}
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
|
|
430
|
-
{/* Footer */}
|
|
431
|
-
<div style={{ flexShrink: 0, padding: "10px 18px", borderTop: `1px solid ${C.border}`, background: C.bg, fontSize: 11, color: "#aaa", textAlign: "center" }}>
|
|
432
|
-
{ICON.medical} For reference only. Always verify codes with a certified medical coder.
|
|
433
|
-
</div>
|
|
434
|
-
</div>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ─── FloatingButton ───────────────────────────────────────────────────────────
|
|
439
|
-
// Defined OUTSIDE the main component so React never remounts it on re-render.
|
|
440
|
-
function FloatingButton({ onOpen }) {
|
|
441
|
-
return (
|
|
442
|
-
<button
|
|
443
|
-
onClick={onOpen}
|
|
444
|
-
title="ICD-10 Coding Assistant"
|
|
445
|
-
style={{
|
|
446
|
-
position: "fixed",
|
|
447
|
-
bottom: 28,
|
|
448
|
-
right: 28,
|
|
449
|
-
zIndex: 99998,
|
|
450
|
-
display: "flex",
|
|
451
|
-
alignItems: "center",
|
|
452
|
-
gap: 8,
|
|
453
|
-
padding: "10px 18px",
|
|
454
|
-
borderRadius: 999,
|
|
455
|
-
border: "none",
|
|
456
|
-
background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`,
|
|
457
|
-
color: C.white,
|
|
458
|
-
fontFamily: FONT,
|
|
459
|
-
fontWeight: 700,
|
|
460
|
-
fontSize: 13,
|
|
461
|
-
cursor: "pointer",
|
|
462
|
-
boxShadow: "0 4px 16px rgba(76,77,220,0.35)",
|
|
463
|
-
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
|
464
|
-
}}
|
|
465
|
-
onMouseEnter={(e) => {
|
|
466
|
-
e.currentTarget.style.transform = "translateY(-2px)";
|
|
467
|
-
e.currentTarget.style.boxShadow = "0 8px 24px rgba(76,77,220,0.4)";
|
|
468
|
-
}}
|
|
469
|
-
onMouseLeave={(e) => {
|
|
470
|
-
e.currentTarget.style.transform = "translateY(0)";
|
|
471
|
-
e.currentTarget.style.boxShadow = "0 4px 16px rgba(76,77,220,0.35)";
|
|
472
|
-
}}
|
|
473
|
-
>
|
|
474
|
-
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 22, height: 22, objectFit: "contain" }} />
|
|
475
|
-
ICD-10 Assistant
|
|
476
|
-
</button>
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ─── Panel ────────────────────────────────────────────────────────────────────
|
|
481
|
-
// Defined OUTSIDE the main component so its DOM is never torn down on re-render.
|
|
482
|
-
function Panel({
|
|
483
|
-
panelRef,
|
|
484
|
-
textareaRef,
|
|
485
|
-
query,
|
|
486
|
-
loading,
|
|
487
|
-
result,
|
|
488
|
-
error,
|
|
489
|
-
history,
|
|
490
|
-
copy,
|
|
491
|
-
copiedKey,
|
|
492
|
-
onQueryChange,
|
|
493
|
-
onKeyDown,
|
|
494
|
-
onSubmit,
|
|
495
|
-
onClear,
|
|
496
|
-
onClose,
|
|
497
|
-
onHistoryClick,
|
|
498
|
-
mode,
|
|
499
|
-
onModeChange,
|
|
500
|
-
}) {
|
|
501
|
-
const allCodesText =
|
|
502
|
-
result?.matches?.map((m) => `${m.code} — ${m.description}`).join("\n") || "";
|
|
503
|
-
|
|
504
|
-
return (
|
|
505
|
-
<div
|
|
506
|
-
style={{
|
|
507
|
-
position: "fixed",
|
|
508
|
-
bottom: 28,
|
|
509
|
-
right: 28,
|
|
510
|
-
zIndex: 99999,
|
|
511
|
-
width: "min(480px, calc(100vw - 32px))",
|
|
512
|
-
maxHeight: "calc(100vh - 56px)",
|
|
513
|
-
display: "flex",
|
|
514
|
-
flexDirection: "column",
|
|
515
|
-
background: C.white,
|
|
516
|
-
borderRadius: 16,
|
|
517
|
-
boxShadow: "0 12px 48px rgba(0,0,0,0.18)",
|
|
518
|
-
fontFamily: FONT,
|
|
519
|
-
overflow: "hidden",
|
|
520
|
-
animation: "icd-slide-in 0.2s ease",
|
|
521
|
-
}}
|
|
522
|
-
ref={panelRef}
|
|
523
|
-
>
|
|
524
|
-
<style>{`
|
|
525
|
-
@keyframes icd-slide-in {
|
|
526
|
-
from { opacity: 0; transform: translateY(20px) scale(0.97); }
|
|
527
|
-
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
528
|
-
}
|
|
529
|
-
`}</style>
|
|
530
|
-
|
|
531
|
-
{/* ── Header ── */}
|
|
532
|
-
<div
|
|
533
|
-
style={{
|
|
534
|
-
background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`,
|
|
535
|
-
padding: "14px 18px",
|
|
536
|
-
flexShrink: 0,
|
|
537
|
-
}}
|
|
538
|
-
>
|
|
539
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
540
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
541
|
-
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
|
542
|
-
<div>
|
|
543
|
-
<div style={{ color: C.white, fontWeight: 700, fontSize: 15, lineHeight: 1.2 }}>
|
|
544
|
-
ICD-10 Coding Assistant
|
|
545
|
-
</div>
|
|
546
|
-
<div style={{ color: "rgba(255,255,255,0.75)", fontSize: 11 }}>
|
|
547
|
-
AI-powered · Medical coding helper
|
|
548
|
-
</div>
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
<button
|
|
552
|
-
onClick={onClose}
|
|
553
|
-
style={{
|
|
554
|
-
background: "rgba(255,255,255,0.2)",
|
|
555
|
-
border: "1px solid rgba(255,255,255,0.3)",
|
|
556
|
-
borderRadius: 8,
|
|
557
|
-
color: C.white,
|
|
558
|
-
cursor: "pointer",
|
|
559
|
-
fontSize: 18,
|
|
560
|
-
fontWeight: "bold",
|
|
561
|
-
width: 32,
|
|
562
|
-
height: 32,
|
|
563
|
-
display: "flex",
|
|
564
|
-
alignItems: "center",
|
|
565
|
-
justifyContent: "center",
|
|
566
|
-
flexShrink: 0,
|
|
567
|
-
}}
|
|
568
|
-
>
|
|
569
|
-
×
|
|
570
|
-
</button>
|
|
571
|
-
</div>
|
|
572
|
-
<ModeToggle mode={mode} onChange={onModeChange} />
|
|
573
|
-
</div>
|
|
574
|
-
|
|
575
|
-
{/* ── Scrollable body ── */}
|
|
576
|
-
<div
|
|
577
|
-
style={{
|
|
578
|
-
overflowY: "auto",
|
|
579
|
-
flex: 1,
|
|
580
|
-
padding: "18px 18px 0",
|
|
581
|
-
display: "flex",
|
|
582
|
-
flexDirection: "column",
|
|
583
|
-
gap: 14,
|
|
584
|
-
}}
|
|
585
|
-
>
|
|
586
|
-
{/* Helper text */}
|
|
587
|
-
<p style={{ margin: 0, fontSize: 13, color: C.muted, lineHeight: 1.6 }}>
|
|
588
|
-
Describe a diagnosis, condition, or procedure in plain language and get
|
|
589
|
-
relevant ICD-10 code suggestions for insurance and billing.
|
|
590
|
-
</p>
|
|
591
|
-
|
|
592
|
-
{/* Textarea */}
|
|
593
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
594
|
-
<label
|
|
595
|
-
style={{ fontSize: 12, fontWeight: 600, color: C.text }}
|
|
596
|
-
htmlFor="icd10-query"
|
|
597
|
-
>
|
|
598
|
-
Diagnosis / Condition / Procedure
|
|
599
|
-
</label>
|
|
600
|
-
<textarea
|
|
601
|
-
id="icd10-query"
|
|
602
|
-
ref={textareaRef}
|
|
603
|
-
value={query}
|
|
604
|
-
onChange={onQueryChange}
|
|
605
|
-
onKeyDown={onKeyDown}
|
|
606
|
-
placeholder="e.g. Type 2 diabetes with hypertension, chest pain on exertion, rotator cuff repair…"
|
|
607
|
-
rows={3}
|
|
608
|
-
style={{
|
|
609
|
-
width: "100%",
|
|
610
|
-
padding: "10px 12px",
|
|
611
|
-
border: `1.5px solid ${C.border}`,
|
|
612
|
-
borderRadius: 8,
|
|
613
|
-
fontSize: 13,
|
|
614
|
-
fontFamily: FONT,
|
|
615
|
-
color: C.text,
|
|
616
|
-
resize: "vertical",
|
|
617
|
-
outline: "none",
|
|
618
|
-
boxSizing: "border-box",
|
|
619
|
-
lineHeight: 1.5,
|
|
620
|
-
transition: "border-color 0.15s",
|
|
621
|
-
}}
|
|
622
|
-
onFocus={(e) => (e.target.style.borderColor = C.primary)}
|
|
623
|
-
onBlur={(e) => (e.target.style.borderColor = C.border)}
|
|
624
|
-
/>
|
|
625
|
-
<div style={{ fontSize: 11, color: "#aaa", textAlign: "right" }}>
|
|
626
|
-
Press <kbd style={{ background: "#f3f4f6", padding: "1px 5px", borderRadius: 3, fontFamily: "monospace" }}>Enter</kbd> to submit · <kbd style={{ background: "#f3f4f6", padding: "1px 5px", borderRadius: 3, fontFamily: "monospace" }}>Shift+Enter</kbd> for new line
|
|
627
|
-
</div>
|
|
628
|
-
</div>
|
|
629
|
-
|
|
630
|
-
{/* Action buttons */}
|
|
631
|
-
<div style={{ display: "flex", gap: 8 }}>
|
|
632
|
-
<button
|
|
633
|
-
onClick={onSubmit}
|
|
634
|
-
disabled={!query.trim() || loading}
|
|
635
|
-
style={{
|
|
636
|
-
flex: 1,
|
|
637
|
-
display: "flex",
|
|
638
|
-
alignItems: "center",
|
|
639
|
-
justifyContent: "center",
|
|
640
|
-
gap: 8,
|
|
641
|
-
padding: "10px 16px",
|
|
642
|
-
background:
|
|
643
|
-
!query.trim() || loading
|
|
644
|
-
? "#d1d5db"
|
|
645
|
-
: `linear-gradient(135deg, ${C.primary} 0%, ${C.primaryDark} 100%)`,
|
|
646
|
-
color: C.white,
|
|
647
|
-
border: "none",
|
|
648
|
-
borderRadius: 8,
|
|
649
|
-
fontSize: 13,
|
|
650
|
-
fontWeight: 700,
|
|
651
|
-
fontFamily: FONT,
|
|
652
|
-
cursor: !query.trim() || loading ? "not-allowed" : "pointer",
|
|
653
|
-
transition: "background 0.15s",
|
|
654
|
-
}}
|
|
655
|
-
>
|
|
656
|
-
{loading ? (
|
|
657
|
-
<>
|
|
658
|
-
<Spinner />
|
|
659
|
-
Searching codes…
|
|
660
|
-
</>
|
|
661
|
-
) : (
|
|
662
|
-
"Find ICD-10 Codes"
|
|
663
|
-
)}
|
|
664
|
-
</button>
|
|
665
|
-
{(query || result) && (
|
|
666
|
-
<button
|
|
667
|
-
onClick={onClear}
|
|
668
|
-
style={{
|
|
669
|
-
padding: "10px 14px",
|
|
670
|
-
background: C.bg,
|
|
671
|
-
border: `1px solid ${C.border}`,
|
|
672
|
-
borderRadius: 8,
|
|
673
|
-
fontSize: 13,
|
|
674
|
-
fontWeight: 600,
|
|
675
|
-
fontFamily: FONT,
|
|
676
|
-
color: C.muted,
|
|
677
|
-
cursor: "pointer",
|
|
678
|
-
}}
|
|
679
|
-
>
|
|
680
|
-
Clear
|
|
681
|
-
</button>
|
|
682
|
-
)}
|
|
683
|
-
</div>
|
|
684
|
-
|
|
685
|
-
{/* Error */}
|
|
686
|
-
{error && (
|
|
687
|
-
<div
|
|
688
|
-
style={{
|
|
689
|
-
background: C.errorLight,
|
|
690
|
-
border: `1px solid #fca5a5`,
|
|
691
|
-
borderRadius: 8,
|
|
692
|
-
padding: "10px 14px",
|
|
693
|
-
fontSize: 13,
|
|
694
|
-
color: C.error,
|
|
695
|
-
display: "flex",
|
|
696
|
-
gap: 8,
|
|
697
|
-
}}
|
|
698
|
-
>
|
|
699
|
-
<span>⚠️</span>
|
|
700
|
-
<span>{error}</span>
|
|
701
|
-
</div>
|
|
702
|
-
)}
|
|
703
|
-
|
|
704
|
-
{/* Results */}
|
|
705
|
-
{result && (
|
|
706
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
707
|
-
{/* Results header */}
|
|
708
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
709
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
710
|
-
<span style={{ fontSize: 13, fontWeight: 700, color: C.text }}>
|
|
711
|
-
Suggested Codes
|
|
712
|
-
</span>
|
|
713
|
-
<Badge color={C.teal} bg={C.tealLight}>
|
|
714
|
-
{result.matches.length} match{result.matches.length !== 1 ? "es" : ""}
|
|
715
|
-
</Badge>
|
|
716
|
-
</div>
|
|
717
|
-
{result.matches.length > 0 && (
|
|
718
|
-
<CopyButton
|
|
719
|
-
text={allCodesText}
|
|
720
|
-
label="All"
|
|
721
|
-
copyKey="copy-all"
|
|
722
|
-
copiedKey={copiedKey}
|
|
723
|
-
copy={copy}
|
|
724
|
-
/>
|
|
725
|
-
)}
|
|
726
|
-
</div>
|
|
727
|
-
|
|
728
|
-
{/* No matches */}
|
|
729
|
-
{result.matches.length === 0 && (
|
|
730
|
-
<div
|
|
731
|
-
style={{
|
|
732
|
-
padding: "20px",
|
|
733
|
-
textAlign: "center",
|
|
734
|
-
color: C.muted,
|
|
735
|
-
fontSize: 13,
|
|
736
|
-
background: C.bg,
|
|
737
|
-
borderRadius: 10,
|
|
738
|
-
}}
|
|
739
|
-
>
|
|
740
|
-
No matching ICD-10 codes found.
|
|
741
|
-
</div>
|
|
742
|
-
)}
|
|
743
|
-
|
|
744
|
-
{/* Code cards */}
|
|
745
|
-
{result.matches.map((match, i) => (
|
|
746
|
-
<ResultCard
|
|
747
|
-
key={i}
|
|
748
|
-
index={i}
|
|
749
|
-
match={match}
|
|
750
|
-
copy={copy}
|
|
751
|
-
copiedKey={copiedKey}
|
|
752
|
-
/>
|
|
753
|
-
))}
|
|
754
|
-
|
|
755
|
-
{/* Clinical note — temporarily hidden, re-enable when UX is confirmed */}
|
|
756
|
-
{/* {result.note && (
|
|
757
|
-
<div
|
|
758
|
-
style={{
|
|
759
|
-
background: C.primaryLight,
|
|
760
|
-
border: `1px solid #c7d2fe`,
|
|
761
|
-
borderRadius: 8,
|
|
762
|
-
padding: "10px 14px",
|
|
763
|
-
fontSize: 12,
|
|
764
|
-
color: C.primaryDark,
|
|
765
|
-
lineHeight: 1.5,
|
|
766
|
-
}}
|
|
767
|
-
>
|
|
768
|
-
📋 <strong>Coding note:</strong> {result.note}
|
|
769
|
-
</div>
|
|
770
|
-
)} */}
|
|
771
|
-
</div>
|
|
772
|
-
)}
|
|
773
|
-
|
|
774
|
-
{/* Search history */}
|
|
775
|
-
{history.length > 0 && (
|
|
776
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
777
|
-
<span style={{ fontSize: 11, fontWeight: 600, color: "#aaa", textTransform: "uppercase", letterSpacing: "0.5px" }}>
|
|
778
|
-
Recent searches
|
|
779
|
-
</span>
|
|
780
|
-
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
|
781
|
-
{history.map((item, i) => (
|
|
782
|
-
<HistoryChip key={i} item={item} onClick={onHistoryClick} />
|
|
783
|
-
))}
|
|
784
|
-
</div>
|
|
785
|
-
</div>
|
|
786
|
-
)}
|
|
787
|
-
</div>
|
|
788
|
-
|
|
789
|
-
{/* ── Footer disclaimer ── */}
|
|
790
|
-
<div
|
|
791
|
-
style={{
|
|
792
|
-
flexShrink: 0,
|
|
793
|
-
padding: "12px 18px",
|
|
794
|
-
borderTop: `1px solid ${C.border}`,
|
|
795
|
-
background: C.bg,
|
|
796
|
-
fontSize: 11,
|
|
797
|
-
color: "#aaa",
|
|
798
|
-
lineHeight: 1.5,
|
|
799
|
-
textAlign: "center",
|
|
800
|
-
}}
|
|
801
|
-
>
|
|
802
|
-
{ICON.medical} For reference only. Always verify codes with a certified medical coder.
|
|
803
|
-
Not a substitute for clinical judgment or official coding guidelines.
|
|
804
|
-
</div>
|
|
805
|
-
</div>
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// ─── Main Component ────────────────────────────────────────────────────────────
|
|
810
|
-
export default function ICD10Assistant() {
|
|
811
|
-
const [open, setOpen] = useState(false);
|
|
812
|
-
const [mode, setMode] = useState("nlm"); // "nlm" | "ai"
|
|
813
|
-
const [query, setQuery] = useState("");
|
|
814
|
-
const [loading, setLoading] = useState(false);
|
|
815
|
-
const [result, setResult] = useState(null);
|
|
816
|
-
const [error, setError] = useState(null);
|
|
817
|
-
const [history, setHistory] = useState([]);
|
|
818
|
-
|
|
819
|
-
const textareaRef = useRef(null);
|
|
820
|
-
const panelRef = useRef(null);
|
|
821
|
-
const { copy, copiedKey } = useClipboard();
|
|
822
|
-
|
|
823
|
-
// Focus textarea when panel opens
|
|
824
|
-
useEffect(() => {
|
|
825
|
-
if (open && textareaRef.current) {
|
|
826
|
-
setTimeout(() => textareaRef.current?.focus(), 80);
|
|
827
|
-
}
|
|
828
|
-
}, [open]);
|
|
829
|
-
|
|
830
|
-
// Close panel on outside click
|
|
831
|
-
useEffect(() => {
|
|
832
|
-
if (!open) return;
|
|
833
|
-
const handler = (e) => {
|
|
834
|
-
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
|
835
|
-
setOpen(false);
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
document.addEventListener("mousedown", handler);
|
|
839
|
-
return () => document.removeEventListener("mousedown", handler);
|
|
840
|
-
}, [open]);
|
|
841
|
-
|
|
842
|
-
const handleSubmit = useCallback(async () => {
|
|
843
|
-
if (!query.trim() || loading) return;
|
|
844
|
-
setLoading(true);
|
|
845
|
-
setError(null);
|
|
846
|
-
setResult(null);
|
|
847
|
-
try {
|
|
848
|
-
const data = await getICDSuggestions(query.trim(), ICD_MODE.CLAUDE_ONLY);
|
|
849
|
-
setResult(data);
|
|
850
|
-
setHistory((prev) => {
|
|
851
|
-
const filtered = prev.filter(
|
|
852
|
-
(h) => h.query.toLowerCase() !== query.trim().toLowerCase()
|
|
853
|
-
);
|
|
854
|
-
return [{ query: query.trim(), matches: data.matches }, ...filtered].slice(0, MAX_HISTORY);
|
|
855
|
-
});
|
|
856
|
-
} catch (err) {
|
|
857
|
-
setError(err.message || "Something went wrong. Please try again.");
|
|
858
|
-
} finally {
|
|
859
|
-
setLoading(false);
|
|
860
|
-
}
|
|
861
|
-
}, [query, loading]);
|
|
862
|
-
|
|
863
|
-
const handleKeyDown = useCallback(
|
|
864
|
-
(e) => {
|
|
865
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
866
|
-
e.preventDefault();
|
|
867
|
-
handleSubmit();
|
|
868
|
-
}
|
|
869
|
-
},
|
|
870
|
-
[handleSubmit]
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
const handleClear = useCallback(() => {
|
|
874
|
-
setQuery("");
|
|
875
|
-
setResult(null);
|
|
876
|
-
setError(null);
|
|
877
|
-
textareaRef.current?.focus();
|
|
878
|
-
}, []);
|
|
879
|
-
|
|
880
|
-
const handleHistoryClick = useCallback((q) => {
|
|
881
|
-
setQuery(q);
|
|
882
|
-
setResult(null);
|
|
883
|
-
setError(null);
|
|
884
|
-
textareaRef.current?.focus();
|
|
885
|
-
}, []);
|
|
886
|
-
|
|
887
|
-
const handleQueryChange = useCallback((e) => setQuery(e.target.value), []);
|
|
888
|
-
|
|
889
|
-
return (
|
|
890
|
-
<>
|
|
891
|
-
{!open && <FloatingButton onOpen={() => setOpen(true)} />}
|
|
892
|
-
{open && mode === "nlm" && (
|
|
893
|
-
<NLMLookupPanel
|
|
894
|
-
panelRef={panelRef}
|
|
895
|
-
onClose={() => setOpen(false)}
|
|
896
|
-
mode={mode}
|
|
897
|
-
onModeChange={setMode}
|
|
898
|
-
/>
|
|
899
|
-
)}
|
|
900
|
-
{open && mode === "ai" && (
|
|
901
|
-
<Panel
|
|
902
|
-
panelRef={panelRef}
|
|
903
|
-
textareaRef={textareaRef}
|
|
904
|
-
query={query}
|
|
905
|
-
loading={loading}
|
|
906
|
-
result={result}
|
|
907
|
-
error={error}
|
|
908
|
-
history={history}
|
|
909
|
-
copy={copy}
|
|
910
|
-
copiedKey={copiedKey}
|
|
911
|
-
onQueryChange={handleQueryChange}
|
|
912
|
-
onKeyDown={handleKeyDown}
|
|
913
|
-
onSubmit={handleSubmit}
|
|
914
|
-
onClear={handleClear}
|
|
915
|
-
onClose={() => setOpen(false)}
|
|
916
|
-
onHistoryClick={handleHistoryClick}
|
|
917
|
-
mode={mode}
|
|
918
|
-
onModeChange={setMode}
|
|
919
|
-
/>
|
|
920
|
-
)}
|
|
921
|
-
</>
|
|
922
|
-
);
|
|
923
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ICD10Assistant.jsx
|
|
3
|
+
* AI-powered ICD-10 coding assistant panel.
|
|
4
|
+
* Floats as a pill button; expands into a full panel on click.
|
|
5
|
+
*/
|
|
6
|
+
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
7
|
+
import { getICDSuggestions, ICD_MODE } from "../services/icdService";
|
|
8
|
+
import { useClipboard } from "../hooks/useClipboard";
|
|
9
|
+
import { IMG, ICON } from "../assets/icons/icdIcons";
|
|
10
|
+
|
|
11
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
12
|
+
const MAX_HISTORY = 3;
|
|
13
|
+
const FONT = '"Nunito", sans-serif';
|
|
14
|
+
|
|
15
|
+
// ─── Colour tokens ────────────────────────────────────────────────────────────
|
|
16
|
+
const C = {
|
|
17
|
+
primary: "#4C4DDC",
|
|
18
|
+
primaryDark: "#3A3BBD",
|
|
19
|
+
primaryLight: "#E8EEF4",
|
|
20
|
+
teal: "#1CC3CE",
|
|
21
|
+
tealLight: "#E0F8F9",
|
|
22
|
+
success: "#16a34a",
|
|
23
|
+
successLight: "#dcfce7",
|
|
24
|
+
error: "#dc2626",
|
|
25
|
+
errorLight: "#fee2e2",
|
|
26
|
+
warn: "#d97706",
|
|
27
|
+
warnLight: "#fef3c7",
|
|
28
|
+
text: "#1a1a1a",
|
|
29
|
+
muted: "#666",
|
|
30
|
+
border: "#E5E5E5",
|
|
31
|
+
bg: "#F5F5F7",
|
|
32
|
+
white: "#FFFFFF",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
36
|
+
function Badge({ children, color = C.primary, bg = C.primaryLight }) {
|
|
37
|
+
return (
|
|
38
|
+
<span
|
|
39
|
+
style={{
|
|
40
|
+
display: "inline-flex",
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
padding: "2px 10px",
|
|
43
|
+
borderRadius: "999px",
|
|
44
|
+
fontSize: "12px",
|
|
45
|
+
fontWeight: 700,
|
|
46
|
+
color,
|
|
47
|
+
background: bg,
|
|
48
|
+
letterSpacing: "0.5px",
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Spinner() {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<style>{`@keyframes icd-spin{to{transform:rotate(360deg)}}`}</style>
|
|
60
|
+
<div
|
|
61
|
+
style={{
|
|
62
|
+
width: 20,
|
|
63
|
+
height: 20,
|
|
64
|
+
border: "2.5px solid #d1d5db",
|
|
65
|
+
borderTop: `2.5px solid ${C.primary}`,
|
|
66
|
+
borderRadius: "50%",
|
|
67
|
+
animation: "icd-spin 0.8s linear infinite",
|
|
68
|
+
flexShrink: 0,
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function CopyButton({ text, label = "Copy", copyKey, copiedKey, copy }) {
|
|
76
|
+
const copied = copiedKey === copyKey;
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => copy(text, copyKey)}
|
|
80
|
+
title={copied ? "Copied!" : `Copy ${label}`}
|
|
81
|
+
style={{
|
|
82
|
+
display: "inline-flex",
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
gap: 4,
|
|
85
|
+
padding: "3px 10px",
|
|
86
|
+
fontSize: 11,
|
|
87
|
+
fontWeight: 600,
|
|
88
|
+
fontFamily: FONT,
|
|
89
|
+
border: `1px solid ${copied ? C.success : C.border}`,
|
|
90
|
+
borderRadius: 5,
|
|
91
|
+
background: copied ? C.successLight : C.white,
|
|
92
|
+
color: copied ? C.success : C.muted,
|
|
93
|
+
cursor: "pointer",
|
|
94
|
+
transition: "all 0.15s ease",
|
|
95
|
+
whiteSpace: "nowrap",
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{copied ? "✓ Copied" : `⎘ ${label}`}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Result Card ──────────────────────────────────────────────────────────────
|
|
104
|
+
function ResultCard({ match, index, copy, copiedKey }) {
|
|
105
|
+
const cardCopyKey = `card-${index}`;
|
|
106
|
+
const fullText = `${match.code} — ${match.description}${match.reason ? `\nNote: ${match.reason}` : ""}`;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
style={{
|
|
111
|
+
border: `1px solid ${C.border}`,
|
|
112
|
+
borderRadius: 10,
|
|
113
|
+
padding: "14px 16px",
|
|
114
|
+
background: C.white,
|
|
115
|
+
display: "flex",
|
|
116
|
+
flexDirection: "column",
|
|
117
|
+
gap: 6,
|
|
118
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.06)",
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 8 }}>
|
|
122
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
123
|
+
<Badge color={C.primary} bg={C.primaryLight}>
|
|
124
|
+
{match.code}
|
|
125
|
+
</Badge>
|
|
126
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: C.text, lineHeight: 1.4 }}>
|
|
127
|
+
{match.description}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<CopyButton
|
|
131
|
+
text={fullText}
|
|
132
|
+
label={match.code}
|
|
133
|
+
copyKey={cardCopyKey}
|
|
134
|
+
copiedKey={copiedKey}
|
|
135
|
+
copy={copy}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
{match.reason && (
|
|
139
|
+
<p style={{ margin: 0, fontSize: 12, color: C.muted, lineHeight: 1.5, paddingLeft: 2 }}>
|
|
140
|
+
{ICON.reason} {match.reason}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── History Chip ─────────────────────────────────────────────────────────────
|
|
148
|
+
function HistoryChip({ item, onClick }) {
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => onClick(item.query)}
|
|
152
|
+
title={`Re-run: ${item.query}`}
|
|
153
|
+
style={{
|
|
154
|
+
padding: "4px 10px",
|
|
155
|
+
border: `1px solid ${C.border}`,
|
|
156
|
+
borderRadius: 999,
|
|
157
|
+
fontSize: 11,
|
|
158
|
+
fontFamily: FONT,
|
|
159
|
+
color: C.muted,
|
|
160
|
+
background: C.white,
|
|
161
|
+
cursor: "pointer",
|
|
162
|
+
maxWidth: 180,
|
|
163
|
+
overflow: "hidden",
|
|
164
|
+
textOverflow: "ellipsis",
|
|
165
|
+
whiteSpace: "nowrap",
|
|
166
|
+
transition: "border-color 0.15s",
|
|
167
|
+
}}
|
|
168
|
+
onMouseEnter={(e) => (e.currentTarget.style.borderColor = C.primary)}
|
|
169
|
+
onMouseLeave={(e) => (e.currentTarget.style.borderColor = C.border)}
|
|
170
|
+
>
|
|
171
|
+
{ICON.history} {item.query}
|
|
172
|
+
</button>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Mode Toggle (tab bar) ──────────────────────────────────────────────────
|
|
177
|
+
function ModeToggle({ mode, onChange }) {
|
|
178
|
+
const tab = (id, label, icon) => (
|
|
179
|
+
<button
|
|
180
|
+
onClick={() => onChange(id)}
|
|
181
|
+
style={{
|
|
182
|
+
flex: 1,
|
|
183
|
+
padding: "6px 0",
|
|
184
|
+
border: "none",
|
|
185
|
+
borderRadius: 6,
|
|
186
|
+
fontSize: 12,
|
|
187
|
+
fontWeight: 700,
|
|
188
|
+
fontFamily: FONT,
|
|
189
|
+
cursor: "pointer",
|
|
190
|
+
transition: "all 0.15s",
|
|
191
|
+
background: mode === id ? C.white : "transparent",
|
|
192
|
+
color: mode === id ? C.primary : "rgba(255,255,255,0.7)",
|
|
193
|
+
boxShadow: mode === id ? "0 1px 4px rgba(0,0,0,0.15)" : "none",
|
|
194
|
+
display: "flex",
|
|
195
|
+
alignItems: "center",
|
|
196
|
+
justifyContent: "center",
|
|
197
|
+
gap: 5,
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<span>{icon}</span> {label}
|
|
201
|
+
</button>
|
|
202
|
+
);
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
style={{
|
|
206
|
+
display: "flex",
|
|
207
|
+
background: "rgba(255,255,255,0.15)",
|
|
208
|
+
borderRadius: 8,
|
|
209
|
+
padding: 3,
|
|
210
|
+
gap: 2,
|
|
211
|
+
marginTop: 10,
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{tab("nlm", "Quick Lookup", "")}
|
|
215
|
+
{tab("ai", "AI Suggest", "")}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── NLM Lookup Panel ────────────────────────────────────────────────────────
|
|
221
|
+
// Uses the free NLM Clinical Tables API — no API key, no credits needed.
|
|
222
|
+
function NLMLookupPanel({ panelRef, onClose, mode, onModeChange }) {
|
|
223
|
+
const [query, setQuery] = useState("");
|
|
224
|
+
const [results, setResults] = useState([]);
|
|
225
|
+
const [status, setStatus] = useState("");
|
|
226
|
+
const [copied, setCopied] = useState("");
|
|
227
|
+
const debounceRef = useRef(null);
|
|
228
|
+
const inputRef = useRef(null);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
setTimeout(() => inputRef.current?.focus(), 80);
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
const search = useCallback(async (term) => {
|
|
235
|
+
if (term.length < 2) {
|
|
236
|
+
setResults([]);
|
|
237
|
+
setStatus("");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
setStatus("Searching…");
|
|
241
|
+
try {
|
|
242
|
+
const data = await getICDSuggestions(term, ICD_MODE.ICD_ONLY);
|
|
243
|
+
const codes = (data.matches || []).map((m) => [m.code, m.description]);
|
|
244
|
+
setResults(codes);
|
|
245
|
+
setStatus(
|
|
246
|
+
codes.length === 0
|
|
247
|
+
? "No matching codes found."
|
|
248
|
+
: `Showing ${codes.length} result${codes.length !== 1 ? "s" : ""}`
|
|
249
|
+
);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
setStatus(err.message || "Could not fetch results. Check your connection.");
|
|
252
|
+
setResults([]);
|
|
253
|
+
}
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
const handleChange = (e) => {
|
|
257
|
+
const val = e.target.value;
|
|
258
|
+
setQuery(val);
|
|
259
|
+
clearTimeout(debounceRef.current);
|
|
260
|
+
debounceRef.current = setTimeout(() => search(val.trim()), 300);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const copyCode = (code) => {
|
|
264
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
265
|
+
setCopied(code);
|
|
266
|
+
setTimeout(() => setCopied(""), 2000);
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
ref={panelRef}
|
|
273
|
+
style={{
|
|
274
|
+
position: "fixed",
|
|
275
|
+
bottom: 28,
|
|
276
|
+
right: 28,
|
|
277
|
+
zIndex: 99999,
|
|
278
|
+
width: "min(480px, calc(100vw - 32px))",
|
|
279
|
+
maxHeight: "calc(100vh - 56px)",
|
|
280
|
+
display: "flex",
|
|
281
|
+
flexDirection: "column",
|
|
282
|
+
background: C.white,
|
|
283
|
+
borderRadius: 16,
|
|
284
|
+
boxShadow: "0 12px 48px rgba(0,0,0,0.18)",
|
|
285
|
+
fontFamily: FONT,
|
|
286
|
+
overflow: "hidden",
|
|
287
|
+
animation: "icd-slide-in 0.2s ease",
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<style>{`@keyframes icd-slide-in{from{opacity:0;transform:translateY(20px) scale(0.97)}to{opacity:1;transform:translateY(0) scale(1)}}`}</style>
|
|
291
|
+
|
|
292
|
+
{/* Header */}
|
|
293
|
+
<div style={{ background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`, padding: "14px 18px", flexShrink: 0 }}>
|
|
294
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
295
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
296
|
+
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
|
297
|
+
<div>
|
|
298
|
+
<div style={{ color: C.white, fontWeight: 700, fontSize: 15, lineHeight: 1.2 }}>ICD-10 Coding Assistant</div>
|
|
299
|
+
<div style={{ color: "rgba(255,255,255,0.75)", fontSize: 11 }}>Quick ICD-10 lookup</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<button onClick={onClose} style={{ background: "rgba(255,255,255,0.2)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: C.white, cursor: "pointer", fontSize: 18, fontWeight: "bold", width: 32, height: 32, display: "flex", alignItems: "center", justifyContent: "center" }}>×</button>
|
|
303
|
+
</div>
|
|
304
|
+
<ModeToggle mode={mode} onChange={onModeChange} />
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Body */}
|
|
308
|
+
<div style={{ flex: 1, overflowY: "auto", padding: "16px 18px", display: "flex", flexDirection: "column", gap: 10 }}>
|
|
309
|
+
<input
|
|
310
|
+
ref={inputRef}
|
|
311
|
+
type="text"
|
|
312
|
+
value={query}
|
|
313
|
+
onChange={handleChange}
|
|
314
|
+
placeholder="Type a diagnosis, e.g. diabetes, hypertension…"
|
|
315
|
+
style={{
|
|
316
|
+
width: "100%",
|
|
317
|
+
padding: "10px 14px",
|
|
318
|
+
fontSize: 14,
|
|
319
|
+
fontFamily: FONT,
|
|
320
|
+
border: `1.5px solid ${C.border}`,
|
|
321
|
+
borderRadius: 8,
|
|
322
|
+
outline: "none",
|
|
323
|
+
boxSizing: "border-box",
|
|
324
|
+
transition: "border-color 0.15s",
|
|
325
|
+
}}
|
|
326
|
+
onFocus={(e) => (e.target.style.borderColor = C.primary)}
|
|
327
|
+
onBlur={(e) => (e.target.style.borderColor = C.border)}
|
|
328
|
+
/>
|
|
329
|
+
|
|
330
|
+
{/* Persistent note */}
|
|
331
|
+
<p style={{ margin: 0, fontSize: 11, color: "#aaa", lineHeight: 1.6, borderLeft: `3px solid ${C.border}`, paddingLeft: 8 }}>
|
|
332
|
+
<strong>Note:</strong> Use short, specific keywords for better results, or try{" "}
|
|
333
|
+
<button
|
|
334
|
+
onClick={() => onModeChange("ai")}
|
|
335
|
+
style={{
|
|
336
|
+
background: "none",
|
|
337
|
+
border: "none",
|
|
338
|
+
padding: 0,
|
|
339
|
+
color: C.primary,
|
|
340
|
+
fontWeight: 700,
|
|
341
|
+
fontSize: 11,
|
|
342
|
+
cursor: "pointer",
|
|
343
|
+
textDecoration: "underline",
|
|
344
|
+
fontFamily: FONT,
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
AI Suggest
|
|
348
|
+
</button>{" "}
|
|
349
|
+
for broader symptom-based recommendations.
|
|
350
|
+
</p>
|
|
351
|
+
|
|
352
|
+
{/* Status — only show while searching or when results found */}
|
|
353
|
+
{status && results.length > 0 && (
|
|
354
|
+
<p style={{ margin: 0, fontSize: 12, color: C.muted }}>{status}</p>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* No results state */}
|
|
358
|
+
{status === "No matching codes found." && results.length === 0 && (
|
|
359
|
+
<div
|
|
360
|
+
style={{
|
|
361
|
+
background: C.warnLight,
|
|
362
|
+
border: `1px solid #fcd34d`,
|
|
363
|
+
borderRadius: 10,
|
|
364
|
+
padding: "14px 16px",
|
|
365
|
+
display: "flex",
|
|
366
|
+
gap: 10,
|
|
367
|
+
alignItems: "flex-start",
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<span style={{ fontSize: 18, flexShrink: 0 }}>🔍</span>
|
|
371
|
+
<div>
|
|
372
|
+
<p style={{ margin: "0 0 4px", fontSize: 13, fontWeight: 700, color: C.warn }}>
|
|
373
|
+
No results found
|
|
374
|
+
</p>
|
|
375
|
+
<p style={{ margin: 0, fontSize: 12, color: C.warn, lineHeight: 1.6 }}>
|
|
376
|
+
We couldn't find a match. Try using more accurate keywords or switch to{" "}
|
|
377
|
+
<button
|
|
378
|
+
onClick={() => onModeChange("ai")}
|
|
379
|
+
style={{
|
|
380
|
+
background: "none",
|
|
381
|
+
border: "none",
|
|
382
|
+
padding: 0,
|
|
383
|
+
color: C.primary,
|
|
384
|
+
fontWeight: 700,
|
|
385
|
+
fontSize: 12,
|
|
386
|
+
cursor: "pointer",
|
|
387
|
+
textDecoration: "underline",
|
|
388
|
+
fontFamily: FONT,
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
AI Suggest
|
|
392
|
+
</button>{" "}
|
|
393
|
+
for better results.
|
|
394
|
+
</p>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
400
|
+
{results.map(([code, name]) => (
|
|
401
|
+
<div
|
|
402
|
+
key={code}
|
|
403
|
+
onClick={() => copyCode(code)}
|
|
404
|
+
style={{
|
|
405
|
+
display: "flex",
|
|
406
|
+
alignItems: "center",
|
|
407
|
+
gap: 12,
|
|
408
|
+
padding: "10px 14px",
|
|
409
|
+
border: `1px solid ${C.border}`,
|
|
410
|
+
borderRadius: 8,
|
|
411
|
+
cursor: "pointer",
|
|
412
|
+
background: C.white,
|
|
413
|
+
transition: "background 0.1s",
|
|
414
|
+
}}
|
|
415
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = C.bg)}
|
|
416
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = C.white)}
|
|
417
|
+
>
|
|
418
|
+
<span style={{ fontFamily: "monospace", fontSize: 12, background: C.primaryLight, color: C.primary, padding: "3px 8px", borderRadius: 6, whiteSpace: "nowrap", fontWeight: 700 }}>
|
|
419
|
+
{code}
|
|
420
|
+
</span>
|
|
421
|
+
<span style={{ fontSize: 13, flex: 1, color: C.text }}>{name}</span>
|
|
422
|
+
<span style={{ fontSize: 11, color: copied === code ? C.success : C.muted, fontWeight: copied === code ? 700 : 400 }}>
|
|
423
|
+
{copied === code ? "✓ copied" : "copy"}
|
|
424
|
+
</span>
|
|
425
|
+
</div>
|
|
426
|
+
))}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Footer */}
|
|
431
|
+
<div style={{ flexShrink: 0, padding: "10px 18px", borderTop: `1px solid ${C.border}`, background: C.bg, fontSize: 11, color: "#aaa", textAlign: "center" }}>
|
|
432
|
+
{ICON.medical} For reference only. Always verify codes with a certified medical coder.
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── FloatingButton ───────────────────────────────────────────────────────────
|
|
439
|
+
// Defined OUTSIDE the main component so React never remounts it on re-render.
|
|
440
|
+
function FloatingButton({ onOpen }) {
|
|
441
|
+
return (
|
|
442
|
+
<button
|
|
443
|
+
onClick={onOpen}
|
|
444
|
+
title="ICD-10 Coding Assistant"
|
|
445
|
+
style={{
|
|
446
|
+
position: "fixed",
|
|
447
|
+
bottom: 28,
|
|
448
|
+
right: 28,
|
|
449
|
+
zIndex: 99998,
|
|
450
|
+
display: "flex",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 8,
|
|
453
|
+
padding: "10px 18px",
|
|
454
|
+
borderRadius: 999,
|
|
455
|
+
border: "none",
|
|
456
|
+
background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`,
|
|
457
|
+
color: C.white,
|
|
458
|
+
fontFamily: FONT,
|
|
459
|
+
fontWeight: 700,
|
|
460
|
+
fontSize: 13,
|
|
461
|
+
cursor: "pointer",
|
|
462
|
+
boxShadow: "0 4px 16px rgba(76,77,220,0.35)",
|
|
463
|
+
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
|
464
|
+
}}
|
|
465
|
+
onMouseEnter={(e) => {
|
|
466
|
+
e.currentTarget.style.transform = "translateY(-2px)";
|
|
467
|
+
e.currentTarget.style.boxShadow = "0 8px 24px rgba(76,77,220,0.4)";
|
|
468
|
+
}}
|
|
469
|
+
onMouseLeave={(e) => {
|
|
470
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
471
|
+
e.currentTarget.style.boxShadow = "0 4px 16px rgba(76,77,220,0.35)";
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 22, height: 22, objectFit: "contain" }} />
|
|
475
|
+
ICD-10 Assistant
|
|
476
|
+
</button>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ─── Panel ────────────────────────────────────────────────────────────────────
|
|
481
|
+
// Defined OUTSIDE the main component so its DOM is never torn down on re-render.
|
|
482
|
+
function Panel({
|
|
483
|
+
panelRef,
|
|
484
|
+
textareaRef,
|
|
485
|
+
query,
|
|
486
|
+
loading,
|
|
487
|
+
result,
|
|
488
|
+
error,
|
|
489
|
+
history,
|
|
490
|
+
copy,
|
|
491
|
+
copiedKey,
|
|
492
|
+
onQueryChange,
|
|
493
|
+
onKeyDown,
|
|
494
|
+
onSubmit,
|
|
495
|
+
onClear,
|
|
496
|
+
onClose,
|
|
497
|
+
onHistoryClick,
|
|
498
|
+
mode,
|
|
499
|
+
onModeChange,
|
|
500
|
+
}) {
|
|
501
|
+
const allCodesText =
|
|
502
|
+
result?.matches?.map((m) => `${m.code} — ${m.description}`).join("\n") || "";
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<div
|
|
506
|
+
style={{
|
|
507
|
+
position: "fixed",
|
|
508
|
+
bottom: 28,
|
|
509
|
+
right: 28,
|
|
510
|
+
zIndex: 99999,
|
|
511
|
+
width: "min(480px, calc(100vw - 32px))",
|
|
512
|
+
maxHeight: "calc(100vh - 56px)",
|
|
513
|
+
display: "flex",
|
|
514
|
+
flexDirection: "column",
|
|
515
|
+
background: C.white,
|
|
516
|
+
borderRadius: 16,
|
|
517
|
+
boxShadow: "0 12px 48px rgba(0,0,0,0.18)",
|
|
518
|
+
fontFamily: FONT,
|
|
519
|
+
overflow: "hidden",
|
|
520
|
+
animation: "icd-slide-in 0.2s ease",
|
|
521
|
+
}}
|
|
522
|
+
ref={panelRef}
|
|
523
|
+
>
|
|
524
|
+
<style>{`
|
|
525
|
+
@keyframes icd-slide-in {
|
|
526
|
+
from { opacity: 0; transform: translateY(20px) scale(0.97); }
|
|
527
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
528
|
+
}
|
|
529
|
+
`}</style>
|
|
530
|
+
|
|
531
|
+
{/* ── Header ── */}
|
|
532
|
+
<div
|
|
533
|
+
style={{
|
|
534
|
+
background: `linear-gradient(135deg, ${C.primary} 0%, ${C.teal} 100%)`,
|
|
535
|
+
padding: "14px 18px",
|
|
536
|
+
flexShrink: 0,
|
|
537
|
+
}}
|
|
538
|
+
>
|
|
539
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
540
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
541
|
+
<img src={IMG.icdIcon} alt="ICD-10" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
|
542
|
+
<div>
|
|
543
|
+
<div style={{ color: C.white, fontWeight: 700, fontSize: 15, lineHeight: 1.2 }}>
|
|
544
|
+
ICD-10 Coding Assistant
|
|
545
|
+
</div>
|
|
546
|
+
<div style={{ color: "rgba(255,255,255,0.75)", fontSize: 11 }}>
|
|
547
|
+
AI-powered · Medical coding helper
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
<button
|
|
552
|
+
onClick={onClose}
|
|
553
|
+
style={{
|
|
554
|
+
background: "rgba(255,255,255,0.2)",
|
|
555
|
+
border: "1px solid rgba(255,255,255,0.3)",
|
|
556
|
+
borderRadius: 8,
|
|
557
|
+
color: C.white,
|
|
558
|
+
cursor: "pointer",
|
|
559
|
+
fontSize: 18,
|
|
560
|
+
fontWeight: "bold",
|
|
561
|
+
width: 32,
|
|
562
|
+
height: 32,
|
|
563
|
+
display: "flex",
|
|
564
|
+
alignItems: "center",
|
|
565
|
+
justifyContent: "center",
|
|
566
|
+
flexShrink: 0,
|
|
567
|
+
}}
|
|
568
|
+
>
|
|
569
|
+
×
|
|
570
|
+
</button>
|
|
571
|
+
</div>
|
|
572
|
+
<ModeToggle mode={mode} onChange={onModeChange} />
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
{/* ── Scrollable body ── */}
|
|
576
|
+
<div
|
|
577
|
+
style={{
|
|
578
|
+
overflowY: "auto",
|
|
579
|
+
flex: 1,
|
|
580
|
+
padding: "18px 18px 0",
|
|
581
|
+
display: "flex",
|
|
582
|
+
flexDirection: "column",
|
|
583
|
+
gap: 14,
|
|
584
|
+
}}
|
|
585
|
+
>
|
|
586
|
+
{/* Helper text */}
|
|
587
|
+
<p style={{ margin: 0, fontSize: 13, color: C.muted, lineHeight: 1.6 }}>
|
|
588
|
+
Describe a diagnosis, condition, or procedure in plain language and get
|
|
589
|
+
relevant ICD-10 code suggestions for insurance and billing.
|
|
590
|
+
</p>
|
|
591
|
+
|
|
592
|
+
{/* Textarea */}
|
|
593
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
594
|
+
<label
|
|
595
|
+
style={{ fontSize: 12, fontWeight: 600, color: C.text }}
|
|
596
|
+
htmlFor="icd10-query"
|
|
597
|
+
>
|
|
598
|
+
Diagnosis / Condition / Procedure
|
|
599
|
+
</label>
|
|
600
|
+
<textarea
|
|
601
|
+
id="icd10-query"
|
|
602
|
+
ref={textareaRef}
|
|
603
|
+
value={query}
|
|
604
|
+
onChange={onQueryChange}
|
|
605
|
+
onKeyDown={onKeyDown}
|
|
606
|
+
placeholder="e.g. Type 2 diabetes with hypertension, chest pain on exertion, rotator cuff repair…"
|
|
607
|
+
rows={3}
|
|
608
|
+
style={{
|
|
609
|
+
width: "100%",
|
|
610
|
+
padding: "10px 12px",
|
|
611
|
+
border: `1.5px solid ${C.border}`,
|
|
612
|
+
borderRadius: 8,
|
|
613
|
+
fontSize: 13,
|
|
614
|
+
fontFamily: FONT,
|
|
615
|
+
color: C.text,
|
|
616
|
+
resize: "vertical",
|
|
617
|
+
outline: "none",
|
|
618
|
+
boxSizing: "border-box",
|
|
619
|
+
lineHeight: 1.5,
|
|
620
|
+
transition: "border-color 0.15s",
|
|
621
|
+
}}
|
|
622
|
+
onFocus={(e) => (e.target.style.borderColor = C.primary)}
|
|
623
|
+
onBlur={(e) => (e.target.style.borderColor = C.border)}
|
|
624
|
+
/>
|
|
625
|
+
<div style={{ fontSize: 11, color: "#aaa", textAlign: "right" }}>
|
|
626
|
+
Press <kbd style={{ background: "#f3f4f6", padding: "1px 5px", borderRadius: 3, fontFamily: "monospace" }}>Enter</kbd> to submit · <kbd style={{ background: "#f3f4f6", padding: "1px 5px", borderRadius: 3, fontFamily: "monospace" }}>Shift+Enter</kbd> for new line
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{/* Action buttons */}
|
|
631
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
632
|
+
<button
|
|
633
|
+
onClick={onSubmit}
|
|
634
|
+
disabled={!query.trim() || loading}
|
|
635
|
+
style={{
|
|
636
|
+
flex: 1,
|
|
637
|
+
display: "flex",
|
|
638
|
+
alignItems: "center",
|
|
639
|
+
justifyContent: "center",
|
|
640
|
+
gap: 8,
|
|
641
|
+
padding: "10px 16px",
|
|
642
|
+
background:
|
|
643
|
+
!query.trim() || loading
|
|
644
|
+
? "#d1d5db"
|
|
645
|
+
: `linear-gradient(135deg, ${C.primary} 0%, ${C.primaryDark} 100%)`,
|
|
646
|
+
color: C.white,
|
|
647
|
+
border: "none",
|
|
648
|
+
borderRadius: 8,
|
|
649
|
+
fontSize: 13,
|
|
650
|
+
fontWeight: 700,
|
|
651
|
+
fontFamily: FONT,
|
|
652
|
+
cursor: !query.trim() || loading ? "not-allowed" : "pointer",
|
|
653
|
+
transition: "background 0.15s",
|
|
654
|
+
}}
|
|
655
|
+
>
|
|
656
|
+
{loading ? (
|
|
657
|
+
<>
|
|
658
|
+
<Spinner />
|
|
659
|
+
Searching codes…
|
|
660
|
+
</>
|
|
661
|
+
) : (
|
|
662
|
+
"Find ICD-10 Codes"
|
|
663
|
+
)}
|
|
664
|
+
</button>
|
|
665
|
+
{(query || result) && (
|
|
666
|
+
<button
|
|
667
|
+
onClick={onClear}
|
|
668
|
+
style={{
|
|
669
|
+
padding: "10px 14px",
|
|
670
|
+
background: C.bg,
|
|
671
|
+
border: `1px solid ${C.border}`,
|
|
672
|
+
borderRadius: 8,
|
|
673
|
+
fontSize: 13,
|
|
674
|
+
fontWeight: 600,
|
|
675
|
+
fontFamily: FONT,
|
|
676
|
+
color: C.muted,
|
|
677
|
+
cursor: "pointer",
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
Clear
|
|
681
|
+
</button>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
{/* Error */}
|
|
686
|
+
{error && (
|
|
687
|
+
<div
|
|
688
|
+
style={{
|
|
689
|
+
background: C.errorLight,
|
|
690
|
+
border: `1px solid #fca5a5`,
|
|
691
|
+
borderRadius: 8,
|
|
692
|
+
padding: "10px 14px",
|
|
693
|
+
fontSize: 13,
|
|
694
|
+
color: C.error,
|
|
695
|
+
display: "flex",
|
|
696
|
+
gap: 8,
|
|
697
|
+
}}
|
|
698
|
+
>
|
|
699
|
+
<span>⚠️</span>
|
|
700
|
+
<span>{error}</span>
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
|
|
704
|
+
{/* Results */}
|
|
705
|
+
{result && (
|
|
706
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
707
|
+
{/* Results header */}
|
|
708
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
709
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
710
|
+
<span style={{ fontSize: 13, fontWeight: 700, color: C.text }}>
|
|
711
|
+
Suggested Codes
|
|
712
|
+
</span>
|
|
713
|
+
<Badge color={C.teal} bg={C.tealLight}>
|
|
714
|
+
{result.matches.length} match{result.matches.length !== 1 ? "es" : ""}
|
|
715
|
+
</Badge>
|
|
716
|
+
</div>
|
|
717
|
+
{result.matches.length > 0 && (
|
|
718
|
+
<CopyButton
|
|
719
|
+
text={allCodesText}
|
|
720
|
+
label="All"
|
|
721
|
+
copyKey="copy-all"
|
|
722
|
+
copiedKey={copiedKey}
|
|
723
|
+
copy={copy}
|
|
724
|
+
/>
|
|
725
|
+
)}
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
{/* No matches */}
|
|
729
|
+
{result.matches.length === 0 && (
|
|
730
|
+
<div
|
|
731
|
+
style={{
|
|
732
|
+
padding: "20px",
|
|
733
|
+
textAlign: "center",
|
|
734
|
+
color: C.muted,
|
|
735
|
+
fontSize: 13,
|
|
736
|
+
background: C.bg,
|
|
737
|
+
borderRadius: 10,
|
|
738
|
+
}}
|
|
739
|
+
>
|
|
740
|
+
No matching ICD-10 codes found.
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
|
|
744
|
+
{/* Code cards */}
|
|
745
|
+
{result.matches.map((match, i) => (
|
|
746
|
+
<ResultCard
|
|
747
|
+
key={i}
|
|
748
|
+
index={i}
|
|
749
|
+
match={match}
|
|
750
|
+
copy={copy}
|
|
751
|
+
copiedKey={copiedKey}
|
|
752
|
+
/>
|
|
753
|
+
))}
|
|
754
|
+
|
|
755
|
+
{/* Clinical note — temporarily hidden, re-enable when UX is confirmed */}
|
|
756
|
+
{/* {result.note && (
|
|
757
|
+
<div
|
|
758
|
+
style={{
|
|
759
|
+
background: C.primaryLight,
|
|
760
|
+
border: `1px solid #c7d2fe`,
|
|
761
|
+
borderRadius: 8,
|
|
762
|
+
padding: "10px 14px",
|
|
763
|
+
fontSize: 12,
|
|
764
|
+
color: C.primaryDark,
|
|
765
|
+
lineHeight: 1.5,
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
📋 <strong>Coding note:</strong> {result.note}
|
|
769
|
+
</div>
|
|
770
|
+
)} */}
|
|
771
|
+
</div>
|
|
772
|
+
)}
|
|
773
|
+
|
|
774
|
+
{/* Search history */}
|
|
775
|
+
{history.length > 0 && (
|
|
776
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
777
|
+
<span style={{ fontSize: 11, fontWeight: 600, color: "#aaa", textTransform: "uppercase", letterSpacing: "0.5px" }}>
|
|
778
|
+
Recent searches
|
|
779
|
+
</span>
|
|
780
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
|
781
|
+
{history.map((item, i) => (
|
|
782
|
+
<HistoryChip key={i} item={item} onClick={onHistoryClick} />
|
|
783
|
+
))}
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
{/* ── Footer disclaimer ── */}
|
|
790
|
+
<div
|
|
791
|
+
style={{
|
|
792
|
+
flexShrink: 0,
|
|
793
|
+
padding: "12px 18px",
|
|
794
|
+
borderTop: `1px solid ${C.border}`,
|
|
795
|
+
background: C.bg,
|
|
796
|
+
fontSize: 11,
|
|
797
|
+
color: "#aaa",
|
|
798
|
+
lineHeight: 1.5,
|
|
799
|
+
textAlign: "center",
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
{ICON.medical} For reference only. Always verify codes with a certified medical coder.
|
|
803
|
+
Not a substitute for clinical judgment or official coding guidelines.
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ─── Main Component ────────────────────────────────────────────────────────────
|
|
810
|
+
export default function ICD10Assistant() {
|
|
811
|
+
const [open, setOpen] = useState(false);
|
|
812
|
+
const [mode, setMode] = useState("nlm"); // "nlm" | "ai"
|
|
813
|
+
const [query, setQuery] = useState("");
|
|
814
|
+
const [loading, setLoading] = useState(false);
|
|
815
|
+
const [result, setResult] = useState(null);
|
|
816
|
+
const [error, setError] = useState(null);
|
|
817
|
+
const [history, setHistory] = useState([]);
|
|
818
|
+
|
|
819
|
+
const textareaRef = useRef(null);
|
|
820
|
+
const panelRef = useRef(null);
|
|
821
|
+
const { copy, copiedKey } = useClipboard();
|
|
822
|
+
|
|
823
|
+
// Focus textarea when panel opens
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
if (open && textareaRef.current) {
|
|
826
|
+
setTimeout(() => textareaRef.current?.focus(), 80);
|
|
827
|
+
}
|
|
828
|
+
}, [open]);
|
|
829
|
+
|
|
830
|
+
// Close panel on outside click
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
if (!open) return;
|
|
833
|
+
const handler = (e) => {
|
|
834
|
+
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
|
835
|
+
setOpen(false);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
document.addEventListener("mousedown", handler);
|
|
839
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
840
|
+
}, [open]);
|
|
841
|
+
|
|
842
|
+
const handleSubmit = useCallback(async () => {
|
|
843
|
+
if (!query.trim() || loading) return;
|
|
844
|
+
setLoading(true);
|
|
845
|
+
setError(null);
|
|
846
|
+
setResult(null);
|
|
847
|
+
try {
|
|
848
|
+
const data = await getICDSuggestions(query.trim(), ICD_MODE.CLAUDE_ONLY);
|
|
849
|
+
setResult(data);
|
|
850
|
+
setHistory((prev) => {
|
|
851
|
+
const filtered = prev.filter(
|
|
852
|
+
(h) => h.query.toLowerCase() !== query.trim().toLowerCase()
|
|
853
|
+
);
|
|
854
|
+
return [{ query: query.trim(), matches: data.matches }, ...filtered].slice(0, MAX_HISTORY);
|
|
855
|
+
});
|
|
856
|
+
} catch (err) {
|
|
857
|
+
setError(err.message || "Something went wrong. Please try again.");
|
|
858
|
+
} finally {
|
|
859
|
+
setLoading(false);
|
|
860
|
+
}
|
|
861
|
+
}, [query, loading]);
|
|
862
|
+
|
|
863
|
+
const handleKeyDown = useCallback(
|
|
864
|
+
(e) => {
|
|
865
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
866
|
+
e.preventDefault();
|
|
867
|
+
handleSubmit();
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
[handleSubmit]
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
const handleClear = useCallback(() => {
|
|
874
|
+
setQuery("");
|
|
875
|
+
setResult(null);
|
|
876
|
+
setError(null);
|
|
877
|
+
textareaRef.current?.focus();
|
|
878
|
+
}, []);
|
|
879
|
+
|
|
880
|
+
const handleHistoryClick = useCallback((q) => {
|
|
881
|
+
setQuery(q);
|
|
882
|
+
setResult(null);
|
|
883
|
+
setError(null);
|
|
884
|
+
textareaRef.current?.focus();
|
|
885
|
+
}, []);
|
|
886
|
+
|
|
887
|
+
const handleQueryChange = useCallback((e) => setQuery(e.target.value), []);
|
|
888
|
+
|
|
889
|
+
return (
|
|
890
|
+
<>
|
|
891
|
+
{!open && <FloatingButton onOpen={() => setOpen(true)} />}
|
|
892
|
+
{open && mode === "nlm" && (
|
|
893
|
+
<NLMLookupPanel
|
|
894
|
+
panelRef={panelRef}
|
|
895
|
+
onClose={() => setOpen(false)}
|
|
896
|
+
mode={mode}
|
|
897
|
+
onModeChange={setMode}
|
|
898
|
+
/>
|
|
899
|
+
)}
|
|
900
|
+
{open && mode === "ai" && (
|
|
901
|
+
<Panel
|
|
902
|
+
panelRef={panelRef}
|
|
903
|
+
textareaRef={textareaRef}
|
|
904
|
+
query={query}
|
|
905
|
+
loading={loading}
|
|
906
|
+
result={result}
|
|
907
|
+
error={error}
|
|
908
|
+
history={history}
|
|
909
|
+
copy={copy}
|
|
910
|
+
copiedKey={copiedKey}
|
|
911
|
+
onQueryChange={handleQueryChange}
|
|
912
|
+
onKeyDown={handleKeyDown}
|
|
913
|
+
onSubmit={handleSubmit}
|
|
914
|
+
onClear={handleClear}
|
|
915
|
+
onClose={() => setOpen(false)}
|
|
916
|
+
onHistoryClick={handleHistoryClick}
|
|
917
|
+
mode={mode}
|
|
918
|
+
onModeChange={setMode}
|
|
919
|
+
/>
|
|
920
|
+
)}
|
|
921
|
+
</>
|
|
922
|
+
);
|
|
923
|
+
}
|