"""Gradio interface""" # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_learning_interface.ipynb. # %% auto 0 __all__ = ['logger', 'create_dashboard_css', 'LearningInterface', 'launch_learning_interface'] # %% ../nbs/02_learning_interface.ipynb 4 from typing import Dict, List, Optional, Tuple, Any import gradio as gr from pathlib import Path import asyncio from datetime import datetime import pandas as pd from .clinical_tutor import ClinicalTutor from .learning_context import setup_logger logger = setup_logger(__name__) # %% ../nbs/02_learning_interface.ipynb 6 def create_dashboard_css() -> str: """Create custom CSS for dashboard styling""" return """ /* Global styles */ .gradio-container { background-color: #0f172a !important; /* slate-900 */ } /* Card styling */ .dashboard-card { background-color: #1e293b !important; /* slate-800 */ border: 1px solid #334155 !important; /* slate-700 */ border-radius: 0.5rem !important; padding: 1rem !important; margin: 0.5rem 0 !important; color: #f1f5f9 !important; /* slate-100 */ } /* Chat container */ .chatbot { background-color: #1e293b !important; /* slate-800 */ border-color: #334155 !important; /* slate-700 */ } /* Message bubbles */ .chatbot .message.user { background-color: #334155 !important; /* slate-700 */ border: 1px solid #475569 !important; /* slate-600 */ color: #f1f5f9 !important; /* slate-100 */ } .chatbot .message.bot { background-color: #1e40af !important; /* blue-800 */ border: 1px solid #1e3a8a !important; /* blue-900 */ color: #f1f5f9 !important; /* slate-100 */ } /* Input fields */ textarea, input[type="text"] { background-color: #334155 !important; /* slate-700 */ color: #f1f5f9 !important; /* slate-100 */ border: 1px solid #475569 !important; /* slate-600 */ } textarea:focus, input[type="text"]:focus { border-color: #3b82f6 !important; /* blue-500 */ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important; } /* Buttons */ button.primary { background-color: #2563eb !important; /* blue-600 */ color: white !important; } button.primary:hover { background-color: #3b82f6 !important; /* blue-500 */ } button.secondary { background-color: #475569 !important; /* slate-600 */ color: white !important; } button.secondary:hover { background-color: #64748b !important; /* slate-500 */ } /* Tabs */ .tab-nav { background-color: #1e293b !important; /* slate-800 */ border-bottom: 1px solid #334155 !important; /* slate-700 */ } .tab-nav button { color: #f1f5f9 !important; /* slate-100 */ } .tab-nav button.selected { border-bottom-color: #3b82f6 !important; /* blue-500 */ } /* Status indicators */ .status-active { color: #22c55e !important; /* green-500 */ font-weight: 500 !important; } .status-completed { color: #94a3b8 !important; /* slate-400 */ } /* Headers */ .dashboard-header { color: #f1f5f9 !important; /* slate-100 */ font-size: 1.5rem !important; font-weight: 600 !important; margin-bottom: 1rem !important; } /* Tables */ table { background-color: #1e293b !important; /* slate-800 */ color: #f1f5f9 !important; /* slate-100 */ } th, td { border-color: #334155 !important; /* slate-700 */ } """ # %% ../nbs/02_learning_interface.ipynb 8 class LearningInterface: """ Gradio interface for clinical learning interactions. Features: - Natural case discussion chat - Dynamic learning dashboard - Post-discussion analysis - Progress tracking """ def __init__( self, context_path: Optional[Path] = None, theme: str = "default" ): """Initialize learning interface.""" self.tutor = ClinicalTutor(context_path) self.theme = theme self.context_path = context_path # Track current discussion state self.current_discussion = { "started": None, "case_type": None, "messages": [] } logger.info("Learning interface initialized") async def process_chat( self, message: str, history: List[List[str]], state: Dict[str, Any] ) -> Tuple[List[List[str]], str, Dict[str, Any]]: """ Process chat messages with state management. Args: message: User input message history: Chat history state: Current interface state Returns: tuple: (updated history, cleared message, updated state) """ try: if not message.strip(): return history, "", state # Start new discussion if none active if not state.get("discussion_active"): state["discussion_active"] = True state["discussion_start"] = datetime.now().isoformat() # Get tutor response response = await self.tutor.discuss_case(message) # Update history - now using list pairs instead of dicts if history is None: history = [] history.append([message, response]) # Changed from dict format to list pair state["last_message"] = datetime.now().isoformat() return history, "", state except Exception as e: logger.error(f"Error in chat: {str(e)}") return history or [], "", state async def end_discussion( self, history: List[List[str]], state: Dict[str, Any] ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ Analyze completed discussion and prepare summary. Args: history: Chat history as list of [user_message, assistant_message] pairs state: Current interface state Returns: tuple: (analysis results, updated state) """ try: if not history: return { "learning_points": [], "gaps": {}, "strengths": [], "suggested_objectives": [] }, state # Convert history format for analysis formatted_history = [] for user_msg, assistant_msg in history: formatted_history.extend([ {"role": "user", "content": user_msg}, {"role": "assistant", "content": assistant_msg} ]) # Get analysis analysis = await self.tutor.analyze_discussion(formatted_history) # Reset discussion state state["discussion_active"] = False state["discussion_start"] = None state["last_message"] = None return analysis, state except Exception as e: logger.error(f"Error analyzing discussion: {str(e)}") return { "learning_points": [], "gaps": {}, "strengths": [], "suggested_objectives": [] }, state def update_rotation( self, specialty: str, start_date: str, end_date: str, focus_areas: str ) -> Tuple[str, str, str, str]: """ Update rotation details and return updated values. Args: specialty: Rotation specialty start_date: Start date string end_date: End date string focus_areas: Comma-separated focus areas Returns: tuple: Updated field values """ try: # Parse focus areas focus_list = [ area.strip() for area in focus_areas.split(",") if area.strip() ] # Update context rotation = { "specialty": specialty, "start_date": start_date, "end_date": end_date, "key_focus_areas": focus_list } self.tutor.learning_context.update_rotation(rotation) # Return updated values return ( specialty, start_date, end_date, ",".join(focus_list) ) except Exception as e: logger.error(f"Error updating rotation: {str(e)}") current = self.tutor.learning_context.current_rotation return ( current["specialty"], current["start_date"] or "", current["end_date"] or "", ",".join(current["key_focus_areas"]) ) def add_objective( self, objective: str, objectives_df: pd.DataFrame ) -> pd.DataFrame: """ Add new learning objective and return updated dataframe. Args: objective: New objective text objectives_df: Current objectives dataframe Returns: pd.DataFrame: Updated objectives list """ try: if not objective.strip(): return objectives_df # Add to context self.tutor.learning_context.add_learning_objective(objective) # Convert to dataframe return pd.DataFrame([ [obj["objective"], obj["status"], obj["added"]] for obj in self.tutor.learning_context.learning_objectives ], columns=["Objective", "Status", "Date Added"]) except Exception as e: logger.error(f"Error adding objective: {str(e)}") return objectives_df def toggle_objective_status( self, evt: gr.SelectData, # Updated to use gr.SelectData objectives_df: pd.DataFrame ) -> pd.DataFrame: """ Toggle objective status between active and completed. Args: evt: Gradio select event containing row index objectives_df: Current objectives dataframe Returns: pd.DataFrame: Updated objectives list """ try: objective_idx = evt.index[0] # Get selected row index if objective_idx >= len(objectives_df): return objectives_df # Get objective objective = objectives_df.iloc[objective_idx]["Objective"] current_status = objectives_df.iloc[objective_idx]["Status"] # Toggle in context if current_status == "active": self.tutor.learning_context.complete_objective(objective) else: self.tutor.learning_context.add_learning_objective(objective) # Update dataframe return pd.DataFrame([ [obj["objective"], obj["status"], obj["added"]] for obj in self.tutor.learning_context.learning_objectives ], columns=["Objective", "Status", "Date Added"]) except Exception as e: logger.error(f"Error toggling objective: {str(e)}") return objectives_df def add_feedback_focus( self, focus: str, feedback_df: pd.DataFrame ) -> pd.DataFrame: """Add new feedback focus area.""" try: if not focus.strip(): return feedback_df # Add to context self.tutor.learning_context.toggle_feedback_focus(focus, True) # Update dataframe return pd.DataFrame([ [pref["focus"], pref["active"]] for pref in self.tutor.learning_context.feedback_preferences ], columns=["Focus Area", "Active"]) except Exception as e: logger.error(f"Error adding feedback focus: {str(e)}") return feedback_df def toggle_feedback_status( self, evt: gr.SelectData, # Updated to use gr.SelectData feedback_df: pd.DataFrame ) -> pd.DataFrame: """Toggle feedback focus active status.""" try: focus_idx = evt.index[0] # Get selected row index if focus_idx >= len(feedback_df): return feedback_df # Get focus area focus = feedback_df.iloc[focus_idx]["Focus Area"] current_status = feedback_df.iloc[focus_idx]["Active"] # Toggle in context self.tutor.learning_context.toggle_feedback_focus( focus, not current_status ) # Update dataframe return pd.DataFrame([ [pref["focus"], pref["active"]] for pref in self.tutor.learning_context.feedback_preferences ], columns=["Focus Area", "Active"]) except Exception as e: logger.error(f"Error toggling feedback: {str(e)}") return feedback_df def create_interface(self) -> gr.Blocks: """Create and configure the Gradio interface""" with gr.Blocks( title="Clinical Learning Assistant", theme=self.theme, css=create_dashboard_css() ) as interface: # State management state = gr.State({ "discussion_active": False, "discussion_start": None, "last_message": None }) # Header with gr.Row(): gr.Markdown( "# Clinical Learning Assistant", elem_classes=["dashboard-header"] ) with gr.Row(): # Left column - Chat interface with gr.Column(scale=2): # Active discussion indicator discussion_status = gr.Markdown( "Start a new case discussion", elem_classes=["dashboard-card"] ) # Chat interface chatbot = gr.Chatbot( height=500, label="Case Discussion", show_label=True, elem_classes=["dashboard-card"] ) with gr.Row(): msg = gr.Textbox( label="Present your case or ask questions", placeholder=( "Present your case as you would to your supervisor:\n" "- Start with the chief complaint\n" "- Include relevant history and findings\n" "- Share your assessment and plan" ), lines=5 ) # Add voice input with updated syntax audio_msg = gr.Audio( label="Or speak your case", sources=["microphone"], type="numpy", streaming=True ) with gr.Row(): clear = gr.Button("Clear Discussion") end_discussion = gr.Button( "End Discussion & Review", variant="primary" ) # Right column - Learning dashboard with gr.Column(scale=1): with gr.Tabs(): # Current Rotation tab with gr.Tab("Current Rotation"): with gr.Column(elem_classes=["dashboard-card"]): specialty = gr.Textbox( label="Specialty", value=self.tutor.learning_context.current_rotation["specialty"] ) start_date = gr.Textbox( label="Start Date (YYYY-MM-DD)", value=self.tutor.learning_context.current_rotation["start_date"] ) end_date = gr.Textbox( label="End Date (YYYY-MM-DD)", value=self.tutor.learning_context.current_rotation["end_date"] ) focus_areas = gr.Textbox( label="Key Focus Areas (comma-separated)", value=",".join( self.tutor.learning_context.current_rotation["key_focus_areas"] ) ) update_rotation_btn = gr.Button( "Update Rotation", variant="secondary" ) # Learning Objectives tab with gr.Tab("Learning Objectives"): with gr.Column(elem_classes=["dashboard-card"]): objectives_df = gr.DataFrame( headers=["Objective", "Status", "Date Added"], value=[[ obj["objective"], obj["status"], obj["added"] ] for obj in self.tutor.learning_context.learning_objectives], interactive=True, wrap=True ) with gr.Row(): new_objective = gr.Textbox( label="New Learning Objective", placeholder="Enter objective..." ) add_objective_btn = gr.Button( "Add", variant="secondary" ) # Feedback Preferences tab with gr.Tab("Feedback Focus"): with gr.Column(elem_classes=["dashboard-card"]): feedback_df = gr.DataFrame( headers=["Focus Area", "Active"], value=[[ pref["focus"], pref["active"] ] for pref in self.tutor.learning_context.feedback_preferences], interactive=True, wrap=True ) with gr.Row(): new_feedback = gr.Textbox( label="New Feedback Focus", placeholder="Enter focus area..." ) add_feedback_btn = gr.Button( "Add", variant="secondary" ) # Knowledge Profile tab with gr.Tab("Knowledge Profile"): with gr.Column(elem_classes=["dashboard-card"]): # Knowledge Gaps gr.Markdown("### Knowledge Gaps") gaps_display = gr.DataFrame( headers=["Topic", "Confidence"], value=[[ topic, confidence ] for topic, confidence in self.tutor.learning_context.knowledge_profile["gaps"].items() ], interactive=False ) # Strengths Display gr.Markdown("### Strengths") strengths_display = gr.DataFrame( headers=["Area"], value=[[strength] for strength in self.tutor.learning_context.knowledge_profile["strengths"] ], interactive=False ) # Recent Progress gr.Markdown("### Recent Progress") progress_display = gr.DataFrame( headers=["Topic", "Improvement", "Date"], value=[[ prog["topic"], f"{prog['improvement']:.2f}", prog["date"] ] for prog in self.tutor.learning_context.knowledge_profile["recent_progress"] ], interactive=False ) # Discussion summary section summary_section = gr.Column(visible=False) with summary_section: gr.Markdown("## Discussion Summary") # Overview section with gr.Row(): with gr.Column(): gr.Markdown("### Session Overview") session_overview = gr.JSON( label="Discussion Details", value={ "duration": "0 minutes", "messages": 0, "topics_covered": [] } ) # Learning Points and Gaps with gr.Row(): with gr.Column(): gr.Markdown("### Key Learning Points") learning_points = gr.JSON(label="Points to Remember") with gr.Column(): gr.Markdown("### Knowledge Profile Updates") with gr.Row(): gaps = gr.JSON(label="Areas for Improvement") strengths = gr.JSON(label="Demonstrated Strengths") # Future Learning section gr.Markdown("### Planning Ahead") with gr.Row(): with gr.Column(): gr.Markdown("#### Suggested Learning Objectives") objectives = gr.JSON(label="Consider Adding") with gr.Column(): gr.Markdown("#### Recommended Focus Areas") recommendations = gr.JSON(label="Next Steps") # Action buttons with gr.Row(): add_selected_objectives = gr.Button( "Add Selected Objectives", variant="primary" ) close_summary = gr.Button("Close Summary") # Event handlers # Add new event handler for voice input def process_audio(audio): if audio is None: return None # Convert audio to text using your preferred method # For example, you could use transformers pipeline here try: from transformers import pipeline transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-small") text = transcriber(audio)["text"] return text except Exception as e: logger.error(f"Error transcribing audio: {str(e)}") return None # Update the event handler: audio_msg.stop_recording( fn=process_audio, outputs=[msg] ).then( fn=self.process_chat, inputs=[msg, chatbot, state], outputs=[chatbot, msg, state] ).then( fn=self._update_discussion_status, inputs=[state], outputs=[discussion_status] ) msg.submit( self.process_chat, inputs=[msg, chatbot, state], outputs=[chatbot, msg, state] ).then( self._update_discussion_status, inputs=[state], outputs=[discussion_status] ) clear.click( lambda: ([], "", { "discussion_active": False, "discussion_start": None, "last_message": None }), outputs=[chatbot, msg, state] ).then( lambda: "Start a new case discussion", outputs=[discussion_status] ) end_discussion.click( self.end_discussion, inputs=[chatbot, state], outputs=[ session_overview, learning_points, gaps, strengths, objectives, recommendations ] ).then( lambda: gr.update(visible=True), None, summary_section ).then( self._refresh_knowledge_profile, outputs=[gaps_display, strengths_display, progress_display] ) close_summary.click( lambda: gr.update(visible=False), None, summary_section ) # Rotation management update_rotation_btn.click( self.update_rotation, inputs=[specialty, start_date, end_date, focus_areas], outputs=[specialty, start_date, end_date, focus_areas] ) # Learning objectives management add_objective_btn.click( self.add_objective, inputs=[new_objective, objectives_df], outputs=[objectives_df] ).then( lambda: "", None, new_objective ) objectives_df.select( self.toggle_objective_status, inputs=[objectives_df], outputs=[objectives_df] ) # Feedback preferences management add_feedback_btn.click( self.add_feedback_focus, inputs=[new_feedback, feedback_df], outputs=[feedback_df] ).then( lambda: "", None, new_feedback ) feedback_df.select( self.toggle_feedback_status, inputs=[feedback_df], outputs=[feedback_df] ) # Add selected objectives from summary add_selected_objectives.click( self._add_suggested_objectives, inputs=[objectives], outputs=[objectives_df] ) return interface def _update_discussion_status(self, state: Dict[str, Any]) -> str: """Update discussion status display""" try: if not state.get("discussion_active"): return "Start a new case discussion" start = datetime.fromisoformat(state["discussion_start"]) duration = datetime.now() - start minutes = int(duration.total_seconds() / 60) return f"Active discussion ({minutes} minutes)" except Exception as e: logger.error(f"Error updating status: {str(e)}") return "Discussion status unknown" def _refresh_knowledge_profile( self ) -> Tuple[List[List[str]], List[List[str]], List[List[str]]]: """Refresh knowledge profile displays""" try: # Gaps gaps_data = [[ topic, f"{confidence:.2f}" ] for topic, confidence in self.tutor.learning_context.knowledge_profile["gaps"].items() ] # Strengths strengths_data = [[ strength ] for strength in self.tutor.learning_context.knowledge_profile["strengths"] ] # Progress progress_data = [[ prog["topic"], f"{prog['improvement']:.2f}", prog["date"] ] for prog in self.tutor.learning_context.knowledge_profile["recent_progress"] ] return gaps_data, strengths_data, progress_data except Exception as e: logger.error(f"Error refreshing profile: {str(e)}") return [], [], [] def _add_suggested_objectives( self, evt: gr.SelectData, # Updated to use gr.SelectData suggested_objectives: List[str] ) -> pd.DataFrame: """Add selected suggested objectives to learning objectives""" try: selected_indices = [evt.index[0]] # Get selected row index for idx in selected_indices: if idx < len(suggested_objectives): objective = suggested_objectives[idx] self.tutor.learning_context.add_learning_objective(objective) return pd.DataFrame([ [obj["objective"], obj["status"], obj["added"]] for obj in self.tutor.learning_context.learning_objectives ], columns=["Objective", "Status", "Date Added"]) except Exception as e: logger.error(f"Error adding objectives: {str(e)}") return pd.DataFrame() # %% ../nbs/02_learning_interface.ipynb 10 async def launch_learning_interface( port: Optional[int] = None, context_path: Optional[Path] = None, share: bool = False, theme: str = "default" ) -> None: """Launch the learning interface application.""" try: interface = LearningInterface(context_path, theme) app = interface.create_interface() app.launch( server_port=port, share=share ) logger.info(f"Interface launched on port: {port}") except Exception as e: logger.error(f"Error launching interface: {str(e)}") raise