Spaces:
Sleeping
Sleeping
[interview] Restore persecution categories + fix CheckboxGroup reveal lag
Browse files- Re-added the persecution-grounds question (Political / Ethnic / Religious /
Gender-based / Sexual orientation / Climate displacement / Other) as a
multi-select, in addition to the free-text "what happened" narrative; options
translated into all 10 languages. Shown in the assessment facts + review.
- Fixed "options only appear after pressing Continue": each transition now
renders in two passes (question with controls hidden, then the control), which
makes the CheckboxGroup appear immediately.
64 fast tests pass.
Co-authored-by: Codex <noreply@openai.com>
- app/interview_script.py +40 -0
- app/phases/assessment.py +1 -0
- app/phases/interview.py +38 -14
app/interview_script.py
CHANGED
|
@@ -34,6 +34,9 @@ QUESTIONS: list[Question] = [
|
|
| 34 |
Question("q_current", "current_country", State.SITUATION, "country"),
|
| 35 |
Question("q_origin", "origin_country", State.SITUATION, "country"),
|
| 36 |
Question("q_reason", "free_text_history", State.SITUATION, "text", alt_sid="q_reason_instill"),
|
|
|
|
|
|
|
|
|
|
| 37 |
Question("q_danger", "immediate_danger", State.SITUATION, "yesno", kind="bool"),
|
| 38 |
Question("q_duration", "displacement_duration", State.HISTORY, "text", skip_if_in_origin=True),
|
| 39 |
Question("q_documents", "documents_available", State.HISTORY, "choice", kind="list",
|
|
@@ -62,6 +65,10 @@ TR: dict[str, dict[str, str]] = {
|
|
| 62 |
"opt_yes": "Yes", "opt_no": "No",
|
| 63 |
"opt_passport": "Passport", "opt_id": "National ID", "opt_birth": "Birth certificate",
|
| 64 |
"opt_none": "None", "opt_other": "Other",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
"ui_answer": "Your answer", "ui_continue": "Continue",
|
| 66 |
},
|
| 67 |
"French": {
|
|
@@ -285,6 +292,39 @@ _EXTRA = {
|
|
| 285 |
for _lang, _d in _EXTRA.items():
|
| 286 |
TR.setdefault(_lang, {}).update(_d)
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
def t(language: str | None, sid: str) -> str:
|
| 290 |
"""Translation lookup with English fallback."""
|
|
|
|
| 34 |
Question("q_current", "current_country", State.SITUATION, "country"),
|
| 35 |
Question("q_origin", "origin_country", State.SITUATION, "country"),
|
| 36 |
Question("q_reason", "free_text_history", State.SITUATION, "text", alt_sid="q_reason_instill"),
|
| 37 |
+
Question("q_grounds", "persecution_types", State.SITUATION, "choice", kind="list",
|
| 38 |
+
options=["opt_political", "opt_ethnic", "opt_religious", "opt_gender",
|
| 39 |
+
"opt_orientation", "opt_climate", "opt_other"]),
|
| 40 |
Question("q_danger", "immediate_danger", State.SITUATION, "yesno", kind="bool"),
|
| 41 |
Question("q_duration", "displacement_duration", State.HISTORY, "text", skip_if_in_origin=True),
|
| 42 |
Question("q_documents", "documents_available", State.HISTORY, "choice", kind="list",
|
|
|
|
| 65 |
"opt_yes": "Yes", "opt_no": "No",
|
| 66 |
"opt_passport": "Passport", "opt_id": "National ID", "opt_birth": "Birth certificate",
|
| 67 |
"opt_none": "None", "opt_other": "Other",
|
| 68 |
+
"q_grounds": "Which of these best describe your situation? (Choose any that apply.)",
|
| 69 |
+
"opt_political": "Political", "opt_ethnic": "Ethnic", "opt_religious": "Religious",
|
| 70 |
+
"opt_gender": "Gender-based", "opt_orientation": "Sexual orientation",
|
| 71 |
+
"opt_climate": "Climate displacement",
|
| 72 |
"ui_answer": "Your answer", "ui_continue": "Continue",
|
| 73 |
},
|
| 74 |
"French": {
|
|
|
|
| 292 |
for _lang, _d in _EXTRA.items():
|
| 293 |
TR.setdefault(_lang, {}).update(_d)
|
| 294 |
|
| 295 |
+
# Persecution-grounds question + category labels (multi-select).
|
| 296 |
+
_GROUNDS = {
|
| 297 |
+
"French": {"q_grounds": "Parmi ces situations, laquelle correspond à la vôtre ? (Choisissez celles qui s'appliquent.)",
|
| 298 |
+
"opt_political": "Politique", "opt_ethnic": "Ethnique", "opt_religious": "Religieux",
|
| 299 |
+
"opt_gender": "Lié au genre", "opt_orientation": "Orientation sexuelle", "opt_climate": "Déplacement climatique"},
|
| 300 |
+
"Spanish": {"q_grounds": "¿Cuál de estas situaciones describe la suya? (Elige las que correspondan.)",
|
| 301 |
+
"opt_political": "Político", "opt_ethnic": "Étnico", "opt_religious": "Religioso",
|
| 302 |
+
"opt_gender": "De género", "opt_orientation": "Orientación sexual", "opt_climate": "Desplazamiento climático"},
|
| 303 |
+
"Portuguese": {"q_grounds": "Qual destas situações descreve a sua? (Escolha as que se aplicam.)",
|
| 304 |
+
"opt_political": "Político", "opt_ethnic": "Étnico", "opt_religious": "Religioso",
|
| 305 |
+
"opt_gender": "De gênero", "opt_orientation": "Orientação sexual", "opt_climate": "Deslocamento climático"},
|
| 306 |
+
"Arabic": {"q_grounds": "أيٌّ من هذه الحالات تنطبق عليك؟ (اختر كل ما ينطبق.)",
|
| 307 |
+
"opt_political": "سياسي", "opt_ethnic": "عِرقي", "opt_religious": "ديني",
|
| 308 |
+
"opt_gender": "قائم على النوع الاجتماعي", "opt_orientation": "الميل الجنسي", "opt_climate": "النزوح المناخي"},
|
| 309 |
+
"Hindi": {"q_grounds": "इनमें से कौन-सी स्थिति आप पर लागू होती है? (जो भी लागू हों चुनें।)",
|
| 310 |
+
"opt_political": "राजनीतिक", "opt_ethnic": "जातीय", "opt_religious": "धार्मिक",
|
| 311 |
+
"opt_gender": "लिंग आधारित", "opt_orientation": "यौन रुझान", "opt_climate": "जलवायु विस्थापन"},
|
| 312 |
+
"Chinese": {"q_grounds": "以下哪些情况符合您的处境?(可多选)",
|
| 313 |
+
"opt_political": "政治", "opt_ethnic": "族裔", "opt_religious": "宗教",
|
| 314 |
+
"opt_gender": "基于性别", "opt_orientation": "性取向", "opt_climate": "气候流离失所"},
|
| 315 |
+
"Japanese": {"q_grounds": "次のうち、あなたの状況に当てはまるものはどれですか?(複数選択可)",
|
| 316 |
+
"opt_political": "政治的", "opt_ethnic": "民族的", "opt_religious": "宗教的",
|
| 317 |
+
"opt_gender": "ジェンダーに基づく", "opt_orientation": "性的指向", "opt_climate": "気候による避難"},
|
| 318 |
+
"Korean": {"q_grounds": "다음 중 귀하의 상황에 해당하는 것은 무엇인가요? (해당되는 것을 모두 선택하세요.)",
|
| 319 |
+
"opt_political": "정치적", "opt_ethnic": "민족적", "opt_religious": "종교적",
|
| 320 |
+
"opt_gender": "성별 기반", "opt_orientation": "성적 지향", "opt_climate": "기후 이재민"},
|
| 321 |
+
"Russian": {"q_grounds": "Что из перечисленного описывает вашу ситуацию? (Выберите все подходящее.)",
|
| 322 |
+
"opt_political": "Политическое", "opt_ethnic": "Этническое", "opt_religious": "Религиозное",
|
| 323 |
+
"opt_gender": "По признаку пола", "opt_orientation": "Сексуальная ориентация", "opt_climate": "Климатическое перемещение"},
|
| 324 |
+
}
|
| 325 |
+
for _lang, _d in _GROUNDS.items():
|
| 326 |
+
TR.setdefault(_lang, {}).update(_d)
|
| 327 |
+
|
| 328 |
|
| 329 |
def t(language: str | None, sid: str) -> str:
|
| 330 |
"""Translation lookup with English fallback."""
|
app/phases/assessment.py
CHANGED
|
@@ -33,6 +33,7 @@ FACT_FIELDS = [
|
|
| 33 |
("Country of origin", "origin_country", "text"),
|
| 34 |
("Current location", "current_country", "text"),
|
| 35 |
("What happened", "free_text_history", "text"),
|
|
|
|
| 36 |
("Immediate danger", "immediate_danger", "bool"),
|
| 37 |
("Time displaced", "displacement_duration", "text"),
|
| 38 |
("Documents", "documents_available", "list"),
|
|
|
|
| 33 |
("Country of origin", "origin_country", "text"),
|
| 34 |
("Current location", "current_country", "text"),
|
| 35 |
("What happened", "free_text_history", "text"),
|
| 36 |
+
("Reason", "persecution_types", "chips"),
|
| 37 |
("Immediate danger", "immediate_danger", "bool"),
|
| 38 |
("Time displaced", "displacement_duration", "text"),
|
| 39 |
("Documents", "documents_available", "list"),
|
app/phases/interview.py
CHANGED
|
@@ -242,6 +242,7 @@ def _labeled_facts(session: SessionState) -> str:
|
|
| 242 |
("Country of origin", iv.origin_country),
|
| 243 |
("Current country", iv.current_country),
|
| 244 |
("What happened", iv.free_text_history),
|
|
|
|
| 245 |
("Immediate danger", None if iv.immediate_danger is None else ("yes" if iv.immediate_danger else "no")),
|
| 246 |
("Time since leaving", iv.displacement_duration),
|
| 247 |
("Documents", ", ".join(iv.documents_available) if iv.documents_available else None),
|
|
@@ -383,19 +384,34 @@ def build(visible: bool = True, session_st=None, loop_st=None, slot_idx_st=None)
|
|
| 383 |
return (render_chat(session.messages), render_rail(session.state),
|
| 384 |
*control_updates(session, idx), session, idx)
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
async def start(session, loop, slot_idx=0):
|
| 387 |
if session is None:
|
| 388 |
session = SessionState(); session.transition_to(State.INTAKE)
|
| 389 |
loop = loop or create_loop()
|
| 390 |
-
|
| 391 |
-
|
|
|
|
| 392 |
|
| 393 |
async def on_continue(radio_v, multi_v, country_v, text_v, session, loop, idx):
|
| 394 |
if session is None:
|
| 395 |
session = SessionState(); session.transition_to(State.INTAKE)
|
| 396 |
loop = loop or create_loop()
|
| 397 |
o = await _present(session, loop, 0)
|
| 398 |
-
|
|
|
|
|
|
|
| 399 |
loop = loop or create_loop()
|
| 400 |
lang = session.language
|
| 401 |
|
|
@@ -403,32 +419,40 @@ def build(visible: bool = True, session_st=None, loop_st=None, slot_idx_st=None)
|
|
| 403 |
if idx == CORRECT_INDEX:
|
| 404 |
correction = (text_v or "").strip()
|
| 405 |
if not correction:
|
| 406 |
-
|
|
|
|
| 407 |
session.messages = list(session.messages) + [{"role": "user", "content": correction}]
|
| 408 |
await _apply_correction(session, loop, correction)
|
| 409 |
-
o = await _present(session, loop, REVIEW_INDEX)
|
| 410 |
-
|
|
|
|
|
|
|
| 411 |
|
| 412 |
# Review step
|
| 413 |
if idx >= REVIEW_INDEX:
|
| 414 |
if not radio_v:
|
| 415 |
-
|
|
|
|
| 416 |
session.messages = list(session.messages) + [{"role": "user", "content": radio_v}]
|
| 417 |
if radio_v == t(lang, "review_yes"):
|
| 418 |
advance_to(session, State.ASSESSMENT) # triggers assessment via .then()
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
o = await _present(session, loop, CORRECT_INDEX) # ask what to change
|
| 423 |
-
|
|
|
|
|
|
|
| 424 |
|
| 425 |
q = QUESTIONS[idx]
|
| 426 |
display = _store(session, q, radio_v, multi_v, country_v, text_v)
|
| 427 |
if display is None:
|
| 428 |
-
|
|
|
|
| 429 |
session.messages = list(session.messages) + [{"role": "user", "content": display}]
|
| 430 |
o = await _present(session, loop, _next_index(session, idx))
|
| 431 |
-
|
|
|
|
| 432 |
|
| 433 |
continue_event = cont.click(
|
| 434 |
on_continue,
|
|
|
|
| 242 |
("Country of origin", iv.origin_country),
|
| 243 |
("Current country", iv.current_country),
|
| 244 |
("What happened", iv.free_text_history),
|
| 245 |
+
("Reason", ", ".join(iv.persecution_types) if iv.persecution_types else None),
|
| 246 |
("Immediate danger", None if iv.immediate_danger is None else ("yes" if iv.immediate_danger else "no")),
|
| 247 |
("Time since leaving", iv.displacement_duration),
|
| 248 |
("Documents", ", ".join(iv.documents_available) if iv.documents_available else None),
|
|
|
|
| 384 |
return (render_chat(session.messages), render_rail(session.state),
|
| 385 |
*control_updates(session, idx), session, idx)
|
| 386 |
|
| 387 |
+
_HIDE4 = (gr.update(visible=False), gr.update(visible=False),
|
| 388 |
+
gr.update(visible=False), gr.update(visible=False))
|
| 389 |
+
|
| 390 |
+
def _frames(o, loop):
|
| 391 |
+
"""Two render passes: show the new question with controls hidden, then
|
| 392 |
+
reveal the control. Splitting the transition makes Gradio render the
|
| 393 |
+
choice controls immediately instead of one Continue-press late."""
|
| 394 |
+
return [
|
| 395 |
+
(o[0], o[1], *_HIDE4, o[6], loop, o[7]),
|
| 396 |
+
(o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7]),
|
| 397 |
+
]
|
| 398 |
+
|
| 399 |
async def start(session, loop, slot_idx=0):
|
| 400 |
if session is None:
|
| 401 |
session = SessionState(); session.transition_to(State.INTAKE)
|
| 402 |
loop = loop or create_loop()
|
| 403 |
+
o = await _present(session, loop, 0)
|
| 404 |
+
for frame in _frames(o, loop):
|
| 405 |
+
yield frame
|
| 406 |
|
| 407 |
async def on_continue(radio_v, multi_v, country_v, text_v, session, loop, idx):
|
| 408 |
if session is None:
|
| 409 |
session = SessionState(); session.transition_to(State.INTAKE)
|
| 410 |
loop = loop or create_loop()
|
| 411 |
o = await _present(session, loop, 0)
|
| 412 |
+
for frame in _frames(o, loop):
|
| 413 |
+
yield frame
|
| 414 |
+
return
|
| 415 |
loop = loop or create_loop()
|
| 416 |
lang = session.language
|
| 417 |
|
|
|
|
| 419 |
if idx == CORRECT_INDEX:
|
| 420 |
correction = (text_v or "").strip()
|
| 421 |
if not correction:
|
| 422 |
+
yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
|
| 423 |
+
return
|
| 424 |
session.messages = list(session.messages) + [{"role": "user", "content": correction}]
|
| 425 |
await _apply_correction(session, loop, correction)
|
| 426 |
+
o = await _present(session, loop, REVIEW_INDEX)
|
| 427 |
+
for frame in _frames(o, loop):
|
| 428 |
+
yield frame
|
| 429 |
+
return
|
| 430 |
|
| 431 |
# Review step
|
| 432 |
if idx >= REVIEW_INDEX:
|
| 433 |
if not radio_v:
|
| 434 |
+
yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
|
| 435 |
+
return
|
| 436 |
session.messages = list(session.messages) + [{"role": "user", "content": radio_v}]
|
| 437 |
if radio_v == t(lang, "review_yes"):
|
| 438 |
advance_to(session, State.ASSESSMENT) # triggers assessment via .then()
|
| 439 |
+
yield (render_chat(session.messages), render_rail(session.state),
|
| 440 |
+
*_HIDE4, session, loop, idx)
|
| 441 |
+
return
|
| 442 |
+
o = await _present(session, loop, CORRECT_INDEX) # ask what to change
|
| 443 |
+
for frame in _frames(o, loop):
|
| 444 |
+
yield frame
|
| 445 |
+
return
|
| 446 |
|
| 447 |
q = QUESTIONS[idx]
|
| 448 |
display = _store(session, q, radio_v, multi_v, country_v, text_v)
|
| 449 |
if display is None:
|
| 450 |
+
yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
|
| 451 |
+
return
|
| 452 |
session.messages = list(session.messages) + [{"role": "user", "content": display}]
|
| 453 |
o = await _present(session, loop, _next_index(session, idx))
|
| 454 |
+
for frame in _frames(o, loop):
|
| 455 |
+
yield frame
|
| 456 |
|
| 457 |
continue_event = cont.click(
|
| 458 |
on_continue,
|