MikelWL commited on
Commit
01ed8e2
·
1 Parent(s): e96ef41

UX: hide ids + human turn gating + save flash

Browse files
backend/api/conversation_service.py CHANGED
@@ -407,6 +407,24 @@ class ConversationService:
407
  except Exception as e:
408
  logger.error(f"Failed to generate human-chat greeting: {e}")
409
  # It's OK to proceed without a greeting.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  return True
412
 
 
407
  except Exception as e:
408
  logger.error(f"Failed to generate human-chat greeting: {e}")
409
  # It's OK to proceed without a greeting.
410
+ else:
411
+ # If the AI is the patient, inject a short deterministic start so "Start" feels responsive
412
+ # without spending an LLM call on a trivial greeting.
413
+ try:
414
+ await self._append_and_broadcast_transcript(
415
+ conversation_id=conversation_id,
416
+ role="system",
417
+ persona="System",
418
+ content="You call the patient, and they picked up the phone.",
419
+ )
420
+ await self._append_and_broadcast_transcript(
421
+ conversation_id=conversation_id,
422
+ role="patient",
423
+ persona=patient_persona.get("name", "Patient"),
424
+ content="Hello?",
425
+ )
426
+ except Exception as e:
427
+ logger.error(f"Failed to inject human-chat AI-patient starter messages: {e}")
428
 
429
  return True
430
 
frontend/pages/config_view.py CHANGED
@@ -32,11 +32,12 @@ def get_config_view_js() -> str:
32
  }
33
  return [];
34
  });
35
- const [patientPromptAddition, setPatientPromptAddition] = React.useState(() => {
36
- const data = getPatientPersonaData(existing || {}, initialEditorPatientId) || {};
37
- return (typeof data.prompt_addition === 'string') ? data.prompt_addition : '';
38
- });
39
- const [savedAt, setSavedAt] = React.useState(existing?.saved_at || null);
 
40
 
41
  const surveyorQuestionBankText = React.useMemo(() => {
42
  const lines = (surveyorQuestionItems || [])
@@ -136,6 +137,8 @@ def get_config_view_js() -> str:
136
  });
137
  saveConfig(cfg);
138
  setSavedAt(cfg.saved_at);
 
 
139
  };
140
 
141
  const TabNav = () => {
@@ -177,9 +180,9 @@ def get_config_view_js() -> str:
177
  value={selectedSurveyorId}
178
  onChange={(e) => setSelectedSurveyorId(e.target.value)}
179
  >
180
- {(personas.surveyors.length ? personas.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
181
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
182
- ))}
183
  </select>
184
  </div>
185
 
@@ -272,9 +275,9 @@ def get_config_view_js() -> str:
272
  value={selectedPatientId}
273
  onChange={(e) => setSelectedPatientId(e.target.value)}
274
  >
275
- {(personas.patients.length ? personas.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
276
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
277
- ))}
278
  </select>
279
 
280
  <label className="block text-sm font-semibold text-slate-700 mt-4 mb-2">Patient prompt addition (optional)</label>
@@ -299,17 +302,18 @@ def get_config_view_js() -> str:
299
  </div>
300
  )}
301
 
302
- <div className="mt-6 flex items-center justify-between gap-4">
303
- <div className="text-xs text-slate-500">
304
- {savedAt ? `Saved: ${new Date(savedAt).toLocaleString()}` : 'Not saved yet.'}
305
- </div>
306
- <button
307
- onClick={onSave}
308
- className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg font-semibold transition-all shadow"
309
- >
310
- Save Configuration
311
- </button>
312
- </div>
 
313
  </div>
314
  );
315
  }
 
32
  }
33
  return [];
34
  });
35
+ const [patientPromptAddition, setPatientPromptAddition] = React.useState(() => {
36
+ const data = getPatientPersonaData(existing || {}, initialEditorPatientId) || {};
37
+ return (typeof data.prompt_addition === 'string') ? data.prompt_addition : '';
38
+ });
39
+ const [savedAt, setSavedAt] = React.useState(existing?.saved_at || null);
40
+ const [saveFlash, setSaveFlash] = React.useState(false);
41
 
42
  const surveyorQuestionBankText = React.useMemo(() => {
43
  const lines = (surveyorQuestionItems || [])
 
137
  });
138
  saveConfig(cfg);
139
  setSavedAt(cfg.saved_at);
140
+ setSaveFlash(true);
141
+ setTimeout(() => setSaveFlash(false), 1000);
142
  };
143
 
144
  const TabNav = () => {
 
180
  value={selectedSurveyorId}
181
  onChange={(e) => setSelectedSurveyorId(e.target.value)}
182
  >
183
+ {(personas.surveyors.length ? personas.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
184
+ <option key={p.id} value={p.id}>{p.name}</option>
185
+ ))}
186
  </select>
187
  </div>
188
 
 
275
  value={selectedPatientId}
276
  onChange={(e) => setSelectedPatientId(e.target.value)}
277
  >
278
+ {(personas.patients.length ? personas.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
279
+ <option key={p.id} value={p.id}>{p.name}</option>
280
+ ))}
281
  </select>
282
 
283
  <label className="block text-sm font-semibold text-slate-700 mt-4 mb-2">Patient prompt addition (optional)</label>
 
302
  </div>
303
  )}
304
 
305
+ <div className="mt-6 flex items-center justify-between gap-4">
306
+ <div className="text-xs text-slate-500">
307
+ {savedAt ? `Saved: ${new Date(savedAt).toLocaleString()}` : 'Not saved yet.'}
308
+ </div>
309
+ <button
310
+ onClick={onSave}
311
+ disabled={saveFlash}
312
+ className={`${saveFlash ? 'bg-emerald-600 text-white' : 'bg-blue-600 hover:bg-blue-700 text-white'} disabled:opacity-90 px-5 py-2.5 rounded-lg font-semibold transition-all shadow`}
313
+ >
314
+ {saveFlash ? 'Saved successfully' : 'Save Configuration'}
315
+ </button>
316
+ </div>
317
  </div>
318
  );
319
  }
frontend/pages/shared_views.py CHANGED
@@ -44,7 +44,7 @@ def get_shared_views_js() -> str:
44
  onChange={(e) => setSurveyorId(e.target.value)}
45
  >
46
  {(personaCatalog?.surveyors?.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
47
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
48
  ))}
49
  </select>
50
  </div>
@@ -57,7 +57,7 @@ def get_shared_views_js() -> str:
57
  onChange={(e) => setPatientId(e.target.value)}
58
  >
59
  {(personaCatalog?.patients?.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
60
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
61
  ))}
62
  </select>
63
  </div>
@@ -99,7 +99,7 @@ def get_shared_views_js() -> str:
99
  </div>
100
  )}
101
 
102
- <div ref={transcriptContainerRef} onScroll={onTranscriptScroll} className="space-y-3 h-96 overflow-y-auto bg-slate-50 p-4 rounded-lg">
103
  {activeMessages.length === 0 && (
104
  <div className="text-center text-slate-400 py-20">
105
  {activePage === 'main'
@@ -107,24 +107,31 @@ def get_shared_views_js() -> str:
107
  : 'Paste or upload text above, then click “Run analysis”.'}
108
  </div>
109
  )}
110
- {activeMessages.map((msg, idx) => (
111
- <div
112
- key={idx}
113
- id={`msg-${idx}`}
114
- className={`p-4 rounded-lg shadow-sm ${msg.role === 'surveyor' ? 'bg-blue-50 border-l-4 border-blue-500' : 'bg-green-50 border-l-4 border-green-500'} ${highlightedEvidence?.message_index === idx ? 'ring-2 ring-yellow-400' : ''}`}
115
- >
116
- <div className="flex items-center gap-2 mb-1">
117
- <span className="font-semibold text-sm">
118
- {msg.role === 'surveyor' ? '🔵' : '🟢'} {msg.persona}
119
- </span>
120
- <span className="text-xs text-slate-500">{msg.time}</span>
121
- </div>
122
- <p className="text-slate-700 whitespace-pre-wrap">
123
- {renderHighlightedText(msg.text, highlightedEvidence?.message_index === idx ? highlightedEvidence.sentence : null)}
124
- </p>
125
- </div>
126
- ))}
127
- </div>
 
 
 
 
 
 
 
128
  </div>
129
  );
130
  }
@@ -158,16 +165,39 @@ def get_shared_views_js() -> str:
158
 
159
  return (
160
  <div className="bg-white rounded-lg shadow-lg p-6">
161
- <div className="flex items-center gap-2 mb-4">
162
- <span className="text-2xl">🧑‍💬</span>
163
- <h2 className="text-xl font-bold text-slate-800">Human-to-AI</h2>
164
- {humanChatActive && <span className="ml-auto text-green-600 font-medium animate-pulse">● LIVE</span>}
165
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- <div className="mb-4 grid grid-cols-2 gap-3">
168
- <div>
169
- <div className="text-xs font-semibold text-slate-600 mb-1">AI role</div>
170
- <div className="flex gap-2">
171
  <button
172
  type="button"
173
  disabled={humanChatActive}
@@ -185,62 +215,73 @@ def get_shared_views_js() -> str:
185
  Patient
186
  </button>
187
  </div>
188
- </div>
189
- <div className="grid grid-cols-2 gap-2">
190
- <div>
191
- <div className="text-xs font-semibold text-slate-600 mb-1">Surveyor</div>
192
- <select
193
- className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
194
- value={surveyorId}
195
- disabled={humanChatActive}
196
- onChange={(e) => setSurveyorId(e.target.value)}
197
- >
198
- {(personaCatalog?.surveyors?.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
199
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
200
- ))}
201
- </select>
202
- </div>
203
- <div>
204
- <div className="text-xs font-semibold text-slate-600 mb-1">Patient</div>
205
- <select
206
- className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
207
- value={patientId}
208
- disabled={humanChatActive}
209
- onChange={(e) => setPatientId(e.target.value)}
210
- >
211
- {(personaCatalog?.patients?.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
212
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
213
- ))}
214
- </select>
215
- </div>
216
- </div>
217
- </div>
 
 
 
218
 
219
- <div className="mb-4">
220
- {!humanChatActive ? (
221
- <div className="text-sm text-slate-500">
222
- Click “Start” to begin. {aiRole === 'patient' ? 'Type as the surveyor.' : 'Type as the patient.'} Use “End session” to run analysis.
223
- </div>
224
- ) : (
225
- <div className="flex items-end gap-3">
226
- <textarea
227
- className="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white h-20"
228
- placeholder={aiRole === 'patient' ? "Type your message as the surveyor… (Ctrl/⌘+Enter to send)" : "Type your message as the patient… (Ctrl/⌘+Enter to send)"}
229
- value={humanDraft}
230
- onChange={(e) => setHumanDraft(e.target.value)}
231
- onKeyDown={onKeyDown}
232
- />
233
- <button
234
- type="button"
235
- onClick={sendHumanChatMessage}
236
- disabled={!humanDraft.trim()}
237
- className="bg-emerald-600 hover:bg-emerald-700 disabled:bg-slate-300 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow"
238
- >
239
- Send
240
- </button>
241
- </div>
242
- )}
243
- </div>
 
 
 
 
 
 
 
 
244
 
245
  <div ref={transcriptContainerRef} onScroll={onTranscriptScroll} className="space-y-3 h-96 overflow-y-auto bg-slate-50 p-4 rounded-lg">
246
  {activeMessages.length === 0 && (
@@ -248,24 +289,31 @@ def get_shared_views_js() -> str:
248
  {humanChatActive ? '🔄 Waiting for the first messages...' : '👋 Start the session to begin.'}
249
  </div>
250
  )}
251
- {activeMessages.map((msg, idx) => (
252
- <div
253
- key={idx}
254
- id={`msg-${idx}`}
255
- className={`p-4 rounded-lg shadow-sm ${msg.role === 'surveyor' ? 'bg-blue-50 border-l-4 border-blue-500' : 'bg-green-50 border-l-4 border-green-500'} ${highlightedEvidence?.message_index === idx ? 'ring-2 ring-yellow-400' : ''}`}
256
- >
257
- <div className="flex items-center gap-2 mb-1">
258
- <span className="font-semibold text-sm">
259
- {msg.role === 'surveyor' ? '🔵' : '🟢'} {msg.persona}
260
- </span>
261
- <span className="text-xs text-slate-500">{msg.time}</span>
262
- </div>
263
- <p className="text-slate-700 whitespace-pre-wrap">
264
- {renderHighlightedText(msg.text, highlightedEvidence?.message_index === idx ? highlightedEvidence.sentence : null)}
265
- </p>
266
- </div>
267
- ))}
268
- </div>
 
 
 
 
 
 
 
269
  </div>
270
  );
271
  }
 
44
  onChange={(e) => setSurveyorId(e.target.value)}
45
  >
46
  {(personaCatalog?.surveyors?.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
47
+ <option key={p.id} value={p.id}>{p.name}</option>
48
  ))}
49
  </select>
50
  </div>
 
