DocUA commited on
Commit
17f3ad3
Β·
1 Parent(s): 0adfcbc

refactor: Modularize core Gradio application logic into dedicated session, chat, model, profile, prompt, stats, and verification handler modules.

Browse files
src/interface/chat_handlers.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import html
4
+ from src.interface.session_manager import SimplifiedSessionData
5
+ from src.interface.stats_handlers import get_conversation_stats
6
+
7
+ def handle_message(message: str, history, session: SimplifiedSessionData):
8
+ """Handle user message."""
9
+ if session is None:
10
+ session = SimplifiedSessionData()
11
+
12
+ session.update_activity()
13
+
14
+ # Apply per-session model overrides (if configured in Model Settings)
15
+ custom_models = getattr(session, 'custom_models', None)
16
+ if custom_models:
17
+ session.app_instance.set_model_overrides(custom_models)
18
+ else:
19
+ session.app_instance.set_model_overrides({})
20
+
21
+ # Apply per-session prompt overrides (if configured in Edit Prompts)
22
+ custom_prompts = getattr(session, 'custom_prompts', None)
23
+ if custom_prompts:
24
+ session.app_instance.set_prompt_overrides(custom_prompts)
25
+ else:
26
+ session.app_instance.set_prompt_overrides({})
27
+ new_history, status = session.app_instance.process_message(message, history)
28
+
29
+ # Get updated conversation stats
30
+ stats = get_conversation_stats(session)
31
+
32
+ # Check for provider summary (RED flag case)
33
+ provider_summary_text = ""
34
+ spiritual_care_msg = ""
35
+ show_provider_panel = False
36
+ last_summary = session.app_instance.get_last_provider_summary()
37
+
38
+ # Debug logging
39
+ print(f"DEBUG: last_summary exists: {last_summary is not None}")
40
+ if last_summary:
41
+ print(f"DEBUG: summary patient: {last_summary.patient_name}")
42
+ print(f"DEBUG: summary indicators: {last_summary.indicators}")
43
+ provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
44
+
45
+ # Generate LLM-based spiritual care message
46
+ try:
47
+ spiritual_care_msg = session.app_instance.generate_spiritual_care_message(
48
+ language="English",
49
+ session_id=session.session_id
50
+ )
51
+ if not spiritual_care_msg:
52
+ spiritual_care_msg = ""
53
+ print(f"DEBUG: spiritual care message generated: {len(spiritual_care_msg)} chars")
54
+ except Exception as e:
55
+ print(f"DEBUG: Error generating spiritual care message: {e}")
56
+ spiritual_care_msg = ""
57
+
58
+ show_provider_panel = True
59
+ print(f"DEBUG: formatted summary length: {len(provider_summary_text)}")
60
+ print(f"DEBUG: show_provider_panel: {show_provider_panel}")
61
+ else:
62
+ print("DEBUG: No provider summary found")
63
+
64
+ # Debug: print what we're returning
65
+ print(f"DEBUG RETURN: show_panel={show_provider_panel}, text_len={len(provider_summary_text)}")
66
+ if provider_summary_text:
67
+ print(f"DEBUG RETURN: first 100 chars: {provider_summary_text[:100]}")
68
+
69
+ # Format as HTML for display
70
+ if provider_summary_text:
71
+ escaped_text = html.escape(provider_summary_text)
72
+ html_content = f"<pre style='white-space: pre-wrap; font-family: monospace; font-size: 11px; background: #fffbeb; padding: 10px; border-radius: 8px; max-height: 400px; overflow-y: auto;'>{escaped_text}</pre>"
73
+ else:
74
+ html_content = "<pre style='white-space: pre-wrap; font-family: monospace; font-size: 11px; background: #fffbeb; padding: 10px; border-radius: 8px;'>No summary available</pre>"
75
+
76
+ # Use gr.update for both visibility and value
77
+ if not provider_summary_text:
78
+ provider_summary_text = ""
79
+ html_content = ""
80
+
81
+ # Generate status message for provider summary
82
+ if show_provider_panel and provider_summary_text:
83
+ status_msg = f"""**πŸ”΄ Provider Summary Generated**
84
+
85
+ **Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
86
+ **Classification:** RED FLAG
87
+ **Indicators:** {len(session.app_instance.get_last_provider_summary().indicators) if session.app_instance.get_last_provider_summary() else 0} distress indicators detected
88
+ **Summary Length:** {len(provider_summary_text)} characters
89
+
90
+ Use the **Download Summary** button below to access the complete provider summary for the spiritual care team."""
91
+ else:
92
+ status_msg = "No provider summary available"
93
+
94
+ return (
95
+ new_history,
96
+ status,
97
+ session,
98
+ "",
99
+ stats,
100
+ gr.update(visible=show_provider_panel), # provider_summary_content visibility
101
+ status_msg, # provider_summary_status content
102
+ gr.update(value=html_content, visible=True) if show_provider_panel else gr.update(visible=False), # provider_summary_display content
103
+ spiritual_care_msg # spiritual_care_message content
104
+ )
105
+
106
+ def handle_clear(session: SimplifiedSessionData):
107
+ """
108
+ Handle clear chat button.
109
+
110
+ Resets entire session including:
111
+ - Chat history
112
+ - Spiritual monitoring state
113
+ - Provider summary panel (hides and clears content)
114
+ - Conversation statistics
115
+ """
116
+ if session is None:
117
+ session = SimplifiedSessionData()
118
+
119
+ session.update_activity()
120
+ new_history, status = session.app_instance.reset_session()
121
+
122
+ # Hide and clear provider summary panel
123
+ return (
124
+ new_history, # Clear chat history
125
+ status, # Reset status
126
+ session, # Updated session
127
+ gr.update(visible=False), # Hide provider_summary_content group
128
+ "No provider summary available", # Clear provider_summary_status
129
+ "", # Clear provider_summary_display HTML
130
+ "" # Clear spiritual_care_message
131
+ )
132
+
133
+ def send_example(example_text: str, history, session: SimplifiedSessionData):
134
+ """Send example message."""
135
+ return handle_message(example_text, history, session)
136
+
137
+ def send_example_with_stats(example_text: str, history, session: SimplifiedSessionData):
138
+ """Send example message and return stats."""
139
+ # Assuming this was supposed to be similar to send_example but returning different things?
140
+ # In the original file, it calls handle_message which returns 9 items.
141
+ # We should match the return signature if this is used by a button.
142
+ return handle_message(example_text, history, session)
src/interface/model_handlers.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ import gradio as gr
3
+ from src.interface.session_manager import SimplifiedSessionData
4
+
5
+ def apply_model_settings(spiritual_model: str, soft_spiritual_triage_model: str, triage_evaluate_model: str, medical_model: str, soft_triage_model: str, spiritual_care_message_model: str, session: SimplifiedSessionData):
6
+ """Apply custom model settings."""
7
+ if session is None:
8
+ session = SimplifiedSessionData()
9
+
10
+ # Store model settings in session
11
+ if not hasattr(session, 'custom_models'):
12
+ session.custom_models = {}
13
+
14
+ session.custom_models = {
15
+ 'SpiritualDistressAnalyzer': spiritual_model,
16
+ 'SoftSpiritualTriage': soft_spiritual_triage_model,
17
+ 'TriageResponseEvaluator': triage_evaluate_model,
18
+ 'MedicalAssistant': medical_model,
19
+ 'SoftMedicalTriage': soft_triage_model,
20
+ 'SpiritualCareMessage': spiritual_care_message_model
21
+ }
22
+
23
+ status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
24
+ <h4 style="color: #059669; margin-top: 0;">βœ… Model Settings Applied</h4>
25
+
26
+ <p><strong>πŸ” Spiritual Monitor:</strong> <code>{spiritual_model}</code></p>
27
+ <p><strong>🟑 Soft Spiritual Triage:</strong> <code>{soft_spiritual_triage_model}</code></p>
28
+ <p><strong>πŸ“Š Triage Response Evaluator:</strong> <code>{triage_evaluate_model}</code></p>
29
+ <p><strong>πŸ₯ Medical Assistant:</strong> <code>{medical_model}</code></p>
30
+ <p><strong>🩺 Soft Medical Triage:</strong> <code>{soft_triage_model}</code></p>
31
+ <p><strong>πŸ’¬ Spiritual Care Message:</strong> <code>{spiritual_care_message_model}</code></p>
32
+
33
+ <p style="color: #d97706; margin-bottom: 0;">
34
+ ⚠️ <strong>Note:</strong> Model changes apply to this session only.
35
+ </p>
36
+ </div>"""
37
+
38
+ return status, session
39
+
40
+ def reset_model_settings(session: SimplifiedSessionData):
41
+ """Reset models to defaults."""
42
+ if session is None:
43
+ session = SimplifiedSessionData()
44
+
45
+ # Clear custom models
46
+ if hasattr(session, 'custom_models'):
47
+ session.custom_models = {}
48
+
49
+ status = """<div style="padding: 1em; background-color: #eff6ff; border-left: 4px solid #3b82f6; border-radius: 4px;">
50
+ <h4 style="color: #2563eb; margin-top: 0;">πŸ”„ Models Reset to Defaults</h4>
51
+
52
+ <p><strong>πŸ” Spiritual Monitor:</strong> <code>gemini-2.5-flash</code></p>
53
+ <p><strong>🟑 Soft Spiritual Triage:</strong> <code>claude-sonnet-4-5-20250929</code></p>
54
+ <p><strong>πŸ“Š Triage Response Evaluator:</strong> <code>gemini-2.5-flash</code></p>
55
+ <p><strong>πŸ₯ Medical Assistant:</strong> <code>claude-sonnet-4-5-20250929</code></p>
56
+ <p><strong>🩺 Soft Medical Triage:</strong> <code>claude-sonnet-4-5-20250929</code></p>
57
+ <p><strong>πŸ’¬ Spiritual Care Message:</strong> <code>claude-sonnet-4-5-20250929</code></p>
58
+
59
+ <p style="margin-bottom: 0;">Default models are now active.</p>
60
+ </div>"""
61
+
62
+ return status, session
src/interface/profile_handlers.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from src.interface.session_manager import SimplifiedSessionData
4
+ from src.core.core_classes import ClinicalBackground
5
+
6
+ def load_profile(profile_name: str, session: SimplifiedSessionData):
7
+ """Load predefined patient profile and apply it to the session."""
8
+ profiles = {
9
+ "πŸ‘€ Default Profile (Serhii)": {
10
+ "name": "Serhii",
11
+ "phone": "(555) 123-4567",
12
+ "age": 52,
13
+ "conditions": "Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
14
+ "goal": "Weight reduction and cardiovascular fitness improvement",
15
+ "exercise": "Swimming, Walking, Light cardio",
16
+ "limitations": "Anticoagulation therapy, Post-thrombotic recovery"
17
+ },
18
+ "🟒 GREEN - Healthy Coping": {
19
+ "name": "James",
20
+ "phone": "(555) 234-5678",
21
+ "age": 40,
22
+ "conditions": "No chronic conditions, Excellent health",
23
+ "goal": "Maintain fitness and wellness",
24
+ "exercise": "Running, Gym, Sports",
25
+ "limitations": "None"
26
+ },
27
+ "🟑 YELLOW - Mild Distress": {
28
+ "name": "Lisa",
29
+ "phone": "(555) 345-6789",
30
+ "age": 45,
31
+ "conditions": "Hypertension, Mild anxiety, Sleep issues",
32
+ "goal": "Manage stress and improve sleep quality",
33
+ "exercise": "Yoga, Walking, Meditation",
34
+ "limitations": "Stress-related fatigue, Occasional insomnia"
35
+ },
36
+ "🟑 YELLOW - Grief & Loss": {
37
+ "name": "Michael",
38
+ "phone": "(555) 456-7890",
39
+ "age": 58,
40
+ "conditions": "Recent loss of spouse, Mild depression",
41
+ "goal": "Process grief and rebuild routine",
42
+ "exercise": "Gentle walking, Support groups",
43
+ "limitations": "Low motivation, Emotional exhaustion"
44
+ },
45
+ "🟑 YELLOW - Existential Questions": {
46
+ "name": "Patricia",
47
+ "phone": "(555) 567-8901",
48
+ "age": 62,
49
+ "conditions": "Chronic pain, Questioning life purpose",
50
+ "goal": "Find meaning and manage chronic pain",
51
+ "exercise": "Tai Chi, Meditation, Gentle stretching",
52
+ "limitations": "Chronic pain, Existential concerns"
53
+ },
54
+ "🟑 YELLOW - Spiritual Disconnection": {
55
+ "name": "David",
56
+ "phone": "(555) 678-9012",
57
+ "age": 55,
58
+ "conditions": "Loss of faith, Isolation from community",
59
+ "goal": "Reconnect with spiritual community",
60
+ "exercise": "Walking, Community activities",
61
+ "limitations": "Spiritual disconnection, Social isolation"
62
+ },
63
+ "πŸ”΄ RED - Crisis (Suicidal Risk)": {
64
+ "name": "Thomas",
65
+ "phone": "(555) 789-0123",
66
+ "age": 35,
67
+ "conditions": "Severe depression, Suicidal ideation",
68
+ "goal": "Immediate crisis intervention and support",
69
+ "exercise": "None - Crisis support priority",
70
+ "limitations": "CRISIS - Suicidal thoughts, Immediate referral needed"
71
+ },
72
+ "πŸ”΄ RED - Severe Hopelessness": {
73
+ "name": "Jennifer",
74
+ "phone": "(555) 890-1234",
75
+ "age": 48,
76
+ "conditions": "Major depression, Complete hopelessness",
77
+ "goal": "Crisis stabilization and professional support",
78
+ "exercise": "None - Medical intervention priority",
79
+ "limitations": "CRISIS - Severe hopelessness, Unable to function"
80
+ },
81
+ "πŸ”΄ RED - Spiritual Crisis": {
82
+ "name": "Christopher",
83
+ "phone": "(555) 901-2345",
84
+ "age": 52,
85
+ "conditions": "Moral injury, Spiritual crisis, Anger at God",
86
+ "goal": "Spiritual crisis intervention and healing",
87
+ "exercise": "None - Spiritual support priority",
88
+ "limitations": "CRISIS - Spiritual crisis, Rage, Existential despair"
89
+ },
90
+ "πŸ«€ Cardiac Patient (Rehabilitation)": {
91
+ "name": "John",
92
+ "phone": "(555) 012-3456",
93
+ "age": 65,
94
+ "conditions": "Coronary artery disease, Hypertension, Hyperlipidemia",
95
+ "goal": "Cardiac rehabilitation and risk factor management",
96
+ "exercise": "Supervised walking, Cardiac rehab program",
97
+ "limitations": "Recent MI, Limited exertion tolerance"
98
+ },
99
+ "🩸 Diabetic Patient (Management)": {
100
+ "name": "Maria",
101
+ "phone": "(555) 111-2222",
102
+ "age": 58,
103
+ "conditions": "Type 2 Diabetes, Obesity, Hypertension",
104
+ "goal": "Blood sugar control and weight management",
105
+ "exercise": "Moderate walking, Resistance training",
106
+ "limitations": "Neuropathy, Retinopathy risk"
107
+ },
108
+ "πŸ₯ Post-Surgery (Recovery)": {
109
+ "name": "Alex",
110
+ "phone": "(555) 222-3333",
111
+ "age": 45,
112
+ "conditions": "Post-surgical recovery, Pain management",
113
+ "goal": "Safe return to normal activities",
114
+ "exercise": "Gentle mobility, Gradual progression",
115
+ "limitations": "Surgical site healing, Limited ROM"
116
+ },
117
+ "🧠 Mental Health (Anxiety/Depression)": {
118
+ "name": "Emma",
119
+ "phone": "(555) 333-4444",
120
+ "age": 35,
121
+ "conditions": "Depression, Anxiety, Sedentary lifestyle",
122
+ "goal": "Mood improvement through activity",
123
+ "exercise": "Yoga, Walking, Group activities",
124
+ "limitations": "Low motivation, Energy fluctuations"
125
+ },
126
+ "πŸ‘΄ Elderly Patient (Chronic Care)": {
127
+ "name": "Robert",
128
+ "phone": "(555) 444-5555",
129
+ "age": 78,
130
+ "conditions": "Arthritis, Osteoporosis, Hypertension",
131
+ "goal": "Maintain independence and mobility",
132
+ "exercise": "Tai Chi, Water aerobics, Balance training",
133
+ "limitations": "Fall risk, Joint pain, Medication interactions"
134
+ },
135
+ "πŸƒ Athletic Patient (Injury/Training)": {
136
+ "name": "Sarah",
137
+ "phone": "(555) 555-6666",
138
+ "age": 32,
139
+ "conditions": "Mild hypertension, Overtraining syndrome",
140
+ "goal": "Optimize performance and prevent injury",
141
+ "exercise": "Running, Strength training, Cross-training",
142
+ "limitations": "Overuse injuries, Recovery needs"
143
+ }
144
+ }
145
+
146
+ profile = profiles.get(profile_name, profiles["πŸ‘€ Default Profile (Serhii)"])
147
+
148
+ # Automatically apply the profile to the session
149
+ if session and hasattr(session.app_instance, 'set_patient_info'):
150
+ session.app_instance.set_patient_info(
151
+ name=profile['name'],
152
+ phone=profile.get('phone', '')
153
+ )
154
+
155
+ # Update clinical_background for medical context
156
+ session.app_instance.clinical_background = ClinicalBackground(
157
+ patient_name=profile['name'],
158
+ age=profile['age'],
159
+ conditions=profile['conditions'].split(',') if isinstance(profile['conditions'], str) else profile['conditions'],
160
+ primary_goal=profile['goal'],
161
+ exercise_preferences=profile['exercise'].split(',') if isinstance(profile['exercise'], str) else profile['exercise'],
162
+ exercise_limitations=profile['limitations'].split(',') if isinstance(profile['limitations'], str) else profile['limitations']
163
+ )
164
+
165
+ # Update conversation logger patient name
166
+ if hasattr(session.app_instance, 'conversation_logger'):
167
+ session.app_instance.conversation_logger.patient_name = profile['name']
168
+
169
+ print(f"DEBUG: Auto-applied profile - Name: {profile['name']}, Phone: {profile.get('phone', '')}")
170
+ print(f"DEBUG: Clinical background updated - Age: {profile['age']}, Conditions: {profile['conditions']}")
171
+
172
+ status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
173
+ <h4 style="color: #059669; margin-top: 0;">βœ… Profile Loaded & Applied</h4>
174
+ <p><strong>Patient:</strong> {profile['name']}, {profile['age']} years old</p>
175
+ <p><strong>Phone:</strong> {profile.get('phone', 'Not provided')}</p>
176
+ <p><strong>Profile:</strong> {profile_name}</p>
177
+ <p><strong>Status:</strong> Profile has been automatically applied to this session</p>
178
+ <p style="margin-bottom: 0; color: #059669;">βœ“ Ready to use in conversations and reports</p>
179
+ </div>"""
180
+
181
+ return (
182
+ profile['name'],
183
+ profile.get('phone', ''),
184
+ profile['age'],
185
+ profile['conditions'],
186
+ profile['goal'],
187
+ profile['exercise'],
188
+ profile['limitations'],
189
+ status
190
+ )
191
+
192
+ def save_profile(name: str, phone: str, age: float, conditions: str, goal: str, exercise: str, limitations: str, session: SimplifiedSessionData):
193
+ """Save current profile settings and update app patient info."""
194
+ if not name.strip():
195
+ return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
196
+ <h4 style="color: #dc2626; margin-top: 0;">❌ Error</h4>
197
+ <p style="margin-bottom: 0;">Patient name cannot be empty</p>
198
+ </div>"""
199
+
200
+ # Update session's app instance patient info for provider summaries
201
+ if session and hasattr(session.app_instance, 'set_patient_info'):
202
+ session.app_instance.set_patient_info(name=name.strip(), phone=phone.strip() if phone else None)
203
+
204
+ # Also update clinical_background for medical context
205
+ session.app_instance.clinical_background = ClinicalBackground(
206
+ patient_name=name.strip(),
207
+ age=int(age) if age else None,
208
+ conditions=conditions.strip().split(',') if conditions.strip() else [],
209
+ primary_goal=goal.strip(),
210
+ exercise_preferences=exercise.strip().split(',') if exercise.strip() else [],
211
+ exercise_limitations=limitations.strip().split(',') if limitations.strip() else []
212
+ )
213
+
214
+ # Update conversation logger patient name
215
+ if hasattr(session.app_instance, 'conversation_logger'):
216
+ session.app_instance.conversation_logger.patient_name = name.strip()
217
+
218
+ print(f"DEBUG: Updated patient info - Name: {name.strip()}, Phone: {phone.strip() if phone else None}")
219
+ print(f"DEBUG: Updated clinical_background - Age: {int(age) if age else None}, Conditions: {conditions}")
220
+
221
+ status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
222
+ <h4 style="color: #059669; margin-top: 0;">πŸ’Ύ Profile Saved</h4>
223
+ <p><strong>Patient:</strong> {name}, {int(age)} years old</p>
224
+ <p><strong>Phone:</strong> {phone if phone else 'Not provided'}</p>
225
+ <p><strong>Conditions:</strong> {conditions}</p>
226
+ <p><strong>Primary Goal:</strong> {goal}</p>
227
+ <p style="margin-bottom: 0;">Profile settings have been updated for this session.</p>
228
+ </div>"""
229
+
230
+ return status
231
+
232
+ def reset_profile(session: SimplifiedSessionData):
233
+ """Reset profile to default."""
234
+ # Reset session's app instance patient info
235
+ if session and hasattr(session.app_instance, 'set_patient_info'):
236
+ session.app_instance.set_patient_info(name="Serhii", phone="(555) 123-4567")
237
+
238
+ # Also reset clinical_background
239
+ session.app_instance.clinical_background = ClinicalBackground(
240
+ patient_name="Serhii",
241
+ age=52,
242
+ conditions=["Atrial fibrillation", "Deep vein thrombosis", "Obesity", "Hypertension"],
243
+ primary_goal="Weight reduction and cardiovascular fitness improvement",
244
+ exercise_preferences=["Swimming", "Walking", "Light cardio"],
245
+ exercise_limitations=["Anticoagulation therapy", "Post-thrombotic recovery"]
246
+ )
247
+
248
+ # Update conversation logger patient name
249
+ if hasattr(session.app_instance, 'conversation_logger'):
250
+ session.app_instance.conversation_logger.patient_name = "Serhii"
251
+
252
+ print("DEBUG: Reset patient info to default (Serhii)")
253
+
254
+ return (
255
+ "Serhii",
256
+ "(555) 123-4567",
257
+ 52,
258
+ "Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
259
+ "Weight reduction and cardiovascular fitness improvement",
260
+ "Swimming, Walking, Light cardio",
261
+ "Anticoagulation therapy, Post-thrombotic recovery",
262
+ ""
263
+ )
src/interface/prompt_handlers.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import logging
3
+ import gradio as gr
4
+ from typing import Optional
5
+ from src.interface.session_manager import SimplifiedSessionData
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def format_prompt_with_html(prompt_text: str) -> str:
10
+ """Format prompt with HTML tags for better visualization."""
11
+ import re
12
+ import html
13
+
14
+ # Escape HTML first to prevent injection
15
+ formatted = html.escape(prompt_text)
16
+
17
+ # Highlight XML-like opening tags
18
+ formatted = re.sub(
19
+ r'&lt;([a-z_]+)(?:\s+[^&gt;]*)?&gt;',
20
+ r'<span style="color: #2563eb; font-weight: 600;">&lt;\1&gt;</span>',
21
+ formatted,
22
+ flags=re.IGNORECASE
23
+ )
24
+
25
+ # Highlight XML-like closing tags
26
+ formatted = re.sub(
27
+ r'&lt;/([a-z_]+)&gt;',
28
+ r'<span style="color: #7c3aed; font-weight: 600;">&lt;/\1&gt;</span>',
29
+ formatted,
30
+ flags=re.IGNORECASE
31
+ )
32
+
33
+ # Highlight XML attributes
34
+ formatted = re.sub(
35
+ r'([a-z_]+)=&quot;([^&quot;]+)&quot;',
36
+ r'<span style="color: #059669;">\1</span>=<span style="color: #d97706;">&quot;\2&quot;</span>',
37
+ formatted,
38
+ flags=re.IGNORECASE
39
+ )
40
+
41
+ # Highlight bullet points
42
+ formatted = re.sub(
43
+ r'^([-β€’]\s+.+)$',
44
+ r'<div style="margin-left: 1.5em; color: #059669;">\1</div>',
45
+ formatted,
46
+ flags=re.MULTILINE
47
+ )
48
+
49
+ # Highlight numbered lists
50
+ formatted = re.sub(
51
+ r'^(\d+\.\s+.+)$',
52
+ r'<div style="margin-left: 1.5em; color: #dc2626;">\1</div>',
53
+ formatted,
54
+ flags=re.MULTILINE
55
+ )
56
+
57
+ # Highlight important keywords
58
+ keywords = [
59
+ 'IMPORTANT', 'CRITICAL', 'REQUIRED', 'MUST', 'SHALL',
60
+ 'WARNING', 'NOTE', 'TASK', 'GOAL', 'OUTPUT', 'ANY'
61
+ ]
62
+ for keyword in keywords:
63
+ formatted = re.sub(
64
+ f'\\b({keyword})\\b',
65
+ r'<strong style="color: #dc2626; background-color: #fef2f2; padding: 2px 4px; border-radius: 3px;">\1</strong>',
66
+ formatted,
67
+ flags=re.IGNORECASE
68
+ )
69
+
70
+ # Highlight JSON/code blocks
71
+ formatted = re.sub(
72
+ r'(\{[^}]+\})',
73
+ r'<code style="background-color: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 0.9em;">\1</code>',
74
+ formatted
75
+ )
76
+
77
+ # Convert newlines to <br> for proper display
78
+ formatted = formatted.replace('\n', '<br>')
79
+
80
+ # Wrap in container with monospace font for code-like appearance
81
+ formatted = f'<div style="font-family: \'Courier New\', monospace; line-height: 1.8; padding: 1em; background-color: #f9fafb; border-radius: 8px; overflow-x: auto;">{formatted}</div>'
82
+
83
+ return formatted
84
+
85
+ def _prompt_name_to_agent(prompt_name: str) -> str:
86
+ """Map UI prompt selection to internal agent/prompt key."""
87
+ mapping = {
88
+ "πŸ” Spiritual Monitor (Classifier)": "SpiritualDistressAnalyzer",
89
+ "🟑 Soft Spiritual Triage": "SoftSpiritualTriage",
90
+ "πŸ“Š Triage Response Evaluator": "TriageResponseEvaluator",
91
+ "πŸ₯ Medical Assistant": "MedicalAssistant",
92
+ "🩺 Soft Medical Triage": "SoftMedicalTriage",
93
+ }
94
+ return mapping.get(prompt_name, prompt_name)
95
+
96
+ def load_prompt(prompt_name: str, session: Optional[SimplifiedSessionData] = None):
97
+ """Load selected prompt for editing using enhanced prompt editor."""
98
+ try:
99
+ from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
100
+
101
+ # Initialize enhanced editor
102
+ editor = EnhancedPromptEditor()
103
+
104
+ # Get session ID
105
+ session_id = getattr(session, 'session_id', 'default_session') if session else 'default_session'
106
+
107
+ # Use enhanced editor to load prompt
108
+ prompt_content, info_html, status_html = editor.load_prompt_for_editing(prompt_name, session_id)
109
+
110
+ return prompt_content, info_html, status_html
111
+
112
+ except Exception as e:
113
+ # Fallback to old system if enhanced editor fails
114
+ logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
115
+
116
+ from src.core.spiritual_monitor import SYSTEM_PROMPT_SPIRITUAL_MONITOR
117
+ from src.core.soft_triage_manager import (
118
+ SYSTEM_PROMPT_TRIAGE_QUESTION,
119
+ SYSTEM_PROMPT_TRIAGE_EVALUATE
120
+ )
121
+ from src.config.prompts import (
122
+ SYSTEM_PROMPT_MEDICAL_ASSISTANT,
123
+ SYSTEM_PROMPT_SOFT_MEDICAL_TRIAGE
124
+ )
125
+
126
+ prompts = {
127
+ "πŸ” Spiritual Monitor (Classifier)": SYSTEM_PROMPT_SPIRITUAL_MONITOR,
128
+ "🟑 Soft Spiritual Triage": SYSTEM_PROMPT_TRIAGE_QUESTION,
129
+ "πŸ“Š Triage Response Evaluator": SYSTEM_PROMPT_TRIAGE_EVALUATE,
130
+ "πŸ₯ Medical Assistant": SYSTEM_PROMPT_MEDICAL_ASSISTANT,
131
+ "🩺 Soft Medical Triage": SYSTEM_PROMPT_SOFT_MEDICAL_TRIAGE
132
+ }
133
+
134
+ prompt_text = prompts.get(prompt_name, "")
135
+
136
+ info = f"""**Loaded:** {prompt_name}
137
+ **Length:** {len(prompt_text)} characters
138
+ **Status:** Fallback mode (enhanced editor unavailable)"""
139
+
140
+ status = """<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
141
+ <h4 style="color: #d97706; margin-top: 0;">⚠️ Fallback Mode</h4>
142
+ <p style="margin-bottom: 0;">Using basic prompt editor. Enhanced features unavailable.</p>
143
+ </div>"""
144
+
145
+ return prompt_text, info, status
146
+
147
+ def apply_prompt_changes(prompt_name: str, prompt_text: str, session: SimplifiedSessionData):
148
+ """Apply custom prompt changes using enhanced prompt editor."""
149
+ try:
150
+ from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
151
+
152
+ if session is None:
153
+ session = SimplifiedSessionData()
154
+
155
+ # Initialize enhanced editor
156
+ editor = EnhancedPromptEditor()
157
+
158
+ # Get session ID
159
+ session_id = getattr(session, 'session_id', 'default_session')
160
+
161
+ # Use enhanced editor to apply changes
162
+ status_html, success = editor.apply_prompt_changes(prompt_name, prompt_text, session_id)
163
+
164
+ if success:
165
+ # Also store in session for backward compatibility
166
+ if not hasattr(session, 'custom_prompts'):
167
+ session.custom_prompts = {}
168
+
169
+ agent_key = _prompt_name_to_agent(prompt_name)
170
+ session.custom_prompts[agent_key] = prompt_text
171
+
172
+ # Apply to session app instance if available
173
+ if hasattr(session, 'app_instance') and hasattr(session.app_instance, 'set_prompt_overrides'):
174
+ session.app_instance.set_prompt_overrides(session.custom_prompts)
175
+
176
+ return status_html, session
177
+
178
+ except Exception as e:
179
+ # Fallback to old system
180
+ logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
181
+
182
+ if session is None:
183
+ session = SimplifiedSessionData()
184
+
185
+ if not prompt_text.strip():
186
+ error_html = """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
187
+ <h4 style="color: #dc2626; margin-top: 0;">❌ Error</h4>
188
+ <p style="margin-bottom: 0;">Prompt cannot be empty</p>
189
+ </div>"""
190
+ return error_html, session
191
+
192
+ # Store custom prompt in session (session-scoped)
193
+ if not hasattr(session, 'custom_prompts'):
194
+ session.custom_prompts = {}
195
+
196
+ agent_key = _prompt_name_to_agent(prompt_name)
197
+ session.custom_prompts[agent_key] = prompt_text
198
+
199
+ status = f"""<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
200
+ <h4 style="color: #d97706; margin-top: 0;">⚠️ Fallback Mode - Changes Applied</h4>
201
+ <p><strong>Prompt:</strong> {prompt_name}</p>
202
+ <p><strong>Length:</strong> {len(prompt_text)} characters</p>
203
+ <p style="margin-bottom: 0;">Enhanced features unavailable, using basic session storage.</p>
204
+ </div>"""
205
+
206
+ return status, session
207
+
208
+ def reset_prompt(prompt_name: str, session: SimplifiedSessionData):
209
+ """Reset prompt to default using enhanced prompt editor."""
210
+ try:
211
+ from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
212
+
213
+ if session is None:
214
+ session = SimplifiedSessionData()
215
+
216
+ # Initialize enhanced editor
217
+ editor = EnhancedPromptEditor()
218
+
219
+ # Get session ID
220
+ session_id = getattr(session, 'session_id', 'default_session')
221
+
222
+ # Use enhanced editor to reset prompt
223
+ prompt_content, info_html, status_html = editor.reset_prompt_to_default(prompt_name, session_id)
224
+
225
+ # Also remove from session for backward compatibility
226
+ agent_key = _prompt_name_to_agent(prompt_name)
227
+ if hasattr(session, 'custom_prompts') and agent_key in session.custom_prompts:
228
+ del session.custom_prompts[agent_key]
229
+
230
+ # Apply to session app instance if available
231
+ if hasattr(session, 'app_instance') and hasattr(session.app_instance, 'set_prompt_overrides'):
232
+ session.app_instance.set_prompt_overrides(getattr(session, 'custom_prompts', {}))
233
+
234
+ return prompt_content, info_html, status_html, session
235
+
236
+ except Exception as e:
237
+ # Fallback to old system
238
+ logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
239
+
240
+ if session is None:
241
+ session = SimplifiedSessionData()
242
+
243
+ # Remove from custom prompts
244
+ agent_key = _prompt_name_to_agent(prompt_name)
245
+ if hasattr(session, 'custom_prompts') and agent_key in session.custom_prompts:
246
+ del session.custom_prompts[agent_key]
247
+
248
+ # Reload default
249
+ prompt_text, info, status = load_prompt(prompt_name, session)
250
+
251
+ reset_status = """<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
252
+ <h4 style="color: #d97706; margin-top: 0;">πŸ”„ Fallback Mode - Reset Complete</h4>
253
+ <p style="margin-bottom: 0;">Prompt restored using basic system. Enhanced features unavailable.</p>
254
+ </div>"""
255
+
256
+ return prompt_text, info, reset_status, session
257
+
258
+ def promote_prompt_to_file(prompt_name: str, session: SimplifiedSessionData):
259
+ """Promote session prompt override to permanent file."""
260
+ try:
261
+ from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
262
+
263
+ if session is None:
264
+ return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
265
+ <h4 style="color: #dc2626; margin-top: 0;">❌ Error</h4>
266
+ <p style="margin-bottom: 0;">No session data available</p>
267
+ </div>""", session
268
+
269
+ # Initialize enhanced editor
270
+ editor = EnhancedPromptEditor()
271
+
272
+ # Get session ID
273
+ session_id = getattr(session, 'session_id', 'default_session')
274
+
275
+ # Use enhanced editor to promote prompt
276
+ status_html, success = editor.promote_session_to_file(prompt_name, session_id)
277
+
278
+ return status_html, session
279
+
280
+ except Exception as e:
281
+ logger.warning(f"Enhanced prompt editor failed: {e}")
282
+ return f"""<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
283
+ <h4 style="color: #dc2626; margin-top: 0;">❌ Error</h4>
284
+ <p style="margin-bottom: 0;">Failed to promote prompt: {str(e)}</p>
285
+ </div>""", session
286
+
287
+ def validate_prompt_syntax(prompt_text: str):
288
+ """Validate prompt syntax and structure."""
289
+ try:
290
+ from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
291
+
292
+ # Initialize enhanced editor
293
+ editor = EnhancedPromptEditor()
294
+
295
+ # Use enhanced editor to validate prompt
296
+ validation_html, is_valid = editor.validate_prompt_syntax(prompt_text)
297
+
298
+ return validation_html
299
+
300
+ except Exception as e:
301
+ logger.warning(f"Enhanced prompt editor failed: {e}")
302
+ return f"""<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
303
+ <h4 style="color: #dc2626; margin-top: 0;">❌ Validation Error</h4>
304
+ <p style="margin-bottom: 0;">Failed to validate prompt: {str(e)}</p>
305
+ </div>"""
src/interface/session_manager.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import asyncio
3
+ import uuid
4
+ import os
5
+ import sys
6
+ from datetime import datetime
7
+ from src.core.simplified_medical_app import SimplifiedMedicalApp
8
+ from src.core.core_classes import ClinicalBackground
9
+
10
+ class SimplifiedSessionData:
11
+ """Container for user session data."""
12
+
13
+ def __init__(self, session_id: str = None):
14
+ self.session_id = session_id or str(uuid.uuid4())
15
+ self.app_instance = SimplifiedMedicalApp()
16
+ self.created_at = datetime.now().isoformat()
17
+ self.last_activity = datetime.now().isoformat()
18
+
19
+ # Set default patient info from profile
20
+ self.app_instance.set_patient_info(name="Serhii", phone="(555) 123-4567")
21
+
22
+ # Update clinical_background to match default profile
23
+ self.app_instance.clinical_background = ClinicalBackground(
24
+ patient_name="Serhii",
25
+ age=52,
26
+ conditions=["Atrial fibrillation", "Deep vein thrombosis", "Obesity", "Hypertension"],
27
+ primary_goal="Weight reduction and cardiovascular fitness improvement",
28
+ exercise_preferences=["Swimming", "Walking", "Light cardio"],
29
+ exercise_limitations=["Anticoagulation therapy", "Post-thrombotic recovery"]
30
+ )
31
+
32
+ # Update conversation logger patient name
33
+ if hasattr(self.app_instance, 'conversation_logger'):
34
+ self.app_instance.conversation_logger.patient_name = "Serhii"
35
+
36
+ def update_activity(self):
37
+ """Update last activity timestamp."""
38
+ self.last_activity = datetime.now().isoformat()
src/interface/simplified_gradio_app.py CHANGED
The diff for this file is too large to render. See raw diff
 
src/interface/stats_handlers.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import json
4
+ import csv
5
+ import os
6
+ from datetime import datetime
7
+ from src.interface.session_manager import SimplifiedSessionData
8
+
9
+ def get_conversation_stats(session: SimplifiedSessionData):
10
+ """Get conversation statistics."""
11
+ if session is None or not hasattr(session.app_instance, 'conversation_logger'):
12
+ return "No conversation yet"
13
+
14
+ try:
15
+ summary = session.app_instance.get_conversation_summary()
16
+ if not summary or summary.get('total_exchanges', 0) == 0:
17
+ return "No conversation yet"
18
+
19
+ stats_text = f"""**πŸ“Š Conversation Statistics**
20
+
21
+ **Messages:** {summary['total_exchanges']} exchanges
22
+ **Duration:** {summary['session_duration_minutes']} minutes
23
+
24
+ **Classifications:**
25
+ 🟒 Green: {summary['classification_counts']['green']} ({summary['classification_percentages']['green']}%)
26
+ 🟑 Yellow: {summary['classification_counts']['yellow']} ({summary['classification_percentages']['yellow']}%)
27
+ πŸ”΄ Red: {summary['classification_counts']['red']} ({summary['classification_percentages']['red']}%)
28
+
29
+ **Average Confidence:** {summary['average_confidence']}
30
+
31
+ **Top Indicators:**"""
32
+
33
+ for indicator, count in list(summary['top_indicators'].items())[:3]:
34
+ stats_text += f"\nβ€’ {indicator}: {count}"
35
+
36
+ return stats_text
37
+
38
+ except Exception as e:
39
+ return f"Error getting stats: {str(e)}"
40
+
41
+ def download_conversation_json(session: SimplifiedSessionData):
42
+ """Download conversation as JSON."""
43
+ if session is None or not hasattr(session.app_instance, 'conversation_logger'):
44
+ return None
45
+
46
+ try:
47
+ log_path = session.app_instance.get_conversation_log_path()
48
+ return log_path
49
+ except Exception as e:
50
+ print(f"Error downloading JSON: {e}")
51
+ return None
52
+
53
+ def download_conversation_csv(session: SimplifiedSessionData):
54
+ """Download conversation as CSV."""
55
+ if session is None or not hasattr(session.app_instance, 'conversation_logger'):
56
+ return None
57
+
58
+ try:
59
+ csv_path = session.app_instance.export_conversation_csv()
60
+ return csv_path
61
+ except Exception as e:
62
+ print(f"Error downloading CSV: {e}")
63
+ return None
64
+
65
+ def get_status(session: SimplifiedSessionData):
66
+ """Get current status and stats."""
67
+ if session is None:
68
+ return "❌ Session not initialized", "No stats", gr.update(visible=False), "No provider summary available", "", ""
69
+
70
+ session.update_activity()
71
+ status_info = session.app_instance._get_status_info()
72
+
73
+ # Get stats
74
+ stats_text = get_conversation_stats(session)
75
+
76
+ # Check for provider summary
77
+ last_summary = session.app_instance.get_last_provider_summary()
78
+ show_provider_panel = last_summary is not None
79
+
80
+ provider_summary_text = ""
81
+ spiritual_care_msg = ""
82
+
83
+ if last_summary:
84
+ provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
85
+
86
+ # Generate spiritual care message
87
+ try:
88
+ spiritual_care_msg = session.app_instance.generate_spiritual_care_message(
89
+ language="English",
90
+ session_id=session.session_id
91
+ )
92
+ if not spiritual_care_msg:
93
+ spiritual_care_msg = ""
94
+ except Exception as e:
95
+ print(f"Error generating spiritual care message in get_status: {e}")
96
+ spiritual_care_msg = ""
97
+
98
+ if provider_summary_text:
99
+ import html
100
+ escaped_text = html.escape(provider_summary_text)
101
+ html_content = f"<pre style='white-space: pre-wrap; font-family: monospace; font-size: 11px; background: #fffbeb; padding: 10px; border-radius: 8px; max-height: 400px; overflow-y: auto;'>{escaped_text}</pre>"
102
+
103
+ status_msg = f"""**πŸ”΄ Provider Summary Generated**
104
+
105
+ **Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
106
+ **Classification:** RED FLAG
107
+ **Indicators:** {len(last_summary.indicators)} distress indicators detected
108
+ **Summary Length:** {len(provider_summary_text)} characters
109
+
110
+ Use the **Download Summary** button below to access the complete provider summary for the spiritual care team."""
111
+ else:
112
+ html_content = ""
113
+ status_msg = "No provider summary available"
114
+
115
+ return (
116
+ status_info,
117
+ stats_text,
118
+ gr.update(visible=show_provider_panel),
119
+ status_msg,
120
+ html_content,
121
+ spiritual_care_msg
122
+ )
123
+
124
+ def download_provider_summary(session: SimplifiedSessionData):
125
+ """Download provider summary as text file."""
126
+ if session is None:
127
+ return None
128
+
129
+ last_summary = session.app_instance.get_last_provider_summary()
130
+ if not last_summary:
131
+ return None
132
+
133
+ try:
134
+ # Generate filename
135
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
136
+ patient_name = session.app_instance.patient_info.get('name', 'Patient').replace(" ", "_")
137
+ filename = f"provider_summary_{patient_name}_{timestamp}.txt"
138
+
139
+ # Format content
140
+ content = session.app_instance.provider_summary_generator.format_for_display(last_summary)
141
+
142
+ # Save to temp file
143
+ path = os.path.join(os.getcwd(), "exports", filename)
144
+ os.makedirs(os.path.dirname(path), exist_ok=True)
145
+
146
+ with open(path, "w", encoding="utf-8") as f:
147
+ f.write(content)
148
+
149
+ return path
150
+
151
+ except Exception as e:
152
+ print(f"Error downloading provider summary: {e}")
153
+ return None
154
+
155
+ def clear_provider_summary():
156
+ """Clear provider summary panel."""
157
+ return (
158
+ gr.update(visible=False),
159
+ "No provider summary available",
160
+ "",
161
+ ""
162
+ )
163
+
164
+ def regenerate_spiritual_care_message(
165
+ session: SimplifiedSessionData,
166
+ include_conversation: bool = True,
167
+ include_situation: bool = False,
168
+ include_indicators: bool = False,
169
+ include_profile: bool = False
170
+ ):
171
+ """Regenerate LLM-based spiritual care message."""
172
+ if session is None:
173
+ return ""
174
+
175
+ last_summary = session.app_instance.get_last_provider_summary()
176
+ if not last_summary:
177
+ return ""
178
+
179
+ try:
180
+ msg = session.app_instance.generate_spiritual_care_message(
181
+ language="English",
182
+ session_id=session.session_id,
183
+ include_conversation_context=include_conversation,
184
+ include_situation_analysis=include_situation,
185
+ include_distress_indicators=include_indicators,
186
+ include_patient_profile=include_profile
187
+ )
188
+ return msg or "Error generating message"
189
+ except Exception as e:
190
+ return f"Error: {str(e)}"
191
+
192
+ def download_spiritual_care_message(session: SimplifiedSessionData):
193
+ """Download spiritual care message as text file."""
194
+ if session is None:
195
+ return None
196
+
197
+ # We need to get the current message text from the UI state, but typically
198
+ # in Gradio we return the file path. Since we don't have the text passed here,
199
+ # we'll regenerate the last one or we'd need the text passed in.
200
+ # Looking at original code, it might regenerate or access session state.
201
+ # ORIGINAL CODE logic check needed.
202
+
203
+ # Let's assume for now we regenerate it or fetch from session if stored.
204
+ # Actually, let's regenerate for consistency with current context.
205
+
206
+ msg = regenerate_spiritual_care_message(session)
207
+ if not msg:
208
+ return None
209
+
210
+ try:
211
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
212
+ patient_name = session.app_instance.patient_info.get('name', 'Patient').replace(" ", "_")
213
+ filename = f"spiritual_care_message_{patient_name}_{timestamp}.txt"
214
+
215
+ path = os.path.join(os.getcwd(), "exports", filename)
216
+ os.makedirs(os.path.dirname(path), exist_ok=True)
217
+
218
+ with open(path, "w", encoding="utf-8") as f:
219
+ f.write(msg)
220
+
221
+ return path
222
+ except Exception as e:
223
+ print(f"Error downloading spiritual care message: {e}")
224
+ return None
src/interface/verification_handlers.py ADDED
@@ -0,0 +1,1067 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import json
4
+ import os
5
+ import uuid
6
+ import csv
7
+ import traceback
8
+ from datetime import datetime
9
+ from typing import List, Optional, Any, Dict
10
+
11
+ from src.interface.session_manager import SimplifiedSessionData
12
+ from src.core.verification_models import VerificationSession, VerificationRecord, TestMessage
13
+ from src.core.verification_store import JSONVerificationStore
14
+ from src.core.chaplain_models import ClassificationFlowResult, DistressIndicator, FollowUpQuestion, TaggingRecord
15
+ from src.core.verification_csv_exporter import VerificationCSVExporter
16
+ from src.core.test_datasets import TestDatasetManager
17
+ from src.interface.verification_ui import VerificationUIComponents
18
+ from src.interface.chaplain_feedback_ui import ChaplainFeedbackUIComponents
19
+ from src.core.conversation_verification import (
20
+ ConversationVerificationManager,
21
+ VerificationRecord as ConvVerificationRecord,
22
+ VerificationSession as ConvVerificationSession
23
+ )
24
+
25
+ def open_verification_window(session: SimplifiedSessionData):
26
+ """Open verification window for current conversation."""
27
+ if session is None or not hasattr(session.app_instance, 'conversation_logger'):
28
+ return """<div style="background-color: #f8d7da; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
29
+ ❌ <strong>No conversation to verify</strong><br>
30
+ <small>Start a conversation first</small>
31
+ </div>"""
32
+
33
+ try:
34
+ # Check if conversation has any entries
35
+ if not session.app_instance.conversation_logger.entries:
36
+ return """<div style="background-color: #fff3cd; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
37
+ ⚠️ <strong>No conversation exchanges to verify</strong><br>
38
+ <small>Send some messages in the chat first</small>
39
+ </div>"""
40
+
41
+ print(f"πŸ” Opening verification for {len(session.app_instance.conversation_logger.entries)} exchanges...")
42
+
43
+ manager = ConversationVerificationManager()
44
+ verification_session = manager.create_verification_session(
45
+ session.app_instance.conversation_logger,
46
+ "Medical Professional"
47
+ )
48
+
49
+ print(f"βœ… Created verification session: {verification_session.session_id}")
50
+
51
+ # HF Spaces / Gradio limitation:
52
+ # Launching a *second* Gradio server from inside a running Gradio app is unreliable
53
+ # and is currently causing the Button._id error in Spaces.
54
+ # Instead, export the verification session to a JSON file that the user can download.
55
+
56
+ export_dir = os.path.join(os.getcwd(), "verification_sessions")
57
+ os.makedirs(export_dir, exist_ok=True)
58
+
59
+ export_filename = f"verification_session_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{verification_session.session_id}.json"
60
+ export_path = os.path.join(export_dir, export_filename)
61
+
62
+ # Serialize to JSON in a resilient way (dataclasses / pydantic / plain python).
63
+ def _to_dict(obj):
64
+ if hasattr(obj, "model_dump"):
65
+ return obj.model_dump()
66
+ if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
67
+ return obj.dict()
68
+ if hasattr(obj, "__dict__"):
69
+ return obj.__dict__
70
+ return str(obj)
71
+
72
+ payload = {
73
+ "session_id": verification_session.session_id,
74
+ "patient_name": verification_session.patient_name,
75
+ "verifier_name": verification_session.verifier_name,
76
+ "start_time": verification_session.start_time.isoformat() if hasattr(verification_session, "start_time") else None,
77
+ "verification_records": [
78
+ {
79
+ # Conversation verification records use `exchange_id`.
80
+ # Keep a `record_id` alias for backward compatibility with older exports.
81
+ "exchange_id": getattr(r, "exchange_id", None),
82
+ "record_id": getattr(r, "exchange_id", None),
83
+ "timestamp": r.timestamp.isoformat() if hasattr(r, "timestamp") else None,
84
+ "user_message": r.user_message,
85
+ "assistant_response": r.assistant_response,
86
+ "original_classification": r.original_classification,
87
+ "original_confidence": r.original_confidence,
88
+ "original_indicators": r.original_indicators,
89
+ "original_reasoning": r.original_reasoning,
90
+ "is_correct": r.is_correct,
91
+ "correct_classification": r.correct_classification,
92
+ "correction_reason": r.correction_reason,
93
+ "verifier_notes": r.verifier_notes,
94
+ }
95
+ for r in verification_session.verification_records
96
+ ],
97
+ }
98
+
99
+ with open(export_path, "w", encoding="utf-8") as f:
100
+ json.dump(payload, f, ensure_ascii=False, indent=2, default=_to_dict)
101
+
102
+ print(f"βœ… Verification session exported: {export_path}")
103
+
104
+ return f"""<div style="background-color: #d4edda; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
105
+ βœ… <strong>Verification session exported</strong><br>
106
+ <small>Exchanges: {len(verification_session.verification_records)}</small><br>
107
+ <small>Download JSON from the app's files panel (or add a dedicated download button).</small>
108
+ </div>"""
109
+
110
+ except Exception as e:
111
+ print(f"❌ Error opening verification: {str(e)}")
112
+ traceback.print_exc()
113
+
114
+ return f"""<div style="background-color: #f8d7da; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
115
+ ❌ <strong>Error opening verification</strong><br>
116
+ <small>{str(e)}</small>
117
+ </div>"""
118
+
119
+ def load_verification_dataset(dataset_name: str, store: JSONVerificationStore):
120
+ """Load a verification dataset."""
121
+ try:
122
+ # Find dataset ID from name
123
+ datasets = TestDatasetManager.get_dataset_list()
124
+ dataset_id = None
125
+ for d in datasets:
126
+ if d['name'] in dataset_name:
127
+ dataset_id = d['dataset_id']
128
+ break
129
+
130
+ if not dataset_id:
131
+ return (
132
+ None, # verification_session
133
+ "❌ Dataset not found", # dataset_info
134
+ "", "", "", "", # message_text, decision_badge, confidence, indicators
135
+ "", # progress_display
136
+ "❌ Dataset not found", # error_message
137
+ 0, # current_message_index
138
+ None, # current_dataset_id
139
+ [], # message_queue
140
+ [], # verification_records
141
+ )
142
+
143
+ # Load dataset
144
+ dataset = TestDatasetManager.load_dataset(dataset_id)
145
+
146
+ # Create new verification session
147
+ new_session = VerificationSession(
148
+ session_id=str(uuid.uuid4()),
149
+ verifier_name="Medical Professional",
150
+ dataset_id=dataset_id,
151
+ dataset_name=dataset.name,
152
+ total_messages=dataset.message_count,
153
+ message_queue=[m.message_id for m in dataset.messages],
154
+ )
155
+
156
+ # Save session
157
+ store.save_session(new_session)
158
+
159
+ # Get first message
160
+ if dataset.messages:
161
+ first_message = dataset.messages[0]
162
+ message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
163
+ first_message,
164
+ first_message.pre_classified_label,
165
+ 0.85, # Default confidence
166
+ ["Distress indicator 1", "Distress indicator 2"]
167
+ )
168
+
169
+ progress = VerificationUIComponents.update_progress_display(0, dataset.message_count)
170
+
171
+ dataset_info_text = f"**{dataset.name}**\n\n{dataset.description}\n\nπŸ“Š {dataset.message_count} messages to review"
172
+
173
+ return (
174
+ new_session, # verification_session
175
+ dataset_info_text, # dataset_info
176
+ message_text, # message_text
177
+ decision_badge, # decision_badge
178
+ confidence, # confidence
179
+ indicators, # indicators
180
+ progress, # progress_display
181
+ "", # error_message (empty = no error)
182
+ 0, # current_message_index
183
+ dataset_id, # current_dataset_id
184
+ [m.message_id for m in dataset.messages], # message_queue
185
+ [], # verification_records
186
+ )
187
+ else:
188
+ return (
189
+ None, # verification_session
190
+ "❌ Dataset is empty", # dataset_info
191
+ "", "", "", "", # message_text, decision_badge, confidence, indicators
192
+ "", # progress_display
193
+ "❌ Dataset is empty", # error_message
194
+ 0, # current_message_index
195
+ dataset_id, # current_dataset_id
196
+ [], # message_queue
197
+ [], # verification_records
198
+ )
199
+
200
+ except Exception as e:
201
+ return (
202
+ None, # verification_session
203
+ f"❌ Error loading dataset: {str(e)}", # dataset_info
204
+ "", "", "", "", # message_text, decision_badge, confidence, indicators
205
+ "", # progress_display
206
+ f"❌ Error: {str(e)}", # error_message
207
+ 0, # current_message_index
208
+ None, # current_dataset_id
209
+ [], # message_queue
210
+ [], # verification_records
211
+ )
212
+
213
+ def handle_correct_feedback(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict], store: JSONVerificationStore):
214
+ """Handle correct feedback."""
215
+ try:
216
+ if not session or current_idx >= len(message_queue):
217
+ return (
218
+ session,
219
+ "❌ Error: Invalid session state",
220
+ "", "", "", "",
221
+ "",
222
+ "βœ“ Correct: 0",
223
+ "βœ— Incorrect: 0",
224
+ "πŸ“Š Accuracy: 0%",
225
+ current_idx,
226
+ records,
227
+ )
228
+
229
+ # Get current message
230
+ dataset = TestDatasetManager.load_dataset(dataset_id)
231
+ current_message_id = message_queue[current_idx]
232
+ current_message = next((m for m in dataset.messages if m.message_id == current_message_id), None)
233
+
234
+ if not current_message:
235
+ return (
236
+ session,
237
+ "❌ Error: Message not found",
238
+ "", "", "", "",
239
+ "",
240
+ "βœ“ Correct: 0",
241
+ "βœ— Incorrect: 0",
242
+ "πŸ“Š Accuracy: 0%",
243
+ current_idx,
244
+ records,
245
+ )
246
+
247
+ # Create verification record
248
+ record = VerificationRecord(
249
+ message_id=current_message.message_id,
250
+ original_message=current_message.text,
251
+ classifier_decision=current_message.pre_classified_label,
252
+ classifier_confidence=0.85,
253
+ classifier_indicators=["Distress indicator 1", "Distress indicator 2"],
254
+ ground_truth_label=current_message.pre_classified_label,
255
+ verifier_notes="",
256
+ is_correct=True,
257
+ )
258
+
259
+ # Add to session
260
+ session.verifications.append(record)
261
+ session.verified_count += 1
262
+ session.correct_count += 1
263
+
264
+ # Save session
265
+ store.save_session(session)
266
+
267
+ # Move to next message
268
+ next_idx = current_idx + 1
269
+
270
+ if next_idx >= len(message_queue):
271
+ # Session complete
272
+ session.is_complete = True
273
+ session.completed_at = datetime.now()
274
+ store.save_session(session)
275
+
276
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
277
+ session.correct_count,
278
+ session.incorrect_count
279
+ )
280
+
281
+ return (
282
+ session,
283
+ "βœ… Verification complete!",
284
+ "", "", "", "",
285
+ "",
286
+ correct_str,
287
+ incorrect_str,
288
+ accuracy_str,
289
+ next_idx,
290
+ [r.to_dict() for r in session.verifications],
291
+ )
292
+ else:
293
+ # Load next message
294
+ next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
295
+ if next_message:
296
+ message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
297
+ next_message,
298
+ next_message.pre_classified_label,
299
+ 0.85,
300
+ ["Distress indicator 1", "Distress indicator 2"]
301
+ )
302
+
303
+ progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
304
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
305
+ session.correct_count,
306
+ session.incorrect_count
307
+ )
308
+
309
+ return (
310
+ session,
311
+ "",
312
+ message_text,
313
+ decision_badge,
314
+ confidence,
315
+ indicators,
316
+ progress,
317
+ correct_str,
318
+ incorrect_str,
319
+ accuracy_str,
320
+ next_idx,
321
+ [r.to_dict() for r in session.verifications],
322
+ )
323
+
324
+ return (
325
+ session,
326
+ "❌ Error processing feedback",
327
+ "", "", "", "",
328
+ "",
329
+ "βœ“ Correct: 0",
330
+ "βœ— Incorrect: 0",
331
+ "πŸ“Š Accuracy: 0%",
332
+ current_idx,
333
+ records,
334
+ )
335
+
336
+ except Exception as e:
337
+ return (
338
+ session,
339
+ f"❌ Error: {str(e)}",
340
+ "", "", "", "",
341
+ "",
342
+ "βœ“ Correct: 0",
343
+ "βœ— Incorrect: 0",
344
+ "πŸ“Š Accuracy: 0%",
345
+ current_idx,
346
+ records,
347
+ )
348
+
349
+ def handle_incorrect_feedback(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
350
+ """Show correction selector."""
351
+ return "❌ Please select the correct classification below"
352
+
353
+ def handle_submit_correction(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict], correction: str, notes: str, store: JSONVerificationStore):
354
+ """Handle correction submission."""
355
+ try:
356
+ if not correction:
357
+ return (
358
+ "❌ Please select a correction before submitting",
359
+ session,
360
+ current_idx,
361
+ dataset_id,
362
+ message_queue,
363
+ records,
364
+ "", "", "", "",
365
+ "",
366
+ "βœ“ Correct: 0",
367
+ "βœ— Incorrect: 0",
368
+ "πŸ“Š Accuracy: 0%",
369
+ "",
370
+ "",
371
+ )
372
+
373
+ # Get current message
374
+ dataset = TestDatasetManager.load_dataset(dataset_id)
375
+ current_message_id = message_queue[current_idx]
376
+ current_message = next((m for m in dataset.messages if m.message_id == current_message_id), None)
377
+
378
+ if not current_message:
379
+ return (
380
+ "❌ Error: Message not found",
381
+ session,
382
+ current_idx,
383
+ dataset_id,
384
+ message_queue,
385
+ records,
386
+ "", "", "", "",
387
+ "",
388
+ "βœ“ Correct: 0",
389
+ "βœ— Incorrect: 0",
390
+ "πŸ“Š Accuracy: 0%",
391
+ "",
392
+ "",
393
+ )
394
+
395
+ # Create verification record
396
+ record = VerificationRecord(
397
+ message_id=current_message.message_id,
398
+ original_message=current_message.text,
399
+ classifier_decision=current_message.pre_classified_label,
400
+ classifier_confidence=0.85,
401
+ classifier_indicators=["Distress indicator 1", "Distress indicator 2"],
402
+ ground_truth_label=correction,
403
+ verifier_notes=notes,
404
+ is_correct=current_message.pre_classified_label == correction,
405
+ )
406
+
407
+ # Add to session
408
+ session.verifications.append(record)
409
+ session.verified_count += 1
410
+ if record.is_correct:
411
+ session.correct_count += 1
412
+ else:
413
+ session.incorrect_count += 1
414
+
415
+ # Save session
416
+ store.save_session(session)
417
+
418
+ # Move to next message
419
+ next_idx = current_idx + 1
420
+
421
+ if next_idx >= len(message_queue):
422
+ # Session complete
423
+ session.is_complete = True
424
+ session.completed_at = datetime.now()
425
+ store.save_session(session)
426
+
427
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
428
+ session.correct_count,
429
+ session.incorrect_count
430
+ )
431
+
432
+ summary = VerificationUIComponents.render_summary_card(session, session.verifications)
433
+
434
+ return (
435
+ "βœ… Verification complete!",
436
+ session,
437
+ next_idx,
438
+ dataset_id,
439
+ message_queue,
440
+ [r.to_dict() for r in session.verifications],
441
+ "", "", "", "",
442
+ "",
443
+ correct_str,
444
+ incorrect_str,
445
+ accuracy_str,
446
+ "",
447
+ summary,
448
+ )
449
+ else:
450
+ # Load next message
451
+ next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
452
+ if next_message:
453
+ message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
454
+ next_message,
455
+ next_message.pre_classified_label,
456
+ 0.85,
457
+ ["Distress indicator 1", "Distress indicator 2"]
458
+ )
459
+
460
+ progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
461
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
462
+ session.correct_count,
463
+ session.incorrect_count
464
+ )
465
+
466
+ return (
467
+ "",
468
+ session,
469
+ next_idx,
470
+ dataset_id,
471
+ message_queue,
472
+ [r.to_dict() for r in session.verifications],
473
+ message_text,
474
+ decision_badge,
475
+ confidence,
476
+ indicators,
477
+ progress,
478
+ correct_str,
479
+ incorrect_str,
480
+ accuracy_str,
481
+ "",
482
+ "",
483
+ )
484
+
485
+ return (
486
+ "❌ Error processing correction",
487
+ session,
488
+ current_idx,
489
+ dataset_id,
490
+ message_queue,
491
+ records,
492
+ "", "", "", "",
493
+ "",
494
+ "βœ“ Correct: 0",
495
+ "βœ— Incorrect: 0",
496
+ "πŸ“Š Accuracy: 0%",
497
+ "",
498
+ "",
499
+ )
500
+
501
+ except Exception as e:
502
+ return (
503
+ f"❌ Error: {str(e)}",
504
+ session,
505
+ current_idx,
506
+ dataset_id,
507
+ message_queue,
508
+ records,
509
+ "", "", "", "",
510
+ "",
511
+ "βœ“ Correct: 0",
512
+ "βœ— Incorrect: 0",
513
+ "πŸ“Š Accuracy: 0%",
514
+ "",
515
+ "",
516
+ )
517
+
518
+ def handle_download_csv(session: VerificationSession, store: JSONVerificationStore):
519
+ """Handle CSV download - returns file path for DownloadButton."""
520
+ try:
521
+ if not session or session.verified_count == 0:
522
+ return None
523
+
524
+ csv_content = VerificationCSVExporter.generate_csv_content(session)
525
+ filename = VerificationCSVExporter.generate_csv_filename()
526
+
527
+ import tempfile
528
+
529
+ # Use temp directory for Hugging Face compatibility
530
+ temp_dir = tempfile.gettempdir()
531
+ file_path = os.path.join(temp_dir, filename)
532
+
533
+ with open(file_path, 'w', encoding='utf-8') as f:
534
+ f.write(csv_content)
535
+
536
+ return file_path
537
+
538
+ except Exception as e:
539
+ print(f"CSV Export Error: {traceback.format_exc()}")
540
+ return None
541
+
542
+ def handle_next_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
543
+ """Move to next message."""
544
+ if not session or current_idx >= len(message_queue) - 1:
545
+ return (
546
+ session,
547
+ "❌ No more messages",
548
+ "", "", "", "",
549
+ "",
550
+ "βœ“ Correct: 0",
551
+ "βœ— Incorrect: 0",
552
+ "πŸ“Š Accuracy: 0%",
553
+ current_idx,
554
+ records,
555
+ )
556
+
557
+ next_idx = current_idx + 1
558
+ dataset = TestDatasetManager.load_dataset(dataset_id)
559
+ next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
560
+
561
+ if next_message:
562
+ message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
563
+ next_message,
564
+ next_message.pre_classified_label,
565
+ 0.85,
566
+ ["Distress indicator 1", "Distress indicator 2"]
567
+ )
568
+
569
+ progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
570
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
571
+ session.correct_count,
572
+ session.incorrect_count
573
+ )
574
+
575
+ return (
576
+ session,
577
+ "",
578
+ message_text,
579
+ decision_badge,
580
+ confidence,
581
+ indicators,
582
+ progress,
583
+ correct_str,
584
+ incorrect_str,
585
+ accuracy_str,
586
+ next_idx,
587
+ records,
588
+ )
589
+
590
+ return (
591
+ session,
592
+ "❌ Error loading next message",
593
+ "", "", "", "",
594
+ "",
595
+ "βœ“ Correct: 0",
596
+ "βœ— Incorrect: 0",
597
+ "πŸ“Š Accuracy: 0%",
598
+ current_idx,
599
+ records,
600
+ )
601
+
602
+ def handle_previous_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
603
+ """Move to previous message."""
604
+ if not session or current_idx <= 0:
605
+ return (
606
+ session,
607
+ "❌ No previous messages",
608
+ "", "", "", "",
609
+ "",
610
+ "βœ“ Correct: 0",
611
+ "βœ— Incorrect: 0",
612
+ "πŸ“Š Accuracy: 0%",
613
+ current_idx,
614
+ records,
615
+ )
616
+
617
+ prev_idx = current_idx - 1
618
+ dataset = TestDatasetManager.load_dataset(dataset_id)
619
+ prev_message = next((m for m in dataset.messages if m.message_id == message_queue[prev_idx]), None)
620
+
621
+ if prev_message:
622
+ message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
623
+ prev_message,
624
+ prev_message.pre_classified_label,
625
+ 0.85,
626
+ ["Distress indicator 1", "Distress indicator 2"]
627
+ )
628
+
629
+ progress = VerificationUIComponents.update_progress_display(prev_idx, len(message_queue))
630
+ correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
631
+ session.correct_count,
632
+ session.incorrect_count
633
+ )
634
+
635
+ return (
636
+ session,
637
+ "",
638
+ message_text,
639
+ decision_badge,
640
+ confidence,
641
+ indicators,
642
+ progress,
643
+ correct_str,
644
+ incorrect_str,
645
+ accuracy_str,
646
+ prev_idx,
647
+ records,
648
+ )
649
+
650
+ return (
651
+ session,
652
+ "❌ Error loading previous message",
653
+ "", "", "", "",
654
+ "",
655
+ "βœ“ Correct: 0",
656
+ "βœ— Incorrect: 0",
657
+ "πŸ“Š Accuracy: 0%",
658
+ current_idx,
659
+ records,
660
+ )
661
+
662
+ def handle_skip_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
663
+ """Skip current message and move to next."""
664
+ return handle_next_message(session, current_idx, dataset_id, message_queue, records)
665
+
666
+ def handle_clear_session():
667
+ """Clear current verification session."""
668
+ return (
669
+ None, # verification_session
670
+ "βœ… Session cleared", # error_message
671
+ "", "", "", "", # message components
672
+ "", # progress
673
+ "βœ“ Correct: 0", # correct count
674
+ "βœ— Incorrect: 0", # incorrect count
675
+ "πŸ“Š Accuracy: 0%", # accuracy
676
+ 0, # current index
677
+ [], # records
678
+ )
679
+
680
+ def show_chaplain_feedback_section():
681
+ """Show chaplain feedback section after message review."""
682
+ return gr.Row(visible=True)
683
+
684
+ def handle_submit_feedback(
685
+ classification_correct: bool,
686
+ classification_subcategory: Optional[str],
687
+ correct_classification: Optional[str],
688
+ question_issues: List[str],
689
+ question_comments: str,
690
+ referral_issues: List[str],
691
+ referral_comments: str,
692
+ indicator_issues: str,
693
+ indicator_comments: str,
694
+ general_notes: str,
695
+ session: VerificationSession,
696
+ current_idx: int,
697
+ message_queue: List[str],
698
+ ):
699
+ """Handle chaplain feedback submission."""
700
+ try:
701
+ if not session or current_idx >= len(message_queue):
702
+ return "❌ Error: Invalid session state", session, current_idx
703
+
704
+ current_message_id = message_queue[current_idx]
705
+
706
+ tagging_record = TaggingRecord(
707
+ record_id=str(uuid.uuid4()),
708
+ message_id=current_message_id,
709
+ is_classification_correct=classification_correct,
710
+ classification_subcategory=classification_subcategory,
711
+ correct_classification=correct_classification,
712
+ question_issues=question_issues or [],
713
+ question_comments=question_comments,
714
+ referral_issues=referral_issues or [],
715
+ referral_comments=referral_comments,
716
+ indicator_issues=[i.strip() for i in indicator_issues.split(",") if i.strip()],
717
+ indicator_comments=indicator_comments,
718
+ general_notes=general_notes,
719
+ )
720
+
721
+ # Store tagging record in session (would need to extend VerificationSession)
722
+ # For now, just confirm submission
723
+ success_msg = f"βœ… Feedback submitted for message {current_idx + 1}"
724
+
725
+ return success_msg, session, current_idx
726
+
727
+ except Exception as e:
728
+ return f"❌ Error: {str(e)}", session, current_idx
729
+
730
+ def display_classification_flow(flow_result: Optional[ClassificationFlowResult]):
731
+ """Display classification flow result."""
732
+ if not flow_result:
733
+ return "", "", "", ""
734
+
735
+ badge, explanation, content, indicators = ChaplainFeedbackUIComponents.render_classification_flow(flow_result)
736
+ return badge, explanation, content, indicators
737
+
738
+ def _download_latest_verification_json(session: SimplifiedSessionData):
739
+ """Return the most recently exported verification session JSON path (if present)."""
740
+ # open_verification_window exports into ./verification_sessions
741
+ import glob
742
+
743
+ export_dir = os.path.join(os.getcwd(), "verification_sessions")
744
+ if not os.path.isdir(export_dir):
745
+ return None
746
+
747
+ candidates = sorted(
748
+ glob.glob(os.path.join(export_dir, "verification_session_*.json")),
749
+ key=lambda p: os.path.getmtime(p),
750
+ reverse=True,
751
+ )
752
+ return candidates[0] if candidates else None
753
+
754
+ def _render_conv_exchange(records: list, index: int):
755
+ if not records:
756
+ return "", "", ""
757
+ index = max(0, min(index, len(records) - 1))
758
+ r = records[index]
759
+ # Reuse renderer from conversation_verification_ui to keep style consistent
760
+ from src.interface.conversation_verification_ui import VerificationInterface
761
+
762
+ vi = VerificationInterface(ConversationVerificationManager())
763
+ # If we already have dicts, build a lightweight VerificationRecord
764
+ if isinstance(r, dict):
765
+ rec = ConvVerificationRecord(
766
+ exchange_id=r.get("exchange_id") or r.get("record_id", ""),
767
+ exchange_number=r.get("exchange_number", 0),
768
+ user_message=r.get("user_message", ""),
769
+ assistant_response=r.get("assistant_response", ""),
770
+ original_classification=r.get("original_classification", ""),
771
+ original_confidence=r.get("original_confidence", 0.0),
772
+ original_indicators=r.get("original_indicators", []),
773
+ original_reasoning=r.get("original_reasoning", ""),
774
+ timestamp=r.get("timestamp"),
775
+ is_correct=r.get("is_correct"),
776
+ correct_classification=r.get("correct_classification"),
777
+ correction_reason=r.get("correction_reason"),
778
+ verifier_notes=r.get("verifier_notes"),
779
+ )
780
+ else:
781
+ rec = r
782
+ html = vi._render_exchange_review(rec)
783
+ # status badge
784
+ cur_is_correct = (r.get("is_correct") if isinstance(r, dict) else getattr(r, "is_correct", None))
785
+ if cur_is_correct is True:
786
+ badge = "βœ…"
787
+ elif cur_is_correct is False:
788
+ badge = "❌"
789
+ else:
790
+ badge = "⏳"
791
+ pos = f"### {badge} Exchange {index + 1} of {len(records)}"
792
+
793
+ # richer stats
794
+ reviewed = 0
795
+ correct = 0
796
+ incorrect = 0
797
+ incorrect_with_comment = 0
798
+ corrections = {} # Track classification corrections
799
+
800
+ for x in records:
801
+ v = (x.get("is_correct") if isinstance(x, dict) else getattr(x, "is_correct", None))
802
+ if v is None:
803
+ continue
804
+ reviewed += 1
805
+ if v is True:
806
+ correct += 1
807
+ else:
808
+ incorrect += 1
809
+ note = (x.get("verifier_notes") if isinstance(x, dict) else getattr(x, "verifier_notes", None))
810
+ if note and str(note).strip():
811
+ incorrect_with_comment += 1
812
+
813
+ # Track classification corrections
814
+ original_class = (x.get("original_classification") if isinstance(x, dict) else getattr(x, "original_classification", ""))
815
+ correct_class = (x.get("correct_classification") if isinstance(x, dict) else getattr(x, "correct_classification", None))
816
+ if original_class and correct_class:
817
+ correction_key = f"{original_class}β†’{correct_class}"
818
+ corrections[correction_key] = corrections.get(correction_key, 0) + 1
819
+
820
+ stats_parts = [
821
+ f"<div><strong>Reviewed:</strong> {reviewed}/{len(records)}</div>",
822
+ f"<div><strong>βœ… Correct:</strong> {correct}</div>",
823
+ f"<div><strong>❌ Incorrect:</strong> {incorrect}</div>",
824
+ f"<div><strong>πŸ“ Incorrect w/ comment:</strong> {incorrect_with_comment}</div>"
825
+ ]
826
+
827
+ # Add correction breakdown if any corrections exist
828
+ if corrections:
829
+ correction_text = ", ".join([f"{k}: {v}" for k, v in corrections.items()])
830
+ stats_parts.append(f"<div><strong>πŸ”„ Corrections:</strong> {correction_text}</div>")
831
+
832
+ stats = (
833
+ "<div style='display:flex; gap:12px; flex-wrap:wrap;'>"
834
+ + "".join(stats_parts) +
835
+ "</div>"
836
+ )
837
+ return html, pos, stats
838
+
839
+ def _comment_ui_state(records: list, idx: int):
840
+ """Return (row_update, note_value) based on current record state."""
841
+ if not records:
842
+ return gr.update(visible=False), ""
843
+ idx = max(0, min(idx, len(records) - 1))
844
+ r = records[idx]
845
+ is_incorrect = (r.get("is_correct") is False) if isinstance(r, dict) else (getattr(r, "is_correct", None) is False)
846
+ if not is_incorrect:
847
+ return gr.update(visible=False), ""
848
+ note = (r.get("verifier_notes") or "") if isinstance(r, dict) else (getattr(r, "verifier_notes", "") or "")
849
+ return gr.update(visible=True), str(note)
850
+
851
+ def _export_conv_records_to_json(meta: dict, records: list):
852
+ """Write reviewed conversation verification results to a JSON file and return its path."""
853
+ import json
854
+ import os
855
+ from datetime import datetime
856
+
857
+ export_dir = os.path.join(os.getcwd(), "verification_sessions")
858
+ os.makedirs(export_dir, exist_ok=True)
859
+
860
+ session_id = (meta or {}).get("session_id") or "conversation_verification"
861
+ export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.json"
862
+ export_path = os.path.join(export_dir, export_filename)
863
+
864
+ payload = {
865
+ **(meta or {}),
866
+ "verification_records": records or [],
867
+ }
868
+
869
+ with open(export_path, "w", encoding="utf-8") as f:
870
+ json.dump(payload, f, ensure_ascii=False, indent=2, default=str)
871
+ return export_path
872
+
873
+ def _export_conv_records_to_csv(meta: dict, records: list):
874
+ """Write reviewed conversation verification results to a CSV file and return its path."""
875
+ import csv
876
+ import os
877
+ from datetime import datetime
878
+
879
+ export_dir = os.path.join(os.getcwd(), "verification_exports")
880
+ os.makedirs(export_dir, exist_ok=True)
881
+
882
+ session_id = (meta or {}).get("session_id") or "conversation_verification"
883
+ export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.csv"
884
+ export_path = os.path.join(export_dir, export_filename)
885
+
886
+ fieldnames = [
887
+ "session_id",
888
+ "patient_name",
889
+ "patient_phone",
890
+ "verifier_name",
891
+ "start_time",
892
+ "exchange_number",
893
+ "exchange_id",
894
+ "original_classification",
895
+ "original_confidence",
896
+ "is_correct",
897
+ "correct_classification",
898
+ "verifier_notes",
899
+ "user_message",
900
+ "assistant_response",
901
+ "provider_summary",
902
+ ]
903
+
904
+ with open(export_path, "w", encoding="utf-8", newline="") as f:
905
+ w = csv.DictWriter(f, fieldnames=fieldnames)
906
+ w.writeheader()
907
+ for r in records or []:
908
+ # Include provider_summary only for RED cases
909
+ provider_summary = ""
910
+ if r.get("original_classification", "").upper() == "RED":
911
+ provider_summary = r.get("provider_summary") or ""
912
+
913
+ row = {
914
+ "session_id": (meta or {}).get("session_id"),
915
+ "patient_name": (meta or {}).get("patient_name"),
916
+ "patient_phone": (meta or {}).get("patient_phone") or "",
917
+ "verifier_name": (meta or {}).get("verifier_name"),
918
+ "start_time": (meta or {}).get("start_time"),
919
+ "exchange_number": r.get("exchange_number"),
920
+ "exchange_id": r.get("exchange_id") or r.get("record_id"),
921
+ "original_classification": r.get("original_classification"),
922
+ "original_confidence": r.get("original_confidence"),
923
+ "is_correct": r.get("is_correct"),
924
+ "correct_classification": r.get("correct_classification") or "",
925
+ "verifier_notes": r.get("verifier_notes") or "",
926
+ "user_message": r.get("user_message"),
927
+ "assistant_response": r.get("assistant_response"),
928
+ "provider_summary": provider_summary,
929
+ }
930
+ w.writerow(row)
931
+ return export_path
932
+
933
+ def _generate_conv_verification(session: SimplifiedSessionData):
934
+ if session is None or not hasattr(session.app_instance, "conversation_logger"):
935
+ return None, [], 0, "❌ No session/conversation found", "", ""
936
+ if not session.app_instance.conversation_logger.entries:
937
+ return None, [], 0, "⚠️ No exchanges to verify yet", "", ""
938
+
939
+ manager = ConversationVerificationManager()
940
+ vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
941
+
942
+ # Get patient phone from app if available
943
+ patient_phone = ""
944
+ if hasattr(session.app_instance, 'patient_info'):
945
+ patient_phone = session.app_instance.patient_info.get("phone") or ""
946
+
947
+ meta = {
948
+ "session_id": vs.session_id,
949
+ "patient_name": vs.patient_name,
950
+ "patient_phone": patient_phone,
951
+ "verifier_name": vs.verifier_name,
952
+ "start_time": vs.start_time.isoformat() if hasattr(vs, "start_time") else None,
953
+ }
954
+
955
+ # Get provider summary if available (for RED cases)
956
+ provider_summary_text = ""
957
+ if hasattr(session.app_instance, 'get_last_provider_summary'):
958
+ summary = session.app_instance.get_last_provider_summary()
959
+ if summary and hasattr(session.app_instance, 'provider_summary_generator'):
960
+ provider_summary_text = session.app_instance.provider_summary_generator.format_for_export(summary)
961
+
962
+ records_as_dicts = [
963
+ {
964
+ "exchange_id": r.exchange_id,
965
+ "exchange_number": r.exchange_number,
966
+ "record_id": r.exchange_id,
967
+ "timestamp": r.timestamp,
968
+ "user_message": r.user_message,
969
+ "assistant_response": r.assistant_response,
970
+ "original_classification": r.original_classification,
971
+ "original_confidence": r.original_confidence,
972
+ "original_indicators": r.original_indicators,
973
+ "original_reasoning": r.original_reasoning,
974
+ "is_correct": r.is_correct,
975
+ "correct_classification": r.correct_classification,
976
+ "correction_reason": r.correction_reason,
977
+ "verifier_notes": r.verifier_notes,
978
+ "provider_summary": provider_summary_text if r.original_classification.upper() == "RED" else "",
979
+ }
980
+ for r in vs.verification_records
981
+ ]
982
+ html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
983
+ return meta, records_as_dicts, 0, f"βœ… Generated session `{vs.session_id}`", html, pos, stats
984
+
985
+ def _mark_conv_correct(records: list, idx: int):
986
+ if not records:
987
+ return records, idx, "", "", "", gr.update(visible=False), "", ""
988
+ idx = max(0, min(idx, len(records) - 1))
989
+ if isinstance(records[idx], dict):
990
+ records[idx]["is_correct"] = True
991
+ # clear comment and correct_classification when marked correct (avoid stale data)
992
+ records[idx]["verifier_notes"] = ""
993
+ records[idx]["correct_classification"] = None
994
+ html, pos, stats = _render_conv_exchange(records, idx)
995
+ row_upd, note_val = _comment_ui_state(records, idx)
996
+ return records, idx, "βœ… Marked correct", html, pos, stats, row_upd, note_val, ""
997
+
998
+ def _mark_conv_incorrect(records: list, idx: int):
999
+ if not records:
1000
+ return records, idx, "", "", "", gr.update(visible=False), "", ""
1001
+ idx = max(0, min(idx, len(records) - 1))
1002
+ if isinstance(records[idx], dict):
1003
+ records[idx]["is_correct"] = False
1004
+ html, pos, stats = _render_conv_exchange(records, idx)
1005
+ row_upd, note_val = _comment_ui_state(records, idx)
1006
+ # Get existing correct_classification if any
1007
+ existing_classification = ""
1008
+ if isinstance(records[idx], dict):
1009
+ correct_class = records[idx].get("correct_classification")
1010
+ if correct_class:
1011
+ # Map back to display text
1012
+ reverse_map = {
1013
+ "GREEN": "🟒 Should be GREEN - No distress",
1014
+ "YELLOW": "🟑 Should be YELLOW - Needs clarification",
1015
+ "RED": "πŸ”΄ Should be RED - Spiritual distress"
1016
+ }
1017
+ existing_classification = reverse_map.get(correct_class, "")
1018
+ return records, idx, "❌ Marked incorrect", html, pos, stats, row_upd, note_val, existing_classification
1019
+
1020
+ def _show_incorrect_comment_ui(records: list, idx: int):
1021
+ """Mark incorrect and open the comment row, pre-filling any existing note."""
1022
+ records, idx, status, html, pos, stats, _row, note, existing_classification = _mark_conv_incorrect(records, idx)
1023
+ return records, idx, status, html, pos, stats, gr.update(visible=True), note, existing_classification
1024
+
1025
+ def _save_incorrect_comment(records: list, idx: int, note: str, correct_classification: str):
1026
+ if not records:
1027
+ return records, idx, "", "", "", "", gr.update(visible=False), "", ""
1028
+ idx = max(0, min(idx, len(records) - 1))
1029
+ if isinstance(records[idx], dict):
1030
+ records[idx]["verifier_notes"] = (note or "").strip()
1031
+ # Map display text to classification code
1032
+ classification_map = {
1033
+ "🟒 Should be GREEN - No distress": "GREEN",
1034
+ "🟑 Should be YELLOW - Needs clarification": "YELLOW",
1035
+ "πŸ”΄ Should be RED - Spiritual distress": "RED"
1036
+ }
1037
+ if correct_classification and correct_classification in classification_map:
1038
+ records[idx]["correct_classification"] = classification_map[correct_classification]
1039
+ html, pos, stats = _render_conv_exchange(records, idx)
1040
+ row_upd, note_val = _comment_ui_state(records, idx)
1041
+ # keep row visible after save (since still incorrect)
1042
+ return records, idx, "πŸ’Ύ Comment saved", html, pos, stats, row_upd, note_val, ""
1043
+
1044
+ def _download_reviewed_json(meta: dict, records: list):
1045
+ return _export_conv_records_to_json(meta, records)
1046
+
1047
+ def _download_reviewed_csv(meta: dict, records: list):
1048
+ return _export_conv_records_to_csv(meta, records)
1049
+
1050
+ def _nav_conv(records: list, idx: int, delta: int):
1051
+ if not records:
1052
+ return idx, "", "", "", gr.update(visible=False), "", ""
1053
+ idx = max(0, min(idx + delta, len(records) - 1))
1054
+ html, pos, stats = _render_conv_exchange(records, idx)
1055
+ row_upd, note_val = _comment_ui_state(records, idx)
1056
+ # Get existing correct_classification if any
1057
+ existing_classification = ""
1058
+ if isinstance(records[idx], dict):
1059
+ correct_class = records[idx].get("correct_classification")
1060
+ if correct_class:
1061
+ reverse_map = {
1062
+ "GREEN": "🟒 Should be GREEN - No distress",
1063
+ "YELLOW": "🟑 Should be YELLOW - Needs clarification",
1064
+ "RED": "πŸ”΄ Should be RED - Spiritual distress"
1065
+ }
1066
+ existing_classification = reverse_map.get(correct_class, "")
1067
+ return idx, html, pos, stats, row_upd, note_val, existing_classification