dyadd commited on
Commit
4939a73
·
verified ·
1 Parent(s): d4fc43a

Upload folder using huggingface_hub

Browse files
app.py CHANGED
@@ -5,11 +5,32 @@ from wardbuddy.learning_interface import LearningInterface
5
  # Load environment variables
6
  load_dotenv()
7
 
8
- # Check for API key
9
- if not os.getenv("OPENROUTER_API_KEY"):
10
- raise ValueError("Please set OPENROUTER_API_KEY in your .env file")
 
 
11
 
12
- # Create and launch interface
13
- interface = LearningInterface()
 
 
 
 
 
14
  demo = interface.create_interface()
15
- demo.launch(server_name='0.0.0.0', server_port=7860, share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  # Load environment variables
6
  load_dotenv()
7
 
8
+ # Check required environment variables
9
+ required_vars = ["OPENROUTER_API_KEY", "API_URL"]
10
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
11
+ if missing_vars:
12
+ raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
13
 
14
+ # Create interface
15
+ interface = LearningInterface(
16
+ model="anthropic/claude-3.5-sonnet",
17
+ api_url=os.getenv("API_URL")
18
+ )
19
+
20
+ # Create app
21
  demo = interface.create_interface()
22
+
23
+ # Launch with appropriate settings
24
+ if os.getenv("SPACE_ID"): # Running on HF Spaces
25
+ demo.launch(
26
+ server_name="0.0.0.0",
27
+ server_port=7860,
28
+ share=False,
29
+ show_api=False
30
+ )
31
+ else: # Local development
32
+ demo.launch(
33
+ server_name="0.0.0.0",
34
+ server_port=7860,
35
+ share=True
36
+ )
nbs/02_learning_interface.ipynb CHANGED
@@ -52,6 +52,17 @@
52
  "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",
53
  " from .autonotebook import tqdm as notebook_tqdm\n"
54
  ]
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
  ],
57
  "source": [
@@ -65,6 +76,7 @@
65
  "import pandas as pd\n",
66
  "from wardbuddy.clinical_tutor import ClinicalTutor\n",
67
  "from wardbuddy.learning_context import setup_logger, LearningCategory, SmartGoal\n",
 
68
  "import json\n",
69
  "\n",
70
  "logger = setup_logger(__name__)"
@@ -87,7 +99,7 @@
87
  "source": [
88
  "#| export\n",
89
  "def create_css() -> str:\n",
90
- " \"\"\"Create custom CSS for interface styling.\"\"\"\n",
91
  " return \"\"\"\n",
92
  " /* Base styles */\n",
93
  " .gradio-container {\n",
@@ -96,14 +108,44 @@
96
  " padding: 1rem !important;\n",
97
  " }\n",
98
  " \n",
99
- " /* Mobile-first approach */\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  " .chat-window {\n",
101
  " background-color: #1e293b !important;\n",
102
  " border: 1px solid #334155 !important;\n",
103
  " border-radius: 0.5rem !important;\n",
104
- " height: calc(100vh - 300px) !important; /* Responsive height */\n",
105
  " min-height: 300px !important;\n",
106
- " max-height: 500px !important; \n",
107
  " }\n",
108
  " \n",
109
  " .chat-message {\n",
@@ -140,12 +182,32 @@
140
  " background-color: #1d4ed8 !important;\n",
141
  " }\n",
142
  " \n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  " /* Mobile optimizations */\n",
144
  " @media (max-width: 768px) {\n",
145
  " .gradio-container {\n",
146
  " padding: 0.5rem !important;\n",
147
  " }\n",
148
  " \n",
 
 
 
 
 
149
  " .chat-window {\n",
150
  " height: calc(100vh - 250px) !important;\n",
151
  " }\n",
@@ -163,6 +225,11 @@
163
  " width: 100% !important;\n",
164
  " margin: 0.25rem 0 !important;\n",
165
  " }\n",
 
 
 
 
 
166
  " }\n",
167
  " \"\"\""
168
  ]
@@ -201,11 +268,13 @@
201
  " def __init__(\n",
202
  " self,\n",
203
  " context_path: Optional[Path] = None,\n",
204
- " model: str = \"anthropic/claude-3.5-sonnet\"\n",
 
205
  " ):\n",
206
  " \"\"\"Initialize interface.\"\"\"\n",
207
  " self.tutor = ClinicalTutor(context_path, model)\n",
208
- " \n",
 
209
  " # Available options\n",
210
  " self.specialties = [\n",
211
  " \"Internal Medicine\",\n",
@@ -218,7 +287,71 @@
218
  " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
219
  " \n",
220
  " logger.info(\"Learning interface initialized\")\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  " \n",
 
 
 
 
 
 
222
  " async def process_chat(\n",
223
  " self,\n",
224
  " message: str,\n",
@@ -226,7 +359,7 @@
226
  " state: Dict[str, Any]\n",
227
  " ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:\n",
228
  " \"\"\"\n",
229
- " Process chat messages with streaming and proper history management.\n",
230
  " \n",
231
  " Args:\n",
232
  " message: User input message\n",
@@ -246,6 +379,8 @@
246
  " state[\"discussion_active\"] = True\n",
247
  " state[\"discussion_start\"] = datetime.now().isoformat()\n",
248
  " self.tutor.current_discussion = [] # Reset tutor's discussion history\n",
 
 
249
  " \n",
250
  " # Initialize history if needed\n",
251
  " if history is None:\n",
@@ -257,6 +392,9 @@
257
  " \"content\": message\n",
258
  " })\n",
259
  " \n",
 
 
 
260
  " # Add initial assistant message\n",
261
  " history.append({\n",
262
  " \"role\": \"assistant\",\n",
@@ -285,6 +423,9 @@
285
  " history[-1][\"content\"] = current_response\n",
286
  " yield history, \"\", state\n",
287
  " \n",
 
 
 
288
  " # Update state and discussion history\n",
289
  " state[\"last_message\"] = datetime.now().isoformat()\n",
290
  " \n",
@@ -299,14 +440,17 @@
299
  " \n",
300
  " except Exception as e:\n",
301
  " logger.error(f\"Error in chat: {str(e)}\")\n",
 
 
 
302
  " if history is None:\n",
303
  " history = []\n",
304
  " history.extend([\n",
305
  " {\"role\": \"user\", \"content\": message},\n",
306
  " {\"role\": \"assistant\", \"content\": \"I apologize, but I encountered an error. Please try again.\"}\n",
307
  " ])\n",
308
- " yield history, \"\", state\n",
309
- " \n",
310
  " def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
311
  " \"\"\"Update discussion status display.\"\"\"\n",
312
  " try:\n",
@@ -363,7 +507,14 @@
363
  " return self._update_displays(state)\n",
364
  "\n",
365
  " def end_discussion(self, state: Dict[str, Any]) -> List:\n",
366
- " \"\"\"End current discussion and analyze results.\"\"\"\n",
 
 
 
 
 
 
 
367
  " self.tutor.end_discussion()\n",
368
  " state[\"discussion_active\"] = False\n",
369
  " state[\"discussion_start\"] = None\n",
@@ -416,213 +567,260 @@
416
  " return [goals_data, progress_data, recent_data, goal_text]\n",
417
  "\n",
418
  " def create_interface(self) -> gr.Blocks:\n",
419
- " \"\"\"Create streaming-enabled interface.\"\"\"\n",
420
  " with gr.Blocks(title=\"Clinical Learning Assistant\", css=create_css()) as interface:\n",
421
  " # State management\n",
422
  " state = gr.State({\n",
423
  " \"discussion_active\": False,\n",
424
  " \"suggested_goals\": [],\n",
425
  " \"discussion_start\": None,\n",
426
- " \"last_message\": None\n",
 
427
  " })\n",
428
- "\n",
429
- " # Header\n",
430
- " gr.Markdown(\"# Clinical Learning Assistant\")\n",
431
- "\n",
432
- " with gr.Row():\n",
433
- " # Context selection\n",
434
- " specialty = gr.Dropdown(\n",
435
- " choices=self.specialties,\n",
436
- " label=\"Specialty\",\n",
437
- " value=self.tutor.learning_context.rotation.specialty or None\n",
438
- " )\n",
439
- " setting = gr.Dropdown(\n",
440
- " choices=self.settings,\n",
441
- " label=\"Setting\",\n",
442
- " value=self.tutor.learning_context.rotation.setting or None\n",
443
- " )\n",
444
- "\n",
445
- " # Main content\n",
446
- " with gr.Row():\n",
447
- " # Left: Discussion interface\n",
448
- " with gr.Column(scale=2):\n",
449
- " # Active goal display\n",
450
- " goal_display = gr.Markdown(value=\"No active learning goal\")\n",
451
- "\n",
452
- " # Discussion status\n",
453
- " discussion_status = gr.Markdown(\n",
454
- " value=\"Start a new case discussion\",\n",
455
- " elem_classes=[\"discussion-status\"]\n",
456
- " )\n",
457
- "\n",
458
- " # Chat interface\n",
459
- " chatbot = gr.Chatbot(\n",
460
- " height=400,\n",
461
- " show_label=False,\n",
462
- " type=\"messages\", # Use newer message format\n",
463
- " elem_classes=[\"chat-window\"]\n",
464
- " )\n",
465
- "\n",
466
- " with gr.Row():\n",
467
- " msg = gr.Textbox(\n",
468
- " label=\"Present your case or ask questions\",\n",
469
- " placeholder=(\n",
470
- " \"Present your case as you would to your supervisor:\\n\"\n",
471
- " \"- Start with the chief complaint\\n\"\n",
472
- " \"- Include relevant history and findings\\n\"\n",
473
- " \"- Share your assessment and plan\"\n",
474
- " ),\n",
475
- " lines=4\n",
476
  " )\n",
477
- "\n",
478
- " audio_input = gr.Audio(\n",
479
- " sources=[\"microphone\"],\n",
480
- " type=\"numpy\",\n",
481
- " label=\"Or speak your case\",\n",
482
- " streaming=True\n",
483
  " )\n",
484
- "\n",
485
- " with gr.Row():\n",
486
- " clear = gr.Button(\"Clear Discussion\")\n",
487
- " end = gr.Button(\n",
488
- " \"End Discussion & Review\",\n",
489
- " variant=\"primary\"\n",
490
  " )\n",
491
- "\n",
492
- " # Right: Progress & Goals\n",
493
- " with gr.Column(scale=1):\n",
494
- " with gr.Tab(\"Learning Goals\"):\n",
495
- " # Goal selection/generation\n",
496
- " with gr.Row():\n",
497
- " generate = gr.Button(\"Generate New Goals\")\n",
498
- " new_goal = gr.Textbox(\n",
499
- " label=\"Or enter your own goal\",\n",
500
- " placeholder=\"What do you want to get better at?\"\n",
501
- " )\n",
502
- " add_goal = gr.Button(\"Add Goal\")\n",
503
- "\n",
504
- " # Goals list\n",
505
- " goals_list = gr.DataFrame(\n",
506
- " headers=[\"Goal\", \"Category\", \"Status\"],\n",
507
- " value=[],\n",
508
- " label=\"Available Goals\",\n",
509
- " interactive=True,\n",
510
- " wrap=True\n",
511
  " )\n",
512
- "\n",
513
- " with gr.Tab(\"Progress\"):\n",
514
- " # Category progress\n",
515
- " progress_display = gr.DataFrame(\n",
516
- " headers=[\"Category\", \"Completed\", \"Total\"],\n",
517
- " value=[],\n",
518
- " label=\"Progress by Category\"\n",
519
  " )\n",
520
- "\n",
521
- " # Recent completions\n",
522
- " recent_display = gr.DataFrame(\n",
523
- " headers=[\"Goal\", \"Category\", \"Completed\"],\n",
524
- " value=[],\n",
525
- " label=\"Recently Completed\"\n",
526
  " )\n",
527
- "\n",
528
- " # Event handlers\n",
529
- " async def update_context(spec: str, set: str, state: Dict) -> List:\n",
530
- " \"\"\"Update rotation context.\"\"\"\n",
531
- " if spec and set:\n",
532
- " self.tutor.learning_context.update_rotation(spec, set)\n",
533
- " if not state.get(\"suggested_goals\"):\n",
534
- " goals = await self.tutor.generate_smart_goals(spec, set)\n",
535
- " state[\"suggested_goals\"] = goals\n",
536
- " return self._update_displays(state)\n",
537
- " return [[], [], [], \"No active learning goal\"]\n",
538
- "\n",
539
- " def clear_discussion() -> Tuple[List, str, Dict]:\n",
540
- " \"\"\"Clear chat history.\"\"\"\n",
541
- " self.tutor.current_discussion = [] # Clear tutor's discussion history\n",
542
- " return [], \"\", {\n",
543
- " \"discussion_active\": False,\n",
544
- " \"suggested_goals\": [],\n",
545
- " \"discussion_start\": None,\n",
546
- " \"last_message\": None\n",
547
- " }\n",
548
  " \n",
549
- " def process_audio(audio: np.ndarray) -> str:\n",
550
- " \"\"\"Convert audio to text.\"\"\"\n",
551
- " if audio is None:\n",
552
- " return \"\"\n",
553
- " # Add your audio processing logic here\n",
554
- " return \"Audio transcription would appear here\"\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  " \n",
556
- " # Wire up events\n",
557
- " specialty.change(\n",
558
- " update_context,\n",
559
- " inputs=[specialty, setting, state],\n",
560
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
561
- " )\n",
562
- "\n",
563
- " setting.change(\n",
564
- " update_context,\n",
565
- " inputs=[specialty, setting, state],\n",
566
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
567
- " )\n",
568
- "\n",
569
- " # Chat events with streaming\n",
570
- " msg.submit(\n",
571
- " self.process_chat,\n",
572
- " inputs=[msg, chatbot, state],\n",
573
- " outputs=[chatbot, msg, state],\n",
574
- " queue=True # Important for streaming\n",
575
- " ).then(\n",
576
- " self._update_discussion_status,\n",
577
- " inputs=[state],\n",
578
- " outputs=[discussion_status]\n",
579
- " )\n",
580
- "\n",
581
- " # Voice input handling\n",
582
- " audio_input.stream(\n",
583
- " process_audio,\n",
584
- " inputs=[audio_input],\n",
585
- " outputs=[msg]\n",
586
- " ).then(\n",
587
- " self.process_chat,\n",
588
- " inputs=[msg, chatbot, state],\n",
589
- " outputs=[chatbot, msg, state]\n",
590
- " )\n",
591
- "\n",
592
- " # Button handlers\n",
593
- " clear.click(\n",
594
- " clear_discussion,\n",
595
- " outputs=[chatbot, msg, state]\n",
596
- " ).then(\n",
597
- " lambda: \"Start a new case discussion\",\n",
598
- " outputs=[discussion_status]\n",
599
- " )\n",
600
- "\n",
601
- " end.click(\n",
602
- " self.end_discussion,\n",
603
- " inputs=[state],\n",
604
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
605
- " )\n",
606
- "\n",
607
- " generate.click(\n",
608
- " self.generate_goals,\n",
609
- " inputs=[state],\n",
610
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
611
- " )\n",
612
- "\n",
613
- " # Goal management\n",
614
- " add_goal.click(\n",
615
- " self.add_user_goal,\n",
616
- " inputs=[new_goal, state],\n",
617
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
618
- " )\n",
619
- "\n",
620
- " goals_list.select(\n",
621
- " self.select_goal,\n",
622
- " inputs=[state],\n",
623
- " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
624
- " )\n",
625
- "\n",
 
 
 
 
 
 
 
 
 
626
  " return interface"
627
  ]
628
  },
 
