{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "7ce0a47c-1c4f-44a4-a9d8-9ea6399a8f84", "metadata": {}, "outputs": [], "source": [ "#| default_exp learning_interface" ] }, { "cell_type": "markdown", "id": "55331735-898e-411b-b751-5b380605be36", "metadata": {}, "source": [ "# Learning Interface\n", "\n", "> Gradio interface" ] }, { "cell_type": "markdown", "id": "dd401991-b919-423e-9da7-961387faf11e", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "execution_count": null, "id": "edc3fbb1-13ef-408a-b5fe-eb7a6821915b", "metadata": {}, "outputs": [], "source": [ "#| hide\n", "from nbdev.showdoc import *" ] }, { "cell_type": "code", "execution_count": null, "id": "4be213cf-89b4-48c8-9592-f509332da485", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "C:\\Users\\deepa\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", " from .autonotebook import tqdm as notebook_tqdm\n" ] }, { "ename": "ModuleNotFoundError", "evalue": "No module named 'wardbuddy.auth'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[1], line 11\u001b[0m\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mclinical_tutor\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ClinicalTutor\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlearning_context\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m setup_logger, LearningCategory, SmartGoal\n\u001b[1;32m---> 11\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mauth\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m AuthManager\n\u001b[0;32m 12\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mjson\u001b[39;00m\n\u001b[0;32m 14\u001b[0m logger \u001b[38;5;241m=\u001b[39m setup_logger(\u001b[38;5;18m__name__\u001b[39m)\n", "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'wardbuddy.auth'" ] } ], "source": [ "#| export\n", "from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator\n", "import gradio as gr\n", "import numpy as np\n", "from pathlib import Path\n", "import asyncio\n", "from datetime import datetime\n", "import pandas as pd\n", "from wardbuddy.clinical_tutor import ClinicalTutor\n", "from wardbuddy.learning_context import setup_logger, LearningCategory, SmartGoal\n", "from wardbuddy.auth import AuthManager\n", "import json\n", "\n", "logger = setup_logger(__name__)" ] }, { "cell_type": "markdown", "id": "c39da1db-e630-4296-93f6-03b9188320cc", "metadata": {}, "source": [ "## Learning Interface" ] }, { "cell_type": "code", "execution_count": null, "id": "aff3d321-2116-475f-b906-f74889e76d66", "metadata": {}, "outputs": [], "source": [ "# moved _create_css function to LearningInterface class" ] }, { "cell_type": "markdown", "id": "de7ace04-4841-461d-89bb-234b5f8b48e1", "metadata": {}, "source": [ "This module provides the user interface for the clinical learning system, including:\n", " * Case presentation and feedback\n", " * Learning preference configuration\n", " * Session management\n", " * Progress visualization" ] }, { "cell_type": "code", "execution_count": null, "id": "192b54a0-8a0d-4cb0-baf5-d47efe62094f", "metadata": {}, "outputs": [], "source": [ "#| export\n", "class LearningInterface:\n", " \"\"\"\n", " Gradio interface for clinical learning system.\n", " \n", " Features:\n", " - Context selection\n", " - Goal management\n", " - Case discussion with streaming\n", " - Progress tracking\n", " \"\"\"\n", " \n", " def __init__(\n", " self,\n", " context_path: Optional[Path] = None,\n", " model: str = \"anthropic/claude-3.5-sonnet\",\n", " api_url: Optional[str] = None\n", " ):\n", " \"\"\"Initialize interface.\"\"\"\n", " default_api_url = \"http://170.64.204.93\"\n", " self.tutor = ClinicalTutor(context_path, model)\n", " self.auth = AuthManager(api_url or default_api_url)\n", "\n", " # Available options\n", " self.specialties = [\n", " \"Internal Medicine\",\n", " \"Emergency Medicine\",\n", " \"Surgery\",\n", " \"Pediatrics\",\n", " \"Family Medicine\"\n", " ]\n", " \n", " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n", " \n", " logger.info(\"Learning interface initialized\")\n", "\n", " def _create_css(self) -> str:\n", " \"\"\"Create custom CSS for improved interface styling.\"\"\"\n", " return \"\"\"\n", " /* Global styles */\n", " .gradio-container {\n", " margin: 0 auto !important;\n", " padding: 0 !important;\n", " max-width: 100% !important;\n", " background-color: #0f172a !important;\n", " }\n", " \n", " /* Authentication styles */\n", " .auth-container {\n", " max-width: 400px !important;\n", " margin: 2rem auto !important;\n", " padding: 2rem !important;\n", " background-color: #1e293b !important;\n", " border-radius: 0.5rem !important;\n", " border: 1px solid #334155 !important;\n", " }\n", " \n", " .auth-input input {\n", " background-color: #1e293b !important;\n", " border: 1px solid #334155 !important;\n", " color: #f1f5f9 !important;\n", " font-size: 1rem !important;\n", " padding: 0.75rem !important;\n", " border-radius: 0.375rem !important;\n", " width: 100% !important;\n", " }\n", " \n", " .auth-button {\n", " width: 100% !important;\n", " margin-top: 1rem !important;\n", " background-color: #2563eb !important;\n", " color: white !important;\n", " font-weight: 500 !important;\n", " border-radius: 0.375rem !important;\n", " padding: 0.75rem !important;\n", " }\n", " \n", " /* Main navigation tabs */\n", " .tabs {\n", " background-color: #1e293b !important;\n", " border-radius: 0.5rem !important;\n", " padding: 0.5rem !important;\n", " margin-bottom: 1rem !important;\n", " }\n", " \n", " .tab-nav {\n", " background-color: transparent !important;\n", " border-bottom: 1px solid #334155 !important;\n", " padding: 0 !important;\n", " margin-bottom: 1rem !important;\n", " }\n", " \n", " .tab-nav button {\n", " color: #94a3b8 !important;\n", " background: transparent !important;\n", " border: none !important;\n", " padding: 0.75rem 1rem !important;\n", " margin-right: 0.5rem !important;\n", " border-radius: 0.375rem 0.375rem 0 0 !important;\n", " font-weight: 500 !important;\n", " transition: all 0.2s !important;\n", " }\n", " \n", " .tab-nav button.selected {\n", " color: #f1f5f9 !important;\n", " background-color: #2563eb !important;\n", " border-bottom: 2px solid #2563eb !important;\n", " }\n", " \n", " /* Header section */\n", " .header-row {\n", " padding: 1rem !important;\n", " background-color: #1e293b !important;\n", " border-radius: 0.5rem !important;\n", " margin-bottom: 1rem !important;\n", " }\n", " \n", " /* Dropdown styling */\n", " .gr-dropdown {\n", " background-color: #1e293b !important;\n", " border: 1px solid #334155 !important;\n", " border-radius: 0.375rem !important;\n", " color: #f1f5f9 !important;\n", " }\n", " \n", " .gr-dropdown:focus {\n", " border-color: #2563eb !important;\n", " box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2) !important;\n", " }\n", " \n", " /* Active goal display */\n", " .active-goal {\n", " background-color: #1e293b !important;\n", " border-left: 4px solid #2563eb !important;\n", " padding: 0.75rem 1rem !important;\n", " margin-bottom: 1rem !important;\n", " color: #f1f5f9 !important;\n", " font-weight: 500 !important;\n", " border-radius: 0 0.375rem 0.375rem 0 !important;\n", " }\n", " \n", " /* Chat interface */\n", " .chat-window {\n", " border: 1px solid #334155 !important;\n", " border-radius: 0.5rem !important;\n", " background-color: #1e293b !important;\n", " padding: 1rem !important;\n", " height: 600px !important;\n", " overflow-y: auto !important;\n", " }\n", " \n", " .message-bubble {\n", " background-color: #2d3748 !important;\n", " border-radius: 0.5rem !important;\n", " padding: 0.75rem 1rem !important;\n", " margin-bottom: 0.5rem !important;\n", " max-width: 80% !important;\n", " }\n", " \n", " .user-message {\n", " background-color: #2563eb !important;\n", " color: white !important;\n", " margin-left: auto !important;\n", " }\n", " \n", " .assistant-message {\n", " background-color: #2d3748 !important;\n", " color: #f1f5f9 !important;\n", " }\n", " \n", " /* Message input area */\n", " .message-input {\n", " margin-top: 1rem !important;\n", " position: relative !important;\n", " }\n", " \n", " textarea {\n", " background-color: #1e293b !important;\n", " border: 1px solid #334155 !important;\n", " color: #f1f5f9 !important;\n", " border-radius: 0.375rem !important;\n", " padding: 0.75rem !important;\n", " resize: none !important;\n", " width: 100% !important;\n", " }\n", " \n", " /* Recording button */\n", " .record-button {\n", " position: absolute !important;\n", " right: 0.5rem !important;\n", " bottom: 0.5rem !important;\n", " width: 40px !important;\n", " height: 40px !important;\n", " padding: 0 !important;\n", " border-radius: 50% !important;\n", " background-color: #2563eb !important;\n", " display: flex !important;\n", " align-items: center !important;\n", " justify-content: center !important;\n", " cursor: pointer !important;\n", " }\n", " \n", " .record-button:hover {\n", " background-color: #1d4ed8 !important;\n", " }\n", " \n", " /* Goals section */\n", " .goals-section {\n", " background-color: #1e293b !important;\n", " border-radius: 0.5rem !important;\n", " padding: 1rem !important;\n", " }\n", " \n", " .goals-list {\n", " height: 400px !important;\n", " overflow-y: auto !important;\n", " border: 1px solid #334155 !important;\n", " border-radius: 0.375rem !important;\n", " margin: 1rem 0 !important;\n", " }\n", " \n", " .goal-item {\n", " padding: 0.75rem 1rem !important;\n", " border-bottom: 1px solid #334155 !important;\n", " cursor: pointer !important;\n", " }\n", " \n", " .goal-item:hover {\n", " background-color: #2d3748 !important;\n", " }\n", " \n", " .goal-item.active {\n", " background-color: #2563eb20 !important;\n", " border-left: 4px solid #2563eb !important;\n", " }\n", " \n", " /* Action buttons */\n", " button {\n", " background-color: #2563eb !important;\n", " color: white !important;\n", " font-weight: 500 !important;\n", " border: none !important;\n", " border-radius: 0.375rem !important;\n", " padding: 0.75rem 1rem !important;\n", " cursor: pointer !important;\n", " transition: all 0.2s !important;\n", " }\n", " \n", " button:hover {\n", " background-color: #1d4ed8 !important;\n", " }\n", " \n", " button.secondary {\n", " background-color: #4b5563 !important;\n", " }\n", " \n", " button.secondary:hover {\n", " background-color: #374151 !important;\n", " }\n", " \n", " /* Case log and Dashboard tables */\n", " .gr-table {\n", " background-color: #1e293b !important;\n", " border: 1px solid #334155 !important;\n", " border-radius: 0.375rem !important;\n", " overflow: hidden !important;\n", " }\n", " \n", " .gr-table th {\n", " background-color: #2d3748 !important;\n", " color: #f1f5f9 !important;\n", " font-weight: 500 !important;\n", " padding: 0.75rem 1rem !important;\n", " text-align: left !important;\n", " }\n", " \n", " .gr-table td {\n", " padding: 0.75rem 1rem !important;\n", " border-bottom: 1px solid #334155 !important;\n", " color: #f1f5f9 !important;\n", " }\n", " \n", " .gr-table tr:hover {\n", " background-color: #2d3748 !important;\n", " }\n", " \n", " /* Mobile optimizations */\n", " @media (max-width: 768px) {\n", " .auth-container {\n", " margin: 1rem !important;\n", " padding: 1rem !important;\n", " }\n", " \n", " .tab-nav button {\n", " padding: 0.5rem 0.75rem !important;\n", " font-size: 0.875rem !important;\n", " }\n", " \n", " .chat-window {\n", " height: 400px !important;\n", " }\n", " \n", " .message-bubble {\n", " max-width: 90% !important;\n", " }\n", " \n", " .goals-list {\n", " height: 300px !important;\n", " }\n", " \n", " button {\n", " padding: 0.5rem 0.75rem !important;\n", " font-size: 0.875rem !important;\n", " }\n", " }\n", " \n", " /* Loading states */\n", " .loading {\n", " opacity: 0.7 !important;\n", " pointer-events: none !important;\n", " }\n", " \n", " .loading::after {\n", " content: \"\" !important;\n", " position: absolute !important;\n", " top: 50% !important;\n", " left: 50% !important;\n", " width: 24px !important;\n", " height: 24px !important;\n", " border: 2px solid #f1f5f9 !important;\n", " border-radius: 50% !important;\n", " border-top-color: transparent !important;\n", " animation: spin 1s linear infinite !important;\n", " }\n", " \n", " @keyframes spin {\n", " to { transform: rotate(360deg); }\n", " }\n", " \n", " /* Accessibility improvements */\n", " :focus {\n", " outline: 2px solid #2563eb !important;\n", " outline-offset: 2px !important;\n", " }\n", " \n", " [role=\"button\"]:focus {\n", " outline: none !important;\n", " box-shadow: 0 0 0 2px #2563eb !important;\n", " }\n", " \"\"\"\n", " \n", " # Authentication Components\n", " def _create_auth_components(self) -> Tuple[gr.Group, gr.Group]:\n", " \"\"\"Create authentication interface components.\"\"\"\n", " with gr.Group() as auth_group:\n", " gr.Markdown(\"# Clinical Learning Assistant\")\n", " \n", " with gr.Tab(\"Login\"):\n", " login_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\"\n", " )\n", " login_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Enter your password\"\n", " )\n", " login_button = gr.Button(\"Login\")\n", " login_message = gr.Markdown()\n", " \n", " with gr.Tab(\"Register\"):\n", " register_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\"\n", " )\n", " register_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Choose a password\"\n", " )\n", " register_button = gr.Button(\"Register\")\n", " register_message = gr.Markdown()\n", " \n", " # Main interface (hidden until auth)\n", " main_group = gr.Group(visible=False)\n", " \n", " return auth_group, main_group, (\n", " login_email, login_password, login_button, login_message,\n", " register_email, register_password, register_button, register_message\n", " )\n", " \n", " def handle_login(\n", " self,\n", " email: str,\n", " password: str,\n", " state: Dict[str, Any]\n", " ) -> Tuple[str, gr.update, gr.update, Dict[str, Any]]:\n", " \"\"\"Handle user login.\"\"\"\n", " success, message = self.auth.login(email, password)\n", " if success:\n", " state[\"authenticated\"] = True\n", " return (\n", " \"Login successful! Redirecting...\", # Success message\n", " gr.update(visible=False), # Hide auth group\n", " gr.update(visible=True), # Show main interface\n", " state\n", " )\n", " return message, gr.update(), gr.update(), state\n", " \n", " def handle_register(self, email: str, password: str) -> str:\n", " \"\"\"Handle user registration.\"\"\"\n", " success, message = self.auth.register(email, password)\n", " return message\n", "\n", " # Updated context update to include session tracking\n", " async def update_context(\n", " self,\n", " specialty: str,\n", " setting: str,\n", " state: Dict[str, Any]\n", " ) -> Tuple[List, List, List, str]: # Explicitly return all outputs\n", " \"\"\"Update rotation context with session tracking.\"\"\"\n", " if specialty and setting:\n", " self.tutor.learning_context.update_rotation(specialty, setting)\n", " \n", " if self.auth.start_session(specialty, setting):\n", " self.auth.log_interaction(\n", " \"context_update\",\n", " f\"Specialty: {specialty}, Setting: {setting}\"\n", " )\n", " \n", " if not state.get(\"suggested_goals\"):\n", " goals = await self.tutor.generate_smart_goals(specialty, setting)\n", " state[\"suggested_goals\"] = goals\n", " \n", " if self.auth.token:\n", " self.auth.log_interaction(\n", " \"goals_generated\",\n", " f\"Generated {len(goals)} goals\"\n", " )\n", " \n", " update_results = self._update_displays(state)\n", " return tuple(update_results) # Return as tuple to match output structure\n", " \n", " empty_results = [[], [], [], \"No active learning goal\"]\n", " return tuple(empty_results) # Return as tuple here too\n", "\n", " # Chat Processing\n", " async def process_chat(\n", " self,\n", " message: str,\n", " history: List[Dict[str, str]],\n", " state: Dict[str, Any]\n", " ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:\n", " \"\"\"\n", " Process chat messages with streaming, history management, and interaction logging.\n", " \n", " Args:\n", " message: User input message\n", " history: Chat history list of message dictionaries\n", " state: Current interface state\n", " \n", " Yields:\n", " tuple: (updated history, cleared message input, updated state)\n", " \"\"\"\n", " try:\n", " if not message.strip():\n", " yield history, \"\", state\n", " return\n", " \n", " # Start new discussion if none active\n", " if not state.get(\"discussion_active\"):\n", " state[\"discussion_active\"] = True\n", " state[\"discussion_start\"] = datetime.now().isoformat()\n", " self.tutor.current_discussion = [] # Reset tutor's discussion history\n", " # Log discussion start\n", " self.auth.log_interaction(\"discussion_start\")\n", " \n", " # Initialize history if needed\n", " if history is None:\n", " history = []\n", " \n", " # Add user message\n", " history.append({\n", " \"role\": \"user\",\n", " \"content\": message\n", " })\n", " \n", " # Log user message\n", " self.auth.log_interaction(\"user_message\", message)\n", " \n", " # Add initial assistant message\n", " history.append({\n", " \"role\": \"assistant\",\n", " \"content\": \"\"\n", " })\n", " \n", " # Update display\n", " yield history, \"\", state\n", " \n", " # Build messages array for LLM including full history\n", " messages = [{\n", " \"role\": \"system\",\n", " \"content\": self.tutor._build_discussion_prompt()\n", " }]\n", " \n", " # Add previous messages from tutor's discussion history\n", " messages.extend(self.tutor.current_discussion)\n", " \n", " # Add current message\n", " messages.append({\"role\": \"user\", \"content\": message})\n", " \n", " # Process response with streaming\n", " current_response = \"\"\n", " async for token in self.tutor._get_completion_stream(messages):\n", " current_response += token\n", " history[-1][\"content\"] = current_response\n", " yield history, \"\", state\n", " \n", " # Log assistant response\n", " self.auth.log_interaction(\"assistant_response\", current_response)\n", " \n", " # Update state and discussion history\n", " state[\"last_message\"] = datetime.now().isoformat()\n", " \n", " # Update tutor's discussion history\n", " self.tutor.current_discussion = history[:-1] # Don't include empty assistant message\n", " self.tutor.current_discussion.append({\n", " \"role\": \"assistant\",\n", " \"content\": current_response\n", " })\n", " \n", " yield history, \"\", state\n", " \n", " except Exception as e:\n", " logger.error(f\"Error in chat: {str(e)}\")\n", " # Log error\n", " self.auth.log_interaction(\"error\", str(e))\n", " \n", " if history is None:\n", " history = []\n", " history.extend([\n", " {\"role\": \"user\", \"content\": message},\n", " {\"role\": \"assistant\", \"content\": \"I apologize, but I encountered an error. Please try again.\"}\n", " ])\n", " yield history, \"\", state \n", " \n", " def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n", " \"\"\"Update discussion status display.\"\"\"\n", " try:\n", " if not state.get(\"discussion_active\"):\n", " return \"Start a new case discussion\"\n", " \n", " start = datetime.fromisoformat(state[\"discussion_start\"])\n", " duration = datetime.now() - start\n", " minutes = int(duration.total_seconds() / 60)\n", " \n", " return f\"Active discussion ({minutes} minutes)\"\n", " \n", " except Exception as e:\n", " logger.error(f\"Error updating status: {str(e)}\")\n", " return \"Discussion status unknown\"\n", "\n", " async def generate_goals(self, state: Dict[str, Any]) -> List:\n", " \"\"\"Generate new goal suggestions.\"\"\"\n", " rotation = self.tutor.learning_context.rotation\n", " if rotation.specialty and rotation.setting:\n", " goals = await self.tutor.generate_smart_goals(\n", " rotation.specialty,\n", " rotation.setting\n", " )\n", " state[\"suggested_goals\"] = goals\n", " return self._update_displays(state)\n", " return [[], [], [], \"No active learning goal\"]\n", "\n", " async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:\n", " \"\"\"Add user-specified goal.\"\"\"\n", " if not text.strip():\n", " return self._update_displays(state)\n", " \n", " rotation = self.tutor.learning_context.rotation\n", " if rotation.specialty and rotation.setting:\n", " goal = await self.tutor.generate_smart_goal(\n", " text,\n", " rotation.specialty,\n", " rotation.setting\n", " )\n", " if goal:\n", " if \"suggested_goals\" not in state:\n", " state[\"suggested_goals\"] = []\n", " state[\"suggested_goals\"].append(goal)\n", " return self._update_displays(state)\n", " return self._update_displays(state)\n", "\n", " def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:\n", " \"\"\"Set selected goal as active.\"\"\"\n", " if \"suggested_goals\" in state and evt.index[0] < len(state[\"suggested_goals\"]):\n", " goal = state[\"suggested_goals\"][evt.index[0]]\n", " self.tutor.learning_context.add_smart_goal(goal)\n", " return self._update_displays(state)\n", " return self._update_displays(state)\n", "\n", " def end_discussion(self, state: Dict[str, Any]) -> List:\n", " \"\"\"End current discussion with analytics.\"\"\"\n", " if state.get(\"discussion_active\"):\n", " self.auth.log_interaction(\"discussion_end\", json.dumps({\n", " \"duration\": (\n", " datetime.now() - \n", " datetime.fromisoformat(state[\"discussion_start\"])\n", " ).total_seconds()\n", " }))\n", " self.tutor.end_discussion()\n", " state[\"discussion_active\"] = False\n", " state[\"discussion_start\"] = None\n", " state[\"last_message\"] = None\n", " self.tutor.current_discussion = [] # Clear discussion history\n", " return self._update_displays(state)\n", "\n", " def _update_displays(self, state: Dict[str, Any]) -> List:\n", " \"\"\"Update all display components.\"\"\"\n", " context = self.tutor.learning_context\n", " \n", " # Update goals list\n", " goals_data = []\n", " for goal in state.get(\"suggested_goals\", []):\n", " status = \"Active\" if (\n", " context.active_goal and \n", " context.active_goal.id == goal.id\n", " ) else \"Available\"\n", " \n", " goals_data.append([\n", " goal.smart_version,\n", " goal.category.value,\n", " status\n", " ])\n", " \n", " # Update progress display\n", " summary = context.get_category_summary()\n", " progress_data = [\n", " [cat, data[\"completed\"], data[\"total\"]]\n", " for cat, data in summary.items()\n", " ]\n", " \n", " # Update recent completions\n", " recent_data = []\n", " for cat, data in summary.items():\n", " for goal in data[\"recent\"]:\n", " recent_data.append([\n", " goal[\"smart_version\"],\n", " cat,\n", " goal[\"completed_at\"]\n", " ])\n", " \n", " # Update active goal display\n", " goal_text = (\n", " f\"Current Goal: {context.active_goal.smart_version}\"\n", " if context.active_goal else\n", " \"No active learning goal\"\n", " )\n", " \n", " return [goals_data, progress_data, recent_data, goal_text]\n", "\n", " def create_interface(self) -> gr.Blocks:\n", " \"\"\"Create streaming-enabled interface with authentication and all features.\"\"\"\n", " \n", " with gr.Blocks(title=\"Clinical Learning Assistant\", css=self._create_css()) as interface:\n", " # State management\n", " state = gr.State({\n", " \"discussion_active\": False,\n", " \"suggested_goals\": [],\n", " \"discussion_start\": None,\n", " \"last_message\": None,\n", " \"authenticated\": False\n", " })\n", " \n", " # Authentication interface\n", " with gr.Group(visible=True) as auth_group:\n", " gr.Markdown(\"# Clinical Learning Assistant\")\n", " \n", " with gr.Tabs() as auth_tabs:\n", " with gr.Tab(\"Login\"):\n", " login_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " login_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Enter your password\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " login_button = gr.Button(\n", " \"Login\",\n", " variant=\"primary\",\n", " elem_classes=[\"auth-button\"]\n", " )\n", " login_message = gr.Markdown()\n", " \n", " with gr.Tab(\"Register\"):\n", " register_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " register_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Choose a password\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " register_button = gr.Button(\n", " \"Register\",\n", " variant=\"primary\",\n", " elem_classes=[\"auth-button\"]\n", " )\n", " register_message = gr.Markdown()\n", " \n", " # Main interface (initially hidden)\n", " with gr.Group(visible=False) as main_group:\n", " # Main navigation tabs\n", " with gr.Tabs() as main_tabs:\n", " # Present/Ask Tab\n", " with gr.Tab(\"Present/Ask\"):\n", " with gr.Row(equal_height=True):\n", " # Compact header with specialty and setting\n", " with gr.Column(scale=2):\n", " with gr.Row():\n", " specialty = gr.Dropdown(\n", " choices=self.specialties,\n", " label=\"Specialty\",\n", " value=self.tutor.learning_context.rotation.specialty or None,\n", " scale=1\n", " )\n", " setting = gr.Dropdown(\n", " choices=self.settings,\n", " label=\"Setting\",\n", " value=self.tutor.learning_context.rotation.setting or None,\n", " scale=1\n", " )\n", " \n", " # Main content area\n", " with gr.Row():\n", " # Left: Chat Interface\n", " with gr.Column(scale=3):\n", " # Update the Markdown component to handle string input\n", " goal_display = gr.Markdown(\n", " value=\"No active learning goal\",\n", " elem_classes=[\"active-goal\"]\n", " )\n", " \n", " # Chat interface with proper sizing\n", " chatbot = gr.Chatbot(\n", " height=600, # Taller chat window\n", " show_label=False,\n", " elem_classes=[\"chat-window\"]\n", " )\n", " \n", " # Message input with recording button\n", " with gr.Row():\n", " with gr.Column(scale=20):\n", " msg = gr.Textbox(\n", " label=\"Present your case or ask questions\",\n", " placeholder=(\n", " \"Present your case as you would to your supervisor:\\n\"\n", " \"- Start with the chief complaint\\n\"\n", " \"- Include relevant history and findings\\n\"\n", " \"- Share your assessment and plan\"\n", " ),\n", " lines=4\n", " )\n", " \n", " # Compact recording button\n", " with gr.Column(scale=1, min_width=50):\n", " audio_input = gr.Audio(\n", " sources=[\"microphone\"],\n", " type=\"numpy\",\n", " streaming=True,\n", " elem_classes=[\"record-button\"]\n", " )\n", " \n", " with gr.Row():\n", " clear = gr.Button(\"Clear\")\n", " end = gr.Button(\"End Discussion\", variant=\"primary\")\n", " \n", " # Right: Learning Goals\n", " with gr.Column(scale=1):\n", " # Goals management\n", " with gr.Group():\n", " new_goal = gr.Textbox(\n", " label=\"Enter your own goal\",\n", " placeholder=\"What do you want to get better at?\",\n", " lines=2\n", " )\n", " add_goal = gr.Button(\"Add\", size=\"sm\")\n", " \n", " # Expanded goals list\n", " goals_list = gr.DataFrame(\n", " headers=[\"Goal\", \"Category\", \"Status\"],\n", " value=[],\n", " label=\"Available Goals\",\n", " interactive=True,\n", " wrap=True,\n", " row_count=10\n", " )\n", " \n", " # Move suggest button to bottom\n", " generate = gr.Button(\n", " \"Suggest New Goals\",\n", " size=\"sm\"\n", " )\n", " \n", " # Case Log Tab\n", " with gr.Tab(\"Case Log\"):\n", " with gr.Column():\n", " gr.Markdown(\"### Previous Cases\")\n", " case_log = gr.DataFrame(\n", " headers=[\"Date\", \"Specialty\", \"Setting\", \"Chief Complaint\", \"Status\", \"Actions\"],\n", " value=[],\n", " label=\"Case History\",\n", " interactive=True,\n", " row_count=10\n", " )\n", " \n", " # Dashboard Tab\n", " with gr.Tab(\"Dashboard\"):\n", " with gr.Column():\n", " gr.Markdown(\"### Learning Progress\")\n", " progress_display = gr.DataFrame(\n", " headers=[\"Category\", \"Completed\", \"Total\"],\n", " value=[],\n", " label=\"Progress by Category\",\n", " row_count=8\n", " )\n", " \n", " gr.Markdown(\"### Knowledge Gaps\")\n", " knowledge_gaps = gr.DataFrame(\n", " headers=[\"Topic\", \"Confidence\", \"Recent Examples\"],\n", " value=[],\n", " label=\"Identified Knowledge Gaps\",\n", " row_count=8\n", " )\n", " gr.Markdown(\"### Recent Goals\")\n", " recent_goals = gr.DataFrame(\n", " headers=[\"Goal\", \"Category\", \"Completed\"],\n", " value=[],\n", " label=\"Recently Completed Goals\",\n", " row_count=5\n", " )\n", " \n", " # Helper Functions\n", " def clear_discussion() -> Tuple[List, str, Dict]:\n", " \"\"\"Clear chat history.\"\"\"\n", " self.tutor.current_discussion = []\n", " return [], \"\", {\n", " \"discussion_active\": False,\n", " \"suggested_goals\": [],\n", " \"discussion_start\": None,\n", " \"last_message\": None\n", " }\n", " \n", " def process_audio(audio: np.ndarray) -> str:\n", " \"\"\"Convert audio to text using Whisper or similar.\"\"\"\n", " if audio is None:\n", " return \"\"\n", " try:\n", " # Add actual audio processing logic here\n", " # This is a placeholder\n", " return \"Audio transcription would appear here\"\n", " except Exception as e:\n", " logger.error(f\"Audio processing error: {str(e)}\")\n", " return \"\"\n", " \n", " # Wire up authentication events\n", " login_button.click(\n", " self.handle_login,\n", " inputs=[login_email, login_password, state],\n", " outputs=[login_message, auth_group, main_group, state]\n", " ).then(\n", " lambda: (None, None), # Clear login form\n", " None,\n", " [login_email, login_password]\n", " )\n", " \n", " register_button.click(\n", " self.handle_register,\n", " inputs=[register_email, register_password],\n", " outputs=register_message\n", " )\n", " \n", " # Wire up main interface events\n", " specialty.change(\n", " fn=self.update_context,\n", " inputs=[specialty, setting, state],\n", " outputs=[\n", " goals_list, # DataFrame\n", " progress_display, # DataFrame\n", " recent_goals, # DataFrame\n", " goal_display # Markdown\n", " ]\n", " )\n", " \n", " setting.change(\n", " fn=self.update_context,\n", " inputs=[specialty, setting, state],\n", " outputs=[\n", " goals_list, # DataFrame\n", " progress_display, # DataFrame\n", " recent_goals, # DataFrame\n", " goal_display # Markdown\n", " ]\n", " )\n", " \n", " # Chat events with streaming\n", " msg.submit(\n", " self.process_chat,\n", " inputs=[msg, chatbot, state],\n", " outputs=[chatbot, msg, state]\n", " )\n", " \n", " # Voice input handling\n", " audio_input.stream(\n", " process_audio,\n", " inputs=[audio_input],\n", " outputs=[msg]\n", " ).then(\n", " self.process_chat,\n", " inputs=[msg, chatbot, state],\n", " outputs=[chatbot, msg, state]\n", " )\n", " \n", " # Button handlers\n", " clear.click(\n", " clear_discussion,\n", " outputs=[chatbot, msg, state]\n", " )\n", " \n", " end.click(\n", " self.end_discussion,\n", " inputs=[state],\n", " outputs=[goals_list, goal_display]\n", " )\n", " \n", " generate.click(\n", " self.generate_goals,\n", " inputs=[state],\n", " outputs=[goals_list, goal_display]\n", " )\n", " \n", " add_goal.click(\n", " self.add_user_goal,\n", " inputs=[new_goal, state],\n", " outputs=[goals_list, goal_display]\n", " )\n", " \n", " goals_list.select(\n", " self.select_goal,\n", " inputs=[state],\n", " outputs=[goals_list, goal_display]\n", " )\n", " \n", " return interface" ] }, { "cell_type": "code", "execution_count": null, "id": "f6edc027-8625-4856-94ce-8f1a0acd4c8f", "metadata": {}, "outputs": [], "source": [ "#| hide\n", "# old, deprecated on 9/02 to make UI cleaner and simpler\n", "class LearningInterface:\n", " \"\"\"\n", " Gradio interface for clinical learning system.\n", " \n", " Features:\n", " - Context selection\n", " - Goal management\n", " - Case discussion with streaming\n", " - Progress tracking\n", " \"\"\"\n", " \n", " def __init__(\n", " self,\n", " context_path: Optional[Path] = None,\n", " model: str = \"anthropic/claude-3.5-sonnet\",\n", " api_url: Optional[str] = None\n", " ):\n", " \"\"\"Initialize interface.\"\"\"\n", " default_api_url = \"http://170.64.204.93\"\n", " self.tutor = ClinicalTutor(context_path, model)\n", " self.auth = AuthManager(api_url or default_api_url)\n", "\n", " # Available options\n", " self.specialties = [\n", " \"Internal Medicine\",\n", " \"Emergency Medicine\",\n", " \"Surgery\",\n", " \"Pediatrics\",\n", " \"Family Medicine\"\n", " ]\n", " \n", " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n", " \n", " logger.info(\"Learning interface initialized\")\n", "\n", " # Authentication Components\n", " def _create_auth_components(self) -> Tuple[gr.Group, gr.Group]:\n", " \"\"\"Create authentication interface components.\"\"\"\n", " with gr.Group() as auth_group:\n", " gr.Markdown(\"# Clinical Learning Assistant\")\n", " \n", " with gr.Tab(\"Login\"):\n", " login_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\"\n", " )\n", " login_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Enter your password\"\n", " )\n", " login_button = gr.Button(\"Login\")\n", " login_message = gr.Markdown()\n", " \n", " with gr.Tab(\"Register\"):\n", " register_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\"\n", " )\n", " register_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Choose a password\"\n", " )\n", " register_button = gr.Button(\"Register\")\n", " register_message = gr.Markdown()\n", " \n", " # Main interface (hidden until auth)\n", " main_group = gr.Group(visible=False)\n", " \n", " return auth_group, main_group, (\n", " login_email, login_password, login_button, login_message,\n", " register_email, register_password, register_button, register_message\n", " )\n", " \n", " def handle_login(\n", " self,\n", " email: str,\n", " password: str,\n", " state: Dict[str, Any]\n", " ) -> Tuple[str, gr.update, gr.update, Dict[str, Any]]:\n", " \"\"\"Handle user login.\"\"\"\n", " success, message = self.auth.login(email, password)\n", " if success:\n", " state[\"authenticated\"] = True\n", " return (\n", " \"Login successful! Redirecting...\", # Success message\n", " gr.update(visible=False), # Hide auth group\n", " gr.update(visible=True), # Show main interface\n", " state\n", " )\n", " return message, gr.update(), gr.update(), state\n", " \n", " def handle_register(self, email: str, password: str) -> str:\n", " \"\"\"Handle user registration.\"\"\"\n", " success, message = self.auth.register(email, password)\n", " return message\n", "\n", " # Updated context update to include session tracking\n", " async def update_context(\n", " self,\n", " specialty: str,\n", " setting: str,\n", " state: Dict[str, Any]\n", " ) -> List:\n", " \"\"\"Update rotation context with session tracking.\"\"\"\n", " if specialty and setting:\n", " self.tutor.learning_context.update_rotation(specialty, setting)\n", " \n", " # Start new session\n", " if self.auth.start_session(specialty, setting):\n", " self.auth.log_interaction(\n", " \"context_update\",\n", " f\"Specialty: {specialty}, Setting: {setting}\"\n", " )\n", " \n", " if not state.get(\"suggested_goals\"):\n", " goals = await self.tutor.generate_smart_goals(specialty, setting)\n", " state[\"suggested_goals\"] = goals\n", " \n", " # Log goal generation\n", " if self.auth.token:\n", " self.auth.log_interaction(\n", " \"goals_generated\",\n", " f\"Generated {len(goals)} goals\"\n", " )\n", " \n", " return self._update_displays(state)\n", " return [[], [], [], \"No active learning goal\"]\n", "\n", " # Chat Processing\n", " async def process_chat(\n", " self,\n", " message: str,\n", " history: List[Dict[str, str]],\n", " state: Dict[str, Any]\n", " ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:\n", " \"\"\"\n", " Process chat messages with streaming, history management, and interaction logging.\n", " \n", " Args:\n", " message: User input message\n", " history: Chat history list of message dictionaries\n", " state: Current interface state\n", " \n", " Yields:\n", " tuple: (updated history, cleared message input, updated state)\n", " \"\"\"\n", " try:\n", " if not message.strip():\n", " yield history, \"\", state\n", " return\n", " \n", " # Start new discussion if none active\n", " if not state.get(\"discussion_active\"):\n", " state[\"discussion_active\"] = True\n", " state[\"discussion_start\"] = datetime.now().isoformat()\n", " self.tutor.current_discussion = [] # Reset tutor's discussion history\n", " # Log discussion start\n", " self.auth.log_interaction(\"discussion_start\")\n", " \n", " # Initialize history if needed\n", " if history is None:\n", " history = []\n", " \n", " # Add user message\n", " history.append({\n", " \"role\": \"user\",\n", " \"content\": message\n", " })\n", " \n", " # Log user message\n", " self.auth.log_interaction(\"user_message\", message)\n", " \n", " # Add initial assistant message\n", " history.append({\n", " \"role\": \"assistant\",\n", " \"content\": \"\"\n", " })\n", " \n", " # Update display\n", " yield history, \"\", state\n", " \n", " # Build messages array for LLM including full history\n", " messages = [{\n", " \"role\": \"system\",\n", " \"content\": self.tutor._build_discussion_prompt()\n", " }]\n", " \n", " # Add previous messages from tutor's discussion history\n", " messages.extend(self.tutor.current_discussion)\n", " \n", " # Add current message\n", " messages.append({\"role\": \"user\", \"content\": message})\n", " \n", " # Process response with streaming\n", " current_response = \"\"\n", " async for token in self.tutor._get_completion_stream(messages):\n", " current_response += token\n", " history[-1][\"content\"] = current_response\n", " yield history, \"\", state\n", " \n", " # Log assistant response\n", " self.auth.log_interaction(\"assistant_response\", current_response)\n", " \n", " # Update state and discussion history\n", " state[\"last_message\"] = datetime.now().isoformat()\n", " \n", " # Update tutor's discussion history\n", " self.tutor.current_discussion = history[:-1] # Don't include empty assistant message\n", " self.tutor.current_discussion.append({\n", " \"role\": \"assistant\",\n", " \"content\": current_response\n", " })\n", " \n", " yield history, \"\", state\n", " \n", " except Exception as e:\n", " logger.error(f\"Error in chat: {str(e)}\")\n", " # Log error\n", " self.auth.log_interaction(\"error\", str(e))\n", " \n", " if history is None:\n", " history = []\n", " history.extend([\n", " {\"role\": \"user\", \"content\": message},\n", " {\"role\": \"assistant\", \"content\": \"I apologize, but I encountered an error. Please try again.\"}\n", " ])\n", " yield history, \"\", state \n", " \n", " def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n", " \"\"\"Update discussion status display.\"\"\"\n", " try:\n", " if not state.get(\"discussion_active\"):\n", " return \"Start a new case discussion\"\n", " \n", " start = datetime.fromisoformat(state[\"discussion_start\"])\n", " duration = datetime.now() - start\n", " minutes = int(duration.total_seconds() / 60)\n", " \n", " return f\"Active discussion ({minutes} minutes)\"\n", " \n", " except Exception as e:\n", " logger.error(f\"Error updating status: {str(e)}\")\n", " return \"Discussion status unknown\"\n", "\n", " async def generate_goals(self, state: Dict[str, Any]) -> List:\n", " \"\"\"Generate new goal suggestions.\"\"\"\n", " rotation = self.tutor.learning_context.rotation\n", " if rotation.specialty and rotation.setting:\n", " goals = await self.tutor.generate_smart_goals(\n", " rotation.specialty,\n", " rotation.setting\n", " )\n", " state[\"suggested_goals\"] = goals\n", " return self._update_displays(state)\n", " return [[], [], [], \"No active learning goal\"]\n", "\n", " async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:\n", " \"\"\"Add user-specified goal.\"\"\"\n", " if not text.strip():\n", " return self._update_displays(state)\n", " \n", " rotation = self.tutor.learning_context.rotation\n", " if rotation.specialty and rotation.setting:\n", " goal = await self.tutor.generate_smart_goal(\n", " text,\n", " rotation.specialty,\n", " rotation.setting\n", " )\n", " if goal:\n", " if \"suggested_goals\" not in state:\n", " state[\"suggested_goals\"] = []\n", " state[\"suggested_goals\"].append(goal)\n", " return self._update_displays(state)\n", " return self._update_displays(state)\n", "\n", " def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:\n", " \"\"\"Set selected goal as active.\"\"\"\n", " if \"suggested_goals\" in state and evt.index[0] < len(state[\"suggested_goals\"]):\n", " goal = state[\"suggested_goals\"][evt.index[0]]\n", " self.tutor.learning_context.add_smart_goal(goal)\n", " return self._update_displays(state)\n", " return self._update_displays(state)\n", "\n", " def end_discussion(self, state: Dict[str, Any]) -> List:\n", " \"\"\"End current discussion with analytics.\"\"\"\n", " if state.get(\"discussion_active\"):\n", " self.auth.log_interaction(\"discussion_end\", json.dumps({\n", " \"duration\": (\n", " datetime.now() - \n", " datetime.fromisoformat(state[\"discussion_start\"])\n", " ).total_seconds()\n", " }))\n", " self.tutor.end_discussion()\n", " state[\"discussion_active\"] = False\n", " state[\"discussion_start\"] = None\n", " state[\"last_message\"] = None\n", " self.tutor.current_discussion = [] # Clear discussion history\n", " return self._update_displays(state)\n", "\n", " def _update_displays(self, state: Dict[str, Any]) -> List:\n", " \"\"\"Update all display components.\"\"\"\n", " context = self.tutor.learning_context\n", " \n", " # Update goals list\n", " goals_data = []\n", " for goal in state.get(\"suggested_goals\", []):\n", " status = \"Active\" if (\n", " context.active_goal and \n", " context.active_goal.id == goal.id\n", " ) else \"Available\"\n", " \n", " goals_data.append([\n", " goal.smart_version,\n", " goal.category.value,\n", " status\n", " ])\n", " \n", " # Update progress display\n", " summary = context.get_category_summary()\n", " progress_data = [\n", " [cat, data[\"completed\"], data[\"total\"]]\n", " for cat, data in summary.items()\n", " ]\n", " \n", " # Update recent completions\n", " recent_data = []\n", " for cat, data in summary.items():\n", " for goal in data[\"recent\"]:\n", " recent_data.append([\n", " goal[\"smart_version\"],\n", " cat,\n", " goal[\"completed_at\"]\n", " ])\n", " \n", " # Update active goal display\n", " goal_text = (\n", " f\"Current Goal: {context.active_goal.smart_version}\"\n", " if context.active_goal else\n", " \"No active learning goal\"\n", " )\n", " \n", " return [goals_data, progress_data, recent_data, goal_text]\n", "\n", " def create_interface(self) -> gr.Blocks:\n", " \"\"\"Create streaming-enabled interface with authentication and all features.\"\"\"\n", " with gr.Blocks(title=\"Clinical Learning Assistant\", css=create_css()) as interface:\n", " # State management\n", " state = gr.State({\n", " \"discussion_active\": False,\n", " \"suggested_goals\": [],\n", " \"discussion_start\": None,\n", " \"last_message\": None,\n", " \"authenticated\": False\n", " })\n", " \n", " # Authentication interface\n", " with gr.Group(elem_classes=[\"auth-container\"]) as auth_group:\n", " gr.Markdown(\"# Clinical Learning Assistant\")\n", " \n", " with gr.Tabs() as auth_tabs:\n", " with gr.Tab(\"Login\"):\n", " login_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " login_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Enter your password\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " login_button = gr.Button(\n", " \"Login\",\n", " variant=\"primary\",\n", " elem_classes=[\"auth-button\"]\n", " )\n", " login_message = gr.Markdown()\n", " \n", " with gr.Tab(\"Register\"):\n", " register_email = gr.Textbox(\n", " label=\"Email\",\n", " placeholder=\"Enter your email\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " register_password = gr.Textbox(\n", " label=\"Password\",\n", " type=\"password\",\n", " placeholder=\"Choose a password\",\n", " elem_classes=[\"auth-input\"]\n", " )\n", " register_button = gr.Button(\n", " \"Register\",\n", " variant=\"primary\",\n", " elem_classes=[\"auth-button\"]\n", " )\n", " register_message = gr.Markdown()\n", " \n", " # Main interface (initially hidden)\n", " with gr.Group(visible=False) as main_group:\n", " with gr.Row():\n", " # Context selection\n", " specialty = gr.Dropdown(\n", " choices=self.specialties,\n", " label=\"Specialty\",\n", " value=self.tutor.learning_context.rotation.specialty or None\n", " )\n", " setting = gr.Dropdown(\n", " choices=self.settings,\n", " label=\"Setting\",\n", " value=self.tutor.learning_context.rotation.setting or None\n", " )\n", " \n", " # Main content\n", " with gr.Row():\n", " # Left: Discussion interface\n", " with gr.Column(scale=2):\n", " # Active goal display\n", " goal_display = gr.Markdown(value=\"No active learning goal\")\n", " \n", " # Discussion status\n", " discussion_status = gr.Markdown(\n", " value=\"Start a new case discussion\",\n", " elem_classes=[\"discussion-status\"]\n", " )\n", " \n", " # Chat interface\n", " chatbot = gr.Chatbot(\n", " value=None,\n", " show_label=False,\n", " type=\"messages\", # Use newer message format\n", " elem_classes=[\"chat-window\"]\n", " )\n", " \n", " with gr.Row():\n", " msg = gr.Textbox(\n", " label=\"Present your case or ask questions\",\n", " placeholder=(\n", " \"Present your case as you would to your supervisor:\\n\"\n", " \"- Start with the chief complaint\\n\"\n", " \"- Include relevant history and findings\\n\"\n", " \"- Share your assessment and plan\"\n", " ),\n", " lines=4\n", " )\n", " audio_input = gr.Audio(\n", " sources=[\"microphone\"],\n", " type=\"numpy\",\n", " label=\"Or speak your case\",\n", " streaming=True\n", " )\n", " \n", " with gr.Row():\n", " clear = gr.Button(\"Clear Discussion\")\n", " end = gr.Button(\n", " \"End Discussion & Review\",\n", " variant=\"primary\"\n", " )\n", " \n", " # Right: Progress & Goals\n", " with gr.Column(scale=1):\n", " with gr.Tab(\"Learning Goals\"):\n", " with gr.Row():\n", " generate = gr.Button(\"Generate New Goals\")\n", " new_goal = gr.Textbox(\n", " label=\"Or enter your own goal\",\n", " placeholder=\"What do you want to get better at?\"\n", " )\n", " add_goal = gr.Button(\"Add Goal\")\n", " \n", " goals_list = gr.DataFrame(\n", " headers=[\"Goal\", \"Category\", \"Status\"],\n", " value=[],\n", " label=\"Available Goals\",\n", " interactive=True,\n", " wrap=True\n", " )\n", " \n", " with gr.Tab(\"Progress\"):\n", " progress_display = gr.DataFrame(\n", " headers=[\"Category\", \"Completed\", \"Total\"],\n", " value=[],\n", " label=\"Progress by Category\"\n", " )\n", " \n", " recent_display = gr.DataFrame(\n", " headers=[\"Goal\", \"Category\", \"Completed\"],\n", " value=[],\n", " label=\"Recently Completed\"\n", " )\n", " \n", " # Wire up auth events\n", " login_button.click(\n", " self.handle_login,\n", " inputs=[login_email, login_password, state],\n", " outputs=[login_message, auth_group, main_group, state]\n", " ).success(\n", " lambda: None, # Clear login form\n", " None,\n", " [login_email, login_password]\n", " )\n", " \n", " register_button.click(\n", " self.handle_register,\n", " inputs=[register_email, register_password],\n", " outputs=register_message\n", " )\n", " \n", " # Define helper functions\n", " def clear_discussion() -> Tuple[List, str, Dict]:\n", " \"\"\"Clear chat history.\"\"\"\n", " self.tutor.current_discussion = [] # Clear tutor's discussion history\n", " return [], \"\", {\n", " \"discussion_active\": False,\n", " \"suggested_goals\": [],\n", " \"discussion_start\": None,\n", " \"last_message\": None\n", " }\n", " \n", " def process_audio(audio: np.ndarray) -> str:\n", " \"\"\"Convert audio to text.\"\"\"\n", " if audio is None:\n", " return \"\"\n", " # Add your audio processing logic here\n", " return \"Audio transcription would appear here\"\n", " \n", " # Wire up main interface events with tracking\n", " specialty.change(\n", " fn=self.update_context,\n", " inputs=[specialty, setting, state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display],\n", " api_name=False # Important for async functions\n", " )\n", " \n", " setting.change(\n", " fn=self.update_context,\n", " inputs=[specialty, setting, state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display],\n", " api_name=False # Important for async functions\n", " )\n", " \n", " # Chat events with streaming\n", " msg.submit(\n", " self.process_chat,\n", " inputs=[msg, chatbot, state],\n", " outputs=[chatbot, msg, state],\n", " queue=True # Important for streaming\n", " ).then(\n", " self._update_discussion_status,\n", " inputs=[state],\n", " outputs=[discussion_status]\n", " )\n", " \n", " # Voice input handling\n", " audio_input.stream(\n", " process_audio,\n", " inputs=[audio_input],\n", " outputs=[msg]\n", " ).then(\n", " self.process_chat,\n", " inputs=[msg, chatbot, state],\n", " outputs=[chatbot, msg, state]\n", " )\n", " \n", " # Button handlers\n", " clear.click(\n", " clear_discussion,\n", " outputs=[chatbot, msg, state]\n", " ).then(\n", " lambda: \"Start a new case discussion\",\n", " outputs=[discussion_status]\n", " )\n", " \n", " end.click(\n", " self.end_discussion,\n", " inputs=[state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display]\n", " )\n", " \n", " generate.click(\n", " self.generate_goals,\n", " inputs=[state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display]\n", " )\n", " \n", " # Goal management\n", " add_goal.click(\n", " self.add_user_goal,\n", " inputs=[new_goal, state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display]\n", " )\n", " \n", " goals_list.select(\n", " self.select_goal,\n", " inputs=[state],\n", " outputs=[goals_list, progress_display, recent_display, goal_display]\n", " )\n", " \n", " return interface" ] }, { "cell_type": "markdown", "id": "5c75de88-f6d5-4a5d-92b5-1ebe85895a84", "metadata": {}, "source": [ "## Tests" ] }, { "cell_type": "code", "execution_count": null, "id": "365bc95a-d189-4ab2-aa30-022d0286b5ba", "metadata": {}, "outputs": [], "source": [ "#| hide\n", "async def test_learning_interface():\n", " \"\"\"Test learning interface functionality\"\"\"\n", " interface = LearningInterface()\n", " \n", " # Test chat processing\n", " history = []\n", " test_input = \"28yo M with chest pain\"\n", " \n", " new_history, msg = await interface.process_chat(test_input, history)\n", " assert isinstance(new_history, list)\n", " assert len(new_history) == 2 # User message + response\n", " assert new_history[0][\"role\"] == \"user\"\n", " assert new_history[0][\"content\"] == test_input\n", " \n", " # Test discussion analysis\n", " analysis = await interface.end_discussion(new_history)\n", " assert isinstance(analysis, dict)\n", " assert all(k in analysis for k in [\n", " 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n", " ])\n", " \n", " # Test rotation updates\n", " rotation = interface.update_rotation(\n", " \"Emergency Medicine\",\n", " \"2025-01-01\",\n", " \"2025-03-31\",\n", " [\"Resuscitation\", \"Procedures\"]\n", " )\n", " assert rotation[\"specialty\"] == \"Emergency Medicine\"\n", " assert \"Resuscitation\" in rotation[\"key_focus_areas\"]\n", " \n", " # Test objective management\n", " objectives = interface.toggle_objective(\"Improve chest pain assessment\", False)\n", " assert len(objectives) == 1\n", " assert objectives[0][\"status\"] == \"active\"\n", " \n", " objectives = interface.toggle_objective(\"Improve chest pain assessment\", True)\n", " assert objectives[0][\"status\"] == \"completed\"\n", " \n", " # Test feedback preferences\n", " preferences = interface.toggle_feedback(\"Include more ddx\", True)\n", " assert len(preferences) == 1\n", " assert preferences[0][\"active\"] == True\n", " \n", " print(\"Interface tests passed!\")\n", "\n", "# Run tests\n", "if __name__ == \"__main__\":\n", " import asyncio\n", " if not asyncio.get_event_loop().is_running():\n", " asyncio.run(test_learning_interface())" ] } ], "metadata": { "kernelspec": { "display_name": "python3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }