Update app.py
Browse files
app.py
CHANGED
@@ -301,6 +301,13 @@ class ClinicalOversightApp:
|
|
301 |
logger.info("AI Agent Ready")
|
302 |
return agent
|
303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
def process_response_stream(self, prompt: str, history: List[dict]) -> Generator[dict, None, None]:
|
305 |
"""Stream the agent's response with proper formatting"""
|
306 |
full_response = ""
|
@@ -398,8 +405,7 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
398 |
yield (chatbot_output, download_output, final_summary, progress_text)
|
399 |
|
400 |
combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
|
401 |
-
|
402 |
-
gc.collect()
|
403 |
|
404 |
# Generate final outputs
|
405 |
final_summary = self.text_processor.summarize_results(combined_response)
|
@@ -423,14 +429,23 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
423 |
progress_text = {"visible": False}
|
424 |
yield (chatbot_output, download_output, final_summary, progress_text)
|
425 |
finally:
|
426 |
-
|
427 |
-
gc.collect()
|
428 |
|
429 |
def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
|
430 |
"""Format progress update for UI"""
|
431 |
progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
|
432 |
return {"value": progress, "visible": True}
|
433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
434 |
def create_interface(self):
|
435 |
"""Create Gradio interface with refined ChatGPT-like design"""
|
436 |
css = """
|
@@ -442,7 +457,7 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
442 |
.gradio-container {
|
443 |
max-width: 800px;
|
444 |
margin: 0 auto;
|
445 |
-
padding:
|
446 |
}
|
447 |
.chat-container {
|
448 |
background: var(--chat-bg);
|
@@ -450,7 +465,7 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
450 |
padding: 24px;
|
451 |
height: 80vh;
|
452 |
overflow-y: auto;
|
453 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.
|
454 |
position: relative;
|
455 |
}
|
456 |
.message {
|
@@ -459,13 +474,15 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
459 |
border-radius: 12px;
|
460 |
max-width: 80%;
|
461 |
transition: all 0.3s ease;
|
|
|
462 |
position: relative;
|
463 |
}
|
464 |
.message:hover {
|
465 |
transform: translateY(-2px);
|
|
|
466 |
}
|
467 |
.message.user {
|
468 |
-
background: #007bff;
|
469 |
color: white;
|
470 |
margin-left: auto;
|
471 |
}
|
@@ -482,11 +499,11 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
482 |
.input-container {
|
483 |
display: flex;
|
484 |
align-items: center;
|
485 |
-
margin-top:
|
486 |
background: var(--chat-bg);
|
487 |
padding: 12px 24px;
|
488 |
border-radius: 30px;
|
489 |
-
box-shadow: 0 4px 8px rgba(0,0,0,0.
|
490 |
position: sticky;
|
491 |
bottom: 0;
|
492 |
}
|
@@ -497,34 +514,52 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
497 |
color: var(--text-color);
|
498 |
outline: none;
|
499 |
font-size: 1em;
|
|
|
500 |
}
|
501 |
.send-btn {
|
502 |
-
background: #007bff;
|
503 |
color: white;
|
504 |
border: none;
|
505 |
border-radius: 20px;
|
506 |
padding: 10px 20px;
|
507 |
margin-left: 12px;
|
508 |
-
transition:
|
509 |
}
|
510 |
.send-btn:hover {
|
511 |
-
|
|
|
|
|
|
|
512 |
}
|
513 |
.sidebar {
|
514 |
background: var(--sidebar-bg);
|
515 |
padding: 24px;
|
516 |
border-radius: 16px;
|
517 |
-
margin-top:
|
518 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.
|
519 |
-
transition: transform 0.
|
520 |
transform: translateX(0);
|
521 |
-
|
522 |
-
.sidebar-hidden {
|
523 |
-
transform: translateX(100%);
|
524 |
-
position: absolute;
|
525 |
right: 0;
|
526 |
top: 100px;
|
527 |
width: 300px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
}
|
529 |
.header {
|
530 |
text-align: center;
|
@@ -532,9 +567,9 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
532 |
}
|
533 |
.theme-toggle {
|
534 |
position: absolute;
|
535 |
-
top:
|
536 |
-
right:
|
537 |
-
background: #007bff;
|
538 |
color: white;
|
539 |
border: none;
|
540 |
border-radius: 20px;
|
@@ -549,11 +584,25 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
549 |
left: 50%;
|
550 |
transform: translateX(-50%);
|
551 |
font-size: 1.2em;
|
552 |
-
animation:
|
553 |
}
|
554 |
-
|
555 |
-
|
556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
557 |
}
|
558 |
:root {
|
559 |
--background: #ffffff;
|
@@ -561,15 +610,14 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
561 |
--chat-bg: #f9fafb;
|
562 |
--message-bg: #e5e5ea;
|
563 |
--sidebar-bg: #f1f3f5;
|
|
|
564 |
}
|
565 |
-
|
566 |
-
:
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
--sidebar-bg: #2a3650;
|
572 |
-
}
|
573 |
}
|
574 |
@media (max-width: 600px) {
|
575 |
.gradio-container {
|
@@ -589,6 +637,7 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
589 |
}
|
590 |
.sidebar {
|
591 |
width: 100%;
|
|
|
592 |
}
|
593 |
.sidebar-hidden {
|
594 |
transform: translateX(100%);
|
@@ -597,16 +646,10 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
597 |
"""
|
598 |
|
599 |
js = """
|
600 |
-
function
|
601 |
-
|
602 |
-
|
603 |
-
|
604 |
-
root.style.setProperty('--text-color', isDark ? '#333333' : '#ffffff');
|
605 |
-
root.style.setProperty('--chat-bg', isDark ? '#f9fafb' : '#2d3b55');
|
606 |
-
root.style.setProperty('--message-bg', isDark ? '#e5e5ea' : '#3e4c6a');
|
607 |
-
root.style.setProperty('--sidebar-bg', isDark ? '#f1f3f5' : '#2a3650');
|
608 |
-
localStorage.setItem('theme', isDark ? 'light' : 'dark');
|
609 |
-
document.querySelector('.theme-toggle').innerHTML = isDark ? 'π Dark Mode' : 'βοΈ Light Mode';
|
610 |
}
|
611 |
|
612 |
function toggleSidebar() {
|
@@ -615,14 +658,16 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
615 |
}
|
616 |
|
617 |
document.addEventListener('DOMContentLoaded', () => {
|
618 |
-
const savedTheme = localStorage.getItem('theme');
|
619 |
-
|
620 |
document.querySelector('.sidebar').classList.add('sidebar-hidden');
|
621 |
-
document.querySelector('.theme-toggle').innerHTML = savedTheme === 'dark' ? 'βοΈ Light Mode' : 'π Dark Mode';
|
622 |
});
|
623 |
"""
|
624 |
|
625 |
with gr.Blocks(theme=gr.themes.Default(), css=css, js=js, title="Clinical Oversight Assistant") as app:
|
|
|
|
|
|
|
626 |
gr.HTML("""
|
627 |
<div class='header'>
|
628 |
<h1 style='color: var(--text-color);'>π©Ί Clinical Oversight Assistant</h1>
|
@@ -630,10 +675,10 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
630 |
AI-powered analysis of patient records for missed diagnoses
|
631 |
</p>
|
632 |
</div>
|
|
|
633 |
""")
|
634 |
-
|
635 |
-
|
636 |
-
)
|
637 |
|
638 |
with gr.Column(elem_classes="chat-container"):
|
639 |
chatbot = gr.Chatbot(
|
@@ -644,29 +689,34 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
644 |
elem_classes="chatbot",
|
645 |
render_markdown=True
|
646 |
)
|
647 |
-
gr.HTML("<div class='loading-spinner'
|
|
|
648 |
|
649 |
with gr.Row():
|
650 |
-
gr.Button("π Tools", variant="secondary")
|
651 |
-
None, None, None, _js="toggleSidebar"
|
652 |
-
)
|
653 |
|
654 |
with gr.Column(elem_classes="sidebar"):
|
655 |
-
gr.Markdown("### π Upload Records")
|
656 |
file_upload = gr.File(
|
657 |
file_types=[".pdf", ".csv", ".xls", ".xlsx"],
|
658 |
file_count="multiple",
|
659 |
-
label="Patient Records"
|
|
|
|
|
660 |
)
|
661 |
-
gr.Markdown("### π Analysis Summary")
|
662 |
final_summary = gr.Markdown(
|
663 |
-
"Analysis results will appear here..."
|
|
|
|
|
664 |
)
|
665 |
-
gr.Markdown("### π Full Report")
|
666 |
download_output = gr.File(
|
667 |
label="Download Report",
|
668 |
visible=False,
|
669 |
-
interactive=False
|
|
|
|
|
670 |
)
|
671 |
|
672 |
with gr.Row(elem_classes="input-container"):
|
@@ -686,50 +736,90 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
686 |
progress_text = gr.Textbox(
|
687 |
label="Progress Status",
|
688 |
visible=False,
|
689 |
-
interactive=False
|
|
|
690 |
)
|
691 |
|
692 |
-
def
|
693 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
694 |
|
695 |
-
|
696 |
-
|
|
|
|
|
|
|
|
|
|
|
697 |
|
|
|
698 |
send_btn.click(
|
699 |
-
|
700 |
-
|
701 |
-
|
|
|
|
|
|
|
|
|
702 |
).then(
|
703 |
self.analyze,
|
704 |
inputs=[msg_input, chatbot, file_upload],
|
705 |
outputs=[chatbot, download_output, final_summary, progress_text],
|
706 |
show_progress="hidden"
|
707 |
).then(
|
708 |
-
|
709 |
-
|
710 |
-
|
|
|
|
|
|
|
|
|
711 |
)
|
712 |
|
713 |
msg_input.submit(
|
714 |
-
|
715 |
-
|
716 |
-
|
|
|
|
|
|
|
|
|
717 |
).then(
|
718 |
self.analyze,
|
719 |
inputs=[msg_input, chatbot, file_upload],
|
720 |
outputs=[chatbot, download_output, final_summary, progress_text],
|
721 |
show_progress="hidden"
|
722 |
).then(
|
723 |
-
|
724 |
-
|
725 |
-
|
|
|
|
|
|
|
|
|
726 |
)
|
727 |
|
728 |
app.load(
|
729 |
lambda: [
|
730 |
-
[], None, "", "", None, {"visible": False}
|
731 |
],
|
732 |
-
outputs=[chatbot, download_output, final_summary, msg_input, file_upload, progress_text],
|
733 |
queue=False
|
734 |
)
|
735 |
|
@@ -737,6 +827,7 @@ Patient Record (Chunk {chunk_idx}/{len(chunks)}):
|
|
737 |
|
738 |
# ==================== APPLICATION ENTRY POINT ====================
|
739 |
if __name__ == "__main__":
|
|
|
740 |
try:
|
741 |
logger.info("Starting Clinical Oversight Assistant...")
|
742 |
app = ClinicalOversightApp()
|
@@ -756,5 +847,5 @@ if __name__ == "__main__":
|
|
756 |
logger.error(f"Application failed to start: {e}")
|
757 |
raise
|
758 |
finally:
|
759 |
-
if
|
760 |
-
|
|
|
301 |
logger.info("AI Agent Ready")
|
302 |
return agent
|
303 |
|
304 |
+
def cleanup_resources(self):
|
305 |
+
"""Clean up GPU memory and collect garbage"""
|
306 |
+
torch.cuda.empty_cache()
|
307 |
+
gc.collect()
|
308 |
+
if torch.distributed.is_initialized():
|
309 |
+
torch.distributed.destroy_process_group()
|
310 |
+
|
311 |
def process_response_stream(self, prompt: str, history: List[dict]) -> Generator[dict, None, None]:
|
312 |
"""Stream the agent's response with proper formatting"""
|
313 |
full_response = ""
|
|
|
405 |
yield (chatbot_output, download_output, final_summary, progress_text)
|
406 |
|
407 |
combined_response += f"--- Analysis for Chunk {chunk_idx} ---\n{chunk_response}\n"
|
408 |
+
self.cleanup_resources()
|
|
|
409 |
|
410 |
# Generate final outputs
|
411 |
final_summary = self.text_processor.summarize_results(combined_response)
|
|
|
429 |
progress_text = {"visible": False}
|
430 |
yield (chatbot_output, download_output, final_summary, progress_text)
|
431 |
finally:
|
432 |
+
self.cleanup_resources()
|
|
|
433 |
|
434 |
def _update_progress(self, current: int, total: int, stage: str = "") -> Dict[str, Any]:
|
435 |
"""Format progress update for UI"""
|
436 |
progress = f"{stage} - {current}/{total}" if stage else f"{current}/{total}"
|
437 |
return {"value": progress, "visible": True}
|
438 |
|
439 |
+
def toggle_theme(self, theme_state: str) -> tuple[str, str]:
|
440 |
+
"""Toggle between light and dark themes"""
|
441 |
+
new_theme = "dark" if theme_state == "light" else "light"
|
442 |
+
button_text = "βοΈ Light Mode" if new_theme == "dark" else "π Dark Mode"
|
443 |
+
return new_theme, button_text
|
444 |
+
|
445 |
+
def toggle_sidebar(self, sidebar_state: bool) -> bool:
|
446 |
+
"""Toggle sidebar visibility"""
|
447 |
+
return not sidebar_state
|
448 |
+
|
449 |
def create_interface(self):
|
450 |
"""Create Gradio interface with refined ChatGPT-like design"""
|
451 |
css = """
|
|
|
457 |
.gradio-container {
|
458 |
max-width: 800px;
|
459 |
margin: 0 auto;
|
460 |
+
padding: 24px;
|
461 |
}
|
462 |
.chat-container {
|
463 |
background: var(--chat-bg);
|
|
|
465 |
padding: 24px;
|
466 |
height: 80vh;
|
467 |
overflow-y: auto;
|
468 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
469 |
position: relative;
|
470 |
}
|
471 |
.message {
|
|
|
474 |
border-radius: 12px;
|
475 |
max-width: 80%;
|
476 |
transition: all 0.3s ease;
|
477 |
+
background: var(--message-bg);
|
478 |
position: relative;
|
479 |
}
|
480 |
.message:hover {
|
481 |
transform: translateY(-2px);
|
482 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
483 |
}
|
484 |
.message.user {
|
485 |
+
background: linear-gradient(135deg, #007bff, #0056b3);
|
486 |
color: white;
|
487 |
margin-left: auto;
|
488 |
}
|
|
|
499 |
.input-container {
|
500 |
display: flex;
|
501 |
align-items: center;
|
502 |
+
margin-top: 24px;
|
503 |
background: var(--chat-bg);
|
504 |
padding: 12px 24px;
|
505 |
border-radius: 30px;
|
506 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
507 |
position: sticky;
|
508 |
bottom: 0;
|
509 |
}
|
|
|
514 |
color: var(--text-color);
|
515 |
outline: none;
|
516 |
font-size: 1em;
|
517 |
+
placeholder-opacity: 0.6;
|
518 |
}
|
519 |
.send-btn {
|
520 |
+
background: linear-gradient(135deg, #007bff, #0056b3);
|
521 |
color: white;
|
522 |
border: none;
|
523 |
border-radius: 20px;
|
524 |
padding: 10px 20px;
|
525 |
margin-left: 12px;
|
526 |
+
transition: transform 0.2s ease;
|
527 |
}
|
528 |
.send-btn:hover {
|
529 |
+
transform: scale(1.05);
|
530 |
+
}
|
531 |
+
.send-btn:active {
|
532 |
+
animation: pulse 0.3s ease;
|
533 |
}
|
534 |
.sidebar {
|
535 |
background: var(--sidebar-bg);
|
536 |
padding: 24px;
|
537 |
border-radius: 16px;
|
538 |
+
margin-top: 24px;
|
539 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
540 |
+
transition: transform 0.4s ease;
|
541 |
transform: translateX(0);
|
542 |
+
position: fixed;
|
|
|
|
|
|
|
543 |
right: 0;
|
544 |
top: 100px;
|
545 |
width: 300px;
|
546 |
+
z-index: 1000;
|
547 |
+
}
|
548 |
+
.sidebar-hidden {
|
549 |
+
transform: translateX(100%);
|
550 |
+
}
|
551 |
+
.sidebar-backdrop {
|
552 |
+
position: fixed;
|
553 |
+
top: 0;
|
554 |
+
left: 0;
|
555 |
+
width: 100%;
|
556 |
+
height: 100%;
|
557 |
+
background: rgba(0,0,0,0.3);
|
558 |
+
z-index: 999;
|
559 |
+
display: none;
|
560 |
+
}
|
561 |
+
.sidebar:not(.sidebar-hidden) ~ .sidebar-backdrop {
|
562 |
+
display: block;
|
563 |
}
|
564 |
.header {
|
565 |
text-align: center;
|
|
|
567 |
}
|
568 |
.theme-toggle {
|
569 |
position: absolute;
|
570 |
+
top: 24px;
|
571 |
+
right: 24px;
|
572 |
+
background: linear-gradient(135deg, #007bff, #0056b3);
|
573 |
color: white;
|
574 |
border: none;
|
575 |
border-radius: 20px;
|
|
|
584 |
left: 50%;
|
585 |
transform: translateX(-50%);
|
586 |
font-size: 1.2em;
|
587 |
+
animation: pulse 1.5s ease infinite;
|
588 |
}
|
589 |
+
.typing-indicator {
|
590 |
+
display: none;
|
591 |
+
font-size: 0.9em;
|
592 |
+
color: var(--text-color);
|
593 |
+
opacity: 0.7;
|
594 |
+
margin: 12px;
|
595 |
+
}
|
596 |
+
.typing-indicator.active {
|
597 |
+
display: block;
|
598 |
+
animation: blink 1s step-end infinite;
|
599 |
+
}
|
600 |
+
@keyframes pulse {
|
601 |
+
0%, 100% { transform: translateX(-50%) scale(1); opacity: 1; }
|
602 |
+
50% { transform: translateX(-50%) scale(1.2); opacity: 0.7; }
|
603 |
+
}
|
604 |
+
@keyframes blink {
|
605 |
+
50% { opacity: 0.3; }
|
606 |
}
|
607 |
:root {
|
608 |
--background: #ffffff;
|
|
|
610 |
--chat-bg: #f9fafb;
|
611 |
--message-bg: #e5e5ea;
|
612 |
--sidebar-bg: #f1f3f5;
|
613 |
+
transition: background 0.3s ease, color 0.3s ease;
|
614 |
}
|
615 |
+
[data-theme="dark"] {
|
616 |
+
--background: #1e2a44;
|
617 |
+
--text-color: #ffffff;
|
618 |
+
--chat-bg: #2d3b55;
|
619 |
+
--message-bg: #3e4c6a;
|
620 |
+
--sidebar-bg: #2a3650;
|
|
|
|
|
621 |
}
|
622 |
@media (max-width: 600px) {
|
623 |
.gradio-container {
|
|
|
637 |
}
|
638 |
.sidebar {
|
639 |
width: 100%;
|
640 |
+
top: 80px;
|
641 |
}
|
642 |
.sidebar-hidden {
|
643 |
transform: translateX(100%);
|
|
|
646 |
"""
|
647 |
|
648 |
js = """
|
649 |
+
function applyTheme(theme) {
|
650 |
+
document.documentElement.setAttribute('data-theme', theme);
|
651 |
+
localStorage.setItem('theme', theme);
|
652 |
+
document.querySelector('.theme-toggle').innerHTML = theme === 'dark' ? 'βοΈ Light Mode' : 'π Dark Mode';
|
|
|
|
|
|
|
|
|
|
|
|
|
653 |
}
|
654 |
|
655 |
function toggleSidebar() {
|
|
|
658 |
}
|
659 |
|
660 |
document.addEventListener('DOMContentLoaded', () => {
|
661 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
662 |
+
applyTheme(savedTheme);
|
663 |
document.querySelector('.sidebar').classList.add('sidebar-hidden');
|
|
|
664 |
});
|
665 |
"""
|
666 |
|
667 |
with gr.Blocks(theme=gr.themes.Default(), css=css, js=js, title="Clinical Oversight Assistant") as app:
|
668 |
+
theme_state = gr.State(value="light")
|
669 |
+
sidebar_state = gr.State(value=False)
|
670 |
+
|
671 |
gr.HTML("""
|
672 |
<div class='header'>
|
673 |
<h1 style='color: var(--text-color);'>π©Ί Clinical Oversight Assistant</h1>
|
|
|
675 |
AI-powered analysis of patient records for missed diagnoses
|
676 |
</p>
|
677 |
</div>
|
678 |
+
<div class='sidebar-backdrop'></div>
|
679 |
""")
|
680 |
+
|
681 |
+
theme_button = gr.Button("π Dark Mode", elem_classes="theme-toggle")
|
|
|
682 |
|
683 |
with gr.Column(elem_classes="chat-container"):
|
684 |
chatbot = gr.Chatbot(
|
|
|
689 |
elem_classes="chatbot",
|
690 |
render_markdown=True
|
691 |
)
|
692 |
+
gr.HTML("<div class='loading-spinner'>β³</div>")
|
693 |
+
gr.HTML("<div class='typing-indicator'>Typing...</div>")
|
694 |
|
695 |
with gr.Row():
|
696 |
+
tools_button = gr.Button("π Tools", variant="secondary")
|
|
|
|
|
697 |
|
698 |
with gr.Column(elem_classes="sidebar"):
|
699 |
+
gr.Markdown("### π Upload Records", elem_classes="tooltip", elem_id="upload-tooltip")
|
700 |
file_upload = gr.File(
|
701 |
file_types=[".pdf", ".csv", ".xls", ".xlsx"],
|
702 |
file_count="multiple",
|
703 |
+
label="Patient Records",
|
704 |
+
elem_classes="tooltip",
|
705 |
+
elem_id="upload-input"
|
706 |
)
|
707 |
+
gr.Markdown("### π Analysis Summary", elem_classes="tooltip", elem_id="summary-tooltip")
|
708 |
final_summary = gr.Markdown(
|
709 |
+
"Analysis results will appear here...",
|
710 |
+
elem_classes="tooltip",
|
711 |
+
elem_id="summary-output"
|
712 |
)
|
713 |
+
gr.Markdown("### π Full Report", elem_classes="tooltip", elem_id="report-tooltip")
|
714 |
download_output = gr.File(
|
715 |
label="Download Report",
|
716 |
visible=False,
|
717 |
+
interactive=False,
|
718 |
+
elem_classes="tooltip",
|
719 |
+
elem_id="download-output"
|
720 |
)
|
721 |
|
722 |
with gr.Row(elem_classes="input-container"):
|
|
|
736 |
progress_text = gr.Textbox(
|
737 |
label="Progress Status",
|
738 |
visible=False,
|
739 |
+
interactive=False,
|
740 |
+
elem_classes="progress-text"
|
741 |
)
|
742 |
|
743 |
+
def show_loading(state: bool) -> dict:
|
744 |
+
return {
|
745 |
+
"value": "<div class='loading-spinner'>β³</div>" if state else "<div class='loading-spinner' style='display: none;'>β³</div>",
|
746 |
+
"visible": state
|
747 |
+
}
|
748 |
+
|
749 |
+
def show_typing(state: bool) -> dict:
|
750 |
+
return {
|
751 |
+
"value": f"<div class='typing-indicator{' active' if state else ''}'>Typing...</div>",
|
752 |
+
"visible": state
|
753 |
+
}
|
754 |
+
|
755 |
+
# Theme toggle handler
|
756 |
+
theme_button.click(
|
757 |
+
self.toggle_theme,
|
758 |
+
inputs=[theme_state],
|
759 |
+
outputs=[theme_state, theme_button],
|
760 |
+
_js="theme => applyTheme(theme)"
|
761 |
+
)
|
762 |
|
763 |
+
# Sidebar toggle handler
|
764 |
+
tools_button.click(
|
765 |
+
self.toggle_sidebar,
|
766 |
+
inputs=[sidebar_state],
|
767 |
+
outputs=[sidebar_state],
|
768 |
+
_js="() => toggleSidebar()"
|
769 |
+
)
|
770 |
|
771 |
+
# Analysis handlers
|
772 |
send_btn.click(
|
773 |
+
show_loading,
|
774 |
+
inputs=[gr.State(value=True)],
|
775 |
+
outputs=[chatbot]
|
776 |
+
).then(
|
777 |
+
show_typing,
|
778 |
+
inputs=[gr.State(value=True)],
|
779 |
+
outputs=[chatbot]
|
780 |
).then(
|
781 |
self.analyze,
|
782 |
inputs=[msg_input, chatbot, file_upload],
|
783 |
outputs=[chatbot, download_output, final_summary, progress_text],
|
784 |
show_progress="hidden"
|
785 |
).then(
|
786 |
+
show_loading,
|
787 |
+
inputs=[gr.State(value=False)],
|
788 |
+
outputs=[chatbot]
|
789 |
+
).then(
|
790 |
+
show_typing,
|
791 |
+
inputs=[gr.State(value=False)],
|
792 |
+
outputs=[chatbot]
|
793 |
)
|
794 |
|
795 |
msg_input.submit(
|
796 |
+
show_loading,
|
797 |
+
inputs=[gr.State(value=True)],
|
798 |
+
outputs=[chatbot]
|
799 |
+
).then(
|
800 |
+
show_typing,
|
801 |
+
inputs=[gr.State(value=True)],
|
802 |
+
outputs=[chatbot]
|
803 |
).then(
|
804 |
self.analyze,
|
805 |
inputs=[msg_input, chatbot, file_upload],
|
806 |
outputs=[chatbot, download_output, final_summary, progress_text],
|
807 |
show_progress="hidden"
|
808 |
).then(
|
809 |
+
show_loading,
|
810 |
+
inputs=[gr.State(value=False)],
|
811 |
+
outputs=[chatbot]
|
812 |
+
).then(
|
813 |
+
show_typing,
|
814 |
+
inputs=[gr.State(value=False)],
|
815 |
+
outputs=[chatbot]
|
816 |
)
|
817 |
|
818 |
app.load(
|
819 |
lambda: [
|
820 |
+
[], None, "", "", None, {"visible": False}, "light", False, "π Dark Mode"
|
821 |
],
|
822 |
+
outputs=[chatbot, download_output, final_summary, msg_input, file_upload, progress_text, theme_state, sidebar_state, theme_button],
|
823 |
queue=False
|
824 |
)
|
825 |
|
|
|
827 |
|
828 |
# ==================== APPLICATION ENTRY POINT ====================
|
829 |
if __name__ == "__main__":
|
830 |
+
app = None
|
831 |
try:
|
832 |
logger.info("Starting Clinical Oversight Assistant...")
|
833 |
app = ClinicalOversightApp()
|
|
|
847 |
logger.error(f"Application failed to start: {e}")
|
848 |
raise
|
849 |
finally:
|
850 |
+
if app:
|
851 |
+
app.cleanup_resources()
|