52
  "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",
53
  " from .autonotebook import tqdm as notebook_tqdm\n"
54
  ]
55
+ },
56
+ {
57
+ "ename": "ModuleNotFoundError",
58
+ "evalue": "No module named 'wardbuddy.auth'",
59
+ "output_type": "error",
60
+ "traceback": [
61
+ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
62
+ "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
63
+ "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",
64
+ "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'wardbuddy.auth'"
65
+ ]
66
  }
67
  ],
68
  "source": [
 
76
  "import pandas as pd\n",
77
  "from wardbuddy.clinical_tutor import ClinicalTutor\n",
78
  "from wardbuddy.learning_context import setup_logger, LearningCategory, SmartGoal\n",
79
+ "from wardbuddy.auth import AuthManager\n",
80
  "import json\n",
81
  "\n",
82
  "logger = setup_logger(__name__)"
 
99
  "source": [
100
  "#| export\n",
101
  "def create_css() -> str:\n",
102
+ " \"\"\"Create custom CSS for interface styling with auth and mobile support.\"\"\"\n",
103
  " return \"\"\"\n",
104
  " /* Base styles */\n",
105
  " .gradio-container {\n",
 
108
  " padding: 1rem !important;\n",
109
  " }\n",
110
  " \n",
111
+ " /* Auth container */\n",
112
+ " .auth-container {\n",
113
+ " max-width: 400px !important;\n",
114
+ " margin: 2rem auto !important;\n",
115
+ " padding: 2rem !important;\n",
116
+ " background-color: #1e293b !important;\n",
117
+ " border-radius: 0.5rem !important;\n",
118
+ " border: 1px solid #334155 !important;\n",
119
+ " }\n",
120
+ " \n",
121
+ " .auth-input input {\n",
122
+ " background-color: #1e293b !important;\n",
123
+ " border: 1px solid #334155 !important;\n",
124
+ " color: #f1f5f9 !important;\n",
125
+ " font-size: 1rem !important;\n",
126
+ " padding: 0.75rem !important;\n",
127
+ " }\n",
128
+ " \n",
129
+ " .auth-button {\n",
130
+ " width: 100% !important;\n",
131
+ " margin-top: 1rem !important;\n",
132
+ " background-color: #2563eb !important;\n",
133
+ " color: white !important;\n",
134
+ " }\n",
135
+ " \n",
136
+ " /* Discussion elements */\n",
137
+ " .discussion-status {\n",
138
+ " color: #94a3b8;\n",
139
+ " font-size: 0.9em;\n",
140
+ " margin-bottom: 1rem;\n",
141
+ " }\n",
142
+ " \n",
143
  " .chat-window {\n",
144
  " background-color: #1e293b !important;\n",
145
  " border: 1px solid #334155 !important;\n",
146
  " border-radius: 0.5rem !important;\n",
147
+ " height: calc(100vh - 300px) !important;\n",
148
  " min-height: 300px !important;\n",
 
149
  " }\n",
150
  " \n",
151
  " .chat-message {\n",
 
182
  " background-color: #1d4ed8 !important;\n",
183
  " }\n",
184
  " \n",
185
+ " /* Progress sections */\n",
186
+ " .tab-nav {\n",
187
+ " border-bottom: 1px solid #334155 !important;\n",
188
+ " }\n",
189
+ " \n",
190
+ " .tab-nav button {\n",
191
+ " background-color: transparent !important;\n",
192
+ " color: #94a3b8 !important;\n",
193
+ " }\n",
194
+ " \n",
195
+ " .tab-nav button.selected {\n",
196
+ " color: #f1f5f9 !important;\n",
197
+ " border-bottom: 2px solid #2563eb !important;\n",
198
+ " }\n",
199
+ " \n",
200
  " /* Mobile optimizations */\n",
201
  " @media (max-width: 768px) {\n",
202
  " .gradio-container {\n",
203
  " padding: 0.5rem !important;\n",
204
  " }\n",
205
  " \n",
206
+ " .auth-container {\n",
207
+ " margin: 1rem !important;\n",
208
+ " padding: 1rem !important;\n",
209
+ " }\n",
210
+ " \n",
211
  " .chat-window {\n",
212
  " height: calc(100vh - 250px) !important;\n",
213
  " }\n",
 
225
  " width: 100% !important;\n",
226
  " margin: 0.25rem 0 !important;\n",
227
  " }\n",
228
+ " \n",
229
+ " .tab-nav button {\n",
230
+ " padding: 0.5rem !important;\n",
231
+ " font-size: 0.9rem !important;\n",
232
+ " }\n",
233
  " }\n",
234
  " \"\"\""
235
  ]
 
268
  " def __init__(\n",
269
  " self,\n",
270
  " context_path: Optional[Path] = None,\n",
271
+ " model: str = \"anthropic/claude-3.5-sonnet\",\n",
272
+ " api_url: Optional[str] = None\n",
273
  " ):\n",
274
  " \"\"\"Initialize interface.\"\"\"\n",
275
  " self.tutor = ClinicalTutor(context_path, model)\n",
276
+ " self.auth = AuthManager(api_url)\n",
277
+ "\n",
278
  " # Available options\n",
279
  " self.specialties = [\n",
280
  " \"Internal Medicine\",\n",
 
287
  " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
288
  " \n",
289
  " logger.info(\"Learning interface initialized\")\n",
290
+ "\n",
291
+ " # Authentication Components\n",
292
+ " def _create_auth_components(self) -> Tuple[gr.Group, gr.Group]:\n",
293
+ " \"\"\"Create authentication interface components.\"\"\"\n",
294
+ " with gr.Group() as auth_group:\n",
295
+ " gr.Markdown(\"# Clinical Learning Assistant\")\n",
296
+ " \n",
297
+ " with gr.Tab(\"Login\"):\n",
298
+ " login_email = gr.Textbox(\n",
299
+ " label=\"Email\",\n",
300
+ " placeholder=\"Enter your email\"\n",
301
+ " )\n",
302
+ " login_password = gr.Textbox(\n",
303
+ " label=\"Password\",\n",
304
+ " type=\"password\",\n",
305
+ " placeholder=\"Enter your password\"\n",
306
+ " )\n",
307
+ " login_button = gr.Button(\"Login\")\n",
308
+ " login_message = gr.Markdown()\n",
309
+ " \n",
310
+ " with gr.Tab(\"Register\"):\n",
311
+ " register_email = gr.Textbox(\n",
312
+ " label=\"Email\",\n",
313
+ " placeholder=\"Enter your email\"\n",
314
+ " )\n",
315
+ " register_password = gr.Textbox(\n",
316
+ " label=\"Password\",\n",
317
+ " type=\"password\",\n",
318
+ " placeholder=\"Choose a password\"\n",
319
+ " )\n",
320
+ " register_button = gr.Button(\"Register\")\n",
321
+ " register_message = gr.Markdown()\n",
322
+ " \n",
323
+ " # Main interface (hidden until auth)\n",
324
+ " main_group = gr.Group(visible=False)\n",
325
+ " \n",
326
+ " return auth_group, main_group, (\n",
327
+ " login_email, login_password, login_button, login_message,\n",
328
+ " register_email, register_password, register_button, register_message\n",
329
+ " )\n",
330
+ " \n",
331
+ " def handle_login(\n",
332
+ " self,\n",
333
+ " email: str,\n",
334
+ " password: str,\n",
335
+ " state: Dict[str, Any]\n",
336
+ " ) -> Tuple[str, gr.update, gr.update, Dict[str, Any]]:\n",
337
+ " \"\"\"Handle user login.\"\"\"\n",
338
+ " success, message = self.auth.login(email, password)\n",
339
+ " if success:\n",
340
+ " state[\"authenticated\"] = True\n",
341
+ " return (\n",
342
+ " \"Login successful! Redirecting...\", # Success message\n",
343
+ " gr.update(visible=False), # Hide auth group\n",
344
+ " gr.update(visible=True), # Show main interface\n",
345
+ " state\n",
346
+ " )\n",
347
+ " return message, gr.update(), gr.update(), state\n",
348
  " \n",
349
+ " def handle_register(self, email: str, password: str) -> str:\n",
350
+ " \"\"\"Handle user registration.\"\"\"\n",
351
+ " success, message = self.auth.register(email, password)\n",
352
+ " return message\n",
353
+ "\n",
354
+ " # Chat Processing\n",
355
  " async def process_chat(\n",
356
  " self,\n",
357
  " message: str,\n",
 
359
  " state: Dict[str, Any]\n",
360
  " ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:\n",
361
  " \"\"\"\n",
362
+ " Process chat messages with streaming, history management, and interaction logging.\n",
363
  " \n",
364
  " Args:\n",
365
  " message: User input message\n",
 
379
  " state[\"discussion_active\"] = True\n",
380
  " state[\"discussion_start\"] = datetime.now().isoformat()\n",
381
  " self.tutor.current_discussion = [] # Reset tutor's discussion history\n",
382
+ " # Log discussion start\n",
383
+ " self.auth.log_interaction(\"discussion_start\")\n",
384
  " \n",
385
  " # Initialize history if needed\n",
386
  " if history is None:\n",
 
392
  " \"content\": message\n",
393
  " })\n",
394
  " \n",
395
+ " # Log user message\n",
396
+ " self.auth.log_interaction(\"user_message\", message)\n",
397
+ " \n",
398
  " # Add initial assistant message\n",
399
  " history.append({\n",
400
  " \"role\": \"assistant\",\n",
 
423
  " history[-1][\"content\"] = current_response\n",
424
  " yield history, \"\", state\n",
425
  " \n",
426
+ " # Log assistant response\n",
427
+ " self.auth.log_interaction(\"assistant_response\", current_response)\n",
428
+ " \n",
429
  " # Update state and discussion history\n",
430
  " state[\"last_message\"] = datetime.now().isoformat()\n",
431
  " \n",
 
440
  " \n",
441
  " except Exception as e:\n",
442
  " logger.error(f\"Error in chat: {str(e)}\")\n",
443
+ " # Log error\n",
444
+ " self.auth.log_interaction(\"error\", str(e))\n",
445
+ " \n",
446
  " if history is None:\n",
447
  " history = []\n",
448
  " history.extend([\n",
449
  " {\"role\": \"user\", \"content\": message},\n",
450
  " {\"role\": \"assistant\", \"content\": \"I apologize, but I encountered an error. Please try again.\"}\n",
451
  " ])\n",
452
+ " yield history, \"\", state \n",
453
+ " \n",
454
  " def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
455
  " \"\"\"Update discussion status display.\"\"\"\n",
456
  " try:\n",
 
507
  " return self._update_displays(state)\n",
508
  "\n",
509
  " def end_discussion(self, state: Dict[str, Any]) -> List:\n",
510
+ " \"\"\"End current discussion with analytics.\"\"\"\n",
511
+ " if state.get(\"discussion_active\"):\n",
512
+ " self.auth.log_interaction(\"discussion_end\", json.dumps({\n",
513
+ " \"duration\": (\n",
514
+ " datetime.now() - \n",
515
+ " datetime.fromisoformat(state[\"discussion_start\"])\n",
516
+ " ).total_seconds()\n",
517
+ " }))\n",
518
  " self.tutor.end_discussion()\n",
519
  " state[\"discussion_active\"] = False\n",
520
  " state[\"discussion_start\"] = None\n",
 
567
  " return [goals_data, progress_data, recent_data, goal_text]\n",
568
  "\n",
569
  " def create_interface(self) -> gr.Blocks:\n",
570
+ " \"\"\"Create streaming-enabled interface with authentication and all features.\"\"\"\n",
571
  " with gr.Blocks(title=\"Clinical Learning Assistant\", css=create_css()) as interface:\n",
572
  " # State management\n",
573
  " state = gr.State({\n",
574
  " \"discussion_active\": False,\n",
575
  " \"suggested_goals\": [],\n",
576
  " \"discussion_start\": None,\n",
577
+ " \"last_message\": None,\n",
578
+ " \"authenticated\": False\n",
579
  " })\n",
580
+ " \n",
581
+ " # Authentication interface\n",
582
+ " with gr.Box(elem_classes=[\"auth-container\"]) as auth_group:\n",
583
+ " gr.Markdown(\"# Clinical Learning Assistant\")\n",
584
+ " \n",
585
+ " with gr.Tabs() as auth_tabs:\n",
586
+ " with gr.Tab(\"Login\"):\n",
587
+ " login_email = gr.Textbox(\n",
588
+ " label=\"Email\",\n",
589
+ " placeholder=\"Enter your email\",\n",
590
+ " elem_classes=[\"auth-input\"]\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  " )\n",
592
+ " login_password = gr.Textbox(\n",
593
+ " label=\"Password\",\n",
594
+ " type=\"password\",\n",
595
+ " placeholder=\"Enter your password\",\n",
596
+ " elem_classes=[\"auth-input\"]\n",
 
