preview/medical-summarizer
0
1import React, { useState, useEffect, useRef } from "react";
2import {
3 Send,
4 Moon,
5 Sun,
6 User,
7 Bot,
8 AlertCircle,
9 CheckCircle,
10 Clock,
11 Heart,
12 Phone,
13 Calendar,
14 MapPin,
15 FileText,
16} from "lucide-react";
17import { apiService } from "./services/apiService";
18
19const MedicalChatUI = () => {
20 // UI State
21 const [isDarkMode, setIsDarkMode] = useState(false);
22 const [currentMessage, setCurrentMessage] = useState("");
23 const [messages, setMessages] = useState([]);
24 const [isTyping, setIsTyping] = useState(false);
25 const [isConnected, setIsConnected] = useState(false);
26 const [connectionError, setConnectionError] = useState("");
27
28 // Patient Data State
29 const [patientRecord, setPatientRecord] = useState({
30 personal_info: {
31 full_name: "",
32 dob: "",
33 gender: "",
34 phone: "",
35 address: "",
36 },
37 medical_info: {
38 current_symptoms: [],
39 symptom_start_date: "",
40 previous_medications: [],
41 allergies: [],
42 severity_level: "",
43 },
44 });
45
46 // Session State
47 const [sessionStatus, setSessionStatus] = useState("idle"); // idle | collecting | completed
48 const [confidenceScores, setConfidenceScores] = useState({
49 extraction: 0,
50 validation: 0,
51 });
52
53 const messagesEndRef = useRef(null);
54
55 // Auto-scroll to bottom
56 const scrollToBottom = () => {
57 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
58 };
59
60 useEffect(() => {
61 scrollToBottom();
62 }, [messages]);
63
64 // Initialize connection and start chat
65 useEffect(() => {
66 initializeChat();
67 }, []);
68
69 const initializeChat = async () => {
70 try {
71 // Health check
72 await apiService.healthCheck();
73 setIsConnected(true);
74 setConnectionError("");
75
76 // Start chat session
77 const response = await apiService.startChat();
78
79 // Add initial AI message
80 setMessages([
81 {
82 id: Date.now(),
83 text: response.message,
84 sender: "ai",
85 timestamp: new Date().toISOString(),
86 },
87 ]);
88
89 setSessionStatus("collecting");
90 } catch (error) {
91 setIsConnected(false);
92 setConnectionError(error.message);
93 console.error("Failed to initialize chat:", error);
94 }
95 };
96
97 const handleSendMessage = async () => {
98 if (!currentMessage.trim() || isTyping) return;
99
100 const userMessage = {
101 id: Date.now(),
102 text: currentMessage.trim(),
103 sender: "user",
104 timestamp: new Date().toISOString(),
105 };
106
107 // Add user message to chat
108 setMessages((prev) => [...prev, userMessage]);
109 setCurrentMessage("");
110 setIsTyping(true);
111
112 try {
113 // Send to backend
114 const response = await apiService.sendMessage(userMessage.text);
115
116 // Update patient record
117 setPatientRecord(response.patient_record);
118 setConfidenceScores(response.confidence_scores);
119 setSessionStatus(response.session_status);
120
121 // Add AI response
122 const aiMessage = {
123 id: Date.now() + 1,
124 text: response.ai_message,
125 sender: "ai",
126 timestamp: new Date().toISOString(),
127 metadata: response.metadata,
128 };
129
130 setMessages((prev) => [...prev, aiMessage]);
131
132 // If conversation completed, show completion message
133 if (response.should_end) {
134 setTimeout(() => {
135 const completionMessage = {
136 id: Date.now() + 2,
137 text: "✅ Information collection completed! Click 'Generate Summary' to create a doctor report.",
138 sender: "system",
139 timestamp: new Date().toISOString(),
140 };
141 setMessages((prev) => [...prev, completionMessage]);
142 }, 1000);
143 }
144 } catch (error) {
145 console.error("Failed to send message:", error);
146
147 // Add error message
148 const errorMessage = {
149 id: Date.now() + 1,
150 text: `❌ Error: ${error.message}. Please try again.`,
151 sender: "system",
152 timestamp: new Date().toISOString(),
153 };
154 setMessages((prev) => [...prev, errorMessage]);
155 } finally {
156 setIsTyping(false);
157 }
158 };
159
160 const handleGenerateSummary = async () => {
161 try {
162 setIsTyping(true);
163 const summary = await apiService.generateSummary();
164
165 const summaryMessage = {
166 id: Date.now(),
167 text: `📋 **Doctor Summary Generated**\n\n**Summary:** ${
168 summary.doctor_summary
169 }\n\n**Urgency Level:** ${
170 summary.urgency_level
171 }\n\n**Key Findings:**\n${summary.key_findings
172 .map((f) => `• ${f}`)
173 .join(
174 "\n"
175 )}\n\n**Recommended Questions:**\n${summary.recommended_questions
176 .map((q) => `• ${q}`)
177 .join("\n")}`,
178 sender: "system",
179 timestamp: new Date().toISOString(),
180 };
181
182 setMessages((prev) => [...prev, summaryMessage]);
183 } catch (error) {
184 console.error("Failed to generate summary:", error);
185 } finally {
186 setIsTyping(false);
187 }
188 };
189
190 const handleResetChat = async () => {
191 try {
192 await apiService.resetSession();
193 setMessages([]);
194 setPatientRecord({
195 personal_info: {
196 full_name: "",
197 dob: "",
198 gender: "",
199 phone: "",
200 address: "",
201 },
202 medical_info: {
203 current_symptoms: [],
204 symptom_start_date: "",
205 previous_medications: [],
206 allergies: [],
207 severity_level: "",
208 },
209 });
210 setSessionStatus("idle");
211 setConfidenceScores({ extraction: 0, validation: 0 });
212
213 // Restart chat
214 await initializeChat();
215 } catch (error) {
216 console.error("Failed to reset chat:", error);
217 }
218 };
219
220 // Components
221 const MessageBubble = ({ message }) => {
222 const isUser = message.sender === "user";
223 const isSystem = message.sender === "system";
224
225 return (
226 <div className={`flex ${isUser ? "justify-end" : "justify-start"} mb-4`}>
227 <div className="flex items-start space-x-3 max-w-3xl">
228 {!isUser && (
229 <div
230 className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
231 isSystem
232 ? isDarkMode
233 ? "bg-yellow-900/50 text-yellow-300"
234 : "bg-yellow-100 text-yellow-700"
235 : isDarkMode
236 ? "bg-blue-600"
237 : "bg-blue-500"
238 }`}
239 >
240 {isSystem ? (
241 <AlertCircle className="w-4 h-4" />
242 ) : (
243 <Bot className="w-4 h-4 text-white" />
244 )}
245 </div>
246 )}
247
248 <div
249 className={`px-4 py-3 rounded-2xl ${
250 isUser
251 ? isDarkMode
252 ? "bg-blue-600 text-white"
253 : "bg-blue-500 text-white"
254 : isSystem
255 ? isDarkMode
256 ? "bg-yellow-900/20 text-yellow-200 border border-yellow-700"
257 : "bg-yellow-50 text-yellow-800 border border-yellow-200"
258 : isDarkMode
259 ? "bg-gray-700 text-gray-200"
260 : "bg-gray-100 text-gray-900"
261 }`}
262 >
263 <div className="whitespace-pre-wrap">{message.text}</div>
264
265 {message.metadata && (
266 <div className="text-xs mt-2 opacity-70">
267 Exchange #{message.metadata.exchange_count} | Confidence:{" "}
268 {(message.metadata.confidence || 0).toFixed(2)}
269 </div>
270 )}
271 </div>
272
273 {isUser && (
274 <div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-400 flex items-center justify-center">
275 <User className="w-4 h-4 text-white" />
276 </div>
277 )}
278 </div>
279 </div>
280 );
281 };
282
283 const TypingIndicator = () => (
284 <div className="flex justify-start mb-4">
285 <div className="flex items-center space-x-3 max-w-xs">
286 <div
287 className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
288 isDarkMode ? "bg-blue-600" : "bg-blue-500"
289 }`}
290 >
291 <Bot className="w-4 h-4 text-white" />
292 </div>
293 <div
294 className={`px-4 py-3 rounded-2xl ${
295 isDarkMode ? "bg-gray-700" : "bg-gray-100"
296 }`}
297 >
298 <div className="flex space-x-1">
299 <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
300 <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:0.1s]"></div>
301 <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:0.2s]"></div>
302 </div>
303 </div>
304 </div>
305 </div>
306 );
307
308 const PatientPanel = () => (
309 <div
310 className={`w-96 ${
311 isDarkMode ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"
312 } border-l flex flex-col`}
313 >
314 {/* Header */}
315 <div
316 className={`p-4 border-b ${
317 isDarkMode ? "border-gray-700" : "border-gray-200"
318 }`}
319 >
320 <h2
321 className={`text-lg font-semibold ${
322 isDarkMode ? "text-gray-200" : "text-gray-900"
323 } flex items-center`}
324 >
325 <FileText className="w-5 h-5 mr-2" />
326 Patient Record
327 </h2>
328 <div className="flex items-center mt-2 space-x-4">
329 <div
330 className={`text-sm ${
331 isDarkMode ? "text-gray-400" : "text-gray-600"
332 }`}
333 >
334 Status: {sessionStatus}
335 </div>
336 <div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
337 Confidence:{" "}
338 {Math.round(
339 ((confidenceScores.extraction + confidenceScores.validation) /
340 2) *
341 100
342 )}
343 %
344 </div>
345 </div>
346 </div>
347
348 {/* Content */}
349 <div className="flex-1 overflow-y-auto p-4 space-y-6">
350 {/* Personal Information */}
351 <div>
352 <h3
353 className={`text-sm font-medium ${
354 isDarkMode ? "text-gray-300" : "text-gray-700"
355 } mb-3 flex items-center`}
356 >
357 <User className="w-4 h-4 mr-2" />
358 Personal Information
359 </h3>
360
361 <div className="space-y-3">
362 <div>
363 <span
364 className={`text-sm ${
365 isDarkMode ? "text-gray-400" : "text-gray-600"
366 }`}
367 >
368 Name:
369 </span>
370 <p
371 className={`text-sm mt-1 ${
372 isDarkMode ? "text-gray-200" : "text-gray-900"
373 }`}
374 >
375 {patientRecord.personal_info.full_name || "Not provided"}
376 </p>
377 </div>
378
379 <div>
380 <span
381 className={`text-sm ${
382 isDarkMode ? "text-gray-400" : "text-gray-600"
383 }`}
384 >
385 Date of Birth:
386 </span>
387 <p
388 className={`text-sm mt-1 ${
389 isDarkMode ? "text-gray-200" : "text-gray-900"
390 } flex items-center`}
391 >
392 <Calendar className="w-3 h-3 mr-1" />
393 {patientRecord.personal_info.dob || "Not provided"}
394 </p>
395 </div>
396
397 <div>
398 <span
399 className={`text-sm ${
400 isDarkMode ? "text-gray-400" : "text-gray-600"
401 }`}
402 >
403 Phone:
404 </span>
405 <p
406 className={`text-sm mt-1 ${
407 isDarkMode ? "text-gray-200" : "text-gray-900"
408 } flex items-center`}
409 >
410 <Phone className="w-3 h-3 mr-1" />
411 {patientRecord.personal_info.phone || "Not provided"}
412 </p>
413 </div>
414
415 <div>
416 <span
417 className={`text-sm ${
418 isDarkMode ? "text-gray-400" : "text-gray-600"
419 }`}
420 >
421 Address:
422 </span>
423 <p
424 className={`text-sm mt-1 ${
425 isDarkMode ? "text-gray-200" : "text-gray-900"
426 } flex items-center`}
427 >
428 <MapPin className="w-3 h-3 mr-1" />
429 {patientRecord.personal_info.address || "Not provided"}
430 </p>
431 </div>
432 </div>
433 </div>
434
435 {/* Medical Information */}
436 <div>
437 <h3
438 className={`text-sm font-medium ${
439 isDarkMode ? "text-gray-300" : "text-gray-700"
440 } mb-3 flex items-center`}
441 >
442 <Heart className="w-4 h-4 mr-2" />
443 Medical Information
444 </h3>
445
446 <div className="space-y-3">
447 <div>
448 <span
449 className={`text-sm ${
450 isDarkMode ? "text-gray-400" : "text-gray-600"
451 }`}
452 >
453 Symptoms:
454 </span>
455 <p
456 className={`text-sm mt-1 ${
457 isDarkMode ? "text-gray-200" : "text-gray-900"
458 }`}
459 >
460 {patientRecord.medical_info.current_symptoms?.length > 0
461 ? patientRecord.medical_info.current_symptoms.join(", ")
462 : "Not provided"}
463 </p>
464 </div>
465
466 <div>
467 <span
468 className={`text-sm ${
469 isDarkMode ? "text-gray-400" : "text-gray-600"
470 }`}
471 >
472 Duration:
473 </span>
474 <p
475 className={`text-sm mt-1 ${
476 isDarkMode ? "text-gray-200" : "text-gray-900"
477 } flex items-center`}
478 >
479 <Clock className="w-3 h-3 mr-1" />
480 {patientRecord.medical_info.symptom_start_date ||
481 "Not provided"}
482 </p>
483 </div>
484
485 <div>
486 <span
487 className={`text-sm ${
488 isDarkMode ? "text-gray-400" : "text-gray-600"
489 }`}
490 >
491 Severity:
492 </span>
493 <p
494 className={`text-sm mt-1 ${
495 isDarkMode ? "text-gray-200" : "text-gray-900"
496 }`}
497 >
498 {patientRecord.medical_info.severity_level || "Not assessed"}
499 </p>
500 </div>
501
502 <div>
503 <span
504 className={`text-sm ${
505 isDarkMode ? "text-gray-400" : "text-gray-600"
506 }`}
507 >
508 Medications:
509 </span>
510 <p
511 className={`text-sm mt-1 ${
512 isDarkMode ? "text-gray-200" : "text-gray-900"
513 }`}
514 >
515 {patientRecord.medical_info.previous_medications?.length > 0
516 ? patientRecord.medical_info.previous_medications.join(", ")
517 : "None reported"}
518 </p>
519 </div>
520
521 <div>
522 <span
523 className={`text-sm ${
524 isDarkMode ? "text-gray-400" : "text-gray-600"
525 }`}
526 >
527 Allergies:
528 </span>
529 <p
530 className={`text-sm mt-1 ${
531 isDarkMode ? "text-gray-200" : "text-gray-900"
532 }`}
533 >
534 {patientRecord.medical_info.allergies?.length > 0
535 ? patientRecord.medical_info.allergies.join(", ")
536 : "None reported"}
537 </p>
538 </div>
539 </div>
540 </div>
541 </div>
542
543 {/* Actions */}
544 <div
545 className={`p-4 border-t ${
546 isDarkMode ? "border-gray-700" : "border-gray-200"
547 } space-y-2`}
548 >
549 <button
550 onClick={handleGenerateSummary}
551 disabled={sessionStatus === "idle"}
552 className={`w-full px-4 py-2 rounded-lg transition-colors ${
553 sessionStatus === "idle"
554 ? `${
555 isDarkMode
556 ? "bg-gray-700 text-gray-500 cursor-not-allowed"
557 : "bg-gray-200 text-gray-400 cursor-not-allowed"
558 }`
559 : `${
560 isDarkMode
561 ? "bg-blue-600 hover:bg-blue-700"
562 : "bg-blue-500 hover:bg-blue-600"
563 } text-white`
564 }`}
565 >
566 Generate Summary
567 </button>
568 <button
569 onClick={handleResetChat}
570 className={`w-full px-4 py-2 rounded-lg transition-colors ${
571 isDarkMode
572 ? "border border-gray-600 hover:bg-gray-700 text-gray-300"
573 : "border border-gray-300 hover:bg-gray-50 text-gray-700"
574 }`}
575 >
576 New Session
577 </button>
578 </div>
579 </div>
580 );
581
582 // Connection error screen
583 if (!isConnected && connectionError) {
584 return (
585 <div
586 className={`h-screen flex items-center justify-center ${
587 isDarkMode ? "bg-gray-900" : "bg-gray-50"
588 }`}
589 >
590 <div
591 className={`text-center p-8 rounded-lg ${
592 isDarkMode ? "bg-gray-800" : "bg-white"
593 } shadow-lg`}
594 >
595 <AlertCircle
596 className={`w-16 h-16 mx-auto mb-4 ${
597 isDarkMode ? "text-red-400" : "text-red-500"
598 }`}
599 />
600 <h2
601 className={`text-xl font-semibold mb-2 ${
602 isDarkMode ? "text-gray-200" : "text-gray-900"
603 }`}
604 >
605 Connection Failed
606 </h2>
607 <p
608 className={`mb-4 ${isDarkMode ? "text-gray-400" : "text-gray-600"}`}
609 >
610 {connectionError}
611 </p>
612 <button
613 onClick={initializeChat}
614 className={`px-4 py-2 rounded-lg ${
615 isDarkMode
616 ? "bg-blue-600 hover:bg-blue-700"
617 : "bg-blue-500 hover:bg-blue-600"
618 } text-white transition-colors`}
619 >
620 Retry Connection
621 </button>
622 </div>
623 </div>
624 );
625 }
626
627 return (
628 <div
629 className={`h-screen flex ${isDarkMode ? "bg-gray-900" : "bg-gray-50"}`}
630 >
631 {/* Main Content */}
632 <div className="flex-1 flex flex-col">
633 {/* Header */}
634 <header
635 className={`${
636 isDarkMode
637 ? "bg-gray-800 border-gray-700"
638 : "bg-white border-gray-200"
639 } border-b px-6 py-4 flex items-center justify-between`}
640 >
641 <div className="flex items-center space-x-4">
642 <h1
643 className={`text-xl font-semibold ${
644 isDarkMode ? "text-gray-200" : "text-gray-900"
645 }`}
646 >
647 Medical Multi-Agent System
648 </h1>
649 </div>
650
651 <div className="flex items-center space-x-2">
652 <div
653 className={`px-3 py-1 rounded-full text-sm flex items-center space-x-2 ${
654 isConnected
655 ? isDarkMode
656 ? "bg-green-900/50 text-green-300 border border-green-700"
657 : "bg-green-100 text-green-700 border border-green-200"
658 : isDarkMode
659 ? "bg-red-900/50 text-red-300 border border-red-700"
660 : "bg-red-100 text-red-700 border border-red-200"
661 }`}
662 >
663 {isConnected ? (
664 <CheckCircle className="w-3 h-3" />
665 ) : (
666 <AlertCircle className="w-3 h-3" />
667 )}
668 <span>
669 {isConnected ? "Connected to Qwen 3-4B" : "Disconnected"}
670 </span>
671 </div>
672 <button
673 onClick={() => setIsDarkMode(!isDarkMode)}
674 className={`p-2 rounded-lg ${
675 isDarkMode
676 ? "hover:bg-gray-700 text-gray-300"
677 : "hover:bg-gray-100 text-gray-600"
678 } transition-colors`}
679 >
680 {isDarkMode ? (
681 <Sun className="w-5 h-5" />
682 ) : (
683 <Moon className="w-5 h-5" />
684 )}
685 </button>
686 </div>
687 </header>
688
689 <div className="flex-1 flex min-h-0 overflow-hidden">
690 {/* Chat Area */}
691 <div className="flex-1 flex flex-col">
692 {/* Messages */}
693 <div className="flex-1 overflow-y-auto p-6">
694 {messages.length === 0 && (
695 <div className="flex items-center justify-center h-full">
696 <div className="text-center">
697 <Bot
698 className={`w-16 h-16 mx-auto mb-4 ${
699 isDarkMode ? "text-gray-600" : "text-gray-400"
700 }`}
701 />
702 <p
703 className={`text-lg ${
704 isDarkMode ? "text-gray-400" : "text-gray-600"
705 }`}
706 >
707 Initializing Medical Multi-Agent System...
708 </p>
709 </div>
710 </div>
711 )}
712
713 {messages.map((message) => (
714 <MessageBubble key={message.id} message={message} />
715 ))}
716 {isTyping && <TypingIndicator />}
717 <div ref={messagesEndRef} />
718 </div>
719
720 {/* Input Area */}
721 <div
722 className={`p-6 border-t ${
723 isDarkMode ? "border-gray-700" : "border-gray-200"
724 }`}
725 >
726 <div className="flex space-x-4">
727 <input
728 type="text"
729 value={currentMessage}
730 onChange={(e) => setCurrentMessage(e.target.value)}
731 onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
732 placeholder="Describe your symptoms or provide requested information..."
733 disabled={!isConnected}
734 className={`flex-1 px-4 py-3 rounded-lg border ${
735 isDarkMode
736 ? "bg-gray-700 border-gray-600 text-gray-200 placeholder-gray-400"
737 : "bg-white border-gray-300 text-gray-900 placeholder-gray-500"
738 } focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50`}
739 />
740 <button
741 onClick={handleSendMessage}
742 disabled={!currentMessage.trim() || isTyping || !isConnected}
743 className={`px-6 py-3 rounded-lg transition-colors flex items-center space-x-2 ${
744 !currentMessage.trim() || isTyping || !isConnected
745 ? `${
746 isDarkMode
747 ? "bg-gray-700 text-gray-500"
748 : "bg-gray-200 text-gray-400"
749 } cursor-not-allowed`
750 : `${
751 isDarkMode
752 ? "bg-blue-600 hover:bg-blue-700"
753 : "bg-blue-500 hover:bg-blue-600"
754 } text-white`
755 }`}
756 >
757 <Send className="w-4 h-4" />
758 <span>{isTyping ? "Sending..." : "Send"}</span>
759 </button>
760 </div>
761 </div>
762 </div>
763
764 {/* Patient Panel */}
765 <PatientPanel />
766 </div>
767 </div>
768 </div>
769 );
770};
771
772export default MedicalChatUI;
773 