Muhammadidrees commited on
Commit
2cca11c
·
verified ·
1 Parent(s): bebb9ca

Upload 5 files

Browse files
Files changed (5) hide show
  1. index.html +113 -19
  2. pdfcode.py +875 -0
  3. requirements.txt +5 -0
  4. script.js +578 -0
  5. styles.css +478 -0
index.html CHANGED
@@ -1,19 +1,113 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Doctor Consultation with Voice</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="notification" id="notification"></div>
11
+
12
+ <div class="container">
13
+ <div class="header">
14
+ <h1>🩺 AI Doctor Consultation</h1>
15
+ <p>Professional Medical Consultation with Voice & PDF Summary</p>
16
+
17
+ <div class="voice-controls">
18
+ <button class="voice-btn" id="continuousModeBtn" onclick="toggleContinuousMode()">
19
+ <span class="voice-indicator"></span>
20
+ <span>🔄 Continuous: OFF</span>
21
+ </button>
22
+ <button class="voice-btn" id="toggleAutoSpeak" onclick="toggleAutoSpeak()">
23
+ <span class="voice-indicator" id="speakIndicator"></span>
24
+ <span id="autoSpeakText">🔊 Auto-Speak: OFF</span>
25
+ </button>
26
+ <button class="voice-btn" id="voiceInputBtn" onclick="toggleVoiceInput()">
27
+ 🎤 Voice Input
28
+ </button>
29
+ </div>
30
+
31
+ <div class="voice-status" id="voiceStatus"></div>
32
+
33
+ <div class="session-info" id="sessionInfo">
34
+ Starting new session...
35
+ </div>
36
+ <div class="session-id-display" id="sessionIdDisplay" onclick="copySessionId()" title="Click to copy">
37
+ Session ID: Loading...
38
+ </div>
39
+ </div>
40
+
41
+ <div class="loading" id="loading">Dr. AI is typing...</div>
42
+
43
+ <div class="chat-container" id="chatContainer">
44
+ <!-- Messages will appear here -->
45
+ </div>
46
+
47
+ <div class="input-area">
48
+ <button class="btn btn-voice" id="voiceBtn" onclick="toggleVoiceInput()">
49
+ 🎤
50
+ </button>
51
+ <input type="text" id="messageInput" placeholder="Type or speak your message..." />
52
+ <button class="btn btn-primary" onclick="sendMessage()">Send 📤</button>
53
+ </div>
54
+
55
+ <div class="controls">
56
+ <button class="btn btn-success" onclick="generateSummary()">📋 Generate Summary & PDF</button>
57
+ <button class="btn btn-info" onclick="showHistoryModal()">📜 Load History</button>
58
+ <button class="btn btn-secondary" onclick="restartSession()">🔄 Restart</button>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Summary Modal with PDF -->
63
+ <div class="modal" id="summaryModal">
64
+ <div class="modal-content">
65
+ <span class="close-btn" onclick="closeSummary()">&times;</span>
66
+ <h2>📋 Consultation Summary</h2>
67
+
68
+ <div id="pdfDownloadInfo" class="pdf-download-info" style="display: none;">
69
+ <div class="pdf-icon">📄</div>
70
+ <h3>PDF Generated Successfully!</h3>
71
+ <p>Your consultation summary has been saved as a PDF document.</p>
72
+ </div>
73
+
74
+ <div class="summary-actions">
75
+ <button class="btn btn-success" id="downloadPdfBtn" onclick="downloadPDF()" style="display: none;">
76
+ 📥 Download PDF
77
+ </button>
78
+ <button class="btn btn-info" id="viewPdfBtn" onclick="togglePDFViewer()" style="display: none;">
79
+ 👁️ View PDF
80
+ </button>
81
+ </div>
82
+
83
+ <div id="pdfViewerContainer" style="display: none;">
84
+ <h3 style="margin-top: 20px;">PDF Preview:</h3>
85
+ <iframe id="pdfViewer" class="pdf-viewer"></iframe>
86
+ </div>
87
+
88
+ <h3 style="margin-top: 20px;">Text Summary:</h3>
89
+ <pre id="summaryText"></pre>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- History Modal -->
94
+ <div class="modal" id="historyModal">
95
+ <div class="modal-content">
96
+ <span class="close-btn" onclick="closeHistory()">&times;</span>
97
+ <h2>📜 Load Previous Consultation</h2>
98
+
99
+ <div class="load-session-form">
100
+ <input type="text" id="sessionIdInput" placeholder="Enter Session ID..." />
101
+ <button class="btn btn-primary" onclick="loadSessionById()">Load</button>
102
+ </div>
103
+
104
+ <h3>Recent Consultations:</h3>
105
+ <div class="history-list" id="historyList">
106
+ <p style="text-align: center; color: #999;">Loading history...</p>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <script src="script.js"></script>
112
+ </body>
113
+ </html>
pdfcode.py ADDED
@@ -0,0 +1,875 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ from typing import List, Optional, Dict
6
+ import google.generativeai as genai
7
+ import os
8
+ from datetime import datetime
9
+ import uuid
10
+ import json
11
+ from pathlib import Path
12
+ from reportlab.lib.pagesizes import letter, A4
13
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
+ from reportlab.lib.units import inch
15
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle
16
+ from reportlab.lib import colors
17
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
18
+ from reportlab.pdfgen import canvas
19
+
20
+ # Configure Gemini API
21
+ os.environ["GOOGLE_API_KEY"] = "AIzaSyDid3I53qFohhZTnLjnOjz4QqlvK4RZm7o"
22
+ genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
23
+
24
+ MODEL_ID = "gemini-2.0-flash-exp"
25
+
26
+ # Create storage directories
27
+ STORAGE_DIR = Path("consultation_storage")
28
+ STORAGE_DIR.mkdir(exist_ok=True)
29
+
30
+ PDF_DIR = Path("consultation_pdfs")
31
+ PDF_DIR.mkdir(exist_ok=True)
32
+
33
+ # System prompt (same as before)
34
+ DOCTOR_SYSTEM_PROMPT = """
35
+ You are Dr. HealBot, a calm, knowledgeable, and empathetic virtual doctor.
36
+
37
+ GOAL:
38
+ Hold a natural, focused conversation with the patient to understand their health issue and offer helpful preliminary medical guidance.
39
+
40
+ CONVERSATION LOGIC:
41
+ - Ask only relevant and concise medical questions necessary for diagnosing the illness.
42
+ - Each question should help clarify symptoms or narrow possible causes.
43
+ - Stop asking once enough information is collected for a basic assessment.
44
+ - Then, provide a structured, friendly, and visually clear medical response using headings, emojis, and bullet points.
45
+
46
+ FINAL RESPONSE FORMAT:
47
+ When giving your full assessment, use this markdown-styled format:
48
+
49
+ 🩺 Based on what you've told me...
50
+ Brief summary of what the patient described.
51
+
52
+ 💡 Possible Causes (Preliminary)
53
+ - List 1–2 possible conditions using phrases like "It could be" or "This sounds like".
54
+ - Include a disclaimer that this is not a confirmed diagnosis.
55
+
56
+ 💊 Suggested Over-the-Counter Medicines
57
+ - Generic medicine names only (e.g., "Paracetamol 500mg every 6 hours if fever or pain")
58
+ - Mention to check packaging or consult a pharmacist for dosage confirmation.
59
+
60
+ 🥗 Lifestyle & Home Care Tips
61
+ - 2–3 practical suggestions (rest, hydration, warm compress, balanced diet, etc.)
62
+
63
+ ⚠ When to See a Real Doctor
64
+ - 2–3 warning signs or conditions when urgent medical care is needed.
65
+
66
+ 📅 Follow-Up Advice
67
+ - Brief recommendation for self-care or follow-up timing (e.g., "If not improving in 3 days, visit a clinic.")
68
+
69
+ TONE & STYLE:
70
+ - Speak like a real, caring doctor — short, clear, and empathetic (1–2 sentences per reply).
71
+ - Use plain language, no jargon.
72
+ - Only one question per turn unless clarification is essential.
73
+ - Keep tone warm, calm, and professional.
74
+ - Early messages: short questions only.
75
+ - Final message: structured output with emojis and headings.
76
+
77
+ IMPORTANT:
78
+ - Always emphasize that this is preliminary guidance and not a substitute for professional care.
79
+ - Never make definitive diagnoses; use phrases like "it sounds like" or "it could be".
80
+ - If symptoms seem serious, always recommend urgent medical attention.
81
+
82
+ CONVERSATION FLOW:
83
+ 1. Ask about the main symptom.
84
+ 2. Ask about its duration, severity, and any triggers.
85
+ 3. Ask about accompanying symptoms.
86
+ 4. Ask about medical history, allergies, or medications.
87
+ 5. Then, provide your structured assessment as described above.
88
+ """
89
+
90
+ # =====================================================
91
+ # PDF GENERATION FUNCTIONS
92
+ # =====================================================
93
+
94
+ def generate_pdf_summary(session_id: str, summary_text: str, patient_data: Dict, history: List[Dict]) -> str:
95
+ """Generate a professional PDF summary of the consultation"""
96
+
97
+ pdf_filename = f"{session_id}_summary.pdf"
98
+ pdf_path = PDF_DIR / pdf_filename
99
+
100
+ # Create PDF document
101
+ doc = SimpleDocTemplate(str(pdf_path), pagesize=letter,
102
+ rightMargin=72, leftMargin=72,
103
+ topMargin=72, bottomMargin=18)
104
+
105
+ # Container for the 'Flowable' objects
106
+ elements = []
107
+
108
+ # Define styles
109
+ styles = getSampleStyleSheet()
110
+
111
+ # Custom styles
112
+ title_style = ParagraphStyle(
113
+ 'CustomTitle',
114
+ parent=styles['Heading1'],
115
+ fontSize=24,
116
+ textColor=colors.HexColor('#667eea'),
117
+ spaceAfter=30,
118
+ alignment=TA_CENTER,
119
+ fontName='Helvetica-Bold'
120
+ )
121
+
122
+ heading_style = ParagraphStyle(
123
+ 'CustomHeading',
124
+ parent=styles['Heading2'],
125
+ fontSize=16,
126
+ textColor=colors.HexColor('#667eea'),
127
+ spaceAfter=12,
128
+ spaceBefore=12,
129
+ fontName='Helvetica-Bold'
130
+ )
131
+
132
+ normal_style = ParagraphStyle(
133
+ 'CustomNormal',
134
+ parent=styles['Normal'],
135
+ fontSize=11,
136
+ spaceAfter=12,
137
+ alignment=TA_JUSTIFY,
138
+ leading=14
139
+ )
140
+
141
+ # Add Title
142
+ elements.append(Paragraph("🩺 AI DOCTOR CONSULTATION SUMMARY", title_style))
143
+ elements.append(Spacer(1, 0.3*inch))
144
+
145
+ # Add horizontal line
146
+ elements.append(Spacer(1, 0.1*inch))
147
+
148
+ # Patient Information Table
149
+ patient_info_data = [
150
+ ['Patient Name:', patient_data.get('name', 'N/A')],
151
+ ['Age:', patient_data.get('age', 'N/A')],
152
+ ['Session ID:', session_id[:20] + '...'],
153
+ ['Consultation Date:', datetime.now().strftime('%B %d, %Y at %I:%M %p')],
154
+ ['Total Messages:', str(len(history))]
155
+ ]
156
+
157
+ patient_table = Table(patient_info_data, colWidths=[2*inch, 4*inch])
158
+ patient_table.setStyle(TableStyle([
159
+ ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f0f0f0')),
160
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
161
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
162
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
163
+ ('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
164
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
165
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
166
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
167
+ ('GRID', (0, 0), (-1, -1), 1, colors.grey)
168
+ ]))
169
+
170
+ elements.append(patient_table)
171
+ elements.append(Spacer(1, 0.3*inch))
172
+
173
+ # Add Consultation Summary
174
+ elements.append(Paragraph("CONSULTATION SUMMARY", heading_style))
175
+
176
+ # Process summary text - split by lines and convert to paragraphs
177
+ summary_lines = summary_text.split('\n')
178
+ for line in summary_lines:
179
+ if line.strip():
180
+ # Replace emojis with text equivalents for PDF compatibility
181
+ line = line.replace('🩺', '[Medical] ')
182
+ line = line.replace('💡', '[Insight] ')
183
+ line = line.replace('💊', '[Medicine] ')
184
+ line = line.replace('🥗', '[Lifestyle] ')
185
+ line = line.replace('⚠️', '[Warning] ')
186
+ line = line.replace('⚠', '[Warning] ')
187
+ line = line.replace('📅', '[Follow-up] ')
188
+ line = line.replace('━', '-')
189
+
190
+ # Check if it's a heading (starts with **)
191
+ if line.strip().startswith('**') and line.strip().endswith('**'):
192
+ elements.append(Paragraph(line.strip('*'), heading_style))
193
+ else:
194
+ elements.append(Paragraph(line, normal_style))
195
+
196
+ elements.append(Spacer(1, 0.3*inch))
197
+
198
+ # Add Conversation History
199
+ elements.append(PageBreak())
200
+ elements.append(Paragraph("CONVERSATION HISTORY", heading_style))
201
+ elements.append(Spacer(1, 0.2*inch))
202
+
203
+ for i, msg in enumerate(history, 1):
204
+ role = "DOCTOR" if msg['role'] == 'assistant' else "PATIENT"
205
+ timestamp = msg.get('timestamp', 'N/A')
206
+
207
+ role_style = ParagraphStyle(
208
+ f'Role{i}',
209
+ parent=styles['Normal'],
210
+ fontSize=10,
211
+ textColor=colors.HexColor('#667eea') if role == "DOCTOR" else colors.HexColor('#28a745'),
212
+ fontName='Helvetica-Bold',
213
+ spaceAfter=4
214
+ )
215
+
216
+ elements.append(Paragraph(f"{role} ({timestamp}):", role_style))
217
+
218
+ content = msg['content'].replace('🩺', '').replace('💡', '').replace('💊', '')
219
+ content = content.replace('🥗', '').replace('⚠️', '').replace('⚠', '').replace('📅', '')
220
+ elements.append(Paragraph(content, normal_style))
221
+ elements.append(Spacer(1, 0.15*inch))
222
+
223
+ # Add disclaimer at the end
224
+ elements.append(Spacer(1, 0.3*inch))
225
+
226
+ disclaimer_style = ParagraphStyle(
227
+ 'Disclaimer',
228
+ parent=styles['Normal'],
229
+ fontSize=9,
230
+ textColor=colors.red,
231
+ alignment=TA_CENTER,
232
+ fontName='Helvetica-Bold',
233
+ borderColor=colors.red,
234
+ borderWidth=1,
235
+ borderPadding=10,
236
+ spaceAfter=12
237
+ )
238
+
239
+ elements.append(Paragraph(
240
+ "⚠ IMPORTANT DISCLAIMER ⚠<br/>" +
241
+ "This is a preliminary AI-generated consultation for informational purposes only.<br/>" +
242
+ "It is NOT a substitute for professional medical advice, diagnosis, or treatment.<br/>" +
243
+ "Always seek the advice of a qualified healthcare provider with any questions regarding a medical condition.",
244
+ disclaimer_style
245
+ ))
246
+
247
+ # Build PDF
248
+ doc.build(elements)
249
+
250
+ return pdf_filename
251
+
252
+ # =====================================================
253
+ # STORAGE FUNCTIONS (same as before)
254
+ # =====================================================
255
+
256
+ def save_session_to_json(session_id: str, memory: 'ConversationMemory'):
257
+ """Save session data to JSON file"""
258
+ file_path = STORAGE_DIR / f"{session_id}.json"
259
+
260
+ session_data = {
261
+ "session_id": session_id,
262
+ "created_at": memory.created_at.isoformat(),
263
+ "last_updated": datetime.now().isoformat(),
264
+ "patient_data": memory.patient_data,
265
+ "questions_asked": memory.questions_asked,
266
+ "history": memory.history,
267
+ "message_count": len(memory.history),
268
+ "pdf_filename": getattr(memory, 'pdf_filename', None)
269
+ }
270
+
271
+ with open(file_path, 'w', encoding='utf-8') as f:
272
+ json.dump(session_data, f, indent=2, ensure_ascii=False)
273
+
274
+ def load_session_from_json(session_id: str) -> Optional[Dict]:
275
+ """Load session data from JSON file"""
276
+ file_path = STORAGE_DIR / f"{session_id}.json"
277
+
278
+ if not file_path.exists():
279
+ return None
280
+
281
+ with open(file_path, 'r', encoding='utf-8') as f:
282
+ return json.load(f)
283
+
284
+ def list_all_sessions() -> List[Dict]:
285
+ """List all stored sessions"""
286
+ sessions_list = []
287
+
288
+ for file_path in STORAGE_DIR.glob("*.json"):
289
+ try:
290
+ with open(file_path, 'r', encoding='utf-8') as f:
291
+ data = json.load(f)
292
+ sessions_list.append({
293
+ "session_id": data["session_id"],
294
+ "created_at": data["created_at"],
295
+ "last_updated": data.get("last_updated", data["created_at"]),
296
+ "patient_name": data["patient_data"].get("name", "Unknown"),
297
+ "message_count": data["message_count"],
298
+ "has_pdf": data.get("pdf_filename") is not None
299
+ })
300
+ except Exception as e:
301
+ print(f"Error reading {file_path}: {e}")
302
+
303
+ return sorted(sessions_list, key=lambda x: x["last_updated"], reverse=True)
304
+
305
+ # =====================================================
306
+ # MEMORY MANAGEMENT (same as before)
307
+ # =====================================================
308
+
309
+ class ConversationMemory:
310
+ """Manages short-term memory for each session"""
311
+ def __init__(self, max_messages: int = 20, session_id: str = None):
312
+ self.max_messages = max_messages
313
+ self.history = []
314
+ self.patient_data = {}
315
+ self.created_at = datetime.now()
316
+ self.questions_asked = 0
317
+ self.session_id = session_id
318
+ self.pdf_filename = None
319
+
320
+ def add_message(self, role: str, content: str):
321
+ """Add message to history with memory management"""
322
+ self.history.append({
323
+ "role": role,
324
+ "content": content,
325
+ "timestamp": datetime.now().isoformat()
326
+ })
327
+
328
+ if role == "assistant" and "?" in content:
329
+ self.questions_asked += 1
330
+
331
+ if len(self.history) > self.max_messages:
332
+ self.history = [self.history[0]] + self.history[-(self.max_messages-1):]
333
+
334
+ if self.session_id:
335
+ save_session_to_json(self.session_id, self)
336
+
337
+ def extract_patient_info(self, message: str):
338
+ """Extract and store patient information from conversation"""
339
+ message_lower = message.lower()
340
+
341
+ if any(word in message_lower for word in ["name is", "i'm", "i am", "im"]):
342
+ words = message.split()
343
+ for i, word in enumerate(words):
344
+ if word.lower() in ["is", "i'm", "am", "im"] and i + 1 < len(words):
345
+ self.patient_data["name"] = words[i + 1].strip(".,!?")
346
+
347
+ if "year" in message_lower or "age" in message_lower:
348
+ import re
349
+ age_match = re.search(r'\b(\d{1,3})\b', message)
350
+ if age_match:
351
+ self.patient_data["age"] = age_match.group(1)
352
+
353
+ if "fever" in message_lower or "pain" in message_lower or "sick" in message_lower:
354
+ self.patient_data["has_symptoms"] = True
355
+
356
+ def should_give_recommendations(self) -> bool:
357
+ """Check if we should provide recommendations now"""
358
+ return (
359
+ self.questions_asked >= 7 or
360
+ self.patient_data.get("has_symptoms", False)
361
+ )
362
+
363
+ def get_context_summary(self) -> str:
364
+ """Generate a brief context summary for the AI"""
365
+ summary = "\n[Session Context: "
366
+ if "name" in self.patient_data:
367
+ summary += f"Name: {self.patient_data['name']}, "
368
+ if "age" in self.patient_data:
369
+ summary += f"Age: {self.patient_data['age']}, "
370
+ summary += f"Questions asked: {self.questions_asked}/7, "
371
+
372
+ if self.questions_asked >= 5:
373
+ summary += "⚠️ IMPORTANT: You've asked enough questions. After the next 1-2 answers, IMMEDIATELY provide comprehensive medical recommendations.]"
374
+ elif self.questions_asked >= 7:
375
+ summary += "⚠️ CRITICAL: You MUST provide comprehensive medical recommendations NOW. Do not ask more questions!]"
376
+ else:
377
+ summary += f"Ask {7 - self.questions_asked} more essential questions then give recommendations.]"
378
+
379
+ return summary
380
+
381
+ def get_gemini_history(self) -> List[Dict]:
382
+ """Convert history to Gemini format"""
383
+ gemini_history = []
384
+ for msg in self.history:
385
+ gemini_history.append({
386
+ "role": "user" if msg["role"] == "user" else "model",
387
+ "parts": [msg["content"]]
388
+ })
389
+ return gemini_history
390
+
391
+ @classmethod
392
+ def from_json(cls, session_data: Dict) -> 'ConversationMemory':
393
+ """Create ConversationMemory from JSON data"""
394
+ memory = cls(session_id=session_data["session_id"])
395
+ memory.history = session_data["history"]
396
+ memory.patient_data = session_data["patient_data"]
397
+ memory.questions_asked = session_data["questions_asked"]
398
+ memory.created_at = datetime.fromisoformat(session_data["created_at"])
399
+ memory.pdf_filename = session_data.get("pdf_filename")
400
+ return memory
401
+
402
+ sessions: Dict[str, ConversationMemory] = {}
403
+
404
+ def cleanup_old_sessions():
405
+ """Remove sessions older than 1 hour from memory"""
406
+ current_time = datetime.now()
407
+ expired_sessions = []
408
+
409
+ for session_id, memory in sessions.items():
410
+ age = (current_time - memory.created_at).total_seconds()
411
+ if age > 3600:
412
+ expired_sessions.append(session_id)
413
+
414
+ for session_id in expired_sessions:
415
+ del sessions[session_id]
416
+
417
+ # =====================================================
418
+ # FASTAPI APPLICATION
419
+ # =====================================================
420
+
421
+ app = FastAPI(
422
+ title="AI Doctor Consultation API with PDF Generation",
423
+ description="Professional medical consultation API with PDF summary generation",
424
+ version="3.0.0"
425
+ )
426
+
427
+ app.add_middleware(
428
+ CORSMiddleware,
429
+ allow_origins=["*"],
430
+ allow_credentials=True,
431
+ allow_methods=["*"],
432
+ allow_headers=["*"],
433
+ )
434
+
435
+ # Pydantic models
436
+ class ChatRequest(BaseModel):
437
+ session_id: Optional[str] = None
438
+ message: str
439
+
440
+ class ChatResponse(BaseModel):
441
+ session_id: str
442
+ response: str
443
+ timestamp: str
444
+ patient_data: Dict
445
+
446
+ class SessionRequest(BaseModel):
447
+ session_id: str
448
+
449
+ class SummaryResponse(BaseModel):
450
+ summary: str
451
+ session_id: str
452
+ pdf_filename: str
453
+ pdf_url: str
454
+
455
+ class HealthCheck(BaseModel):
456
+ status: str
457
+ timestamp: str
458
+ active_sessions: int
459
+ stored_sessions: int
460
+ stored_pdfs: int
461
+
462
+ # =====================================================
463
+ # API ENDPOINTS
464
+ # =====================================================
465
+
466
+ @app.get("/", response_model=HealthCheck)
467
+ async def root():
468
+ """Health check endpoint"""
469
+ cleanup_old_sessions()
470
+ stored_count = len(list(STORAGE_DIR.glob("*.json")))
471
+ pdf_count = len(list(PDF_DIR.glob("*.pdf")))
472
+ return {
473
+ "status": "healthy",
474
+ "timestamp": datetime.now().isoformat(),
475
+ "active_sessions": len(sessions),
476
+ "stored_sessions": stored_count,
477
+ "stored_pdfs": pdf_count
478
+ }
479
+
480
+ @app.post("/start-session")
481
+ async def start_session():
482
+ """Start a new consultation session"""
483
+ session_id = str(uuid.uuid4())
484
+ sessions[session_id] = ConversationMemory(max_messages=20, session_id=session_id)
485
+
486
+ initial_message = "Hello! I'm Dr. AI Assistant. I'm here to help you today.\n\n👤 May I have your name, please?"
487
+
488
+ sessions[session_id].add_message("assistant", initial_message)
489
+
490
+ return {
491
+ "session_id": session_id,
492
+ "message": initial_message,
493
+ "timestamp": datetime.now().isoformat()
494
+ }
495
+
496
+ @app.post("/chat", response_model=ChatResponse)
497
+ async def chat(request: ChatRequest):
498
+ """Send a message and get doctor's response"""
499
+ try:
500
+ if not request.session_id or request.session_id not in sessions:
501
+ session_id = str(uuid.uuid4())
502
+ sessions[session_id] = ConversationMemory(max_messages=20, session_id=session_id)
503
+ else:
504
+ session_id = request.session_id
505
+
506
+ memory = sessions[session_id]
507
+ memory.extract_patient_info(request.message)
508
+ memory.add_message("user", request.message)
509
+
510
+ context = memory.get_context_summary()
511
+ system_prompt = DOCTOR_SYSTEM_PROMPT + context
512
+
513
+ model = genai.GenerativeModel(
514
+ model_name=MODEL_ID,
515
+ system_instruction=system_prompt
516
+ )
517
+
518
+ chat = model.start_chat(history=memory.get_gemini_history()[:-1])
519
+ response = chat.send_message(request.message)
520
+ doctor_response = response.text
521
+
522
+ memory.add_message("assistant", doctor_response)
523
+
524
+ return {
525
+ "session_id": session_id,
526
+ "response": doctor_response,
527
+ "timestamp": datetime.now().isoformat(),
528
+ "patient_data": memory.patient_data
529
+ }
530
+
531
+ except Exception as e:
532
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
533
+
534
+ @app.post("/summary", response_model=SummaryResponse)
535
+ async def generate_summary(request: SessionRequest):
536
+ """Generate consultation summary and PDF"""
537
+ if request.session_id not in sessions:
538
+ session_data = load_session_from_json(request.session_id)
539
+ if not session_data:
540
+ raise HTTPException(status_code=404, detail="Session not found")
541
+ memory = ConversationMemory.from_json(session_data)
542
+ sessions[request.session_id] = memory
543
+ else:
544
+ memory = sessions[request.session_id]
545
+
546
+ summary_request = """Please generate a COMPREHENSIVE and DETAILED medical consultation summary based on our entire conversation. Make it thorough and professional:
547
+
548
+ 📋 **COMPREHENSIVE MEDICAL CONSULTATION SUMMARY**
549
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
550
+
551
+ **PATIENT INFORMATION:**
552
+ - Full Name: [Patient's name]
553
+ - Age: [Patient's age if mentioned]
554
+ - Gender: [If mentioned]
555
+ - Consultation Date: [Current date and time]
556
+ - Session Duration: [Approximate]
557
+ - Current Medications: [List all mentioned]
558
+ - Known Allergies: [If mentioned]
559
+
560
+ **CHIEF COMPLAINTS & SYMPTOMS:**
561
+ [Provide a detailed description of ALL symptoms mentioned, including:]
562
+ - Primary symptom and severity
563
+ - Duration of each symptom
564
+ - Onset and progression
565
+ - Associated symptoms
566
+ - Aggravating and relieving factors
567
+ - Impact on daily activities
568
+
569
+ **DETAILED MEDICAL HISTORY:**
570
+ [Include everything discussed:]
571
+ - Current medications and dosages
572
+ - Past medical conditions
573
+ - Recent illnesses or infections
574
+ - Family medical history (if mentioned)
575
+ - Lifestyle factors (sleep, stress, diet)
576
+ - Recent travel or exposures
577
+
578
+ **CLINICAL ASSESSMENT:**
579
+ [Provide detailed analysis:]
580
+ - Most likely diagnosis with explanation
581
+ - Differential diagnoses (2-3 possibilities)
582
+ - Reasoning behind each possibility
583
+ - Risk factors present
584
+ - Severity assessment
585
+
586
+ **COMPREHENSIVE TREATMENT PLAN:**
587
+
588
+ 1. **IMMEDIATE CARE RECOMMENDATIONS:**
589
+ - What to do in the next 24-48 hours
590
+ - Symptom management strategies
591
+ - Warning signs to watch for
592
+
593
+ 2. **MEDICATION RECOMMENDATIONS:**
594
+ - Primary medications (generic names, dosages, frequency, duration)
595
+ - Alternative options if first choice unavailable
596
+ - Potential side effects to monitor
597
+ - Drug interactions to avoid
598
+ - When to take each medication (with/without food)
599
+ - Important: Check with pharmacist for exact dosing
600
+
601
+ 3. **DETAILED DIETARY RECOMMENDATIONS:**
602
+ - Foods to eat (specific examples and portions)
603
+ - Foods to avoid completely
604
+ - Meal timing and frequency
605
+ - Hydration guidelines (specific amounts)
606
+ - Nutritional supplements if needed
607
+ - Sample meal plan for recovery
608
+
609
+ 4. **LIFESTYLE MODIFICATIONS:**
610
+ - Sleep recommendations (hours, timing, environment)
611
+ - Rest and activity balance
612
+ - Stress management techniques
613
+ - Environmental modifications
614
+ - Work/school attendance guidance
615
+ - Specific activities to avoid
616
+
617
+ 5. **HOME CARE REMEDIES:**
618
+ - Natural remedies that may help
619
+ - Temperature management techniques
620
+ - Pain relief methods
621
+ - Steam inhalation or other therapies
622
+ - Specific home treatments for symptoms
623
+
624
+ 6. **EXERCISE & PHYSICAL ACTIVITY:**
625
+ - Current activity restrictions
626
+ - Safe exercises during recovery
627
+ - When to resume normal activities
628
+ - Gradual activity progression plan
629
+ - Post-recovery exercise recommendations
630
+
631
+ 7. **PREVENTIVE MEASURES:**
632
+ - How to prevent recurrence
633
+ - Hygiene practices
634
+ - Vaccination recommendations
635
+ - Family/household precautions
636
+ - Long-term health maintenance
637
+
638
+ 8. **MONITORING PLAN:**
639
+ - Symptoms to track daily
640
+ - How to measure improvement
641
+ - When improvement should be expected
642
+ - What to document for doctor visit
643
+
644
+ **CRITICAL WARNING SIGNS - SEEK IMMEDIATE MEDICAL ATTENTION IF:**
645
+ [List 5-7 specific warning signs that require emergency care:]
646
+ - [Specific symptom with threshold]
647
+ - [Specific symptom with threshold]
648
+ - [Continue with detailed warnings]
649
+
650
+ **FOLLOW-UP CARE PLAN:**
651
+ - Timeline for self-care (e.g., "Monitor for 48 hours")
652
+ - When to schedule doctor appointment (specific timeframe)
653
+ - What information to bring to doctor
654
+ - Specialist referral recommendations if needed
655
+ - Follow-up testing that may be needed
656
+
657
+ **PROGNOSIS & EXPECTED RECOVERY:**
658
+ - Expected recovery timeline
659
+ - What to expect during recovery process
660
+ - Signs of improvement to look for
661
+ - Long-term outlook
662
+
663
+ **ADDITIONAL RESOURCES:**
664
+ - Reputable health information sources
665
+ - Support resources if applicable
666
+ - Emergency contact information reminder
667
+
668
+ **PATIENT EDUCATION:**
669
+ - Understanding your condition
670
+ - How the body fights this illness
671
+ - Why specific recommendations are important
672
+ - Common misconceptions about this condition
673
+
674
+ ━━━━━━━��━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
675
+ ⚠️ **CRITICAL DISCLAIMER** ⚠️
676
+ This is a preliminary AI-generated consultation for informational and educational purposes ONLY.
677
+ This is NOT a substitute for professional medical advice, diagnosis, or treatment.
678
+ This AI cannot examine you physically, run laboratory tests, or make definitive diagnoses.
679
+ ALWAYS seek the advice of a qualified, licensed healthcare provider with any questions regarding a medical condition.
680
+ Never disregard professional medical advice or delay seeking it because of this AI consultation.
681
+ In case of emergency, call your local emergency services immediately.
682
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
683
+
684
+ Please make this summary as detailed, professional, and helpful as possible. Include specific, actionable advice."""
685
+
686
+ try:
687
+ model = genai.GenerativeModel(
688
+ model_name=MODEL_ID,
689
+ system_instruction=DOCTOR_SYSTEM_PROMPT
690
+ )
691
+
692
+ chat = model.start_chat(history=memory.get_gemini_history())
693
+ response = chat.send_message(summary_request)
694
+ summary_text = response.text
695
+
696
+ # Generate PDF
697
+ pdf_filename = generate_pdf_summary(
698
+ request.session_id,
699
+ summary_text,
700
+ memory.patient_data,
701
+ memory.history
702
+ )
703
+
704
+ # Save PDF filename to memory
705
+ memory.pdf_filename = pdf_filename
706
+ save_session_to_json(request.session_id, memory)
707
+
708
+ return {
709
+ "summary": summary_text,
710
+ "session_id": request.session_id,
711
+ "pdf_filename": pdf_filename,
712
+ "pdf_url": f"/download-pdf/{request.session_id}"
713
+ }
714
+
715
+ except Exception as e:
716
+ raise HTTPException(status_code=500, detail=f"Error generating summary: {str(e)}")
717
+
718
+ @app.get("/download-pdf/{session_id}")
719
+ async def download_pdf(session_id: str):
720
+ """Download PDF summary for a session"""
721
+ # Check if session exists
722
+ if session_id in sessions:
723
+ memory = sessions[session_id]
724
+ else:
725
+ session_data = load_session_from_json(session_id)
726
+ if not session_data:
727
+ raise HTTPException(status_code=404, detail="Session not found")
728
+ memory = ConversationMemory.from_json(session_data)
729
+
730
+ if not memory.pdf_filename:
731
+ raise HTTPException(status_code=404, detail="PDF not generated yet. Please generate summary first.")
732
+
733
+ pdf_path = PDF_DIR / memory.pdf_filename
734
+
735
+ if not pdf_path.exists():
736
+ raise HTTPException(status_code=404, detail="PDF file not found")
737
+
738
+ patient_name = memory.patient_data.get('name', 'Patient')
739
+ download_filename = f"Consultation_Summary_{patient_name}_{datetime.now().strftime('%Y%m%d')}.pdf"
740
+
741
+ return FileResponse(
742
+ path=str(pdf_path),
743
+ media_type='application/pdf',
744
+ filename=download_filename
745
+ )
746
+
747
+ @app.get("/load-session/{session_id}")
748
+ async def load_session(session_id: str):
749
+ """Load a previous consultation session by ID"""
750
+ if session_id in sessions:
751
+ memory = sessions[session_id]
752
+ return {
753
+ "session_id": session_id,
754
+ "loaded": True,
755
+ "from_cache": True,
756
+ "history": memory.history,
757
+ "patient_data": memory.patient_data,
758
+ "created_at": memory.created_at.isoformat(),
759
+ "questions_asked": memory.questions_asked,
760
+ "has_pdf": memory.pdf_filename is not None,
761
+ "pdf_url": f"/download-pdf/{session_id}" if memory.pdf_filename else None
762
+ }
763
+
764
+ session_data = load_session_from_json(session_id)
765
+
766
+ if not session_data:
767
+ raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
768
+
769
+ memory = ConversationMemory.from_json(session_data)
770
+ sessions[session_id] = memory
771
+
772
+ return {
773
+ "session_id": session_id,
774
+ "loaded": True,
775
+ "from_cache": False,
776
+ "history": memory.history,
777
+ "patient_data": memory.patient_data,
778
+ "created_at": memory.created_at.isoformat(),
779
+ "questions_asked": memory.questions_asked,
780
+ "has_pdf": memory.pdf_filename is not None,
781
+ "pdf_url": f"/download-pdf/{session_id}" if memory.pdf_filename else None,
782
+ "message": "Session loaded successfully. You can continue the conversation."
783
+ }
784
+
785
+ @app.get("/all-sessions")
786
+ async def get_all_sessions():
787
+ """Get list of all stored consultation sessions"""
788
+ return {
789
+ "total_sessions": len(list(STORAGE_DIR.glob("*.json"))),
790
+ "sessions": list_all_sessions()
791
+ }
792
+
793
+ @app.post("/restart-session")
794
+ async def restart_session(request: SessionRequest):
795
+ """Restart a consultation session"""
796
+ if request.session_id in sessions:
797
+ del sessions[request.session_id]
798
+
799
+ sessions[request.session_id] = ConversationMemory(max_messages=20, session_id=request.session_id)
800
+
801
+ initial_message = "Consultation restarted. Hello! I'm Dr. AI Assistant. May I have your name please?"
802
+ sessions[request.session_id].add_message("assistant", initial_message)
803
+
804
+ return {
805
+ "session_id": request.session_id,
806
+ "message": initial_message,
807
+ "timestamp": datetime.now().isoformat()
808
+ }
809
+
810
+ @app.delete("/session/{session_id}")
811
+ async def delete_session(session_id: str):
812
+ """Delete a consultation session (from memory, JSON, and PDF)"""
813
+ if session_id in sessions:
814
+ memory = sessions[session_id]
815
+ pdf_filename = memory.pdf_filename
816
+ del sessions[session_id]
817
+ else:
818
+ session_data = load_session_from_json(session_id)
819
+ pdf_filename = session_data.get('pdf_filename') if session_data else None
820
+
821
+ # Remove JSON file
822
+ file_path = STORAGE_DIR / f"{session_id}.json"
823
+ if file_path.exists():
824
+ file_path.unlink()
825
+
826
+ # Remove PDF file if exists
827
+ if pdf_filename:
828
+ pdf_path = PDF_DIR / pdf_filename
829
+ if pdf_path.exists():
830
+ pdf_path.unlink()
831
+
832
+ return {"message": "Session and associated files deleted successfully"}
833
+
834
+ @app.get("/session/{session_id}/history")
835
+ async def get_session_history(session_id: str):
836
+ """Get conversation history for a session"""
837
+ if session_id in sessions:
838
+ memory = sessions[session_id]
839
+ else:
840
+ session_data = load_session_from_json(session_id)
841
+ if not session_data:
842
+ raise HTTPException(status_code=404, detail="Session not found")
843
+ memory = ConversationMemory.from_json(session_data)
844
+
845
+ return {
846
+ "session_id": session_id,
847
+ "history": memory.history,
848
+ "patient_data": memory.patient_data,
849
+ "created_at": memory.created_at.isoformat(),
850
+ "questions_asked": memory.questions_asked,
851
+ "has_pdf": memory.pdf_filename is not None
852
+ }
853
+
854
+ @app.get("/active-sessions")
855
+ async def get_active_sessions():
856
+ """Get list of all active sessions in memory"""
857
+ cleanup_old_sessions()
858
+ return {
859
+ "active_sessions": len(sessions),
860
+ "sessions": [
861
+ {
862
+ "session_id": sid,
863
+ "created_at": mem.created_at.isoformat(),
864
+ "message_count": len(mem.history),
865
+ "questions_asked": mem.questions_asked,
866
+ "patient_data": mem.patient_data,
867
+ "has_pdf": mem.pdf_filename is not None
868
+ }
869
+ for sid, mem in sessions.items()
870
+ ]
871
+ }
872
+
873
+ if __name__ == "__main__":
874
+ import uvicorn
875
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.32.0
3
+ google-generativeai==0.8.3
4
+ pydantic==2.9.2
5
+ python-multipart==0.0.12
script.js ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_URL = 'http://localhost:8000';
2
+ let sessionId = null;
3
+ let patientData = {};
4
+ let currentPdfUrl = null;
5
+
6
+ // Voice recognition variables
7
+ let recognition = null;
8
+ let isRecording = false;
9
+ let autoSpeak = false;
10
+ let currentUtterance = null;
11
+ let isSpeaking = false;
12
+ let continuousMode = false;
13
+
14
+ // Initialize Speech Recognition
15
+ function initSpeechRecognition() {
16
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
17
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
18
+ recognition = new SpeechRecognition();
19
+ recognition.continuous = false;
20
+ recognition.interimResults = false;
21
+ recognition.lang = 'en-US';
22
+
23
+ recognition.onstart = function() {
24
+ isRecording = true;
25
+ document.getElementById('voiceBtn').classList.add('recording');
26
+ document.getElementById('voiceInputBtn').classList.add('active');
27
+ updateVoiceStatus('🎤 Listening... Speak now');
28
+ };
29
+
30
+ recognition.onresult = function(event) {
31
+ const transcript = event.results[0][0].transcript;
32
+ document.getElementById('messageInput').value = transcript;
33
+ updateVoiceStatus('✓ Voice input received: "' + transcript + '"');
34
+
35
+ // In continuous mode, automatically send the message
36
+ if (continuousMode) {
37
+ setTimeout(() => {
38
+ sendMessage();
39
+ }, 500);
40
+ } else {
41
+ setTimeout(() => hideVoiceStatus(), 2000);
42
+ }
43
+ };
44
+
45
+ recognition.onerror = function(event) {
46
+ console.error('Speech recognition error:', event.error);
47
+ updateVoiceStatus('❌ Error: ' + event.error, 'error');
48
+ stopRecording();
49
+
50
+ // Restart listening in continuous mode if it wasn't a user abort
51
+ if (continuousMode && event.error !== 'aborted' && event.error !== 'no-speech') {
52
+ setTimeout(() => {
53
+ if (continuousMode && !isSpeaking) {
54
+ startListening();
55
+ }
56
+ }, 1000);
57
+ }
58
+ };
59
+
60
+ recognition.onend = function() {
61
+ stopRecording();
62
+
63
+ // Restart listening in continuous mode after a brief pause
64
+ if (continuousMode && !isSpeaking) {
65
+ setTimeout(() => {
66
+ if (continuousMode) {
67
+ startListening();
68
+ }
69
+ }, 500);
70
+ }
71
+ };
72
+ } else {
73
+ showNotification('Voice recognition not supported in this browser', 'error');
74
+ }
75
+ }
76
+
77
+ function startListening() {
78
+ if (!recognition) {
79
+ initSpeechRecognition();
80
+ }
81
+
82
+ if (!isRecording) {
83
+ try {
84
+ recognition.start();
85
+ } catch (e) {
86
+ console.error('Error starting recognition:', e);
87
+ }
88
+ }
89
+ }
90
+
91
+ function toggleVoiceInput() {
92
+ if (!recognition) {
93
+ initSpeechRecognition();
94
+ }
95
+
96
+ if (isRecording) {
97
+ recognition.stop();
98
+ } else {
99
+ startListening();
100
+ }
101
+ }
102
+
103
+ function stopRecording() {
104
+ isRecording = false;
105
+ document.getElementById('voiceBtn').classList.remove('recording');
106
+ document.getElementById('voiceInputBtn').classList.remove('active');
107
+ }
108
+
109
+ function updateVoiceStatus(message, type = 'info') {
110
+ const status = document.getElementById('voiceStatus');
111
+ status.textContent = message;
112
+ status.className = 'voice-status active';
113
+ if (type === 'error') {
114
+ status.style.background = '#ffebee';
115
+ status.style.color = '#c62828';
116
+ } else {
117
+ status.style.background = '#e3f2fd';
118
+ status.style.color = '#1565c0';
119
+ }
120
+ }
121
+
122
+ function hideVoiceStatus() {
123
+ document.getElementById('voiceStatus').classList.remove('active');
124
+ }
125
+
126
+ // Text-to-Speech functions
127
+ function speakText(text) {
128
+ if (!('speechSynthesis' in window)) {
129
+ console.error('Speech synthesis not supported');
130
+ showNotification('Text-to-speech not supported in this browser', 'error');
131
+ return;
132
+ }
133
+
134
+ if (isSpeaking) {
135
+ window.speechSynthesis.cancel();
136
+ }
137
+
138
+ let cleanText = text
139
+ .replace(/🩺|💡|💊|🥗|⚠️|⚠|📅|🎯|📊|🛡️|🏃|👤/g, '')
140
+ .replace(/━+/g, '')
141
+ .replace(/\*\*/g, '')
142
+ .replace(/\n{3,}/g, '\n\n');
143
+
144
+ currentUtterance = new SpeechSynthesisUtterance(cleanText);
145
+ currentUtterance.rate = 0.85;
146
+ currentUtterance.pitch = 1.1;
147
+ currentUtterance.volume = 1;
148
+ currentUtterance.lang = 'en-US';
149
+
150
+ const voices = window.speechSynthesis.getVoices();
151
+
152
+ if (voices.length > 0) {
153
+ const preferredVoice = voices.find(voice =>
154
+ (voice.lang.includes('en-US') || voice.lang.includes('en-GB')) &&
155
+ (voice.name.includes('Female') ||
156
+ voice.name.includes('Samantha') ||
157
+ voice.name.includes('Victoria') ||
158
+ voice.name.includes('Google') ||
159
+ voice.name.includes('Microsoft'))
160
+ ) || voices.find(voice => voice.lang.includes('en')) || voices[0];
161
+
162
+ currentUtterance.voice = preferredVoice;
163
+ }
164
+
165
+ currentUtterance.onstart = function() {
166
+ isSpeaking = true;
167
+ };
168
+
169
+ currentUtterance.onend = function() {
170
+ isSpeaking = false;
171
+
172
+ // In continuous mode, start listening again after doctor finishes speaking
173
+ if (continuousMode) {
174
+ setTimeout(() => {
175
+ if (continuousMode && !isRecording) {
176
+ startListening();
177
+ }
178
+ }, 500);
179
+ }
180
+ };
181
+
182
+ currentUtterance.onerror = function(event) {
183
+ console.error('Speech synthesis error:', event);
184
+ isSpeaking = false;
185
+ if (event.error !== 'interrupted') {
186
+ showNotification('Speech error: ' + event.error, 'error');
187
+ }
188
+ };
189
+
190
+ setTimeout(() => {
191
+ window.speechSynthesis.speak(currentUtterance);
192
+ }, 100);
193
+ }
194
+
195
+ function stopSpeaking() {
196
+ window.speechSynthesis.cancel();
197
+ isSpeaking = false;
198
+ }
199
+
200
+ function toggleAutoSpeak() {
201
+ autoSpeak = !autoSpeak;
202
+ const indicator = document.getElementById('speakIndicator');
203
+ const text = document.getElementById('autoSpeakText');
204
+
205
+ if (autoSpeak) {
206
+ indicator.style.background = '#4CAF50';
207
+ text.textContent = '🔊 Auto-Speak: ON';
208
+ showNotification('Auto-speak enabled - Doctor responses will be spoken', 'success');
209
+ } else {
210
+ indicator.style.background = '#999';
211
+ text.textContent = '🔊 Auto-Speak: OFF';
212
+ stopSpeaking();
213
+ showNotification('Auto-speak disabled', 'info');
214
+ }
215
+ }
216
+
217
+ function toggleContinuousMode() {
218
+ continuousMode = !continuousMode;
219
+ const btn = document.getElementById('continuousModeBtn');
220
+
221
+ if (continuousMode) {
222
+ btn.classList.add('active');
223
+ btn.innerHTML = '<span class="voice-indicator recording"></span><span>🔄 Continuous: ON</span>';
224
+ autoSpeak = true;
225
+ toggleAutoSpeak(); // Enable auto-speak
226
+ showNotification('Continuous conversation mode enabled! Speak naturally.', 'success');
227
+ startListening();
228
+ } else {
229
+ btn.classList.remove('active');
230
+ btn.innerHTML = '<span class="voice-indicator"></span><span>🔄 Continuous: OFF</span>';
231
+ showNotification('Continuous mode disabled', 'info');
232
+ if (isRecording) {
233
+ recognition.stop();
234
+ }
235
+ }
236
+ }
237
+
238
+ window.onload = async () => {
239
+ await startNewSession();
240
+ initSpeechRecognition();
241
+
242
+ if ('speechSynthesis' in window) {
243
+ window.speechSynthesis.onvoiceschanged = function() {
244
+ window.speechSynthesis.getVoices();
245
+ };
246
+ }
247
+ };
248
+
249
+ async function startNewSession() {
250
+ try {
251
+ const response = await fetch(`${API_URL}/start-session`, {
252
+ method: 'POST'
253
+ });
254
+ const data = await response.json();
255
+ sessionId = data.session_id;
256
+
257
+ updateSessionInfo();
258
+ addMessage('doctor', data.message);
259
+
260
+ if (autoSpeak) {
261
+ speakText(data.message);
262
+ }
263
+
264
+ showNotification('New consultation started!', 'success');
265
+ } catch (error) {
266
+ console.error('Error starting session:', error);
267
+ showNotification('Failed to connect to server', 'error');
268
+ }
269
+ }
270
+
271
+ function updateSessionInfo() {
272
+ const info = document.getElementById('sessionInfo');
273
+ const patientInfo = patientData.name ? `Patient: ${patientData.name}` : 'New Patient';
274
+ info.innerHTML = `${patientInfo}`;
275
+
276
+ const sessionDisplay = document.getElementById('sessionIdDisplay');
277
+ sessionDisplay.textContent = `Session ID: ${sessionId} (Click to copy)`;
278
+ }
279
+
280
+ function copySessionId() {
281
+ navigator.clipboard.writeText(sessionId).then(() => {
282
+ showNotification('Session ID copied to clipboard!', 'success');
283
+ }).catch(() => {
284
+ showNotification('Failed to copy Session ID', 'error');
285
+ });
286
+ }
287
+
288
+ async function sendMessage() {
289
+ const input = document.getElementById('messageInput');
290
+ const message = input.value.trim();
291
+
292
+ if (!message) return;
293
+
294
+ addMessage('user', message);
295
+ input.value = '';
296
+ showLoading(true);
297
+
298
+ try {
299
+ const response = await fetch(`${API_URL}/chat`, {
300
+ method: 'POST',
301
+ headers: {
302
+ 'Content-Type': 'application/json'
303
+ },
304
+ body: JSON.stringify({
305
+ session_id: sessionId,
306
+ message: message
307
+ })
308
+ });
309
+
310
+ const data = await response.json();
311
+ sessionId = data.session_id;
312
+ patientData = data.patient_data;
313
+ updateSessionInfo();
314
+ addMessage('doctor', data.response);
315
+
316
+ if (autoSpeak || continuousMode) {
317
+ speakText(data.response);
318
+ }
319
+ } catch (error) {
320
+ console.error('Error sending message:', error);
321
+ addMessage('doctor', '❌ Sorry, there was an error. Please try again.');
322
+ } finally {
323
+ showLoading(false);
324
+ }
325
+ }
326
+
327
+ function addMessage(type, text) {
328
+ const container = document.getElementById('chatContainer');
329
+ const messageDiv = document.createElement('div');
330
+ messageDiv.className = `message ${type}`;
331
+
332
+ const contentDiv = document.createElement('div');
333
+ contentDiv.className = 'message-content';
334
+ contentDiv.textContent = text;
335
+
336
+ if (type === 'doctor' && 'speechSynthesis' in window) {
337
+ const speakerIcon = document.createElement('span');
338
+ speakerIcon.className = 'speaker-icon';
339
+ speakerIcon.innerHTML = '🔊';
340
+ speakerIcon.title = 'Click to hear this message';
341
+ speakerIcon.onclick = function() {
342
+ if (isSpeaking) {
343
+ stopSpeaking();
344
+ speakerIcon.classList.remove('speaking');
345
+ } else {
346
+ speakText(text);
347
+ speakerIcon.classList.add('speaking');
348
+ setTimeout(() => speakerIcon.classList.remove('speaking'), 3000);
349
+ }
350
+ };
351
+ contentDiv.appendChild(speakerIcon);
352
+ }
353
+
354
+ messageDiv.appendChild(contentDiv);
355
+ container.appendChild(messageDiv);
356
+ container.scrollTop = container.scrollHeight;
357
+ }
358
+
359
+ function showLoading(show) {
360
+ const loading = document.getElementById('loading');
361
+ loading.className = show ? 'loading active' : 'loading';
362
+ }
363
+
364
+ async function generateSummary() {
365
+ if (!sessionId) {
366
+ showNotification('No active session', 'error');
367
+ return;
368
+ }
369
+
370
+ showLoading(true);
371
+ showNotification('Generating comprehensive detailed summary and PDF... This may take 30-60 seconds', 'success');
372
+
373
+ try {
374
+ const response = await fetch(`${API_URL}/summary`, {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Content-Type': 'application/json'
378
+ },
379
+ body: JSON.stringify({
380
+ session_id: sessionId
381
+ })
382
+ });
383
+
384
+ const data = await response.json();
385
+
386
+ currentPdfUrl = `${API_URL}${data.pdf_url}`;
387
+ document.getElementById('summaryText').textContent = data.summary;
388
+ document.getElementById('pdfDownloadInfo').style.display = 'block';
389
+ document.getElementById('downloadPdfBtn').style.display = 'inline-block';
390
+ document.getElementById('viewPdfBtn').style.display = 'inline-block';
391
+ document.getElementById('summaryModal').classList.add('active');
392
+
393
+ showNotification('Comprehensive summary and professional PDF generated successfully!', 'success');
394
+ } catch (error) {
395
+ console.error('Error generating summary:', error);
396
+ showNotification('Failed to generate summary. Please try again.', 'error');
397
+ } finally {
398
+ showLoading(false);
399
+ }
400
+ }
401
+
402
+ function downloadPDF() {
403
+ if (!currentPdfUrl) {
404
+ showNotification('No PDF available', 'error');
405
+ return;
406
+ }
407
+
408
+ const link = document.createElement('a');
409
+ link.href = currentPdfUrl;
410
+ link.download = `Consultation_Summary_${sessionId.substring(0, 8)}.pdf`;
411
+ document.body.appendChild(link);
412
+ link.click();
413
+ document.body.removeChild(link);
414
+
415
+ showNotification('PDF download started!', 'success');
416
+ }
417
+
418
+ function togglePDFViewer() {
419
+ const viewerContainer = document.getElementById('pdfViewerContainer');
420
+ const viewer = document.getElementById('pdfViewer');
421
+ const btn = document.getElementById('viewPdfBtn');
422
+
423
+ if (viewerContainer.style.display === 'none') {
424
+ viewer.src = currentPdfUrl;
425
+ viewerContainer.style.display = 'block';
426
+ btn.textContent = '🙈 Hide PDF';
427
+ } else {
428
+ viewerContainer.style.display = 'none';
429
+ btn.textContent = '👁️ View PDF';
430
+ }
431
+ }
432
+
433
+ function closeSummary() {
434
+ document.getElementById('summaryModal').classList.remove('active');
435
+ document.getElementById('pdfViewerContainer').style.display = 'none';
436
+ document.getElementById('viewPdfBtn').textContent = '👁️ View PDF';
437
+ }
438
+
439
+ async function showHistoryModal() {
440
+ document.getElementById('historyModal').classList.add('active');
441
+ await loadHistoryList();
442
+ }
443
+
444
+ function closeHistory() {
445
+ document.getElementById('historyModal').classList.remove('active');
446
+ }
447
+
448
+ async function loadHistoryList() {
449
+ const historyList = document.getElementById('historyList');
450
+ historyList.innerHTML = '<p style="text-align: center; color: #999;">Loading...</p>';
451
+
452
+ try {
453
+ const response = await fetch(`${API_URL}/all-sessions`);
454
+ const data = await response.json();
455
+
456
+ if (data.sessions.length === 0) {
457
+ historyList.innerHTML = '<p style="text-align: center; color: #999;">No previous consultations found</p>';
458
+ return;
459
+ }
460
+
461
+ historyList.innerHTML = '';
462
+ data.sessions.forEach(session => {
463
+ const item = document.createElement('div');
464
+ item.className = 'history-item';
465
+ item.onclick = () => loadSession(session.session_id);
466
+
467
+ const date = new Date(session.last_updated).toLocaleString();
468
+ const pdfBadge = session.has_pdf ? '<span class="pdf-badge">📄 PDF Available</span>' : '';
469
+
470
+ item.innerHTML = `
471
+ <h4>👤 ${session.patient_name} ${pdfBadge}</h4>
472
+ <p>📅 ${date}</p>
473
+ <p>💬 Messages: ${session.message_count}</p>
474
+ <p style="font-family: monospace; font-size: 0.8em;">🔑 ${session.session_id.substring(0, 20)}...</p>
475
+ `;
476
+ historyList.appendChild(item);
477
+ });
478
+ } catch (error) {
479
+ console.error('Error loading history:', error);
480
+ historyList.innerHTML = '<p style="text-align: center; color: #dc3545;">Failed to load history</p>';
481
+ }
482
+ }
483
+
484
+ async function loadSessionById() {
485
+ const input = document.getElementById('sessionIdInput');
486
+ const id = input.value.trim();
487
+
488
+ if (!id) {
489
+ showNotification('Please enter a Session ID', 'error');
490
+ return;
491
+ }
492
+
493
+ await loadSession(id);
494
+ }
495
+
496
+ async function loadSession(id) {
497
+ showLoading(true);
498
+ closeHistory();
499
+
500
+ try {
501
+ const response = await fetch(`${API_URL}/load-session/${id}`);
502
+
503
+ if (!response.ok) {
504
+ throw new Error('Session not found');
505
+ }
506
+
507
+ const data = await response.json();
508
+
509
+ stopSpeaking();
510
+
511
+ document.getElementById('chatContainer').innerHTML = '';
512
+
513
+ sessionId = data.session_id;
514
+ patientData = data.patient_data;
515
+ updateSessionInfo();
516
+
517
+ data.history.forEach(msg => {
518
+ addMessage(msg.role === 'user' ? 'user' : 'doctor', msg.content);
519
+ });
520
+
521
+ if (data.has_pdf && data.pdf_url) {
522
+ currentPdfUrl = `${API_URL}${data.pdf_url}`;
523
+ }
524
+
525
+ showNotification('Consultation loaded successfully!', 'success');
526
+ } catch (error) {
527
+ console.error('Error loading session:', error);
528
+ showNotification('Failed to load session. Check the ID and try again.', 'error');
529
+ } finally {
530
+ showLoading(false);
531
+ }
532
+ }
533
+
534
+ async function restartSession() {
535
+ if (!confirm('Are you sure you want to start a new consultation? Current session will be saved.')) {
536
+ return;
537
+ }
538
+
539
+ // Disable continuous mode when restarting
540
+ if (continuousMode) {
541
+ toggleContinuousMode();
542
+ }
543
+
544
+ stopSpeaking();
545
+ document.getElementById('chatContainer').innerHTML = '';
546
+ patientData = {};
547
+ currentPdfUrl = null;
548
+ await startNewSession();
549
+ }
550
+
551
+ function showNotification(message, type) {
552
+ const notification = document.getElementById('notification');
553
+ notification.textContent = message;
554
+ notification.className = `notification ${type} active`;
555
+
556
+ setTimeout(() => {
557
+ notification.classList.remove('active');
558
+ }, 3000);
559
+ }
560
+
561
+ document.getElementById('messageInput').addEventListener('keypress', (e) => {
562
+ if (e.key === 'Enter') {
563
+ sendMessage();
564
+ }
565
+ });
566
+
567
+ document.getElementById('sessionIdInput').addEventListener('keypress', (e) => {
568
+ if (e.key === 'Enter') {
569
+ loadSessionById();
570
+ }
571
+ });
572
+
573
+ window.addEventListener('beforeunload', () => {
574
+ stopSpeaking();
575
+ if (continuousMode && recognition) {
576
+ recognition.stop();
577
+ }
578
+ });
styles.css ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ display: flex;
12
+ justify-content: center;
13
+ align-items: center;
14
+ padding: 20px;
15
+ }
16
+
17
+ .container {
18
+ max-width: 900px;
19
+ width: 100%;
20
+ background: white;
21
+ border-radius: 20px;
22
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
23
+ overflow: hidden;
24
+ }
25
+
26
+ .header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ color: white;
29
+ padding: 30px;
30
+ text-align: center;
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 2em;
35
+ margin-bottom: 10px;
36
+ }
37
+
38
+ .session-info {
39
+ background: rgba(255,255,255,0.2);
40
+ padding: 10px;
41
+ border-radius: 10px;
42
+ margin-top: 15px;
43
+ font-size: 0.9em;
44
+ }
45
+
46
+ .session-id-display {
47
+ background: rgba(0,0,0,0.2);
48
+ padding: 8px;
49
+ border-radius: 5px;
50
+ margin-top: 10px;
51
+ font-family: monospace;
52
+ font-size: 0.85em;
53
+ cursor: pointer;
54
+ transition: background 0.3s;
55
+ }
56
+
57
+ .session-id-display:hover {
58
+ background: rgba(0,0,0,0.3);
59
+ }
60
+
61
+ .voice-controls {
62
+ display: flex;
63
+ gap: 10px;
64
+ justify-content: center;
65
+ padding: 15px;
66
+ background: rgba(255,255,255,0.1);
67
+ margin-top: 15px;
68
+ border-radius: 10px;
69
+ }
70
+
71
+ .voice-btn {
72
+ padding: 10px 20px;
73
+ border: 2px solid white;
74
+ border-radius: 20px;
75
+ background: transparent;
76
+ color: white;
77
+ cursor: pointer;
78
+ transition: all 0.3s;
79
+ font-weight: bold;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ }
84
+
85
+ .voice-btn:hover {
86
+ background: white;
87
+ color: #667eea;
88
+ }
89
+
90
+ .voice-btn.active {
91
+ background: #ff4444;
92
+ border-color: #ff4444;
93
+ animation: pulse 1.5s infinite;
94
+ }
95
+
96
+ @keyframes pulse {
97
+ 0%, 100% { transform: scale(1); }
98
+ 50% { transform: scale(1.05); }
99
+ }
100
+
101
+ .voice-indicator {
102
+ width: 12px;
103
+ height: 12px;
104
+ border-radius: 50%;
105
+ background: #4CAF50;
106
+ display: inline-block;
107
+ }
108
+
109
+ .voice-indicator.recording {
110
+ background: #ff4444;
111
+ animation: blink 1s infinite;
112
+ }
113
+
114
+ @keyframes blink {
115
+ 0%, 100% { opacity: 1; }
116
+ 50% { opacity: 0.3; }
117
+ }
118
+
119
+ .chat-container {
120
+ height: 500px;
121
+ overflow-y: auto;
122
+ padding: 20px;
123
+ background: #f8f9fa;
124
+ }
125
+
126
+ .message {
127
+ margin-bottom: 20px;
128
+ display: flex;
129
+ animation: slideIn 0.3s ease;
130
+ }
131
+
132
+ @keyframes slideIn {
133
+ from {
134
+ opacity: 0;
135
+ transform: translateY(10px);
136
+ }
137
+ to {
138
+ opacity: 1;
139
+ transform: translateY(0);
140
+ }
141
+ }
142
+
143
+ .message.user {
144
+ justify-content: flex-end;
145
+ }
146
+
147
+ .message-content {
148
+ max-width: 70%;
149
+ padding: 15px 20px;
150
+ border-radius: 20px;
151
+ word-wrap: break-word;
152
+ white-space: pre-wrap;
153
+ position: relative;
154
+ }
155
+
156
+ .message.doctor .message-content {
157
+ background: white;
158
+ color: #333;
159
+ border: 2px solid #667eea;
160
+ border-radius: 20px 20px 20px 5px;
161
+ }
162
+
163
+ .message.user .message-content {
164
+ background: #667eea;
165
+ color: white;
166
+ border-radius: 20px 20px 5px 20px;
167
+ }
168
+
169
+ .speaker-icon {
170
+ position: absolute;
171
+ bottom: 8px;
172
+ right: 8px;
173
+ cursor: pointer;
174
+ font-size: 18px;
175
+ opacity: 0.6;
176
+ transition: opacity 0.3s;
177
+ }
178
+
179
+ .speaker-icon:hover {
180
+ opacity: 1;
181
+ }
182
+
183
+ .speaker-icon.speaking {
184
+ animation: speakPulse 0.5s infinite;
185
+ }
186
+
187
+ @keyframes speakPulse {
188
+ 0%, 100% { transform: scale(1); }
189
+ 50% { transform: scale(1.2); }
190
+ }
191
+
192
+ .input-area {
193
+ padding: 20px;
194
+ background: white;
195
+ border-top: 2px solid #e0e0e0;
196
+ display: flex;
197
+ gap: 10px;
198
+ }
199
+
200
+ .input-area input {
201
+ flex: 1;
202
+ padding: 15px;
203
+ border: 2px solid #e0e0e0;
204
+ border-radius: 25px;
205
+ font-size: 16px;
206
+ transition: border 0.3s;
207
+ }
208
+
209
+ .input-area input:focus {
210
+ outline: none;
211
+ border-color: #667eea;
212
+ }
213
+
214
+ .btn {
215
+ padding: 15px 30px;
216
+ border: none;
217
+ border-radius: 25px;
218
+ font-size: 16px;
219
+ cursor: pointer;
220
+ transition: all 0.3s;
221
+ font-weight: bold;
222
+ }
223
+
224
+ .btn-primary {
225
+ background: #667eea;
226
+ color: white;
227
+ }
228
+
229
+ .btn-primary:hover {
230
+ background: #5568d3;
231
+ transform: translateY(-2px);
232
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
233
+ }
234
+
235
+ .btn-secondary {
236
+ background: #6c757d;
237
+ color: white;
238
+ }
239
+
240
+ .btn-secondary:hover {
241
+ background: #5a6268;
242
+ }
243
+
244
+ .btn-success {
245
+ background: #28a745;
246
+ color: white;
247
+ }
248
+
249
+ .btn-success:hover {
250
+ background: #218838;
251
+ }
252
+
253
+ .btn-info {
254
+ background: #17a2b8;
255
+ color: white;
256
+ }
257
+
258
+ .btn-info:hover {
259
+ background: #138496;
260
+ }
261
+
262
+ .btn-voice {
263
+ background: #ff6b6b;
264
+ color: white;
265
+ min-width: 60px;
266
+ }
267
+
268
+ .btn-voice:hover {
269
+ background: #ff5252;
270
+ }
271
+
272
+ .btn-voice.recording {
273
+ background: #f44336;
274
+ animation: pulse 1.5s infinite;
275
+ }
276
+
277
+ .controls {
278
+ padding: 15px 20px;
279
+ background: #f8f9fa;
280
+ display: flex;
281
+ gap: 10px;
282
+ justify-content: center;
283
+ flex-wrap: wrap;
284
+ }
285
+
286
+ .loading {
287
+ display: none;
288
+ text-align: center;
289
+ padding: 10px;
290
+ color: #667eea;
291
+ font-style: italic;
292
+ }
293
+
294
+ .loading.active {
295
+ display: block;
296
+ }
297
+
298
+ .modal {
299
+ display: none;
300
+ position: fixed;
301
+ top: 0;
302
+ left: 0;
303
+ width: 100%;
304
+ height: 100%;
305
+ background: rgba(0,0,0,0.7);
306
+ justify-content: center;
307
+ align-items: center;
308
+ z-index: 1000;
309
+ }
310
+
311
+ .modal.active {
312
+ display: flex;
313
+ }
314
+
315
+ .modal-content {
316
+ background: white;
317
+ padding: 30px;
318
+ border-radius: 15px;
319
+ max-width: 800px;
320
+ max-height: 80vh;
321
+ overflow-y: auto;
322
+ width: 90%;
323
+ }
324
+
325
+ .modal-content pre {
326
+ white-space: pre-wrap;
327
+ font-family: 'Segoe UI', sans-serif;
328
+ line-height: 1.6;
329
+ }
330
+
331
+ .close-btn {
332
+ float: right;
333
+ font-size: 28px;
334
+ cursor: pointer;
335
+ color: #999;
336
+ }
337
+
338
+ .close-btn:hover {
339
+ color: #333;
340
+ }
341
+
342
+ .history-list {
343
+ max-height: 400px;
344
+ overflow-y: auto;
345
+ margin-top: 20px;
346
+ }
347
+
348
+ .history-item {
349
+ padding: 15px;
350
+ border: 2px solid #e0e0e0;
351
+ border-radius: 10px;
352
+ margin-bottom: 10px;
353
+ cursor: pointer;
354
+ transition: all 0.3s;
355
+ }
356
+
357
+ .history-item:hover {
358
+ border-color: #667eea;
359
+ background: #f8f9fa;
360
+ transform: translateX(5px);
361
+ }
362
+
363
+ .history-item h4 {
364
+ color: #667eea;
365
+ margin-bottom: 5px;
366
+ }
367
+
368
+ .history-item p {
369
+ font-size: 0.9em;
370
+ color: #666;
371
+ margin: 5px 0;
372
+ }
373
+
374
+ .pdf-badge {
375
+ display: inline-block;
376
+ background: #28a745;
377
+ color: white;
378
+ padding: 3px 8px;
379
+ border-radius: 5px;
380
+ font-size: 0.8em;
381
+ margin-left: 10px;
382
+ }
383
+
384
+ .load-session-form {
385
+ margin: 20px 0;
386
+ display: flex;
387
+ gap: 10px;
388
+ }
389
+
390
+ .load-session-form input {
391
+ flex: 1;
392
+ padding: 10px;
393
+ border: 2px solid #e0e0e0;
394
+ border-radius: 10px;
395
+ font-family: monospace;
396
+ }
397
+
398
+ .notification {
399
+ position: fixed;
400
+ top: 20px;
401
+ right: 20px;
402
+ padding: 15px 25px;
403
+ border-radius: 10px;
404
+ color: white;
405
+ font-weight: bold;
406
+ z-index: 2000;
407
+ animation: slideInRight 0.3s ease;
408
+ display: none;
409
+ }
410
+
411
+ .notification.success {
412
+ background: #28a745;
413
+ }
414
+
415
+ .notification.error {
416
+ background: #dc3545;
417
+ }
418
+
419
+ .notification.info {
420
+ background: #17a2b8;
421
+ }
422
+
423
+ .notification.active {
424
+ display: block;
425
+ }
426
+
427
+ @keyframes slideInRight {
428
+ from {
429
+ transform: translateX(100%);
430
+ }
431
+ to {
432
+ transform: translateX(0);
433
+ }
434
+ }
435
+
436
+ .pdf-viewer {
437
+ width: 100%;
438
+ height: 600px;
439
+ border: none;
440
+ border-radius: 10px;
441
+ margin-top: 20px;
442
+ }
443
+
444
+ .summary-actions {
445
+ display: flex;
446
+ gap: 10px;
447
+ margin-top: 20px;
448
+ justify-content: center;
449
+ }
450
+
451
+ .pdf-download-info {
452
+ text-align: center;
453
+ padding: 15px;
454
+ background: #e8f5e9;
455
+ border-radius: 10px;
456
+ margin-top: 15px;
457
+ color: #2e7d32;
458
+ }
459
+
460
+ .pdf-icon {
461
+ font-size: 48px;
462
+ margin-bottom: 10px;
463
+ }
464
+
465
+ .voice-status {
466
+ text-align: center;
467
+ padding: 10px;
468
+ background: #e3f2fd;
469
+ border-radius: 10px;
470
+ margin: 10px 0;
471
+ font-size: 0.9em;
472
+ color: #1565c0;
473
+ display: none;
474
+ }
475
+
476
+ .voice-status.active {
477
+ display: block;
478
+ }