57
  onChange={(e) => setPatientId(e.target.value)}
58
  >
59
  {(personaCatalog?.patients?.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
60
+ <option key={p.id} value={p.id}>{p.name}</option>
61
  ))}
62
  </select>
63
  </div>
 
99
  </div>
100
  )}
101
 
102
+ <div ref={transcriptContainerRef} onScroll={onTranscriptScroll} className="space-y-3 h-96 overflow-y-auto bg-slate-50 p-4 rounded-lg">
103
  {activeMessages.length === 0 && (
104
  <div className="text-center text-slate-400 py-20">
105
  {activePage === 'main'
 
107
  : 'Paste or upload text above, then click “Run analysis”.'}
108
  </div>
109
  )}
110
+ {activeMessages.map((msg, idx) => {
111
+ const isSystem = msg.role === 'system';
112
+ const isSurveyor = msg.role === 'surveyor';
113
+ const tone = isSystem
114
+ ? 'bg-slate-100 border-l-4 border-slate-400'
115
+ : (isSurveyor ? 'bg-blue-50 border-l-4 border-blue-500' : 'bg-green-50 border-l-4 border-green-500');
116
+ const icon = isSystem ? '⚙️' : (isSurveyor ? '🔵' : '🟢');
117
+ return (
118
+ <div
119
+ key={idx}
120
+ id={`msg-${idx}`}
121
+ className={`p-4 rounded-lg shadow-sm ${tone} ${highlightedEvidence?.message_index === idx ? 'ring-2 ring-yellow-400' : ''}`}
122
+ >
123
+ <div className="flex items-center gap-2 mb-1">
124
+ <span className="font-semibold text-sm">
125
+ {icon} {msg.persona}
126
+ </span>
127
+ <span className="text-xs text-slate-500">{msg.time}</span>
128
+ </div>
129
+ <p className="text-slate-700 whitespace-pre-wrap">
130
+ {renderHighlightedText(msg.text, highlightedEvidence?.message_index === idx ? highlightedEvidence.sentence : null)}
131
+ </p>
132
+ </div>
133
+ )})}
134
+ </div>
135
  </div>
136
  );
137
  }
 
165
 
166
  return (
167
  <div className="bg-white rounded-lg shadow-lg p-6">
168
+ <div className="flex items-center gap-2 mb-4">
169
+ <span className="text-2xl">🧑‍💬</span>
170
+ <h2 className="text-xl font-bold text-slate-800">Human-to-AI</h2>
171
+ {humanChatActive && <span className="ml-auto text-green-600 font-medium animate-pulse">● LIVE</span>}
172
+ </div>
173
+
174
+ {(() => {
175
+ const lastRole = (activeMessages && activeMessages.length) ? activeMessages[activeMessages.length - 1].role : null;
176
+ const humanRole = (aiRole === 'patient') ? 'surveyor' : 'patient';
177
+ const humanTurn = humanChatActive && lastRole === (aiRole === 'patient' ? 'patient' : 'surveyor');
178
+ const label = !humanChatActive
179
+ ? null
180
+ : (humanTurn ? 'Your turn' : 'AI turn');
181
+ const cls = humanTurn
182
+ ? 'bg-emerald-100 text-emerald-800 border-emerald-200'
183
+ : 'bg-slate-100 text-slate-800 border-slate-200';
184
+ const note = !humanChatActive
185
+ ? null
186
+ : (humanTurn ? `You are the ${humanRole}.` : `AI is the ${aiRole}.`);
187
+ return label ? (
188
+ <div className="mb-4 flex items-center justify-between gap-3">
189
+ <div className={`inline-flex items-center gap-2 border px-3 py-1.5 rounded-full text-sm font-semibold ${cls}`}>
190
+ {label}
191
+ </div>
192
+ <div className="text-xs text-slate-500">{note}</div>
193
+ </div>
194
+ ) : null;
195
+ })()}
196
 
197
+ <div className="mb-4 grid grid-cols-2 gap-3">
198
+ <div>
199
+ <div className="text-xs font-semibold text-slate-600 mb-1">AI role</div>
200
+ <div className="flex gap-2">
201
  <button
202
  type="button"
203
  disabled={humanChatActive}
 
215
  Patient
216
  </button>
217
  </div>
218
+ </div>
219
+ <div>
220
+ {aiRole === 'patient' ? (
221
+ <div>
222
+ <div className="text-xs font-semibold text-slate-600 mb-1">AI patient</div>
223
+ <select
224
+ className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
225
+ value={patientId}
226
+ disabled={humanChatActive}
227
+ onChange={(e) => setPatientId(e.target.value)}
228
+ >
229
+ {(personaCatalog?.patients?.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
230
+ <option key={p.id} value={p.id}>{p.name}</option>
231
+ ))}
232
+ </select>
233
+ </div>
234
+ ) : (
235
+ <div>
236
+ <div className="text-xs font-semibold text-slate-600 mb-1">AI surveyor</div>
237
+ <select
238
+ className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
239
+ value={surveyorId}
240
+ disabled={humanChatActive}
241
+ onChange={(e) => setSurveyorId(e.target.value)}
242
+ >
243
+ {(personaCatalog?.surveyors?.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
244
+ <option key={p.id} value={p.id}>{p.name}</option>
245
+ ))}
246
+ </select>
247
+ </div>
248
+ )}
249
+ </div>
250
+ </div>
251
 