597
  " )\n",
598
+ " login_button = gr.Button(\n",
599
+ " \"Login\",\n",
600
+ " variant=\"primary\",\n",
601
+ " elem_classes=[\"auth-button\"]\n",
 
 
602
  " )\n",
603
+ " login_message = gr.Markdown()\n",
604
+ " \n",
605
+ " with gr.Tab(\"Register\"):\n",
606
+ " register_email = gr.Textbox(\n",
607
+ " label=\"Email\",\n",
608
+ " placeholder=\"Enter your email\",\n",
609
+ " elem_classes=[\"auth-input\"]\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
610
  " )\n",
611
+ " register_password = gr.Textbox(\n",
612
+ " label=\"Password\",\n",
613
+ " type=\"password\",\n",
614
+ " placeholder=\"Choose a password\",\n",
615
+ " elem_classes=[\"auth-input\"]\n",
 
 
616
  " )\n",
617
+ " register_button = gr.Button(\n",
618
+ " \"Register\",\n",
619
+ " variant=\"primary\",\n",
620
+ " elem_classes=[\"auth-button\"]\n",
 
 
621
  " )\n",
622
+ " register_message = gr.Markdown()\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  " \n",
624
+ " # Main interface (initially hidden)\n",
625
+ " with gr.Group(visible=False) as main_group:\n",
626
+ " with gr.Row():\n",
627
+ " # Context selection\n",
628
+ " specialty = gr.Dropdown(\n",
629
+ " choices=self.specialties,\n",
630
+ " label=\"Specialty\",\n",
631
+ " value=self.tutor.learning_context.rotation.specialty or None\n",
632
+ " )\n",
633
+ " setting = gr.Dropdown(\n",
634
+ " choices=self.settings,\n",
635
+ " label=\"Setting\",\n",
636
+ " value=self.tutor.learning_context.rotation.setting or None\n",
637
+ " )\n",
638
+ " \n",
639
+ " # Main content\n",
640
+ " with gr.Row():\n",
641
+ " # Left: Discussion interface\n",
642
+ " with gr.Column(scale=2):\n",
643
+ " # Active goal display\n",
644
+ " goal_display = gr.Markdown(value=\"No active learning goal\")\n",
645
+ " \n",
646
+ " # Discussion status\n",
647
+ " discussion_status = gr.Markdown(\n",
648
+ " value=\"Start a new case discussion\",\n",
649
+ " elem_classes=[\"discussion-status\"]\n",
650
+ " )\n",
651
+ " \n",
652
+ " # Chat interface\n",
653
+ " chatbot = gr.Chatbot(\n",
654
+ " height=400,\n",
655
+ " show_label=False,\n",
656
+ " type=\"messages\", # Use newer message format\n",
657
+ " elem_classes=[\"chat-window\"]\n",
658
+ " )\n",
659
+ " \n",
660
+ " with gr.Row():\n",
661
+ " msg = gr.Textbox(\n",
662
+ " label=\"Present your case or ask questions\",\n",
663
+ " placeholder=(\n",
664
+ " \"Present your case as you would to your supervisor:\\n\"\n",
665
+ " \"- Start with the chief complaint\\n\"\n",
666
+ " \"- Include relevant history and findings\\n\"\n",
667
+ " \"- Share your assessment and plan\"\n",
668
+ " ),\n",
669
+ " lines=4\n",
670
+ " )\n",
671
+ " audio_input = gr.Audio(\n",
672
+ " sources=[\"microphone\"],\n",
673
+ " type=\"numpy\",\n",
674
+ " label=\"Or speak your case\",\n",
675
+ " streaming=True\n",
676
+ " )\n",
677
+ " \n",
678
+ " with gr.Row():\n",
679
+ " clear = gr.Button(\"Clear Discussion\")\n",
680
+ " end = gr.Button(\n",
681
+ " \"End Discussion & Review\",\n",
682
+ " variant=\"primary\"\n",
683
+ " )\n",
684
+ " \n",
685
+ " # Right: Progress & Goals\n",
686
+ " with gr.Column(scale=1):\n",
687
+ " with gr.Tab(\"Learning Goals\"):\n",
688
+ " with gr.Row():\n",
689
+ " generate = gr.Button(\"Generate New Goals\")\n",
690
+ " new_goal = gr.Textbox(\n",
691
+ " label=\"Or enter your own goal\",\n",
692
+ " placeholder=\"What do you want to get better at?\"\n",
693
+ " )\n",
694
+ " add_goal = gr.Button(\"Add Goal\")\n",
695
+ " \n",
696
+ " goals_list = gr.DataFrame(\n",
697
+ " headers=[\"Goal\", \"Category\", \"Status\"],\n",
698
+ " value=[],\n",
699
+ " label=\"Available Goals\",\n",
700
+ " interactive=True,\n",
701
+ " wrap=True\n",
702
+ " )\n",
703
+ " \n",
704
+ " with gr.Tab(\"Progress\"):\n",
705
+ " progress_display = gr.DataFrame(\n",
706
+ " headers=[\"Category\", \"Completed\", \"Total\"],\n",
707
+ " value=[],\n",
708
+ " label=\"Progress by Category\"\n",
709
+ " )\n",
710
+ " \n",
711
+ " recent_display = gr.DataFrame(\n",
712
+ " headers=[\"Goal\", \"Category\", \"Completed\"],\n",
713
+ " value=[],\n",
714
+ " label=\"Recently Completed\"\n",
715
+ " )\n",
716
+ " \n",
717
+ " # Wire up auth events\n",
718
+ " login_button.click(\n",
719
+ " self.handle_login,\n",
720
+ " inputs=[login_email, login_password, state],\n",
721
+ " outputs=[login_message, auth_group, main_group, state]\n",
722
+ " ).success(\n",
723
+ " lambda: None, # Clear login form\n",
724
+ " None,\n",
725
+ " [login_email, login_password]\n",
726
+ " )\n",
727
+ " \n",
728
+ " register_button.click(\n",
729
+ " self.handle_register,\n",
730
+ " inputs=[register_email, register_password],\n",
731
+ " outputs=register_message\n",
732
+ " )\n",
733
+ " \n",
734
+ " # Define helper functions\n",
735
+ " def clear_discussion() -> Tuple[List, str, Dict]:\n",
736
+ " \"\"\"Clear chat history.\"\"\"\n",
737
+ " self.tutor.current_discussion = [] # Clear tutor's discussion history\n",
738
+ " return [], \"\", {\n",
739
+ " \"discussion_active\": False,\n",
740
+ " \"suggested_goals\": [],\n",
741
+ " \"discussion_start\": None,\n",
742
+ " \"last_message\": None\n",
743
+ " }\n",
744
  " \n",
