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.
@@ -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&apos;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&apos;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
+ }