252
+ <div className="mb-4">
253
+ {!humanChatActive ? (
254
+ <div className="text-sm text-slate-500">
255
+ Click “Start” to begin. {aiRole === 'patient' ? 'Type as the surveyor.' : 'Type as the patient.'} Use “End session” to run analysis.
256
+ </div>
257
+ ) : (
258
+ (() => {
259
+ const lastRole = (activeMessages && activeMessages.length) ? activeMessages[activeMessages.length - 1].role : null;
260
+ const humanTurn = humanChatActive && lastRole === (aiRole === 'patient' ? 'patient' : 'surveyor');
261
+ const disabled = !humanTurn;
262
+ return (
263
+ <div className="flex items-end gap-3">
264
+ <textarea
265
+ className="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white h-20 disabled:bg-slate-100"
266
+ placeholder={aiRole === 'patient' ? "Type your message as the surveyor… (Ctrl/⌘+Enter to send)" : "Type your message as the patient… (Ctrl/⌘+Enter to send)"}
267
+ value={humanDraft}
268
+ onChange={(e) => setHumanDraft(e.target.value)}
269
+ onKeyDown={(e) => { if (disabled) return; onKeyDown(e); }}
270
+ disabled={disabled}
271
+ />
272
+ <button
273
+ type="button"
274
+ onClick={sendHumanChatMessage}
275
+ disabled={disabled || !humanDraft.trim()}
276
+ className="bg-emerald-600 hover:bg-emerald-700 disabled:bg-slate-300 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow"
277
+ >
278
+ Send
279
+ </button>
280
+ </div>
281
+ );
282
+ })()
283
+ )}
284
+ </div>
285
 
286
  <div ref={transcriptContainerRef} onScroll={onTranscriptScroll} className="space-y-3 h-96 overflow-y-auto bg-slate-50 p-4 rounded-lg">
287
  {activeMessages.length === 0 && (
 
289
  {humanChatActive ? '🔄 Waiting for the first messages...' : '👋 Start the session to begin.'}
290
  </div>
291
  )}
292
+ {activeMessages.map((msg, idx) => {
293
+ const isSystem = msg.role === 'system';
294
+ const isSurveyor = msg.role === 'surveyor';
295
+ const tone = isSystem
296
+ ? 'bg-slate-100 border-l-4 border-slate-400'
297
+ : (isSurveyor ? 'bg-blue-50 border-l-4 border-blue-500' : 'bg-green-50 border-l-4 border-green-500');
298
+ const icon = isSystem ? '⚙️' : (isSurveyor ? '🔵' : '🟢');
299
+ return (
300
+ <div
301
+ key={idx}
302
+ id={`msg-${idx}`}
303
+ className={`p-4 rounded-lg shadow-sm ${tone} ${highlightedEvidence?.message_index === idx ? 'ring-2 ring-yellow-400' : ''}`}
304
+ >
305
+ <div className="flex items-center gap-2 mb-1">
306
+ <span className="font-semibold text-sm">
307
+ {icon} {msg.persona}
308
+ </span>
309
+ <span className="text-xs text-slate-500">{msg.time}</span>
310
+ </div>
311
+ <p className="text-slate-700 whitespace-pre-wrap">
312
+ {renderHighlightedText(msg.text, highlightedEvidence?.message_index === idx ? highlightedEvidence.sentence : null)}
313
+ </p>
314
+ </div>
315
+ )})}
316
+ </div>
317
  </div>
318
  );
319
  }