feat(dashboard): console fallbacks for the real API (#12)
CI / ruff · pytest (pull_request) Successful in 1m37s

- personas degrade: /demo/personas → doctor's patients (hero first) → env token
- scenario file guessed from the transliterated first name (andrey.jsonl), like the backend
- «Подключить демо-пациента»: connects the synthetic hero via /patients/connect when no seed
- connect code prefilled from ?code= of the console link

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 12:50:42 +03:00
co-authored by Claude Opus 5.5
parent 260422138a
commit 8d12aa3d08
5 changed files with 130 additions and 13 deletions
+47 -4
View File
@@ -89,16 +89,55 @@ def select_persona(container: Any = None) -> dict[str, Any]:
where = container or st.sidebar
personas, error = _personas(get_api().base_url)
if error:
where.caption(f"⚠️ персоны из API недоступны ({error}); используется токен из окружения")
where.caption(f"⚠️ {error}")
tokens = [p["token"] for p in personas]
current = st.session_state.get("console_token")
index = tokens.index(current) if current in tokens else 0
labels = {p["token"]: f"{p.get('display_name') or p['token']} ({p['token']})" for p in personas}
token = where.selectbox("Пациент демо", tokens, index=index, format_func=lambda t: labels[t])
labels = {p["token"]: f"{p.get('display_name') or p['token']} ({p['token'][:12]})" for p in personas}
token = where.selectbox("Пациент демо", tokens, index=index, format_func=lambda t: labels.get(t, t))
st.session_state["console_token"] = token
return next(p for p in personas if p["token"] == token)
# Synthetic demo hero (docs/product/01_demo_persona.md); the SNILS is fake and used only to derive the token.
DEMO_HERO = {
"display_name": "Андрей С.",
"birth_year": 1992,
"sex": "m",
"snils": "000-000-017 17",
"drug": "сертралин",
"dose": "50 мг",
"times": ["08:00"],
"instructions": "1 раз в сутки утром после еды",
"modules": ["veteran", "depression"],
}
def _connect_hero() -> None:
name = st.session_state.get("console_hero_name") or DEMO_HERO["display_name"]
body = {**DEMO_HERO, "doctor_id": doctor_id(), "display_name": name}
try:
out = get_api().connect(body)
except ApiError as exc:
hint = " — пациент уже подключён, выберите его в списке" if "409" in str(exc) else ""
flash(f"Подключение не выполнено: {exc}{hint}", "warning" if hint else "error")
return
token = str(out.get("token", ""))
st.session_state["console_token"] = token
st.session_state[f"console_code:{token}"] = str(out.get("code", ""))
_personas.clear()
flash(f"Пациент подключён, код {out.get('code', '')}. Нажмите «Начать» в чате.", "success")
def render_connect_hero(container: Any = None) -> None:
"""Fallback when the demo seed is missing: connect the synthetic hero through the doctor API."""
where = container or st.sidebar
with where.expander("Подключить демо-пациента"):
st.caption("Если сид демо не загружен: врач подключает Андрея (синтетика) — как на приёме.")
st.text_input("Имя", value="Андрей С.", key="console_hero_name")
st.button("Подключить", key="console-connect", on_click=_connect_hero)
# ------------------------------------------------------------------------------------------------ clock
def _drain_quiet(api: ConsoleApi, token: str) -> None:
try:
@@ -137,6 +176,7 @@ def _reset() -> None:
flash(f"Сброс не выполнен: {exc}")
return
forget_demo_state()
_personas.clear() # the seed may be recreated on reset
flash("Демо сброшено: часы вернулись к реальному времени, чат и прогресс сценария очищены.", "success")
@@ -263,7 +303,10 @@ def render_chat(token: str, *, height: int = 520, key: str = "chat") -> None:
left, right = st.columns([5, 1])
left.text_input("Сообщение", key=input_key, label_visibility="collapsed", placeholder="Сообщение пациента…")
right.form_submit_button("➤", width="stretch", on_click=_submit_text, args=(token, input_key))
with st.expander("Начать диалог / код подключения"):
code = st.query_params.get("code") # connect links from the doctor point to the console with ?code=
if code and not st.session_state.get(f"console_code:{token}"):
st.session_state[f"console_code:{token}"] = code
with st.expander("Начать диалог / код подключения", expanded=bool(code)):
st.text_input("Код подключения (необязательно)", key=f"console_code:{token}", placeholder="DEMO-0001")
c1, c2 = st.columns(2)
c1.button("🚀 Начать", key=f"{key}-start", on_click=_send, args=(token, "start"), width="stretch")