745
+ " def process_audio(audio: np.ndarray) -> str:\n",
746
+ " \"\"\"Convert audio to text.\"\"\"\n",
747
+ " if audio is None:\n",
748
+ " return \"\"\n",
749
+ " # Add your audio processing logic here\n",
750
+ " return \"Audio transcription would appear here\"\n",
751
+ " \n",
752
+ " # Wire up main interface events with tracking\n",
753
+ " specialty.change(\n",
754
+ " fn=self.update_context,\n",
755
+ " inputs=[specialty, setting, state],\n",
756
+ " outputs=[goals_list, progress_display, recent_display, goal_display],\n",
757
+ " api_name=False # Important for async functions\n",
758
+ " )\n",
759
+ " \n",
760
+ " setting.change(\n",
761
+ " fn=self.update_context,\n",
762
+ " inputs=[specialty, setting, state],\n",
763
+ " outputs=[goals_list, progress_display, recent_display, goal_display],\n",
764
+ " api_name=False # Important for async functions\n",
765
+ " )\n",
766
+ " \n",
767
+ " # Chat events with streaming\n",
768
+ " msg.submit(\n",
769
+ " self.process_chat,\n",
770
+ " inputs=[msg, chatbot, state],\n",
771
+ " outputs=[chatbot, msg, state],\n",
772
+ " queue=True # Important for streaming\n",
773
+ " ).then(\n",
774
+ " self._update_discussion_status,\n",
775
+ " inputs=[state],\n",
776
+ " outputs=[discussion_status]\n",
777
+ " )\n",
778
+ " \n",
779
+ " # Voice input handling\n",
780
+ " audio_input.stream(\n",
781
+ " process_audio,\n",
782
+ " inputs=[audio_input],\n",
783
+ " outputs=[msg]\n",
784
+ " ).then(\n",
785
+ " self.process_chat,\n",
786
+ " inputs=[msg, chatbot, state],\n",
787
+ " outputs=[chatbot, msg, state]\n",
788
+ " )\n",
789
+ " \n",
790
+ " # Button handlers\n",
791
+ " clear.click(\n",
792
+ " clear_discussion,\n",
793
+ " outputs=[chatbot, msg, state]\n",
794
+ " ).then(\n",
795
+ " lambda: \"Start a new case discussion\",\n",
796
+ " outputs=[discussion_status]\n",
797
+ " )\n",
798
+ " \n",
799
+ " end.click(\n",
800
+ " self.end_discussion,\n",
801
+ " inputs=[state],\n",
802
+ " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
803
+ " )\n",
804
+ " \n",
805
+ " generate.click(\n",
806
+ " self.generate_goals,\n",
807
+ " inputs=[state],\n",
808
+ " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
809
+ " )\n",
810
+ " \n",
811
+ " # Goal management\n",
812
+ " add_goal.click(\n",
813
+ " self.add_user_goal,\n",
814
+ " inputs=[new_goal, state],\n",
815
+ " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
816
+ " )\n",
817
+ " \n",
818
+ " goals_list.select(\n",
819
+ " self.select_goal,\n",
820
+ " inputs=[state],\n",
821
+ " outputs=[goals_list, progress_display, recent_display, goal_display]\n",
822
+ " )\n",
823
+ " \n",
824
  " return interface"
825
  ]
826
  },
