Ali2206 commited on
Commit
94b553f
·
verified ·
1 Parent(s): 7f5790c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -143
app.py CHANGED
@@ -44,10 +44,14 @@ os.environ.update({
44
  "TOKENIZERS_PARALLELISM": "false",
45
  "CUDA_LAUNCH_BLOCKING": "1"
46
  })
 
 
47
  current_dir = os.path.dirname(os.path.abspath(__file__))
48
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
49
  sys.path.insert(0, src_path)
 
50
  from txagent.txagent import TxAgent
 
51
  # ==================== UTILITY FUNCTIONS ====================
52
  def sanitize_text(text: str) -> str:
53
  """Clean and sanitize text input"""
@@ -79,8 +83,12 @@ def log_system_resources(tag: str = "") -> None:
79
  # ==================== FILE PROCESSING ====================
80
  class FileProcessor:
81
  @staticmethod
82
- def extract_pdf_text(file_path: str) -> str:
83
- """Extract text from PDF with parallel processing"""
 
 
 
 
84
  try:
85
  with pdfplumber.open(file_path) as pdf:
86
  total_pages = len(pdf.pages)
@@ -100,31 +108,43 @@ class FileProcessor:
100
  batches = [(i, min(i+batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
101
  text_chunks = [""] * total_pages
102
 
103
- with ThreadPoolExecutor(max_workers=6) as executor:
104
  futures = [executor.submit(process_page_range, start, end) for start, end in batches]
105
  for future in as_completed(futures):
106
  for page_num, text in future.result():
107
  text_chunks[page_num] = text
108
 
109
- return "\n\n".join(filter(None, text_chunks))
 
 
110
  except Exception as e:
111
  logger.error(f"PDF processing error: {e}")
112
  return f"PDF processing error: {str(e)}"
113
 
114
  @staticmethod
115
- def excel_to_data(file_path: str) -> List[Dict]:
116
- """Convert Excel file to structured data"""
 
 
 
 
117
  try:
118
  df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
119
  content = df.where(pd.notnull(df), "").astype(str).values.tolist()
120
- return [{"filename": os.path.basename(file_path), "rows": content, "type": "excel"}]
 
 
121
  except Exception as e:
122
  logger.error(f"Excel processing error: {e}")
123
  return [{"error": f"Excel processing error: {str(e)}"}]
124
 
125
  @staticmethod
126
- def csv_to_data(file_path: str) -> List[Dict]:
127
- """Convert CSV file to structured data"""
 
 
 
 
128
  try:
129
  chunks = []
130
  for chunk in pd.read_csv(
@@ -135,13 +155,15 @@ class FileProcessor:
135
 
136
  df = pd.concat(chunks) if chunks else pd.DataFrame()
137
  content = df.where(pd.notnull(df), "").astype(str).values.tolist()
138
- return [{"filename": os.path.basename(file_path), "rows": content, "type": "csv"}]
 
 
139
  except Exception as e:
140
  logger.error(f"CSV processing error: {e}")
141
  return [{"error": f"CSV processing error: {str(e)}"}]
142
 
143
  @classmethod
144
- def process_file(cls, file_path: str, file_type: str) -> List[Dict]:
145
  """Route file processing based on type"""
146
  processors = {
147
  "pdf": cls.extract_pdf_text,
@@ -154,7 +176,7 @@ class FileProcessor:
154
  return [{"error": f"Unsupported file type: {file_type}"}]
155
 
156
  try:
157
- result = processors[file_type](file_path)
158
  if file_type == "pdf":
159
  return [{
160
  "filename": os.path.basename(file_path),
@@ -173,7 +195,7 @@ class TextProcessor:
173
  self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
174
  self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
175
 
176
- def chunk_text(self, text: str, max_tokens: int = 1800) -> List[str]:
177
  """Split text into token-limited chunks"""
178
  tokens = self.tokenizer.encode(text)
179
  return [
@@ -184,11 +206,7 @@ class TextProcessor:
184
  def clean_response(self, text: str) -> str:
185
  """Clean and format model response"""
186
  text = sanitize_text(text)
187
- text = re.sub(
188
- r"\[.*?\]|\bNone\b|To analyze the patient record excerpt.*?medications\."
189
- r"|Since the previous attempts.*?\.|I need to.*?medications\."
190
- r"|Retrieving tools.*?\.", "", text, flags=re.DOTALL
191
- )
192
 
193
  diagnoses = []
194
  in_diagnoses = False
@@ -236,7 +254,7 @@ class TextProcessor:
236
  if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
237
  diagnoses.append(diagnosis)
238
 
239
- unique_diagnoses = list(dict.fromkeys(diagnoses)) # Remove duplicates
240
 
241
  if not unique_diagnoses:
242
  return "No missed diagnoses were identified in the provided records."
@@ -302,50 +320,43 @@ class ClinicalOversightApp:
302
  full_response += cleaned + " "
303
  yield {"role": "assistant", "content": full_response}
304
 
305
- def analyze(self, message: str, history: List[dict], files: List) -> Generator[Dict[str, Any], None, None]:
306
  """Main analysis pipeline with proper output formatting"""
307
- # Initialize all output components
308
- outputs = {
309
- "chatbot": history.copy(),
310
- "download_output": None,
311
- "final_summary": "",
312
- "progress_text": {"value": "Starting analysis...", "visible": True}
313
- }
314
- yield outputs
315
 
316
  try:
317
  # Add user message to history
318
- history.append({"role": "user", "content": message})
319
- outputs["chatbot"] = history
320
- yield outputs
321
 
322
  # Process uploaded files
 
323
  extracted = []
324
  file_hash_value = ""
325
 
326
  if files:
327
- with ThreadPoolExecutor(max_workers=4) as executor:
328
  futures = []
329
  for f in files:
330
  file_type = f.name.split(".")[-1].lower()
331
- futures.append(executor.submit(self.file_processor.process_file, f.name, file_type))
332
 
333
  for i, future in enumerate(as_completed(futures), 1):
334
  try:
335
  extracted.extend(future.result())
336
- outputs["progress_text"] = self._update_progress(i, len(files), "Processing files")
337
- yield outputs
338
  except Exception as e:
339
  logger.error(f"File processing error: {e}")
340
  extracted.append({"error": f"Error processing file: {str(e)}"})
341
 
342
  file_hash_value = get_file_hash(files[0].name) if files else ""
343
- history.append({"role": "assistant", "content": "✅ File processing complete"})
344
- outputs.update({
345
- "chatbot": history,
346
- "progress_text": self._update_progress(len(files), len(files), "Files processed")
347
- })
348
- yield outputs
349
 
350
  # Analyze content
351
  text_content = "\n".join(json.dumps(item) for item in extracted)
@@ -360,159 +371,258 @@ with their potential implications and urgent review recommendations. If no misse
360
  are found, state 'No missed diagnoses identified'.
361
 
362
  Patient Record (Chunk {chunk_idx}/{len(chunks)}):
363
- {chunk[:1800]}
364
  """
365
- history.append({"role": "assistant", "content": ""})
366
- outputs.update({
367
- "chatbot": history,
368
- "progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
369
- })
370
- yield outputs
371
 
372
  # Stream response
373
  chunk_response = ""
374
- for update in self.process_response_stream(prompt, history):
375
- history[-1] = update
376
  chunk_response = update["content"]
377
- outputs.update({
378
- "chatbot": history,
379
- "progress_text": self._update_progress(chunk_idx, len(chunks), "Analyzing")
380
- })
381
- yield outputs
382
 
383
  combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
384
  torch.cuda.empty_cache()
385
  gc.collect()
386
 
387
  # Generate final outputs
388
- summary = self.text_processor.summarize_results(combined_response)
389
  report_path = os.path.join(DIRECTORIES["reports"], f"{file_hash_value}_report.txt") if file_hash_value else None
390
 
391
  if report_path:
392
  with open(report_path, "w", encoding="utf-8") as f:
393
- f.write(combined_response + "\n\n" + summary)
394
 
395
- outputs.update({
396
- "download_output": report_path if report_path else None,
397
- "final_summary": summary,
398
- "progress_text": {"visible": False}
399
- })
400
- yield outputs
401
 
402
  except Exception as e:
403
  logger.error(f"Analysis error: {e}")
404
- history.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
405
- outputs.update({
406
- "chatbot": history,
407
- "final_summary": f"Error occurred: {str(e)}",
408
- "progress_text": {"visible": False}
409
- })
410
- yield outputs
411
 
412
  def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
413
  """Format progress update for UI"""
414
  progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
415
- return {"value": progress, "visible": True, "label": f"Progress: {progress}"}
416
 
417
  def create_interface(self):
418
- """Create Gradio interface with improved layout"""
419
- with gr.Blocks(
420
- theme=gr.themes.Soft(
421
- primary_hue="indigo",
422
- secondary_hue="blue",
423
- neutral_hue="slate"
424
- ),
425
- title="Clinical Oversight Assistant",
426
- css="""
427
- .diagnosis-summary {
428
- border-left: 4px solid #4f46e5;
429
- padding: 12px;
430
- background: #f8fafc;
431
- border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
- .file-upload {
434
- border: 2px dashed #cbd5e1;
435
- border-radius: 8px;
436
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
437
  }
438
- """
439
- ) as app:
440
- # Header Section
441
- gr.Markdown("""
442
- <div style='text-align: center; margin-bottom: 20px;'>
443
- <h1 style='color: #4f46e5;'>🩺 Clinical Oversight Assistant</h1>
444
- <p style='color: #64748b;'>
445
- AI-powered analysis of patient records for potential missed diagnoses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  </p>
447
  </div>
448
  """)
449
-
450
- with gr.Row(equal_height=False):
451
- # Main Chat Column
452
- with gr.Column(scale=3):
453
- chatbot = gr.Chatbot(
454
- label="Clinical Analysis",
455
- height=600,
456
- show_copy_button=True,
457
- avatar_images=(
458
- "assets/user.png",
459
- "assets/assistant.png"
460
- ) if os.path.exists("assets/user.png") else None,
461
- bubble_full_width=False,
462
- type="messages",
463
- elem_classes=["chat-container"]
464
- )
465
-
466
- # Results Column
467
- with gr.Column(scale=1):
468
- with gr.Group():
469
- gr.Markdown("### 📝 Summary of Findings")
470
- final_summary = gr.Markdown(
471
- "Analysis results will appear here...",
472
- elem_classes=["diagnosis-summary"]
473
- )
474
-
475
- with gr.Group():
476
- gr.Markdown("### 📂 Report Download")
477
- download_output = gr.File(
478
- label="Full Report",
479
- visible=False,
480
- interactive=False
481
- )
482
-
483
- # Input Section
484
  with gr.Row():
 
 
 
 
 
485
  file_upload = gr.File(
486
  file_types=[".pdf", ".csv", ".xls", ".xlsx"],
487
  file_count="multiple",
488
- label="Upload Patient Records",
489
- elem_classes=["file-upload"]
490
  )
491
-
492
- # Interaction Section
493
- with gr.Row():
 
 
 
 
 
 
 
 
 
494
  msg_input = gr.Textbox(
495
  placeholder="Ask about potential oversights or upload files...",
496
  show_label=False,
497
  container=False,
498
- scale=7,
499
  autofocus=True
500
  )
501
  send_btn = gr.Button(
502
  "Analyze",
503
  variant="primary",
504
- scale=1,
505
- min_width=100
506
  )
507
-
508
- # Progress Indicator
509
  progress_text = gr.Textbox(
510
  label="Progress Status",
511
  visible=False,
512
  interactive=False
513
  )
514
 
515
- # Event Handlers
516
  send_btn.click(
517
  self.analyze,
518
  inputs=[msg_input, chatbot, file_upload],
 
44
  "TOKENIZERS_PARALLELISM": "false",
45
  "CUDA_LAUNCH_BLOCKING": "1"
46
  })
47
+
48
+ # Add src path for txagent
49
  current_dir = os.path.dirname(os.path.abspath(__file__))
50
  src_path = os.path.abspath(os.path.join(current_dir, "src"))
51
  sys.path.insert(0, src_path)
52
+
53
  from txagent.txagent import TxAgent
54
+
55
  # ==================== UTILITY FUNCTIONS ====================
56
  def sanitize_text(text: str) -> str:
57
  """Clean and sanitize text input"""
 
83
  # ==================== FILE PROCESSING ====================
84
  class FileProcessor:
85
  @staticmethod
86
+ def extract_pdf_text(file_path: str, cache: Cache) -> str:
87
+ """Extract text from PDF with caching"""
88
+ cache_key = f"pdf_{get_file_hash(file_path)}"
89
+ if cache_key in cache:
90
+ return cache[cache_key]
91
+
92
  try:
93
  with pdfplumber.open(file_path) as pdf:
94
  total_pages = len(pdf.pages)
 
108
  batches = [(i, min(i+batch_size, total_pages)) for i in range(0, total_pages, batch_size)]
109
  text_chunks = [""] * total_pages
110
 
111
+ with ThreadPoolExecutor(max_workers=2) as executor:
112
  futures = [executor.submit(process_page_range, start, end) for start, end in batches]
113
  for future in as_completed(futures):
114
  for page_num, text in future.result():
115
  text_chunks[page_num] = text
116
 
117
+ result = "\n\n".join(filter(None, text_chunks))
118
+ cache[cache_key] = result
119
+ return result
120
  except Exception as e:
121
  logger.error(f"PDF processing error: {e}")
122
  return f"PDF processing error: {str(e)}"
123
 
124
  @staticmethod
125
+ def excel_to_data(file_path: str, cache: Cache) -> List[Dict]:
126
+ """Convert Excel file to structured data with caching"""
127
+ cache_key = f"excel_{get_file_hash(file_path)}"
128
+ if cache_key in cache:
129
+ return cache[cache_key]
130
+
131
  try:
132
  df = pd.read_excel(file_path, engine='openpyxl', header=None, dtype=str)
133
  content = df.where(pd.notnull(df), "").astype(str).values.tolist()
134
+ result = [{"filename": os.path.basename(file_path), "rows": content, "type": "excel"}]
135
+ cache[cache_key] = result
136
+ return result
137
  except Exception as e:
138
  logger.error(f"Excel processing error: {e}")
139
  return [{"error": f"Excel processing error: {str(e)}"}]
140
 
141
  @staticmethod
142
+ def csv_to_data(file_path: str, cache: Cache) -> List[Dict]:
143
+ """Convert CSV file to structured data with caching"""
144
+ cache_key = f"csv_{get_file_hash(file_path)}"
145
+ if cache_key in cache:
146
+ return cache[cache_key]
147
+
148
  try:
149
  chunks = []
150
  for chunk in pd.read_csv(
 
155
 
156
  df = pd.concat(chunks) if chunks else pd.DataFrame()
157
  content = df.where(pd.notnull(df), "").astype(str).values.tolist()
158
+ result = [{"filename": os.path.basename(file_path), "rows": content, "type": "csv"}]
159
+ cache[cache_key] = result
160
+ return result
161
  except Exception as e:
162
  logger.error(f"CSV processing error: {e}")
163
  return [{"error": f"CSV processing error: {str(e)}"}]
164
 
165
  @classmethod
166
+ def process_file(cls, file_path: str, file_type: str, cache: Cache) -> List[Dict]:
167
  """Route file processing based on type"""
168
  processors = {
169
  "pdf": cls.extract_pdf_text,
 
176
  return [{"error": f"Unsupported file type: {file_type}"}]
177
 
178
  try:
179
+ result = processors[file_type](file_path, cache)
180
  if file_type == "pdf":
181
  return [{
182
  "filename": os.path.basename(file_path),
 
195
  self.tokenizer = AutoTokenizer.from_pretrained("mims-harvard/TxAgent-T1-Llama-3.1-8B")
196
  self.cache = Cache(DIRECTORIES["cache"], size_limit=10*1024**3)
197
 
198
+ def chunk_text(self, text: str, max_tokens: int = 1200) -> List[str]:
199
  """Split text into token-limited chunks"""
200
  tokens = self.tokenizer.encode(text)
201
  return [
 
206
  def clean_response(self, text: str) -> str:
207
  """Clean and format model response"""
208
  text = sanitize_text(text)
209
+ text = re.sub(r"\[.*?\]|\bNone\b", "", text)
 
 
 
 
210
 
211
  diagnoses = []
212
  in_diagnoses = False
 
254
  if diagnosis and not re.match(r"No issues identified", diagnosis, re.IGNORECASE):
255
  diagnoses.append(diagnosis)
256
 
257
+ unique_diagnoses = list(dict.fromkeys(diagnoses))
258
 
259
  if not unique_diagnoses:
260
  return "No missed diagnoses were identified in the provided records."
 
320
  full_response += cleaned + " "
321
  yield {"role": "assistant", "content": full_response}
322
 
323
+ def analyze(self, message: str, history: List[dict], files: List) -> Generator[tuple, None, None]:
324
  """Main analysis pipeline with proper output formatting"""
325
+ chatbot_output = history.copy()
326
+ download_output = None
327
+ final_summary = ""
328
+ progress_text = {"value": "Starting analysis...", "visible": True}
 
 
 
 
329
 
330
  try:
331
  # Add user message to history
332
+ chatbot_output.append({"role": "user", "content": message})
333
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
334
 
335
  # Process uploaded files
336
+ .ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0
337
  extracted = []
338
  file_hash_value = ""
339
 
340
  if files:
341
+ with ThreadPoolExecutor(max_workers=2) as executor:
342
  futures = []
343
  for f in files:
344
  file_type = f.name.split(".")[-1].lower()
345
+ futures.append(executor.submit(self.file_processor.process_file, f.name, file_type, self.text_processor.cache))
346
 
347
  for i, future in enumerate(as_completed(futures), 1):
348
  try:
349
  extracted.extend(future.result())
350
+ progress_text = self._update_progress(i, len(files), "Processing files")
351
+ yield (chatbot_output, download_output, final_summary, progress_text)
352
  except Exception as e:
353
  logger.error(f"File processing error: {e}")
354
  extracted.append({"error": f"Error processing file: {str(e)}"})
355
 
356
  file_hash_value = get_file_hash(files[0].name) if files else ""
357
+ chatbot_output.append({"role": "assistant", "content": "✅ File processing complete"})
358
+ progress_text = self._update_progress(len(files), len(files), "Files processed")
359
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
 
 
360
 
361
  # Analyze content
362
  text_content = "\n".join(json.dumps(item) for item in extracted)
 
371
  are found, state 'No missed diagnoses identified'.
372
 
373
  Patient Record (Chunk {chunk_idx}/{len(chunks)}):
374
+ {chunk[:1200]}
375
  """
376
+ chatbot_output.append({"role": "assistant", "content": ""})
377
+ progress_text = self._update_progress(chunk_idx, len(chunks), "Analyzing")
378
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
 
 
379
 
380
  # Stream response
381
  chunk_response = ""
382
+ for update in self.process_response_stream(prompt, chatbot_output):
383
+ chatbot_output[-1] = update
384
  chunk_response = update["content"]
385
+ progress_text = self._update_progress(chunk_idx, len(chunks), "Analyzing")
386
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
 
 
387
 
388
  combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
389
  torch.cuda.empty_cache()
390
  gc.collect()
391
 
392
  # Generate final outputs
393
+ final_summary = self.text_processor.summarize_results(combined_response)
394
  report_path = os.path.join(DIRECTORIES["reports"], f"{file_hash_value}_report.txt") if file_hash_value else None
395
 
396
  if report_path:
397
  with open(report_path, "w", encoding="utf-8") as f:
398
+ f.write(combined_response + "\n\n" + final_summary)
399
 
400
+ download_output = report_path if report_path and os.path.exists(report_path) else None
401
+ progress_text = {"visible": False}
402
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
 
 
403
 
404
  except Exception as e:
405
  logger.error(f"Analysis error: {e}")
406
+ chatbot_output.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
407
+ final_summary = f"Error occurred: {str(e)}"
408
+ progress_text = {"visible": False}
409
+ yield (chatbot_output, download_output, final_summary, progress_text)
 
 
 
410
 
411
  def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
412
  """Format progress update for UI"""
413
  progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
414
+ return {"value": progress, "visible": True}
415
 
416
  def create_interface(self):
417
+ """Create Gradio interface with ChatGPT-like design"""
418
+ css = """
419
+ body, .gradio-container {
420
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
421
+ background: var(--background);
422
+ color: var(--text-color);
423
+ }
424
+ .gradio-container {
425
+ max-width: 800px;
426
+ margin: 0 auto;
427
+ padding: 20px;
428
+ }
429
+ .chat-container {
430
+ background: var(--chat-bg);
431
+ border-radius: 12px;
432
+ padding: 20px;
433
+ height: 80vh;
434
+ overflow-y: auto;
435
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
436
+ }
437
+ .message {
438
+ margin: 10px 0;
439
+ padding: 12px 16px;
440
+ border-radius: 12px;
441
+ max-width: 80%;
442
+ transition: all 0.2s ease;
443
+ }
444
+ .message.user {
445
+ background: #007bff;
446
+ color: white;
447
+ margin-left: auto;
448
+ }
449
+ .message.assistant {
450
+ background: var(--message-bg);
451
+ color: var(--text-color);
452
+ }
453
+ .input-container {
454
+ display: flex;
455
+ align-items: center;
456
+ margin-top: 20px;
457
+ background: var(--chat-bg);
458
+ padding: 10px 20px;
459
+ border-radius: 25px;
460
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
461
+ }
462
+ .input-textbox {
463
+ flex-grow: 1;
464
+ border: none;
465
+ background: transparent;
466
+ color: var(--text-color);
467
+ outline: none;
468
+ }
469
+ .send-btn {
470
+ background: #007bff;
471
+ color: white;
472
+ border: none;
473
+ border-radius: 20px;
474
+ padding: 8px 16px;
475
+ margin-left: 10px;
476
+ }
477
+ .send-btn:hover {
478
+ background: #0056b3;
479
+ }
480
+ .sidebar {
481
+ background: var(--sidebar-bg);
482
+ padding: 20px;
483
+ border-radius: 12px;
484
+ margin-top: 20px;
485
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
486
+ }
487
+ .sidebar-hidden {
488
+ display: none;
489
+ }
490
+ .header {
491
+ text-align: center;
492
+ margin-bottom: 20px;
493
+ }
494
+ .theme-toggle {
495
+ position: absolute;
496
+ top: 20px;
497
+ right: 20px;
498
+ background: #007bff;
499
+ color: white;
500
+ border: none;
501
+ border-radius: 20px;
502
+ padding: 8px 16px;
503
+ }
504
+ :root {
505
+ --background: #ffffff;
506
+ --text-color: #333333;
507
+ --chat-bg: #f7f7f8;
508
+ --message-bg: #e5e5ea;
509
+ --sidebar-bg: #f1f1f1;
510
+ }
511
+ @media (prefers-color-scheme: dark) {
512
+ :root {
513
+ --background: #1e2a44;
514
+ --text-color: #ffffff;
515
+ --chat-bg: #2d3b55;
516
+ --message-bg: #3e4c6a;
517
+ --sidebar-bg: #2a3650;
518
  }
519
+ }
520
+ @media (max-width: 600px) {
521
+ .gradio-container {
522
+ padding: 10px;
523
+ }
524
+ .chat-container {
525
+ height: 70vh;
526
+ }
527
+ .input-container {
528
+ flex-direction: column;
529
+ gap: 10px;
530
+ }
531
+ .send-btn {
532
+ width: 100%;
533
+ margin-left: 0;
534
  }
535
+ }
536
+ """
537
+
538
+ js = """
539
+ function toggleTheme() {
540
+ const root = document.documentElement;
541
+ const isDark = root.style.getPropertyValue('--background') === '#1e2a44';
542
+ root.style.setProperty('--background', isDark ? '#ffffff' : '#1e2a44');
543
+ root.style.setProperty('--text-color', isDark ? '#333333' : '#ffffff');
544
+ root.style.setProperty('--chat-bg', isDark ? '#f7f7f8' : '#2d3b55');
545
+ root.style.setProperty('--message-bg', isDark ? '#e5e5ea' : '#3e4c6a');
546
+ root.style.setProperty('--sidebar-bg', isDark ? '#f1f1f1' : '#2a3650');
547
+ localStorage.setItem('theme', isDark ? 'light' : 'dark');
548
+ }
549
+
550
+ function toggleSidebar() {
551
+ const sidebar = document.querySelector('.sidebar');
552
+ sidebar.classList.toggle('sidebar-hidden');
553
+ }
554
+
555
+ document.addEventListener('DOMContentLoaded', () => {
556
+ const savedTheme = localStorage.getItem('theme');
557
+ if (savedTheme === 'dark') toggleTheme();
558
+ document.querySelector('.sidebar').classList.add('sidebar-hidden');
559
+ });
560
+ """
561
+
562
+ with gr.Blocks(theme=gr.themes.Default(), css=css, js=js, title="Clinical Oversight Assistant") as app:
563
+ gr.HTML("""
564
+ <div class='header'>
565
+ <h1 style='color: var(--text-color);'>🩺 Clinical Oversight Assistant</h1>
566
+ <p style='color: var(--text-color); opacity: 0.7;'>
567
+ AI-powered analysis of patient records for missed diagnoses
568
  </p>
569
  </div>
570
  """)
571
+ gr.Button("Toggle Light/Dark Mode", elem_classes="theme-toggle").click(
572
+ None, None, None, _js="toggleTheme"
573
+ )
574
+
575
+ with gr.Column(elem_classes="chat-container"):
576
+ chatbot = gr.Chatbot(
577
+ label="Clinical Analysis",
578
+ height="100%",
579
+ show_copy_button=True,
580
+ type="messages",
581
+ elem_classes="chatbot"
582
+ )
583
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  with gr.Row():
585
+ gr.Button("Show/Hide Tools", variant="secondary").click(
586
+ None, None, None, _js="toggleSidebar"
587
+ )
588
+
589
+ with gr.Column(elem_classes="sidebar"):
590
  file_upload = gr.File(
591
  file_types=[".pdf", ".csv", ".xls", ".xlsx"],
592
  file_count="multiple",
593
+ label="Upload Patient Records"
 
594
  )
595
+ gr.Markdown("### 📝 Summary of Findings")
596
+ final_summary = gr.Markdown(
597
+ "Analysis results will appear here..."
598
+ )
599
+ gr.Markdown("### 📂 Report Download")
600
+ download_output = gr.File(
601
+ label="Full Report",
602
+ visible=False,
603
+ interactive=False
604
+ )
605
+
606
+ with gr.Row(elem_classes="input-container"):
607
  msg_input = gr.Textbox(
608
  placeholder="Ask about potential oversights or upload files...",
609
  show_label=False,
610
  container=False,
611
+ elem_classes="input-textbox",
612
  autofocus=True
613
  )
614
  send_btn = gr.Button(
615
  "Analyze",
616
  variant="primary",
617
+ elem_classes="send-btn"
 
618
  )
619
+
 
620
  progress_text = gr.Textbox(
621
  label="Progress Status",
622
  visible=False,
623
  interactive=False
624
  )
625
 
 
626
  send_btn.click(
627
  self.analyze,
628
  inputs=[msg_input, chatbot, file_upload],