MikelWL commited on
Commit
a6ea22d
·
1 Parent(s): b5a872a

UI: move persona selectors into panels + add AI role toggle

Browse files
backend/api/conversation_service.py CHANGED
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
50
  _SENTENCE_SEGMENTER = pysbd.Segmenter(language="en", clean=False)
51
 
52
  SURVEYOR_MAX_TOKENS = 140
 
53
 
54
  SURVEYOR_STYLE_GUIDANCE = (
55
  "Constraints:\n"
@@ -59,6 +60,13 @@ SURVEYOR_STYLE_GUIDANCE = (
59
  "- Avoid long explanations or multi-paragraph monologues.\n"
60
  )
61
 
 
 
 
 
 
 
 
62
 
63
  def _split_sentences(text: str) -> List[str]:
64
  normalized = " ".join((text or "").split())
@@ -291,6 +299,7 @@ class HumanChatInfo:
291
  patient_prompt_addition: Optional[str] = None
292
  surveyor_attributes: List[str] = field(default_factory=list)
293
  surveyor_question_bank: Optional[str] = None
 
294
  asked_question_ids: List[str] = field(default_factory=list)
295
  lock: asyncio.Lock = field(default_factory=asyncio.Lock)
296
  client: Any = None
@@ -333,6 +342,7 @@ class ConversationService:
333
  patient_prompt_addition: Optional[str] = None,
334
  surveyor_attributes: Optional[List[str]] = None,
335
  surveyor_question_bank: Optional[str] = None,
 
336
  ) -> bool:
337
  """Start a new human-to-surveyor chat session."""
338
  if conversation_id in self.active_conversations or conversation_id in self.active_human_chats:
@@ -349,6 +359,8 @@ class ConversationService:
349
  resolved_model = model or self.settings.llm.model
350
  resolved_backend = self.settings.llm.backend
351
 
 
 
352
  chat_info = HumanChatInfo(
353
  conversation_id=conversation_id,
354
  surveyor_persona_id=surveyor_persona_id,
@@ -359,6 +371,7 @@ class ConversationService:
359
  patient_prompt_addition=patient_prompt_addition,
360
  surveyor_attributes=[s.strip() for s in (surveyor_attributes or []) if isinstance(s, str) and s.strip()],
361
  surveyor_question_bank=surveyor_question_bank if isinstance(surveyor_question_bank, str) and surveyor_question_bank.strip() else None,
 
362
  status=ConversationStatus.STARTING,
363
  created_at=datetime.now(),
364
  )
@@ -374,25 +387,26 @@ class ConversationService:
374
  await self._send_status_update(conversation_id, ConversationStatus.STARTING)
375
  await self._send_status_update(conversation_id, ConversationStatus.RUNNING)
376
 
377
- # Optional: generate an initial greeting + first question.
378
- try:
379
- greeting = await self._generate_human_chat_surveyor_message(
380
- chat_info,
381
- transcript=[],
382
- user_prompt=(
383
- "Please greet the patient briefly and ask your first survey question.\n\n"
384
- f"{SURVEYOR_STYLE_GUIDANCE}"
385
- ),
386
- )
387
- await self._append_and_broadcast_transcript(
388
- conversation_id=conversation_id,
389
- role="surveyor",
390
- persona=surveyor_persona.get("name", "Surveyor"),
391
- content=greeting,
392
- )
393
- except Exception as e:
394
- logger.error(f"Failed to generate human-chat greeting: {e}")
395
- # It's OK to proceed without a greeting.
 
396
 
397
  return True
398
 
@@ -410,6 +424,38 @@ class ConversationService:
410
  patient_persona = self.persona_system.get_persona(chat_info.patient_persona_id) or {}
411
  surveyor_persona = self.persona_system.get_persona(chat_info.surveyor_persona_id) or {}
412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  patient_label = patient_persona.get("name", "Patient")
414
  await self._append_and_broadcast_transcript(
415
  conversation_id=conversation_id,
@@ -563,6 +609,36 @@ class ConversationService:
563
 
564
  return cleaned or "I apologize—I'm having trouble responding right now. Could you repeat that?"
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  async def start_conversation(self,
567
  conversation_id: str,
568
  surveyor_persona_id: str,
 
50
  _SENTENCE_SEGMENTER = pysbd.Segmenter(language="en", clean=False)
51
 
52
  SURVEYOR_MAX_TOKENS = 140
53
+ PATIENT_MAX_TOKENS = 220
54
 
55
  SURVEYOR_STYLE_GUIDANCE = (
56
  "Constraints:\n"
 
60
  "- Avoid long explanations or multi-paragraph monologues.\n"
61
  )
62
 
63
+ PATIENT_STYLE_GUIDANCE = (
64
+ "Constraints:\n"
65
+ "- Be concise (1–3 short sentences).\n"
66
+ "- Answer the question as the patient; avoid medical advice.\n"
67
+ "- Stay consistent with your persona and scenario.\n"
68
+ )
69
+
70
 
71
  def _split_sentences(text: str) -> List[str]:
72
  normalized = " ".join((text or "").split())
 
299
  patient_prompt_addition: Optional[str] = None
300
  surveyor_attributes: List[str] = field(default_factory=list)
301
  surveyor_question_bank: Optional[str] = None
302
+ ai_role: str = "surveyor" # "surveyor" or "patient"
303
  asked_question_ids: List[str] = field(default_factory=list)
304
  lock: asyncio.Lock = field(default_factory=asyncio.Lock)
305
  client: Any = None
 
342
  patient_prompt_addition: Optional[str] = None,
343
  surveyor_attributes: Optional[List[str]] = None,
344
  surveyor_question_bank: Optional[str] = None,
345
+ ai_role: Optional[str] = None,
346
  ) -> bool:
347
  """Start a new human-to-surveyor chat session."""
348
  if conversation_id in self.active_conversations or conversation_id in self.active_human_chats:
 
359
  resolved_model = model or self.settings.llm.model
360
  resolved_backend = self.settings.llm.backend
361
 
362
+ resolved_ai_role = ai_role if ai_role in ("surveyor", "patient") else "surveyor"
363
+
364
  chat_info = HumanChatInfo(
365
  conversation_id=conversation_id,
366
  surveyor_persona_id=surveyor_persona_id,
 
371
  patient_prompt_addition=patient_prompt_addition,
372
  surveyor_attributes=[s.strip() for s in (surveyor_attributes or []) if isinstance(s, str) and s.strip()],
373
  surveyor_question_bank=surveyor_question_bank if isinstance(surveyor_question_bank, str) and surveyor_question_bank.strip() else None,
374
+ ai_role=resolved_ai_role,
375
  status=ConversationStatus.STARTING,
376
  created_at=datetime.now(),
377
  )
 
387
  await self._send_status_update(conversation_id, ConversationStatus.STARTING)
388
  await self._send_status_update(conversation_id, ConversationStatus.RUNNING)
389
 
390
+ # If the AI is the surveyor, we can optionally generate an initial greeting + first question.
391
+ if chat_info.ai_role == "surveyor":
392
+ try:
393
+ greeting = await self._generate_human_chat_surveyor_message(
394
+ chat_info,
395
+ transcript=[],
396
+ user_prompt=(
397
+ "Please greet the patient briefly and ask your first survey question.\n\n"
398
+ f"{SURVEYOR_STYLE_GUIDANCE}"
399
+ ),
400
+ )
401
+ await self._append_and_broadcast_transcript(
402
+ conversation_id=conversation_id,
403
+ role="surveyor",
404
+ persona=surveyor_persona.get("name", "Surveyor"),
405
+ content=greeting,
406
+ )
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
 
 
424
  patient_persona = self.persona_system.get_persona(chat_info.patient_persona_id) or {}
425
  surveyor_persona = self.persona_system.get_persona(chat_info.surveyor_persona_id) or {}
426
 
427
+ transcript = self.transcripts.get(conversation_id, [])
428
+
429
+ if chat_info.ai_role == "patient":
430
+ # Human is the surveyor; AI responds as the patient.
431
+ await self._append_and_broadcast_transcript(
432
+ conversation_id=conversation_id,
433
+ role="surveyor",
434
+ persona=f"{surveyor_persona.get('name', 'Surveyor')} (Human)",
435
+ content=text,
436
+ )
437
+
438
+ transcript = self.transcripts.get(conversation_id, [])
439
+ last_surveyor_msg = next((m for m in reversed(transcript) if m.get("role") == "surveyor"), None)
440
+ last_text = (last_surveyor_msg or {}).get("content", text)
441
+ reply = await self._generate_human_chat_patient_message(
442
+ chat_info,
443
+ transcript=transcript,
444
+ user_prompt=(
445
+ f"The interviewer just said: '{last_text}'. "
446
+ "Please respond naturally as your persona would.\n\n"
447
+ f"{PATIENT_STYLE_GUIDANCE}"
448
+ ),
449
+ )
450
+ await self._append_and_broadcast_transcript(
451
+ conversation_id=conversation_id,
452
+ role="patient",
453
+ persona=patient_persona.get("name", "Patient"),
454
+ content=reply,
455
+ )
456
+ return
457
+
458
+ # Default: Human is the patient; AI responds as the surveyor.
459
  patient_label = patient_persona.get("name", "Patient")
460
  await self._append_and_broadcast_transcript(
461
  conversation_id=conversation_id,
 
609
 
610
  return cleaned or "I apologize—I'm having trouble responding right now. Could you repeat that?"
611
 
612
+ async def _generate_human_chat_patient_message(
613
+ self,
614
+ chat_info: HumanChatInfo,
615
+ *,
616
+ transcript: List[Dict[str, Any]],
617
+ user_prompt: str,
618
+ ) -> str:
619
+ conversation_history = [
620
+ {"role": "assistant" if msg.get("role") == "patient" else "user", "content": msg.get("content", "")}
621
+ for msg in (transcript or [])
622
+ ]
623
+
624
+ system_prompt, prompt_with_history = self.persona_system.build_conversation_prompt(
625
+ persona_id=chat_info.patient_persona_id,
626
+ conversation_history=conversation_history,
627
+ user_prompt=user_prompt,
628
+ )
629
+
630
+ system_prompt = (system_prompt or "").strip()
631
+ if chat_info.patient_prompt_addition:
632
+ system_prompt = (system_prompt + "\n\nAdditional instructions:\n" + chat_info.patient_prompt_addition).strip()
633
+
634
+ response = await chat_info.client.generate(
635
+ prompt=prompt_with_history,
636
+ system_prompt=system_prompt,
637
+ max_tokens=PATIENT_MAX_TOKENS,
638
+ temperature=0.7,
639
+ )
640
+ return (response or "").strip() or "I'm sorry—I'm having trouble responding right now."
641
+
642
  async def start_conversation(self,
643
  conversation_id: str,
644
  surveyor_persona_id: str,
backend/api/conversation_ws.py CHANGED
@@ -386,6 +386,7 @@ async def handle_start_human_chat(data: dict, conversation_id: str):
386
  patient_persona_id = data.get("patient_persona_id")
387
  host = data.get("host")
388
  model = data.get("model")
 
389
  patient_prompt_addition = data.get("patient_prompt_addition")
390
  surveyor_attributes = data.get("surveyor_attributes")
391
  surveyor_question_bank = data.get("surveyor_question_bank")
@@ -407,6 +408,7 @@ async def handle_start_human_chat(data: dict, conversation_id: str):
407
  patient_prompt_addition=patient_prompt_addition,
408
  surveyor_attributes=surveyor_attributes,
409
  surveyor_question_bank=surveyor_question_bank,
 
410
  )
411
 
412
  if success:
 
386
  patient_persona_id = data.get("patient_persona_id")
387
  host = data.get("host")
388
  model = data.get("model")
389
+ ai_role = data.get("ai_role")
390
  patient_prompt_addition = data.get("patient_prompt_addition")
391
  surveyor_attributes = data.get("surveyor_attributes")
392
  surveyor_question_bank = data.get("surveyor_question_bank")
 
408
  patient_prompt_addition=patient_prompt_addition,
409
  surveyor_attributes=surveyor_attributes,
410
  surveyor_question_bank=surveyor_question_bank,
411
+ ai_role=ai_role,
412
  )
413
 
414
  if success:
frontend/pages/main_page.py CHANGED
@@ -115,7 +115,10 @@ def get_main_page_html(auth_enabled: bool = False) -> str:
115
  const bucket = (run && run[kind] && typeof run[kind] === 'object') ? run[kind] : {};
116
  return {
117
  surveyor_persona_id: bucket.surveyor_persona_id || cfg?.surveyor_persona_id || 'friendly_researcher_001',
118
- patient_persona_id: bucket.patient_persona_id || cfg?.patient_persona_id || 'cooperative_senior_001'
 
 
 
119
  };
120
  }
121
 
@@ -274,6 +277,7 @@ def get_main_page_html(auth_enabled: bool = False) -> str:
274
  const [mainPatientId, setMainPatientId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'ai_to_ai').patient_persona_id);
275
  const [humanSurveyorId, setHumanSurveyorId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'human_to_ai').surveyor_persona_id);
276
  const [humanPatientId, setHumanPatientId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'human_to_ai').patient_persona_id);
 
277
 
278
  const [historyBusy, setHistoryBusy] = useState(false);
279
  const [historyError, setHistoryError] = useState(null);
@@ -712,6 +716,7 @@ def get_main_page_html(auth_enabled: bool = False) -> str:
712
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
713
  wsRef.current.send(JSON.stringify({
714
  type: 'start_human_chat',
 
715
  surveyor_persona_id: surveyorId,
716
  patient_persona_id: patientId,
717
  surveyor_question_bank: surveyorQuestionBank || undefined,
@@ -1035,78 +1040,6 @@ def get_main_page_html(auth_enabled: bool = False) -> str:
1035
  <div className="bg-white rounded-lg shadow-lg p-4 mb-6">
1036
  <div className="flex items-center justify-between gap-4">
1037
  <PageNav active={activePage} onChange={setActivePage} />
1038
- {activePage === 'main' && (
1039
- <div className="flex items-end gap-3">
1040
- <div>
1041
- <div className="text-xs font-semibold text-slate-600 mb-1">Surveyor</div>
1042
- <select
1043
- className="border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
1044
- value={mainSurveyorId}
1045
- onChange={(e) => {
1046
- const next = e.target.value;
1047
- setMainSurveyorId(next);
1048
- persistRunSelection('ai_to_ai', { surveyor_persona_id: next });
1049
- }}
1050
- >
1051
- {(personaCatalog.surveyors.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
1052
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
1053
- ))}
1054
- </select>
1055
- </div>
1056
- <div>
1057
- <div className="text-xs font-semibold text-slate-600 mb-1">Patient</div>
1058
- <select
1059
- className="border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
1060
- value={mainPatientId}
1061
- onChange={(e) => {
1062
- const next = e.target.value;
1063
- setMainPatientId(next);
1064
- persistRunSelection('ai_to_ai', { patient_persona_id: next });
1065
- }}
1066
- >
1067
- {(personaCatalog.patients.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
1068
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
1069
- ))}
1070
- </select>
1071
- </div>
1072
- </div>
1073
- )}
1074
- {activePage === 'human' && (
1075
- <div className="flex items-end gap-3">
1076
- <div>
1077
- <div className="text-xs font-semibold text-slate-600 mb-1">Surveyor</div>
1078
- <select
1079
- className="border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
1080
- value={humanSurveyorId}
1081
- onChange={(e) => {
1082
- const next = e.target.value;
1083
- setHumanSurveyorId(next);
1084
- persistRunSelection('human_to_ai', { surveyor_persona_id: next });
1085
- }}
1086
- >
1087
- {(personaCatalog.surveyors.length ? personaCatalog.surveyors : [{id: 'friendly_researcher_001', name: 'Alex Thompson'}]).map(p => (
1088
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
1089
- ))}
1090
- </select>
1091
- </div>
1092
- <div>
1093
- <div className="text-xs font-semibold text-slate-600 mb-1">Patient scenario</div>
1094
- <select
1095
- className="border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
1096
- value={humanPatientId}
1097
- onChange={(e) => {
1098
- const next = e.target.value;
1099
- setHumanPatientId(next);
1100
- persistRunSelection('human_to_ai', { patient_persona_id: next });
1101
- }}
1102
- >
1103
- {(personaCatalog.patients.length ? personaCatalog.patients : [{id: 'cooperative_senior_001', name: 'Margaret Thompson'}]).map(p => (
1104
- <option key={p.id} value={p.id}>{p.name} ({p.id})</option>
1105
- ))}
1106
- </select>
1107
- </div>
1108
- </div>
1109
- )}
1110
  {(activePage === 'main' || activePage === 'human' || activePage === 'analyze' || activePage === 'history') && activeStatus === 'complete' && activeResources && (
1111
  <div className="flex items-center gap-2">
1112
  <button
@@ -1263,26 +1196,38 @@ def get_main_page_html(auth_enabled: bool = False) -> str:
1263
  </div>
1264
  ) : (
1265
  <div className="grid grid-cols-[2fr_1fr_2fr] gap-6 items-start">
1266
- {(activePage === 'human') ? (
1267
- <HumanChatPanel
1268
- humanChatActive={humanChatActive}
1269
- humanDraft={humanDraft}
1270
- setHumanDraft={setHumanDraft}
1271
- sendHumanChatMessage={sendHumanChatMessage}
1272
- transcriptContainerRef={transcriptContainerRef}
1273
- onTranscriptScroll={onTranscriptScroll}
 
 
 
 
 
 
 
1274
  activeMessages={activeMessages}
1275
  highlightedEvidence={highlightedEvidence}
1276
  renderHighlightedText={renderHighlightedText}
1277
  />
1278
- ) : (
1279
- <ConversationUploadPanel
1280
- activePage={activePage}
1281
- conversationActive={conversationActive}
1282
- analysisBusy={analysisBusy}
1283
- analysisText={analysisText}
1284
- setAnalysisText={setAnalysisText}
1285
- analysisSourceName={analysisSourceName}
 
 
 
 
 
1286
  loadTextFile={loadTextFile}
1287
  runTextAnalysis={runTextAnalysis}
1288
  analysisStatus={analysisStatus}
 
115
  const bucket = (run && run[kind] && typeof run[kind] === 'object') ? run[kind] : {};
116
  return {
117
  surveyor_persona_id: bucket.surveyor_persona_id || cfg?.surveyor_persona_id || 'friendly_researcher_001',
118
+ patient_persona_id: bucket.patient_persona_id || cfg?.patient_persona_id || 'cooperative_senior_001',
119
+ ai_role: (kind === 'human_to_ai')
120
+ ? (bucket.ai_role || 'surveyor')
121
+ : undefined
122
  };
123
  }
124
 
 
277
  const [mainPatientId, setMainPatientId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'ai_to_ai').patient_persona_id);
278
  const [humanSurveyorId, setHumanSurveyorId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'human_to_ai').surveyor_persona_id);
279
  const [humanPatientId, setHumanPatientId] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'human_to_ai').patient_persona_id);
280
+ const [humanAiRole, setHumanAiRole] = useState(() => getRunPersonaSelection(loadConfig() || {}, 'human_to_ai').ai_role || 'surveyor');
281
 
282
  const [historyBusy, setHistoryBusy] = useState(false);
283
  const [historyError, setHistoryError] = useState(null);
 
716
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
717
  wsRef.current.send(JSON.stringify({
718
  type: 'start_human_chat',
719
+ ai_role: humanAiRole || 'surveyor',
720
  surveyor_persona_id: surveyorId,
721
  patient_persona_id: patientId,
722
  surveyor_question_bank: surveyorQuestionBank || undefined,
 
1040
  <div className="bg-white rounded-lg shadow-lg p-4 mb-6">
1041
  <div className="flex items-center justify-between gap-4">
1042
  <PageNav active={activePage} onChange={setActivePage} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
  {(activePage === 'main' || activePage === 'human' || activePage === 'analyze' || activePage === 'history') && activeStatus === 'complete' && activeResources && (
1044
  <div className="flex items-center gap-2">
1045
  <button
 
1196
  </div>
1197
  ) : (
1198
  <div className="grid grid-cols-[2fr_1fr_2fr] gap-6 items-start">
1199
+ {(activePage === 'human') ? (
1200
+ <HumanChatPanel
1201
+ humanChatActive={humanChatActive}
1202
+ aiRole={humanAiRole}
1203
+ setAiRole={(next) => { setHumanAiRole(next); persistRunSelection('human_to_ai', { ai_role: next }); }}
1204
+ personaCatalog={personaCatalog}
1205
+ surveyorId={humanSurveyorId}
1206
+ setSurveyorId={(next) => { setHumanSurveyorId(next); persistRunSelection('human_to_ai', { surveyor_persona_id: next }); }}
1207
+ patientId={humanPatientId}
1208
+ setPatientId={(next) => { setHumanPatientId(next); persistRunSelection('human_to_ai', { patient_persona_id: next }); }}
1209
+ humanDraft={humanDraft}
1210
+ setHumanDraft={setHumanDraft}
1211
+ sendHumanChatMessage={sendHumanChatMessage}
1212
+ transcriptContainerRef={transcriptContainerRef}
1213
+ onTranscriptScroll={onTranscriptScroll}
1214
  activeMessages={activeMessages}
1215
  highlightedEvidence={highlightedEvidence}
1216
  renderHighlightedText={renderHighlightedText}
1217
  />
1218
+ ) : (
1219
+ <ConversationUploadPanel
1220
+ activePage={activePage}
1221
+ conversationActive={conversationActive}
1222
+ personaCatalog={personaCatalog}
1223
+ surveyorId={mainSurveyorId}
1224
+ setSurveyorId={(next) => { setMainSurveyorId(next); persistRunSelection('ai_to_ai', { surveyor_persona_id: next }); }}
1225
+ patientId={mainPatientId}
1226
+ setPatientId={(next) => { setMainPatientId(next); persistRunSelection('ai_to_ai', { patient_persona_id: next }); }}
1227
+ analysisBusy={analysisBusy}
1228
+ analysisText={analysisText}
1229
+ setAnalysisText={setAnalysisText}
1230
+ analysisSourceName={analysisSourceName}
1231
  loadTextFile={loadTextFile}
1232
  runTextAnalysis={runTextAnalysis}
1233
  analysisStatus={analysisStatus}
frontend/pages/shared_views.py CHANGED
@@ -1,13 +1,18 @@
1
  def get_shared_views_js() -> str:
2
  return r"""
3
- function ConversationUploadPanel(props) {
4
- const {
5
- activePage,
6
- conversationActive,
7
- analysisBusy,
8
- analysisText,
9
- setAnalysisText,
10
- analysisSourceName,
 
 
 
 
 
11
  loadTextFile,
12
  runTextAnalysis,
13
  analysisStatus,
@@ -20,13 +25,44 @@ def get_shared_views_js() -> str:
20
  } = props;
21
 
22
  return (
23
- <div className="bg-white rounded-lg shadow-lg p-6">
24
- <div className="flex items-center gap-2 mb-4">
25
- <span className="text-2xl">{activePage === 'analyze' ? '🧾' : '💬'}</span>
26
- <h2 className="text-xl font-bold text-slate-800">{activePage === 'analyze' ? 'Upload Text' : 'AI-to-AI'}</h2>
27
- {activePage === 'main' && conversationActive && <span className="ml-auto text-green-600 font-medium animate-pulse">● LIVE</span>}
28
- {activePage === 'analyze' && analysisBusy && <span className="ml-auto text-purple-600 font-medium animate-pulse">● RUNNING</span>}
29
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  {activePage === 'analyze' && (
32
  <div className="mb-4 space-y-3">
@@ -93,13 +129,20 @@ def get_shared_views_js() -> str:
93
  );
94
  }
95
 
96
- function HumanChatPanel(props) {
97
- const {
98
- humanChatActive,
99
- humanDraft,
100
- setHumanDraft,
101
- sendHumanChatMessage,
102
- transcriptContainerRef,
 
 
 
 
 
 
 
103
  onTranscriptScroll,
104
  activeMessages,
105
  highlightedEvidence,
@@ -113,28 +156,80 @@ def get_shared_views_js() -> str:
113
  }
114
  };
115
 
116
- return (
117
- <div className="bg-white rounded-lg shadow-lg p-6">
118
- <div className="flex items-center gap-2 mb-4">
119
- <span className="text-2xl">🧑‍💬</span>
120
- <h2 className="text-xl font-bold text-slate-800">Human-to-AI</h2>
121
- {humanChatActive && <span className="ml-auto text-green-600 font-medium animate-pulse">● LIVE</span>}
122
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- <div className="mb-4">
125
- {!humanChatActive ? (
126
- <div className="text-sm text-slate-500">
127
- Click “Start” to begin. Type as the patient. Use “End session” to run analysis.
128
- </div>
129
- ) : (
130
- <div className="flex items-end gap-3">
131
- <textarea
132
- className="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white h-20"
133
- placeholder="Type your message as the patient… (Ctrl/⌘+Enter to send)"
134
- value={humanDraft}
135
- onChange={(e) => setHumanDraft(e.target.value)}
136
- onKeyDown={onKeyDown}
137
- />
138
  <button
139
  type="button"
140
  onClick={sendHumanChatMessage}
 
1
  def get_shared_views_js() -> str:
2
  return r"""
3
+ function ConversationUploadPanel(props) {
4
+ const {
5
+ activePage,
6
+ conversationActive,
7
+ personaCatalog,
8
+ surveyorId,
9
+ setSurveyorId,
10
+ patientId,
11
+ setPatientId,
12
+ analysisBusy,
13
+ analysisText,
14
+ setAnalysisText,
15
+ analysisSourceName,
16
  loadTextFile,
17
  runTextAnalysis,
18
  analysisStatus,
 
25
  } = props;
26
 
27
  return (
28
+ <div className="bg-white rounded-lg shadow-lg p-6">
29
+ <div className="flex items-center gap-2 mb-4">
30
+ <span className="text-2xl">{activePage === 'analyze' ? '🧾' : '💬'}</span>
31
+ <h2 className="text-xl font-bold text-slate-800">{activePage === 'analyze' ? 'Upload Text' : 'AI-to-AI'}</h2>
32
+ {activePage === 'main' && conversationActive && <span className="ml-auto text-green-600 font-medium animate-pulse">● LIVE</span>}
33
+ {activePage === 'analyze' && analysisBusy && <span className="ml-auto text-purple-600 font-medium animate-pulse">● RUNNING</span>}
34
+ </div>
35
+
36
+ {activePage === 'main' && (
37
+ <div className="mb-4 grid grid-cols-2 gap-3">
38
+ <div>
39
+ <div className="text-xs font-semibold text-slate-600 mb-1">Surveyor</div>
40
+ <select
41
+ className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
42
+ value={surveyorId}
43
+ disabled={conversationActive}
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>
51
+ <div>
52
+ <div className="text-xs font-semibold text-slate-600 mb-1">Patient scenario</div>
53
+ <select
54
+ className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
55
+ value={patientId}
56
+ disabled={conversationActive}
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>
64
+ </div>
65
+ )}
66
 
67
  {activePage === 'analyze' && (
68
  <div className="mb-4 space-y-3">
 
129
  );
130
  }
131
 
132
+ function HumanChatPanel(props) {
133
+ const {
134
+ humanChatActive,
135
+ aiRole,
136
+ setAiRole,
137
+ personaCatalog,
138
+ surveyorId,
139
+ setSurveyorId,
140
+ patientId,
141
+ setPatientId,
142
+ humanDraft,
143
+ setHumanDraft,
144
+ sendHumanChatMessage,
145
+ transcriptContainerRef,
146
  onTranscriptScroll,
147
  activeMessages,
148
  highlightedEvidence,
 
156
  }
157
  };
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}
174
+ onClick={() => setAiRole('surveyor')}
175
+ className={`${aiRole === 'surveyor' ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-700 border-slate-300'} border px-3 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50`}
176
+ >
177
+ Surveyor
178
+ </button>
179
+ <button
180
+ type="button"
181
+ disabled={humanChatActive}
182
+ onClick={() => setAiRole('patient')}
183
+ className={`${aiRole === 'patient' ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-700 border-slate-300'} border px-3 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50`}
184
+ >
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}
frontend/react_gradio_hybrid.py CHANGED
@@ -185,6 +185,7 @@ async def frontend_websocket(websocket: WebSocket, conversation_id: str):
185
  manager.send_message({
186
  "type": "start_human_chat",
187
  "content": "start",
 
188
  "surveyor_persona_id": data.get("surveyor_persona_id", "friendly_researcher_001"),
189
  "patient_persona_id": data.get("patient_persona_id", "cooperative_senior_001"),
190
  "surveyor_question_bank": data.get("surveyor_question_bank"),
 
185
  manager.send_message({
186
  "type": "start_human_chat",
187
  "content": "start",
188
+ "ai_role": data.get("ai_role"),
189
  "surveyor_persona_id": data.get("surveyor_persona_id", "friendly_researcher_001"),
190
  "patient_persona_id": data.get("patient_persona_id", "cooperative_senior_001"),
191
  "surveyor_question_bank": data.get("surveyor_question_bank"),