"""Scenario playback panel of the demo console (#17): day list, «Проиграть день N», «Прокрутить до дня N».""" from __future__ import annotations from typing import Any import streamlit as st from . import widgets as w from .playback import PlaybackState from .scenario import Scenario def _forget_upload(token: str) -> None: st.session_state.pop(f"console_scenario_text:{token}", None) st.session_state.pop(f"console_up:{token}", None) # resets the uploader widget too def _scenario_source_controls(persona: dict[str, Any], source: str) -> None: token = persona["token"] with st.expander(f"Файл сценария: {source}"): st.text_input( "Путь в репозитории", key=f"console_scenario_path:{token}", placeholder=str(persona.get("scenario") or "data/synthetic/andrey.jsonl"), ) upload = st.file_uploader("…или загрузить .jsonl", type=["jsonl", "json", "txt"], key=f"console_up:{token}") if upload is not None: try: st.session_state[f"console_scenario_text:{token}"] = upload.getvalue().decode("utf-8") except UnicodeDecodeError: st.error("Файл не в UTF-8") if st.session_state.get(f"console_scenario_text:{token}"): st.button("Забыть загруженный файл", on_click=_forget_upload, args=(token,)) def _day_table(scenario: Scenario, state: PlaybackState, next_day: int | None) -> None: rows = [] for d in scenario.days: mark = "✅" if d.day in state.done else ("▶️" if d.day == next_day else "") rows.append({"": mark, "День": d.day, "Шагов": len(d.actions), "Подсказка ведущему": d.note}) st.dataframe(rows, hide_index=True, width="stretch", height=min(38 + 35 * len(rows), 330)) def _clear_failure(state: PlaybackState) -> None: state.failed = None state.position = None def render_playback_panel(persona: dict[str, Any]) -> None: token = persona["token"] scenario, source = w.scenario_for(persona) state = w.playback_state(token) st.subheader("🎬 Сценарий демо") _scenario_source_controls(persona, source) if scenario.errors: with st.expander(f"⚠️ Замечания к сценарию: {len(scenario.errors)}", expanded=not scenario.days): for err in scenario.errors: st.write(f"- {err}") if not scenario.days: st.info("Сценарий не загружен. Демо можно вести вручную: чат слева и кнопки часов сверху.") return delay = st.slider("Пауза между шагами, с", 0.0, 3.0, 1.0, 0.25, key=f"console_delay:{token}") runner = w.make_runner(persona, delay=delay) nxt = runner.next_day(scenario) _day_table(scenario, state, nxt.day if nxt else None) numbers = scenario.day_numbers default = numbers.index(nxt.day) if nxt else len(numbers) - 1 day = st.selectbox("День", numbers, index=default, key=f"console_day:{token}", format_func=lambda n: f"день {n}") c1, c2, c3 = st.columns(3) action: tuple[str, Any] | None = None if c1.button(f"▶ Проиграть день {day}", type="primary", width="stretch"): action = ("Проигрываю день", runner.play_day(scenario, day)) if c2.button(f"⏭ Прокрутить до дня {day}", width="stretch", help="Быстро, без пауз: все непроигранные дни"): runner.delay = 0.0 action = (f"Прокрутка до дня {day}", runner.scroll_to(scenario, day)) if c3.button( f"▶ Следующий: день {nxt.day}" if nxt else "Все дни проиграны", width="stretch", disabled=nxt is None, ): assert nxt is not None action = ("Проигрываю день", runner.play_day(scenario, nxt.day)) point = state.failed or state.position if point is not None and action is None: what = "остановлено на ошибке" if state.failed else "прервано" st.warning(f"Проигрывание {what}: день {point.day}, шаг {point.step + 1}.") r1, r2, r3 = st.columns(3) if r1.button("🔁 Повторить шаг", width="stretch"): action = ("Продолжаю", runner.resume(scenario, skip=False)) if r2.button("⏩ Пропустить шаг", width="stretch"): action = ("Продолжаю", runner.resume(scenario, skip=True)) r3.button("✋ Продолжить вручную", width="stretch", on_click=_clear_failure, args=(state,)) if action is not None: title, logs = action w.execute(logs, state, title=title) st.session_state["console_log_open"] = True st.rerun() # refresh the chat and the clock with what playback produced if state.log: with st.expander("Журнал последнего проигрывания", expanded=st.session_state.pop("console_log_open", False)): st.text("\n".join(state.log[-80:]))