| """ |
| Gradio Space: «Слепой Детектив Легковесных Моделей» |
| |
| Поток игры: |
| 1. Выбор произведения → bootstrap (3 вопроса × 3 ответа) |
| 2. Пользователь задаёт вопрос → judge-фильтр → answer (3 ответа) |
| 3. Пользователь размечает цвета → расчёт очков → Продолжить/Закончить |
| 4. Шаги 2-3 повторяются ROUND_LIMIT раз |
| 5. Финальный отчёт с пометкой верных цветов |
| """ |
|
|
| from pathlib import Path |
| from dotenv import load_dotenv |
| |
| load_dotenv() |
|
|
| import json |
| import os |
| import random |
| from typing import Dict, List |
|
|
| import gradio as gr |
| import httpx |
| import pandas as pd |
|
|
| |
| |
| ROOT = Path(__file__).resolve().parent |
| CORPUS_PATH = Path(os.getenv("CORPUS_PATH", str(ROOT / "data" / "corpus.json"))) |
| ROUND_LIMIT = int(os.getenv("ROUND_LIMIT", "2")) |
| BOOTSTRAP_URL = os.getenv("BOOTSTRAP_URL", "") |
| ANSWER_URL = os.getenv("ANSWER_URL", "") |
| JUDGE_URL = os.getenv("JUDGE_URL", "") |
| HEALTH_URL = os.getenv("HEALTH_URL", "") |
|
|
| print(f"CORPUS_PATH = {CORPUS_PATH}") |
| print(f"LLAMA_ROLE = {os.getenv('LLAMA_ROLE', 'reader')}") |
| print(f"QWEN_ROLE = {os.getenv('QWEN_ROLE', 'philosopher')}") |
| print(f"NEMO_ROLE = {os.getenv('NEMO_ROLE', 'scholar')}") |
|
|
| if not all([BOOTSTRAP_URL, ANSWER_URL, JUDGE_URL]): |
| raise RuntimeError( |
| "Не заданы Modal URL! Установи BOOTSTRAP_URL, ANSWER_URL, JUDGE_URL " |
| "через переменные окружения или .env" |
| ) |
|
|
| COLORS = ["red", "yellow", "green"] |
| MODEL_KEYS = ["llama", "qwen", "nemo"] |
| MODEL_LABELS = {"llama": "Saiga / Llama", "qwen": "Qwen", "nemo": "Vikhr / Nemo"} |
| COLOR_RU = {"red": "🔴 Красная", "yellow": "🟡 Жёлтая", "green": "🟢 Зелёная"} |
| COLOR_RU_SHORT = {"red": "Красный", "yellow": "Жёлтый", "green": "Зелёный"} |
|
|
| |
| CSS = """ |
| :root { |
| --bg: #101217; |
| --card: #171b22; |
| --text: #f0f4ff; |
| --muted: #c8d0e0; |
| --red: #ff6b6b; |
| --yellow: #fde047; |
| --green: #4ade80; |
| --accent: #60a5fa; |
| } |
| body, .gradio-container { |
| background: var(--bg) !important; |
| color: var(--text) !important; |
| font-family: ui-sans-serif, system-ui, sans-serif; |
| } |
| |
| /* Тёмный текст для элементов на белом фоне */ |
| .gradio-container label { color: #333 !important; } |
| .gradio-container h1 { color: #f0f4ff !important; } |
| .gradio-container h2 { color: #f0f4ff !important; } |
| .gradio-container h3 { color: #333 !important; } |
| .gradio-container p { color: #333 !important; } |
| .gradio-container .markdown h1 { color: #f0f4ff !important; } |
| .gradio-container .markdown h2 { color: #f0f4ff !important; } |
| .gradio-container .markdown h3 { color: #333 !important; } |
| .gradio-container .markdown p { color: #333 !important; } |
| .gradio-container .markdown strong { color: #111 !important; } |
| .gradio-container .radio label { color: #333 !important; } |
| .gradio-container .radio-group label { color: #333 !important; } |
| .gradio-container .markdown > p:first-of-type { color: #f0f4ff !important; } |
| |
| /* Светлый текст только внутри тёмных панелей */ |
| .panel p, .panel span, .panel div, |
| .bs-question p, .bs-question span, |
| .answer-red p, .answer-red span, |
| .answer-yellow p, .answer-yellow span, |
| .answer-green p, .answer-green span, |
| .neutral-answer p, .neutral-answer span, |
| .score-box, .final-report { color: #f0f4ff !important; } |
| .panel { |
| border-radius: 14px; |
| border: 1px solid #2d3442; |
| background: var(--card); |
| padding: 14px 18px; |
| margin-bottom: 10px; |
| line-height: 1.65; |
| color: var(--text) !important; |
| } |
| .bs-question { |
| background: #1c2130; |
| border-left: 4px solid #3d4a6b; |
| border-radius: 10px; |
| padding: 10px 16px; |
| margin-bottom: 6px; |
| font-weight: 600; |
| color: var(--text) !important; |
| } |
| .answer-red { border-left: 6px solid var(--red); background: #1e1316; } |
| .answer-yellow { border-left: 6px solid var(--yellow); background: #1c1b10; } |
| .answer-green { border-left: 6px solid var(--green); background: #101c14; } |
| .answer-red p, .answer-red span, .answer-red div, |
| .answer-yellow p, .answer-yellow span, .answer-yellow div, |
| .answer-green p, .answer-green span, .answer-green div { |
| color: var(--text) !important; |
| } |
| .answer-label-red { color: var(--red); font-weight: 700; font-size: 13px; } |
| .answer-label-yellow { color: var(--yellow); font-weight: 700; font-size: 13px; } |
| .answer-label-green { color: var(--green); font-weight: 700; font-size: 13px; } |
| .score-box { |
| font-size: 20px; font-weight: 700; |
| color: var(--text) !important; |
| padding: 12px 20px; |
| background: var(--card); |
| border-radius: 12px; |
| border: 1px solid #2d3442; |
| } |
| .final-report { |
| font-size: 22px; font-weight: 700; |
| color: var(--accent) !important; |
| padding: 16px; border-radius: 14px; |
| border: 1px solid var(--accent); |
| background: #0d1b2a; |
| } |
| .neutral-answer { |
| border-left: 4px solid #555; |
| background: #1a1d25; |
| border-radius: 10px; |
| padding: 12px 16px; |
| margin-bottom: 8px; |
| color: var(--text) !important; |
| } |
| |
| .color-radio label { font-size: 15px !important; color: #333 !important; } |
| .color-radio input[type="radio"] { accent-color: #60a5fa; } |
| .reveal-correct { color: #16a34a !important; font-weight: 700; } |
| .reveal-wrong { color: #dc2626 !important; } |
| .loading-idle { color: #888 !important; font-size: 14px; } |
| .loading-active { color: #2563eb !important; font-size: 16px; font-weight: 600; animation: pulse 1.5s ease-in-out infinite; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } |
| """ |
|
|
| |
| def load_corpus() -> List[Dict]: |
| if CORPUS_PATH.exists(): |
| with open(CORPUS_PATH, "r", encoding="utf-8") as f: |
| data = json.load(f) |
| if data: |
| return data |
| return [{ |
| "work_id": "demo-1", |
| "title": "Я вас любил", |
| "author": "А.С. Пушкин", |
| "genre": "poem", |
| "difficulty": "easy", |
| "text": ( |
| "Я вас любил: любовь ещё, быть может,\n" |
| "В душе моей угасла не совсем;\n" |
| "Но пусть она вас больше не тревожит;\n" |
| "Я не хочу печалить вас ничем.\n\n" |
| "Я вас любил безмолвно, безнадежно,\n" |
| "То робостью, то ревностью томим;\n" |
| "Я вас любил так искренно, так нежно,\n" |
| "Как дай вам Бог любимой быть другим." |
| ), |
| }] |
|
|
| |
| _DIFFICULTY_ORDER = {"easy": 0, "medium": 1, "hard": 2} |
| CORPUS = sorted( |
| load_corpus(), |
| key=lambda x: _DIFFICULTY_ORDER.get(x.get("difficulty", "easy"), 1) |
| ) |
|
|
| WORK_INDEX = { |
| f"{x.get('author','?')} — {x.get('title','?')} [{x.get('genre','?')}, {x.get('difficulty','easy')}]" |
| : x for x in CORPUS |
| } |
|
|
| |
| async def post_json(url: str, payload: dict) -> dict: |
| async with httpx.AsyncClient(timeout=300.0) as client: |
| resp = await client.post(url, json=payload) |
| resp.raise_for_status() |
| return resp.json() |
|
|
| |
| def fresh_state() -> dict: |
| return { |
| "round": 0, |
| "score": 0, |
| "history": [], |
| "color_map": {}, |
| "current_work": None, |
| "bootstrap_answers": [], |
| "last_answers": [], |
| "last_question": None, |
| "game_over": False, |
| } |
|
|
| def _is_bootstrap_duplicate(question: str, state: dict) -> bool: |
| """True если вопрос совпадает (с точностью до регистра/пробелов) с одним из bootstrap-вопросов.""" |
| q_norm = question.strip().lower() |
| for item in state.get("bootstrap_answers", []): |
| bs_q = item.get("question", "").strip().lower() |
| if q_norm == bs_q: |
| return True |
| return False |
|
|
| def reshuffle_colors(state: dict) -> dict: |
| keys = MODEL_KEYS[:] |
| random.shuffle(keys) |
| state["color_map"] = dict(zip(COLORS, keys)) |
| return state |
|
|
| |
| def render_score(state: dict) -> str: |
| r, s = state["round"], state["score"] |
| max_possible = r * 3 |
| return f'<div class="score-box">Раунд {r}/{ROUND_LIMIT} | Очки: {s}/{max_possible}</div>' |
|
|
| def render_bootstrap(questions_data: list, color_map: dict) -> str: |
| model_to_color = {v: k for k, v in color_map.items()} |
| html = '<div style="margin-bottom:16px;">' |
| html += '<h3 style="color:#e0e8ff !important;margin-bottom:12px;">📖 Ознакомительные вопросы</h3>' |
| for i, item in enumerate(questions_data, 1): |
| q = item.get("question", "—") |
| html += ( |
| f'<div class="bs-question">' |
| f'<span style="color:#8898b0;font-size:12px;">Вопрос {i}</span><br>' |
| f'<span style="color:#f0f4ff;">{q}</span>' |
| f'</div>' |
| ) |
| answers = item.get("answers", []) |
| for ans in answers: |
| text = ans.get("answer", "—") |
| model_key = ans.get("model_key", "") |
| color = model_to_color.get(model_key, "red") |
| color_label = COLOR_RU.get(color, color) |
| html += ( |
| f'<div class="panel answer-{color}">' |
| f'<span class="answer-label-{color}">{color_label}</span>' |
| f'<p style="margin-top:6px;color:#f0f4ff;">{text}</p>' |
| f'</div>' |
| ) |
| html += '<hr style="border-color:#2d3442;margin:12px 0;">' |
| html += '</div>' |
| return html |
|
|
| def render_answers_neutral(last_answers: list) -> str: |
| html = "" |
| for i, ans in enumerate(last_answers, 1): |
| text = ans.get("answer", "—") |
| html += ( |
| f'<div class="neutral-answer">' |
| f'<b style="color:#a0b0d0;">Ответ {i}</b><br>' |
| f'<span style="color:#f0f4ff;">{text}</span>' |
| f'</div>' |
| ) |
| return html |
|
|
| def build_final_report(state: dict) -> str: |
| history = state["history"] |
| if not history: |
| return '<div class="final-report">Нет данных.</div>' |
|
|
| total_rounds = len(history) |
| total_score = state["score"] |
| max_possible = total_rounds * 3 |
| pct = int(total_score / max_possible * 100) if max_possible else 0 |
|
|
| html = f'<div class="final-report">🏁 Итого: {total_score} очков из {max_possible} ({pct}%)</div>\n' |
| html += '<div style="margin-top:16px;">' |
| html += '<h3 style="color:#c8d0e0;">Подробная статистика по раундам</h3>' |
|
|
| for h in history: |
| rnd = h["round"] |
| question = h["question"] |
| color_map = h["color_map"] |
| user_guess = h["user_guess"] |
| answers = h["answers"] |
| round_correct = h.get("round_correct", 0) |
|
|
| color = "#4ade80" if round_correct == 3 else "#fde047" if round_correct >= 1 else "#ff6b6b" |
| html += ( |
| f'<div class="panel" style="margin-bottom:12px;">' |
| f'<b style="color:{color};">Раунд {rnd} — {round_correct}/3 очков</b><br>' |
| f'<span style="color:#8898b0;">Вопрос:</span> ' |
| f'<span style="color:#f0f4ff;">{question}</span>' |
| ) |
|
|
| for i, ans in enumerate(answers, 1): |
| model_key = ans["model_key"] |
| correct_color = "?" |
| for c, mk in color_map.items(): |
| if mk == model_key: |
| correct_color = c |
| break |
| user_chosen_color = user_guess.get(f"answer_{i}", "?") |
| is_this_one_correct = (user_chosen_color == correct_color) |
|
|
| color_label = COLOR_RU.get(correct_color, correct_color) |
| model_label = MODEL_LABELS.get(model_key, model_key) |
| chosen_color_label = COLOR_RU.get(user_chosen_color, user_chosen_color) |
|
|
| check = "✅" if is_this_one_correct else "❌" |
| html += ( |
| f'<br><span style="color:#8898b0;">Ответ {i}:</span> ' |
| f'{color_label} — <b style="color:#f0f4ff;">{model_label}</b>. ' |
| f'<span style="color:#c8d0e0;">Вы выбрали: {chosen_color_label}</span> {check}' |
| ) |
|
|
| html += '</div>' |
|
|
| html += '</div>' |
| return html |
|
|
|
|
| |
| async def select_work(work_label: str, state: dict): |
| if not work_label: |
| return state, "", "", "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) |
|
|
| work = WORK_INDEX[work_label] |
| state = fresh_state() |
| state["current_work"] = work |
| state = reshuffle_colors(state) |
|
|
| payload = { |
| "work_id": work["work_id"], |
| "title": work["title"], |
| "author": work["author"], |
| "genre": work.get("genre", "poem"), |
| "text": work["text"], |
| "seed": random.randint(0, 9999), |
| } |
|
|
| try: |
| data = await post_json(BOOTSTRAP_URL, payload) |
| except Exception as e: |
| return state, f"...Ошибка bootstrap...", "", "", \ |
| gr.update(visible=False), gr.update(visible=False), \ |
| gr.update(visible=False), gr.update(visible=False), \ |
| gr.update(visible=False), \ |
| gr.update(interactive=True) |
| questions_data = data.get("questions", []) |
| state["bootstrap_answers"] = questions_data |
|
|
| bootstrap_html = render_bootstrap(questions_data, state["color_map"]) |
| work_html = ( |
| f'<div class="panel">' |
| f'<b style="font-size:16px;">{work["title"]}</b> — {work["author"]}<br>' |
| f'<pre style="white-space:pre-wrap;font-family:inherit;color:#c8d0e0;' |
| f'margin-top:8px;">{work["text"]}</pre>' |
| f'</div>' |
| ) |
| score_html = render_score(state) |
|
|
| return ( |
| state, work_html, bootstrap_html, score_html, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(interactive=False) |
| ) |
|
|
|
|
| async def submit_question(question: str, state: dict): |
| if not question.strip(): |
| return state, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) |
|
|
| work = state["current_work"] |
| payload = { |
| "work_id": work["work_id"], |
| "title": work["title"], |
| "author": work["author"], |
| "genre": work.get("genre", "poem"), |
| "text": work["text"], |
| "question": question.strip(), |
| } |
|
|
| try: |
| judge_data = await post_json(JUDGE_URL, payload) |
| except Exception as e: |
| return ( |
| state, |
| f'<p style="color:#ff6b6b">Ошибка judge: {e}</p>', |
| "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
| if judge_data.get("decision") == "block": |
| return ( |
| state, |
| '<p style="color:#facc15;font-size:14px">⚠️ Вопрос не связан с произведением ' |
| 'или выглядит как спам. Попробуй переформулировать.</p>', |
| "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
| |
| if _is_bootstrap_duplicate(question, state): |
| return ( |
| state, |
| '<p style="color:#facc15;font-size:14px">⚠️ Этот вопрос уже был в ознакомительном блоке — ' |
| 'модели ответят так же, и разметка очевидна. ' |
| 'Придумай свой вопрос, чтобы игра была честной!</p>', |
| "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
| try: |
| answer_data = await post_json(ANSWER_URL, payload) |
| except Exception as e: |
| return ( |
| state, |
| f'<p style="color:#ff6b6b">Ошибка answer: {e}</p>', |
| "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
| items = answer_data.get("items", []) |
| ans_by_model = { |
| a["model_key"]: a["answer"] |
| for a in items if isinstance(a, dict) and "model_key" in a |
| } |
|
|
| answers_ordered = [] |
| for model_key in MODEL_KEYS: |
| answers_ordered.append({ |
| "answer": ans_by_model.get(model_key, "Нет ответа."), |
| "model_key": model_key, |
| }) |
| random.shuffle(answers_ordered) |
|
|
| state["last_answers"] = answers_ordered |
| state["last_question"] = question.strip() |
|
|
| answers_html = render_answers_neutral(answers_ordered) |
|
|
| return ( |
| state, |
| '<p style="color:#16a34a;font-size:14px;">✅ Вопрос принят. Выбери цвета для ответов.</p>', |
| answers_html, |
| gr.update(visible=True), |
| gr.update(visible=True), |
| ) |
|
|
|
|
| def submit_guess( |
| guess_1: str, guess_2: str, guess_3: str, state: dict |
| ): |
| if state["game_over"]: |
| return state, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) |
|
|
| color_map = state["color_map"] |
| last_answers = state["last_answers"] |
|
|
| user_guess_colors = [guess_1, guess_2, guess_3] |
| correct_count = 0 |
| total_answers = len(last_answers) |
|
|
| for i, ans in enumerate(last_answers): |
| model_key = ans["model_key"] |
| correct_color = None |
| for c, mk in color_map.items(): |
| if mk == model_key: |
| correct_color = c |
| break |
| if i < len(user_guess_colors) and user_guess_colors[i] == correct_color: |
| correct_count += 1 |
|
|
| is_correct = (correct_count == total_answers) |
| state["score"] += correct_count |
|
|
| state["history"].append({ |
| "round": state["round"] + 1, |
| "question": state["last_question"], |
| "answers": list(state["last_answers"]), |
| "color_map": dict(color_map), |
| "user_guess": { |
| f"answer_{i+1}": user_guess_colors[i] |
| for i in range(len(user_guess_colors)) |
| }, |
| "correct": is_correct, |
| "round_correct": correct_count, |
| }) |
| state["round"] += 1 |
|
|
| score_html = render_score(state) |
| game_over = state["round"] >= ROUND_LIMIT |
| state["game_over"] = game_over |
|
|
| return ( |
| state, |
| score_html, |
| gr.update(visible=True), |
| gr.update(visible=not game_over), |
| gr.update(visible=not game_over), |
| gr.update(visible=game_over), |
| ) |
|
|
|
|
| def reset_for_next_round(state: dict): |
| return ( |
| state, |
| "", |
| "", |
| "", |
| "", |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(value=None), |
| gr.update(value=None), |
| gr.update(value=None), |
| ) |
|
|
|
|
| |
| |
| with gr.Blocks(title="Слепой Детектив") as demo: |
|
|
| state = gr.State(fresh_state()) |
|
|
| gr.Markdown( |
| "# 🔍 Слепой Детектив Легковесных Моделей\n" |
| "<span style='color:#c8d0e0 !important;font-size:18px;'>" |
| "Три модели отвечают на вопросы о литературном тексте. " |
| "Угадай, кто есть кто, по стилю рассуждений.</span>" |
| ) |
|
|
| with gr.Row(): |
| work_dropdown = gr.Dropdown( |
| choices=list(WORK_INDEX.keys()), |
| label="Выберите произведение", |
| scale=4, |
| ) |
| start_btn = gr.Button("▶ Начать", variant="primary", scale=1) |
|
|
| work_display = gr.HTML() |
| bootstrap_html = gr.HTML() |
| score_html = gr.HTML() |
|
|
| |
| with gr.Group(visible=False) as question_section: |
| gr.Markdown("### ✏️ Задай свой вопрос по произведению") |
| question_input = gr.Textbox( |
| placeholder="Например: О чём говорит последняя строфа?", |
| label="Вопрос", |
| lines=2, |
| ) |
| ask_btn = gr.Button("Задать вопрос", variant="primary") |
| judge_msg = gr.HTML() |
|
|
| loading_idle = gr.HTML( |
| value='<div class="loading-idle">💡 Задай вопрос и нажми «Задать вопрос»</div>', |
| visible=False, |
| ) |
| loading_active = gr.HTML( |
| value='<div class="loading-active">⏳ Модели обрабатывают вопрос...</div>', |
| visible=False, |
| ) |
|
|
| |
| with gr.Group(visible=False) as answers_section: |
| gr.Markdown("### 💬 Ответы моделей") |
| answers_html = gr.HTML() |
|
|
| |
| with gr.Group(visible=False) as guess_section: |
| gr.Markdown("### 🎯 Кто есть кто? Разметь цвета") |
| gr.Markdown("Для каждого ответа выбери цвет модели:") |
|
|
| color_choices = [ |
| ("🔴 Красный", "red"), |
| ("🟡 Жёлтый", "yellow"), |
| ("🟢 Зелёный", "green"), |
| ] |
| with gr.Row(): |
| guess_1 = gr.Radio( |
| choices=color_choices, |
| label="Ответ 1 — цвет модели:", |
| elem_classes=["color-radio"], |
| ) |
| guess_2 = gr.Radio( |
| choices=color_choices, |
| label="Ответ 2 — цвет модели:", |
| elem_classes=["color-radio"], |
| ) |
| guess_3 = gr.Radio( |
| choices=color_choices, |
| label="Ответ 3 — цвет модели:", |
| elem_classes=["color-radio"], |
| ) |
|
|
| guess_btn = gr.Button("Проверить", variant="primary") |
|
|
| |
| with gr.Group(visible=False) as round_result_section: |
| gr.Markdown("### 📊 Результат раунда") |
| round_result_html = gr.HTML() |
| |
| with gr.Row(visible=False) as action_row: |
| next_btn = gr.Button("➡ Продолжить", variant="primary") |
| finish_btn = gr.Button("🏁 Закончить", variant="secondary") |
|
|
| |
| with gr.Group(visible=False) as final_section: |
| gr.Markdown("## 🏁 Игра окончена!") |
| final_html = gr.HTML() |
| restart_btn = gr.Button("🔄 Начать заново", variant="secondary") |
|
|
| |
| start_btn.click( |
| fn=select_work, |
| inputs=[work_dropdown, state], |
| outputs=[ |
| state, work_display, bootstrap_html, score_html, |
| question_section, answers_section, guess_section, round_result_section, |
| loading_idle, start_btn, |
| ], |
| ) |
|
|
| ask_btn.click( |
| fn=lambda: ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ), |
| outputs=[loading_idle, loading_active, answers_section, guess_section], |
| ).then( |
| fn=submit_question, |
| inputs=[question_input, state], |
| outputs=[state, judge_msg, answers_html, answers_section, guess_section], |
| ).then( |
| fn=lambda: gr.update(visible=False), |
| outputs=[loading_active], |
| ) |
|
|
|
|
| def _handle_guess_result(g1, g2, g3, s): |
| state_out, score_out, _rr_vis, _next_vis, _finish_vis, _final_vis = submit_guess(g1, g2, g3, s) |
| history = state_out["history"] |
| if history: |
| h = history[-1] |
| rnd = h["round"] |
| correct_count = h["round_correct"] |
| color = "4ade80" if correct_count == 3 else "fde047" if correct_count >= 1 else "ff6b6b" |
| rr_html = f'<p style="color:#{color};font-weight:700;font-size:18px">Раунд {rnd} — {correct_count}/3 очков</p>' |
| else: |
| rr_html = "" |
| game_over = state_out["game_over"] |
| final_html_content = build_final_report(state_out) if game_over else "" |
| return ( |
| state_out, |
| score_out, |
| rr_html, |
| gr.update(visible=not game_over), |
| gr.update(visible=True), |
| gr.update(visible=not game_over), |
| gr.update(visible=game_over), |
| final_html_content, |
| gr.update(interactive=not game_over), |
| ) |
|
|
|
|
| guess_btn.click( |
| fn=_handle_guess_result, |
| inputs=[guess_1, guess_2, guess_3, state], |
| outputs=[ |
| state, score_html, round_result_html, |
| guess_section, |
| round_result_section, |
| action_row, |
| final_section, |
| final_html, |
| ask_btn, |
| ], |
| ) |
|
|
| next_btn.click( |
| fn=reset_for_next_round, |
| inputs=[state], |
| outputs=[ |
| state, question_input, judge_msg, answers_html, round_result_html, |
| question_section, answers_section, guess_section, round_result_section, |
| loading_active, loading_idle, |
| guess_1, guess_2, guess_3 |
| ], |
| ) |
|
|
| finish_btn.click( |
| fn=lambda s: ( |
| s, |
| build_final_report(s), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| ), |
| inputs=[state], |
| outputs=[state, final_html, round_result_section, action_row, guess_section, final_section], |
| ) |
|
|
| restart_btn.click( |
| fn=lambda: ( |
| fresh_state(), |
| "", |
| "", |
| "", |
| "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(interactive=True), |
| gr.update(interactive=True), |
| ), |
| outputs=[ |
| state, work_display, bootstrap_html, score_html, question_input, |
| question_section, answers_section, guess_section, action_row, |
| round_result_section, |
| final_section, |
| loading_active, |
| loading_idle, |
| start_btn, ask_btn, |
| ], |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| |
| demo.launch(css=CSS) |