CoolFace
Apppublic

preview/medical-summarizer

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
App.js773 linesDownload Raw Back to src
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