nbs/04_auth.ipynb ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "7a034d30-7f66-4665-993c-8a0f1ea15dbe",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "#| default_exp auth"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "38a7ad92-86f4-4614-a795-2bed5194f8e3",
16
+ "metadata": {},
17
+ "source": [
18
+ "# User authentication and session management"
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "code",
23
+ "execution_count": null,
24
+ "id": "d5379c12-23d7-4cc5-9353-b773c643e111",
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "#| hide\n",
29
+ "from nbdev.showdoc import *\n"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "code",
34
+ "execution_count": null,
35
+ "id": "6e5fadd3-8fb0-4ff2-834b-c861e32f6eed",
36
+ "metadata": {},
37
+ "outputs": [],
38
+ "source": [
39
+ "#| export\n",
40
+ "from typing import Optional, Dict, Tuple\n",
41
+ "import requests\n",
42
+ "from datetime import datetime\n",
43
+ "import os\n",
44
+ "from pathlib import Path\n",
45
+ "import logging\n",
46
+ "from wardbuddy.utils import setup_logger\n",
47
+ "\n",
48
+ "logger = setup_logger(__name__)"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": null,
54
+ "id": "841c79b1-1301-4165-a44d-5193cd18e454",
55
+ "metadata": {},
56
+ "outputs": [],
57
+ "source": [
58
+ "#| export\n",
59
+ "class AuthManager:\n",
60
+ " \"\"\"Manages user authentication and session tracking.\"\"\"\n",
61
+ " \n",
62
+ " def __init__(self, api_url: Optional[str] = None):\n",
63
+ " \"\"\"Initialize auth manager.\"\"\"\n",
64
+ " self.api_url = api_url or os.getenv(\"API_URL\")\n",
65
+ " if not self.api_url:\n",
66
+ " raise ValueError(\"API_URL must be provided or set in environment\")\n",
67
+ " \n",
68
+ " self.token: Optional[str] = None\n",
69
+ " self.session_id: Optional[int] = None\n",
70
+ " \n",
71
+ " logger.info(f\"Auth manager initialized with API: {self.api_url}\")\n",
72
+ " \n",
73
+ " def login(self, email: str, password: str) -> Tuple[bool, str]:\n",
74
+ " \"\"\"Log in user and get access token.\"\"\"\n",
75
+ " try:\n",
76
+ " response = requests.post(\n",
77
+ " f\"{self.api_url}/token\",\n",
78
+ " data={\"username\": email, \"password\": password}\n",
79
+ " )\n",
80
+ " \n",
81
+ " if response.status_code == 200:\n",
82
+ " self.token = response.json()[\"access_token\"]\n",
83
+ " return True, \"\"\n",
84
+ " return False, response.json().get(\"detail\", \"Login failed\")\n",
85
+ " \n",
86
+ " except Exception as e:\n",
87
+ " logger.error(f\"Login error: {str(e)}\")\n",
88
+ " return False, f\"Error during login: {str(e)}\"\n",
89
+ " \n",
90
+ " def register(self, email: str, password: str) -> Tuple[bool, str]:\n",
91
+ " \"\"\"Register new user.\"\"\"\n",
92
+ " try:\n",
93
+ " response = requests.post(\n",
94
+ " f\"{self.api_url}/register\",\n",
95
+ " json={\"email\": email, \"password\": password}\n",
96
+ " )\n",
97
+ " \n",
98
+ " if response.status_code == 200:\n",
99
+ " return True, \"Registration successful\"\n",
100
+ " return False, response.json().get(\"detail\", \"Registration failed\")\n",
101
+ " \n",
102
+ " except Exception as e:\n",
103
+ " logger.error(f\"Registration error: {str(e)}\")\n",
104
+ " return False, f\"Error during registration: {str(e)}\"\n",
105
+ " \n",
106
+ " def start_session(self, specialty: str, setting: str) -> Optional[int]:\n",
107
+ " \"\"\"Start new learning session.\"\"\"\n",
108
+ " if not self.token:\n",
109
+ " return None\n",
110
+ " \n",
111
+ " try:\n",
112
+ " response = requests.post(\n",
113
+ " f\"{self.api_url}/sessions\",\n",
114
+ " json={\"specialty\": specialty, \"setting\": setting},\n",
115
+ " headers={\"Authorization\": f\"Bearer {self.token}\"}\n",
116
+ " )\n",
117
+ " \n",
118
+ " if response.status_code == 200:\n",
119
+ " self.session_id = response.json()[\"session_id\"]\n",
120
+ " return self.session_id\n",
121
+ " \n",
122
+ " except Exception as e:\n",
123
+ " logger.error(f\"Session start error: {str(e)}\")\n",
124
+ " return None\n",
125
+ " \n",
126
+ " def log_interaction(self, interaction_type: str, content: Optional[str] = None) -> bool:\n",
127
+ " \"\"\"Log user interaction.\"\"\"\n",
128
+ " if not self.token or not self.session_id:\n",
129
+ " return False\n",
130
+ " \n",
131
+ " try:\n",
132
+ " response = requests.post(\n",
133
+ " f\"{self.api_url}/interactions\",\n",
134
+ " params={\"session_id\": self.session_id},\n",
135
+ " json={\n",
136
+ " \"interaction_type\": interaction_type,\n",
137
+ " \"content\": content\n",
138
+ " },\n",
139
+ " headers={\"Authorization\": f\"Bearer {self.token}\"}\n",
140
+ " )\n",
141
+ " \n",
142
+ " return response.status_code == 200\n",
143
+ " \n",
144
+ " except Exception as e:\n",
145
+ " logger.error(f\"Interaction logging error: {str(e)}\")\n",
146
+ " return False"
147
+ ]
148
+ },
149
+ {
150
+ "cell_type": "markdown",
151
+ "id": "ad32e0c8-4575-4af4-b65f-259752f1c461",
152
+ "metadata": {},
153
+ "source": [
154
+ "# Tests"
155
+ ]
156
+ },
157
+ {
158
+ "cell_type": "code",
159
+ "execution_count": null,
160
+ "id": "1f4331c1-e547-4c26-95e2-602224454fb5",
161
+ "metadata": {},
162
+ "outputs": [
163
+ {
164
+ "name": "stdout",
165
+ "output_type": "stream",
166
+ "text": [
167
+ "Skipping tests: No API_URL set\n"
168
+ ]
169
+ }
170
+ ],
171
+ "source": [
172
+ "#| hide\n",
173
+ "def test_auth_manager():\n",
174
+ " \"\"\"Test auth manager functionality.\"\"\"\n",
175
+ " # Skip if no API URL\n",
176
+ " if not os.getenv(\"API_URL\"):\n",
177
+ " print(\"Skipping tests: No API_URL set\")\n",
178
+ " return\n",
179
+ " \n",
180
+ " auth = AuthManager()\n",
181
+ " \n",
182
+ " # Test registration\n",
183
+ " success, msg = auth.register(\"test@example.com\", \"testpass123\")\n",
184
+ " assert success or \"already exists\" in msg.lower()\n",
185
+ " \n",
186
+ " # Test login\n",
187
+ " success, msg = auth.login(\"test@example.com\", \"testpass123\")\n",
188
+ " assert success\n",
189
+ " assert auth.token is not None\n",
190
+ " \n",
191
+ " # Test session\n",
192
+ " session_id = auth.start_session(\"Internal Medicine\", \"Wards\")\n",
193
+ " assert session_id is not None\n",
194
+ " \n",
195
+ " # Test interaction logging\n",
196
+ " success = auth.log_interaction(\"test_interaction\", \"test content\")\n",
197
+ " assert success\n",
198
+ " \n",
199
+ " print(\"Auth manager tests passed!\")\n",
200
+ "\n",
201
+ "if __name__ == \"__main__\":\n",
202
+ " test_auth_manager()"
203
+ ]
204
+ },
205
+ {
206
+ "cell_type": "code",
207
+ "execution_count": null,
208
+ "id": "2e037f7c-afa8-4109-b34b-2975cdd4f4f1",
209
+ "metadata": {},
210
+ "outputs": [],
211
+ "source": []
212
+ }
213
+ ],
214
+ "metadata": {
215
+ "kernelspec": {
216
+ "display_name": "python3",
217
+ "language": "python",
218
+ "name": "python3"
219
+ }
220
+ },
221
+ "nbformat": 4,
222
+ "nbformat_minor": 5
223
+ }
wardbuddy/_modidx.py CHANGED
@@ -5,7 +5,14 @@ d = { 'settings': { 'branch': 'main',
5
  'doc_host': 'https://Dyadd.github.io',
6
  'git_url': 'https://github.com/Dyadd/wardbuddy',
7
  'lib_path': 'wardbuddy'},
8
- 'syms': { 'wardbuddy.clinical_tutor': { 'wardbuddy.clinical_tutor.ClinicalTutor': ( 'clinical_tutor.html#clinicaltutor',
 
 
 
 
 
 
 
9
  'wardbuddy/clinical_tutor.py'),
10
  'wardbuddy.clinical_tutor.ClinicalTutor.__init__': ( 'clinical_tutor.html#clinicaltutor.__init__',
11
  'wardbuddy/clinical_tutor.py'),
@@ -62,6 +69,8 @@ d = { 'settings': { 'branch': 'main',
62
  'wardbuddy/learning_interface.py'),
63
  'wardbuddy.learning_interface.LearningInterface.__init__': ( 'learning_interface.html#learninginterface.__init__',
64
  'wardbuddy/learning_interface.py'),
 
 
65
  'wardbuddy.learning_interface.LearningInterface._update_discussion_status': ( 'learning_interface.html#learninginterface._update_discussion_status',
66
  'wardbuddy/learning_interface.py'),
67
  'wardbuddy.learning_interface.LearningInterface._update_displays': ( 'learning_interface.html#learninginterface._update_displays',
@@ -74,6 +83,10 @@ d = { 'settings': { 'branch': 'main',
74
  'wardbuddy/learning_interface.py'),
75
  'wardbuddy.learning_interface.LearningInterface.generate_goals': ( 'learning_interface.html#learninginterface.generate_goals',
76
  'wardbuddy/learning_interface.py'),
 
 
 
 
77
  'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
78
  'wardbuddy/learning_interface.py'),
79
  'wardbuddy.learning_interface.LearningInterface.select_goal': ( 'learning_interface.html#learninginterface.select_goal',
 
5
  'doc_host': 'https://Dyadd.github.io',
6
  'git_url': 'https://github.com/Dyadd/wardbuddy',
7
  'lib_path': 'wardbuddy'},
8
+ 'syms': { 'wardbuddy.auth': { 'wardbuddy.auth.AuthManager': ('auth.html#authmanager', 'wardbuddy/auth.py'),
9
+ 'wardbuddy.auth.AuthManager.__init__': ('auth.html#authmanager.__init__', 'wardbuddy/auth.py'),
10
+ 'wardbuddy.auth.AuthManager.log_interaction': ( 'auth.html#authmanager.log_interaction',
11
+ 'wardbuddy/auth.py'),
12
+ 'wardbuddy.auth.AuthManager.login': ('auth.html#authmanager.login', 'wardbuddy/auth.py'),
13
+ 'wardbuddy.auth.AuthManager.register': ('auth.html#authmanager.register', 'wardbuddy/auth.py'),
14
+ 'wardbuddy.auth.AuthManager.start_session': ('auth.html#authmanager.start_session', 'wardbuddy/auth.py')},
15
+ 'wardbuddy.clinical_tutor': { 'wardbuddy.clinical_tutor.ClinicalTutor': ( 'clinical_tutor.html#clinicaltutor',
16
  'wardbuddy/clinical_tutor.py'),
17
  'wardbuddy.clinical_tutor.ClinicalTutor.__init__': ( 'clinical_tutor.html#clinicaltutor.__init__',
18
  'wardbuddy/clinical_tutor.py'),
 
69
  'wardbuddy/learning_interface.py'),
70
  'wardbuddy.learning_interface.LearningInterface.__init__': ( 'learning_interface.html#learninginterface.__init__',
71
  'wardbuddy/learning_interface.py'),
72
+ 'wardbuddy.learning_interface.LearningInterface._create_auth_components': ( 'learning_interface.html#learninginterface._create_auth_components',
73
+ 'wardbuddy/learning_interface.py'),
74
  'wardbuddy.learning_interface.LearningInterface._update_discussion_status': ( 'learning_interface.html#learninginterface._update_discussion_status',
75
  'wardbuddy/learning_interface.py'),
76
  'wardbuddy.learning_interface.LearningInterface._update_displays': ( 'learning_interface.html#learninginterface._update_displays',
 
83
  'wardbuddy/learning_interface.py'),
84
  'wardbuddy.learning_interface.LearningInterface.generate_goals': ( 'learning_interface.html#learninginterface.generate_goals',
85
  'wardbuddy/learning_interface.py'),
86
+ 'wardbuddy.learning_interface.LearningInterface.handle_login': ( 'learning_interface.html#learninginterface.handle_login',
87
+ 'wardbuddy/learning_interface.py'),
88
+ 'wardbuddy.learning_interface.LearningInterface.handle_register': ( 'learning_interface.html#learninginterface.handle_register',
89
+ 'wardbuddy/learning_interface.py'),
90
  'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
91
  'wardbuddy/learning_interface.py'),
92
  'wardbuddy.learning_interface.LearningInterface.select_goal': ( 'learning_interface.html#learninginterface.select_goal',
wardbuddy/auth.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_auth.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['logger', 'AuthManager']
5
+
6
+ # %% ../nbs/04_auth.ipynb 3
7
+ from typing import Optional, Dict, Tuple
8
+ import requests
9
+ from datetime import datetime
10
+ import os
11
+ from pathlib import Path
12
+ import logging
13
+ from .utils import setup_logger
14
+
15
+ logger = setup_logger(__name__)
16
+
17
+ # %% ../nbs/04_auth.ipynb 4
18
+ class AuthManager:
19
+ """Manages user authentication and session tracking."""
20
+
21
+ def __init__(self, api_url: Optional[str] = None):
22
+ """Initialize auth manager."""
23
+ self.api_url = api_url or os.getenv("API_URL")
24
+ if not self.api_url:
25
+ raise ValueError("API_URL must be provided or set in environment")
26
+
27
+ self.token: Optional[str] = None
28
+ self.session_id: Optional[int] = None
29
+
30
+ logger.info(f"Auth manager initialized with API: {self.api_url}")
31
+
32
+ def login(self, email: str, password: str) -> Tuple[bool, str]:
33
+ """Log in user and get access token."""
34
+ try:
35
+ response = requests.post(
36
+ f"{self.api_url}/token",
37
+ data={"username": email, "password": password}
38
+ )
39
+
40
+ if response.status_code == 200:
41
+ self.token = response.json()["access_token"]
42
+ return True, ""
43
+ return False, response.json().get("detail", "Login failed")
44
+
45
+ except Exception as e:
46
+ logger.error(f"Login error: {str(e)}")
47
+ return False, f"Error during login: {str(e)}"
48
+
49
+ def register(self, email: str, password: str) -> Tuple[bool, str]:
50
+ """Register new user."""
51
+ try:
52
+ response = requests.post(
53
+ f"{self.api_url}/register",
54
+ json={"email": email, "password": password}
55
+ )
56
+
57
+ if response.status_code == 200:
58
+ return True, "Registration successful"
59
+ return False, response.json().get("detail", "Registration failed")
60
+
61
+ except Exception as e:
62
+ logger.error(f"Registration error: {str(e)}")
63
+ return False, f"Error during registration: {str(e)}"
64
+
65
+ def start_session(self, specialty: str, setting: str) -> Optional[int]:
66
+ """Start new learning session."""
67
+ if not self.token:
68
+ return None
69
+
70
+ try:
71
+ response = requests.post(
72
+ f"{self.api_url}/sessions",
73
+ json={"specialty": specialty, "setting": setting},
74
+ headers={"Authorization": f"Bearer {self.token}"}
75
+ )
76
+
77
+ if response.status_code == 200:
78
+ self.session_id = response.json()["session_id"]
79
+ return self.session_id
80
+
81
+ except Exception as e:
82
+ logger.error(f"Session start error: {str(e)}")
83
+ return None
84
+
85
+ def log_interaction(self, interaction_type: str, content: Optional[str] = None) -> bool:
86
+ """Log user interaction."""
87
+ if not self.token or not self.session_id:
88
+ return False
89
+
90
+ try:
91
+ response = requests.post(
92
+ f"{self.api_url}/interactions",
93
+ params={"session_id": self.session_id},
94
+ json={
95
+ "interaction_type": interaction_type,
96
+ "content": content
97
+ },
98
+ headers={"Authorization": f"Bearer {self.token}"}
99
+ )
100
+
101
+ return response.status_code == 200
102
+
103
+ except Exception as e:
104
+ logger.error(f"Interaction logging error: {str(e)}")
105
+ return False
wardbuddy/learning_interface.py CHANGED
@@ -15,13 +15,14 @@ from datetime import datetime
15
  import pandas as pd
16
  from .clinical_tutor import ClinicalTutor
17
  from .learning_context import setup_logger, LearningCategory, SmartGoal
 
18
  import json
19
 
20
  logger = setup_logger(__name__)
21
 
22
  # %% ../nbs/02_learning_interface.ipynb 6
23
  def create_css() -> str:
24
- """Create custom CSS for interface styling."""
25
  return """
26
  /* Base styles */
27
  .gradio-container {
@@ -30,14 +31,44 @@ def create_css() -> str:
30
  padding: 1rem !important;
31
  }
32
 
33
- /* Mobile-first approach */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  .chat-window {
35
  background-color: #1e293b !important;
36
  border: 1px solid #334155 !important;
37
  border-radius: 0.5rem !important;
38
- height: calc(100vh - 300px) !important; /* Responsive height */
39
  min-height: 300px !important;
40
- max-height: 500px !important;
41
  }
42
 
43
  .chat-message {
@@ -74,12 +105,32 @@ def create_css() -> str:
74
  background-color: #1d4ed8 !important;
75
  }
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  /* Mobile optimizations */
78
  @media (max-width: 768px) {
79
  .gradio-container {
80
  padding: 0.5rem !important;
81
  }
82
 
 
 
 
 
 
83
  .chat-window {
84
  height: calc(100vh - 250px) !important;
85
  }
@@ -97,6 +148,11 @@ def create_css() -> str:
97
  width: 100% !important;
98
  margin: 0.25rem 0 !important;
99
  }
 
 
 
 
 
100
  }
101
  """
102
 
@@ -115,11 +171,13 @@ class LearningInterface:
115
  def __init__(
116
  self,
117
  context_path: Optional[Path] = None,
118
- model: str = "anthropic/claude-3.5-sonnet"
 
119
  ):
120
  """Initialize interface."""
121
  self.tutor = ClinicalTutor(context_path, model)
122
-
 
123
  # Available options
124
  self.specialties = [
125
  "Internal Medicine",
@@ -132,7 +190,71 @@ class LearningInterface:
132
  self.settings = ["Clinic", "Wards", "ED"]
133
 
134
  logger.info("Learning interface initialized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
 
 
 
 
 
 
136
  async def process_chat(
137
  self,
138
  message: str,
@@ -140,7 +262,7 @@ class LearningInterface:
140
  state: Dict[str, Any]
141
  ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:
142
  """
143
- Process chat messages with streaming and proper history management.
144
 
145
  Args:
146
  message: User input message
@@ -160,6 +282,8 @@ class LearningInterface:
160
  state["discussion_active"] = True
161
  state["discussion_start"] = datetime.now().isoformat()
162
  self.tutor.current_discussion = [] # Reset tutor's discussion history
 
 
163
 
164
  # Initialize history if needed
165
  if history is None:
@@ -171,6 +295,9 @@ class LearningInterface:
171
  "content": message
172
  })
173
 
 
 
 
174
  # Add initial assistant message
175
  history.append({
176
  "role": "assistant",
@@ -199,6 +326,9 @@ class LearningInterface:
199
  history[-1]["content"] = current_response
200
  yield history, "", state
201
 
 
 
 
202
  # Update state and discussion history
203
  state["last_message"] = datetime.now().isoformat()
204
 
@@ -213,14 +343,17 @@ class LearningInterface:
213
 
214
  except Exception as e:
215
  logger.error(f"Error in chat: {str(e)}")
 
 
 
216
  if history is None:
217
  history = []
218
  history.extend([
219
  {"role": "user", "content": message},
220
  {"role": "assistant", "content": "I apologize, but I encountered an error. Please try again."}
221
  ])
222
- yield history, "", state
223
-
224
  def _update_discussion_status(self, state: Dict[str, Any]) -> str:
225
  """Update discussion status display."""
226
  try:
@@ -277,7 +410,14 @@ class LearningInterface:
277
  return self._update_displays(state)
278
 
279
  def end_discussion(self, state: Dict[str, Any]) -> List:
280
- """End current discussion and analyze results."""
 
 
 
 
 
 
 
281
  self.tutor.end_discussion()
282
  state["discussion_active"] = False
283
  state["discussion_start"] = None
@@ -330,211 +470,258 @@ class LearningInterface:
330
  return [goals_data, progress_data, recent_data, goal_text]
331
 
332
  def create_interface(self) -> gr.Blocks:
333
- """Create streaming-enabled interface."""
334
  with gr.Blocks(title="Clinical Learning Assistant", css=create_css()) as interface:
335
  # State management
336
  state = gr.State({
337
  "discussion_active": False,
338
  "suggested_goals": [],
339
  "discussion_start": None,
340
- "last_message": None
 
341
  })
342
-
343
- # Header
344
- gr.Markdown("# Clinical Learning Assistant")
345
-
346
- with gr.Row():
347
- # Context selection
348
- specialty = gr.Dropdown(
349
- choices=self.specialties,
350
- label="Specialty",
351
- value=self.tutor.learning_context.rotation.specialty or None
352
- )
353
- setting = gr.Dropdown(
354
- choices=self.settings,
355
- label="Setting",
356
- value=self.tutor.learning_context.rotation.setting or None
357
- )
358
-
359
- # Main content
360
- with gr.Row():
361
- # Left: Discussion interface
362
- with gr.Column(scale=2):
363
- # Active goal display
364
- goal_display = gr.Markdown(value="No active learning goal")
365
-
366
- # Discussion status
367
- discussion_status = gr.Markdown(
368
- value="Start a new case discussion",
369
- elem_classes=["discussion-status"]
370
- )
371
-
372
- # Chat interface
373
- chatbot = gr.Chatbot(
374
- height=400,
375
- show_label=False,
376
- type="messages", # Use newer message format
377
- elem_classes=["chat-window"]
378
- )
379
-
380
- with gr.Row():
381
- msg = gr.Textbox(
382
- label="Present your case or ask questions",
383
- placeholder=(
384
- "Present your case as you would to your supervisor:\n"
385
- "- Start with the chief complaint\n"
386
- "- Include relevant history and findings\n"
387
- "- Share your assessment and plan"
388
- ),
389
- lines=4
390
  )
391
-
392
- audio_input = gr.Audio(
393
- sources=["microphone"],
394
- type="numpy",
395
- label="Or speak your case",
396
- streaming=True
397
  )
398
-
399
- with gr.Row():
400
- clear = gr.Button("Clear Discussion")
401
- end = gr.Button(
402
- "End Discussion & Review",
403
- variant="primary"
404
  )
405
-
406
- # Right: Progress & Goals
407
- with gr.Column(scale=1):
408
- with gr.Tab("Learning Goals"):
409
- # Goal selection/generation
410
- with gr.Row():
411
- generate = gr.Button("Generate New Goals")
412
- new_goal = gr.Textbox(
413
- label="Or enter your own goal",
414
- placeholder="What do you want to get better at?"
415
- )
416
- add_goal = gr.Button("Add Goal")
417
-
418
- # Goals list
419
- goals_list = gr.DataFrame(
420
- headers=["Goal", "Category", "Status"],
421
- value=[],
422
- label="Available Goals",
423
- interactive=True,
424
- wrap=True
425
  )
426
-
427
- with gr.Tab("Progress"):
428
- # Category progress
429
- progress_display = gr.DataFrame(
430
- headers=["Category", "Completed", "Total"],
431
- value=[],
432
- label="Progress by Category"
433
  )
434
-
435
- # Recent completions
436
- recent_display = gr.DataFrame(
437
- headers=["Goal", "Category", "Completed"],
438
- value=[],
439
- label="Recently Completed"
440
  )
441
-
442
- # Event handlers
443
- async def update_context(spec: str, set: str, state: Dict) -> List:
444
- """Update rotation context."""
445
- if spec and set:
446
- self.tutor.learning_context.update_rotation(spec, set)
447
- if not state.get("suggested_goals"):
448
- goals = await self.tutor.generate_smart_goals(spec, set)
449
- state["suggested_goals"] = goals
450
- return self._update_displays(state)
451
- return [[], [], [], "No active learning goal"]
452
-
453
- def clear_discussion() -> Tuple[List, str, Dict]:
454
- """Clear chat history."""
455
- self.tutor.current_discussion = [] # Clear tutor's discussion history
456
- return [], "", {
457
- "discussion_active": False,
458
- "suggested_goals": [],
459
- "discussion_start": None,
460
- "last_message": None
461
- }
462
 
463
- def process_audio(audio: np.ndarray) -> str:
464
- """Convert audio to text."""
465
- if audio is None:
466
- return ""
467
- # Add your audio processing logic here
468
- return "Audio transcription would appear here"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
- # Wire up events
471
- specialty.change(
472
- update_context,
473
- inputs=[specialty, setting, state],
474
- outputs=[goals_list, progress_display, recent_display, goal_display]
475
- )
476
-
477
- setting.change(
478
- update_context,
479
- inputs=[specialty, setting, state],
480
- outputs=[goals_list, progress_display, recent_display, goal_display]
481
- )
482
-
483
- # Chat events with streaming
484
- msg.submit(
485
- self.process_chat,
486
- inputs=[msg, chatbot, state],
487
- outputs=[chatbot, msg, state],
488
- queue=True # Important for streaming
489
- ).then(
490
- self._update_discussion_status,
491
- inputs=[state],
492
- outputs=[discussion_status]
493
- )
494
-
495
- # Voice input handling
496
- audio_input.stream(
497
- process_audio,
498
- inputs=[audio_input],
499
- outputs=[msg]
500
- ).then(
501
- self.process_chat,
502
- inputs=[msg, chatbot, state],
503
- outputs=[chatbot, msg, state]
504
- )
505
-
506
- # Button handlers
507
- clear.click(
508
- clear_discussion,
509
- outputs=[chatbot, msg, state]
510
- ).then(
511
- lambda: "Start a new case discussion",
512
- outputs=[discussion_status]
513
- )
514
-
515
- end.click(
516
- self.end_discussion,
517
- inputs=[state],
518
- outputs=[goals_list, progress_display, recent_display, goal_display]
519
- )
520
-
521
- generate.click(
522
- self.generate_goals,
523
- inputs=[state],
524
- outputs=[goals_list, progress_display, recent_display, goal_display]
525
- )
526
-
527
- # Goal management
528
- add_goal.click(
529
- self.add_user_goal,
530
- inputs=[new_goal, state],
531
- outputs=[goals_list, progress_display, recent_display, goal_display]
532
- )
533
-
534
- goals_list.select(
535
- self.select_goal,
536
- inputs=[state],
537
- outputs=[goals_list, progress_display, recent_display, goal_display]
538
- )
539
-
 
 
 
 
 
 
 
 
 
540
  return interface
 
15
  import pandas as pd
16
  from .clinical_tutor import ClinicalTutor
17
  from .learning_context import setup_logger, LearningCategory, SmartGoal
18
+ from .auth import AuthManager
19
  import json
20
 
21
  logger = setup_logger(__name__)
22
 
23
  # %% ../nbs/02_learning_interface.ipynb 6
24
  def create_css() -> str:
25
+ """Create custom CSS for interface styling with auth and mobile support."""
26
  return """
27
  /* Base styles */
28
  .gradio-container {
 
31
  padding: 1rem !important;
32
  }
33
 
34
+ /* Auth container */
35
+ .auth-container {
36
+ max-width: 400px !important;
37
+ margin: 2rem auto !important;
38
+ padding: 2rem !important;
39
+ background-color: #1e293b !important;
40
+ border-radius: 0.5rem !important;
41
+ border: 1px solid #334155 !important;
42
+ }
43
+
44
+ .auth-input input {
45
+ background-color: #1e293b !important;
46
+ border: 1px solid #334155 !important;
47
+ color: #f1f5f9 !important;
48
+ font-size: 1rem !important;
49
+ padding: 0.75rem !important;
50
+ }
51
+
52
+ .auth-button {
53
+ width: 100% !important;
54
+ margin-top: 1rem !important;
55
+ background-color: #2563eb !important;
56
+ color: white !important;
57
+ }
58
+
59
+ /* Discussion elements */
60
+ .discussion-status {
61
+ color: #94a3b8;
62
+ font-size: 0.9em;
63
+ margin-bottom: 1rem;
64
+ }
65
+
66
  .chat-window {
67
  background-color: #1e293b !important;
68
  border: 1px solid #334155 !important;
69
  border-radius: 0.5rem !important;
70
+ height: calc(100vh - 300px) !important;
71
  min-height: 300px !important;
 
72
  }
73
 
74
  .chat-message {
 
105
  background-color: #1d4ed8 !important;
106
  }
107
 
108
+ /* Progress sections */
109
+ .tab-nav {
110
+ border-bottom: 1px solid #334155 !important;
111
+ }
112
+
113
+ .tab-nav button {
114
+ background-color: transparent !important;
115
+ color: #94a3b8 !important;
116
+ }
117
+
118
+ .tab-nav button.selected {
119
+ color: #f1f5f9 !important;
120
+ border-bottom: 2px solid #2563eb !important;
121
+ }
122
+
123
  /* Mobile optimizations */
124
  @media (max-width: 768px) {
125
  .gradio-container {
126
  padding: 0.5rem !important;
127
  }
128
 
129
+ .auth-container {
130
+ margin: 1rem !important;
131
+ padding: 1rem !important;
132
+ }
133
+
134
  .chat-window {
135
  height: calc(100vh - 250px) !important;
136
  }
 
148
  width: 100% !important;
149
  margin: 0.25rem 0 !important;
150
  }
151
+
152
+ .tab-nav button {
153
+ padding: 0.5rem !important;
154
+ font-size: 0.9rem !important;
155
+ }
156
  }
157
  """
158
 
 
171
  def __init__(
172
  self,
173
  context_path: Optional[Path] = None,
174
+ model: str = "anthropic/claude-3.5-sonnet",
175
+ api_url: Optional[str] = None
176
  ):
177
  """Initialize interface."""
178
  self.tutor = ClinicalTutor(context_path, model)
179
+ self.auth = AuthManager(api_url)
180
+
181
  # Available options
182
  self.specialties = [
183
  "Internal Medicine",
 
190
  self.settings = ["Clinic", "Wards", "ED"]
191
 
192
  logger.info("Learning interface initialized")
193
+
194
+ # Authentication Components
195
+ def _create_auth_components(self) -> Tuple[gr.Group, gr.Group]:
196
+ """Create authentication interface components."""
197
+ with gr.Group() as auth_group:
198
+ gr.Markdown("# Clinical Learning Assistant")
199
+
200
+ with gr.Tab("Login"):
201
+ login_email = gr.Textbox(
202
+ label="Email",
203
+ placeholder="Enter your email"
204
+ )
205
+ login_password = gr.Textbox(
206
+ label="Password",
207
+ type="password",
208
+ placeholder="Enter your password"
209
+ )
210
+ login_button = gr.Button("Login")
211
+ login_message = gr.Markdown()
212
+
213
+ with gr.Tab("Register"):
214
+ register_email = gr.Textbox(
215
+ label="Email",
216
+ placeholder="Enter your email"
217
+ )
218
+ register_password = gr.Textbox(
219
+ label="Password",
220
+ type="password",
221
+ placeholder="Choose a password"
222
+ )
223
+ register_button = gr.Button("Register")
224
+ register_message = gr.Markdown()
225
+
226
+ # Main interface (hidden until auth)
227
+ main_group = gr.Group(visible=False)
228
+
229
+ return auth_group, main_group, (
230
+ login_email, login_password, login_button, login_message,
231
+ register_email, register_password, register_button, register_message
232
+ )
233
+
234
+ def handle_login(
235
+ self,
236
+ email: str,
237
+ password: str,
238
+ state: Dict[str, Any]
239
+ ) -> Tuple[str, gr.update, gr.update, Dict[str, Any]]:
240
+ """Handle user login."""
241
+ success, message = self.auth.login(email, password)
242
+ if success:
243
+ state["authenticated"] = True
244
+ return (
245
+ "Login successful! Redirecting...", # Success message
246
+ gr.update(visible=False), # Hide auth group
247
+ gr.update(visible=True), # Show main interface
248
+ state
249
+ )
250
+ return message, gr.update(), gr.update(), state
251
 
252
+ def handle_register(self, email: str, password: str) -> str:
253
+ """Handle user registration."""
254
+ success, message = self.auth.register(email, password)
255
+ return message
256
+
257
+ # Chat Processing
258
  async def process_chat(
259
  self,
260
  message: str,
 
262
  state: Dict[str, Any]
263
  ) -> AsyncGenerator[Tuple[List[Dict[str, str]], str, Dict[str, Any]], None]:
264
  """
265
+ Process chat messages with streaming, history management, and interaction logging.
266
 
267
  Args:
268
  message: User input message
 
282
  state["discussion_active"] = True
283
  state["discussion_start"] = datetime.now().isoformat()
284
  self.tutor.current_discussion = [] # Reset tutor's discussion history
285
+ # Log discussion start
286
+ self.auth.log_interaction("discussion_start")
287
 
288
  # Initialize history if needed
289
  if history is None:
 
295
  "content": message
296
  })
297
 
298
+ # Log user message
299
+ self.auth.log_interaction("user_message", message)
300
+
301
  # Add initial assistant message
302
  history.append({
303
  "role": "assistant",
 
326
  history[-1]["content"] = current_response
327
  yield history, "", state
328
 
329
+ # Log assistant response
330
+ self.auth.log_interaction("assistant_response", current_response)
331
+
332
  # Update state and discussion history
333
  state["last_message"] = datetime.now().isoformat()
334
 
 
343
 
344
  except Exception as e:
345
  logger.error(f"Error in chat: {str(e)}")
346
+ # Log error
347
+ self.auth.log_interaction("error", str(e))
348
+
349
  if history is None:
350
  history = []
351
  history.extend([
352
  {"role": "user", "content": message},
353
  {"role": "assistant", "content": "I apologize, but I encountered an error. Please try again."}
354
  ])
355
+ yield history, "", state
356
+
357
  def _update_discussion_status(self, state: Dict[str, Any]) -> str:
358
  """Update discussion status display."""
359
  try:
 
410
  return self._update_displays(state)
411
 
412
  def end_discussion(self, state: Dict[str, Any]) -> List:
413
+ """End current discussion with analytics."""
414
+ if state.get("discussion_active"):
415
+ self.auth.log_interaction("discussion_end", json.dumps({
416
+ "duration": (
417
+ datetime.now() -
418
+ datetime.fromisoformat(state["discussion_start"])
419
+ ).total_seconds()
420
+ }))
421
  self.tutor.end_discussion()
422
  state["discussion_active"] = False
423
  state["discussion_start"] = None
 
470
  return [goals_data, progress_data, recent_data, goal_text]
471
 
472
  def create_interface(self) -> gr.Blocks:
473
+ """Create streaming-enabled interface with authentication and all features."""
474
  with gr.Blocks(title="Clinical Learning Assistant", css=create_css()) as interface:
475
  # State management
476
  state = gr.State({
477
  "discussion_active": False,
478
  "suggested_goals": [],
479
  "discussion_start": None,
480
+ "last_message": None,
481
+ "authenticated": False
482
  })
483
+
484
+ # Authentication interface
485
+ with gr.Box(elem_classes=["auth-container"]) as auth_group:
486
+ gr.Markdown("# Clinical Learning Assistant")
487
+
488
+ with gr.Tabs() as auth_tabs:
489
+ with gr.Tab("Login"):
490
+ login_email = gr.Textbox(
491
+ label="Email",
492
+ placeholder="Enter your email",
493
+ elem_classes=["auth-input"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  )
495
+ login_password = gr.Textbox(
496
+ label="Password",
497
+ type="password",
498
+ placeholder="Enter your password",
499
+ elem_classes=["auth-input"]
 
500
  )
501
+ login_button = gr.Button(
502
+ "Login",
503
+ variant="primary",
504
+ elem_classes=["auth-button"]
 
 
505
  )
506
+ login_message = gr.Markdown()
507
+
508
+ with gr.Tab("Register"):
509
+ register_email = gr.Textbox(
510
+ label="Email",
511
+ placeholder="Enter your email",
512
+ elem_classes=["auth-input"]
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  )
514
+ register_password = gr.Textbox(
515
+ label="Password",
516
+ type="password",
517
+ placeholder="Choose a password",
518
+ elem_classes=["auth-input"]
 
 
519
  )
520
+ register_button = gr.Button(
521
+ "Register",
522
+ variant="primary",
523
+ elem_classes=["auth-button"]
 
 
524
  )
525
+ register_message = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
+ # Main interface (initially hidden)
528
+ with gr.Group(visible=False) as main_group:
529
+ with gr.Row():
530
+ # Context selection
531
+ specialty = gr.Dropdown(
532
+ choices=self.specialties,
533
+ label="Specialty",
534
+ value=self.tutor.learning_context.rotation.specialty or None
535
+ )
536
+ setting = gr.Dropdown(
537
+ choices=self.settings,
538
+ label="Setting",
539
+ value=self.tutor.learning_context.rotation.setting or None
540
+ )
541
+
542
+ # Main content
543
+ with gr.Row():
544
+ # Left: Discussion interface
545
+ with gr.Column(scale=2):
546
+ # Active goal display
547
+ goal_display = gr.Markdown(value="No active learning goal")
548
+
549
+ # Discussion status
550
+ discussion_status = gr.Markdown(
551
+ value="Start a new case discussion",
552
+ elem_classes=["discussion-status"]
553
+ )
554
+
555
+ # Chat interface
556
+ chatbot = gr.Chatbot(
557
+ height=400,
558
+ show_label=False,
559
+ type="messages", # Use newer message format
560
+ elem_classes=["chat-window"]
561
+ )
562
+
563
+ with gr.Row():
564
+ msg = gr.Textbox(
565
+ label="Present your case or ask questions",
566
+ placeholder=(
567
+ "Present your case as you would to your supervisor:\n"
568
+ "- Start with the chief complaint\n"
569
+ "- Include relevant history and findings\n"
570
+ "- Share your assessment and plan"
571
+ ),
572
+ lines=4
573
+ )
574
+ audio_input = gr.Audio(
575
+ sources=["microphone"],
576
+ type="numpy",
577
+ label="Or speak your case",
578
+ streaming=True
579
+ )
580
+
581
+ with gr.Row():
582
+ clear = gr.Button("Clear Discussion")
583
+ end = gr.Button(
584
+ "End Discussion & Review",
585
+ variant="primary"
586
+ )
587
+
588
+ # Right: Progress & Goals
589
+ with gr.Column(scale=1):
590
+ with gr.Tab("Learning Goals"):
591
+ with gr.Row():
592
+ generate = gr.Button("Generate New Goals")
593
+ new_goal = gr.Textbox(
594
+ label="Or enter your own goal",
595
+ placeholder="What do you want to get better at?"
596
+ )
597
+ add_goal = gr.Button("Add Goal")
598
+
599
+ goals_list = gr.DataFrame(
600
+ headers=["Goal", "Category", "Status"],
601
+ value=[],
602
+ label="Available Goals",
603
+ interactive=True,
604
+ wrap=True
605
+ )
606
+
607
+ with gr.Tab("Progress"):
608
+ progress_display = gr.DataFrame(
609
+ headers=["Category", "Completed", "Total"],
610
+ value=[],
611
+ label="Progress by Category"
612
+ )
613
+
614
+ recent_display = gr.DataFrame(
615
+ headers=["Goal", "Category", "Completed"],
616
+ value=[],
617
+ label="Recently Completed"
618
+ )
619
+
620
+ # Wire up auth events
621
+ login_button.click(
622
+ self.handle_login,
623
+ inputs=[login_email, login_password, state],
624
+ outputs=[login_message, auth_group, main_group, state]
625
+ ).success(
626
+ lambda: None, # Clear login form
627
+ None,
628
+ [login_email, login_password]
629
+ )
630
+
631
+ register_button.click(
632
+ self.handle_register,
633
+ inputs=[register_email, register_password],
634
+ outputs=register_message
635
+ )
636
+
637
+ # Define helper functions
638
+ def clear_discussion() -> Tuple[List, str, Dict]:
639
+ """Clear chat history."""
640
+ self.tutor.current_discussion = [] # Clear tutor's discussion history
641
+ return [], "", {
642
+ "discussion_active": False,
643
+ "suggested_goals": [],
644
+ "discussion_start": None,
645
+ "last_message": None
646
+ }
647
 
648
+ def process_audio(audio: np.ndarray) -> str:
649
+ """Convert audio to text."""
650
+ if audio is None:
651
+ return ""
652
+ # Add your audio processing logic here
653
+ return "Audio transcription would appear here"
654
+
655
+ # Wire up main interface events with tracking
656
+ specialty.change(
657
+ fn=self.update_context,
658
+ inputs=[specialty, setting, state],
659
+ outputs=[goals_list, progress_display, recent_display, goal_display],
660
+ api_name=False # Important for async functions
661
+ )
662
+
663
+ setting.change(
664
+ fn=self.update_context,
665
+ inputs=[specialty, setting, state],
666
+ outputs=[goals_list, progress_display, recent_display, goal_display],
667
+ api_name=False # Important for async functions
668
+ )
669
+
670
+ # Chat events with streaming
671
+ msg.submit(
672
+ self.process_chat,
673
+ inputs=[msg, chatbot, state],
674
+ outputs=[chatbot, msg, state],
675
+ queue=True # Important for streaming
676
+ ).then(
677
+ self._update_discussion_status,
678
+ inputs=[state],
679
+ outputs=[discussion_status]
680
+ )
681
+
682
+ # Voice input handling
683
+ audio_input.stream(
684
+ process_audio,
685
+ inputs=[audio_input],
686
+ outputs=[msg]
687
+ ).then(
688
+ self.process_chat,
689
+ inputs=[msg, chatbot, state],
690
+ outputs=[chatbot, msg, state]
691
+ )
692
+
693
+ # Button handlers
694
+ clear.click(
695
+ clear_discussion,
696
+ outputs=[chatbot, msg, state]
697
+ ).then(
698
+ lambda: "Start a new case discussion",
699
+ outputs=[discussion_status]
700
+ )
701
+
702
+ end.click(
703
+ self.end_discussion,
704
+ inputs=[state],
705
+ outputs=[goals_list, progress_display, recent_display, goal_display]
706
+ )
707
+
708
+ generate.click(
709
+ self.generate_goals,
710
+ inputs=[state],
711
+ outputs=[goals_list, progress_display, recent_display, goal_display]
712
+ )
713
+
714
+ # Goal management
715
+ add_goal.click(
716
+ self.add_user_goal,
717
+ inputs=[new_goal, state],
718
+ outputs=[goals_list, progress_display, recent_display, goal_display]
719
+ )
720
+
721
+ goals_list.select(
722
+ self.select_goal,
723
+ inputs=[state],
724
+ outputs=[goals_list, progress_display, recent_display, goal_display]
725
+ )
726
+
727
  return interface