from __future__ import annotations
import os
import re
import tempfile
import threading
import time
from html import escape
import gradio as gr
try:
import spaces
except ImportError: # Local tests should not require the HF Spaces runtime package.
class _SpacesFallback:
@staticmethod
def GPU(*args, **kwargs):
def decorator(fn):
return fn
return decorator
spaces = _SpacesFallback()
from study_engine import (
DEFAULT_MODEL_ID,
DEMO_CASES,
EXAMPLE_INPUT,
VISION_MODEL_ID,
answer_drills,
build_rescue_plan,
classify_gpu_failure,
coach_state,
ensure_weights,
extract_topics_from_image,
free_resident_vlm,
load_resident_vlm,
packet_to_markdown,
time_blocks,
)
CSS = """
:root {
--ink: #071613;
--muted: #1c342f;
--muted-soft: #27423c;
--paper: #f4e2c5;
--card: #fffaf0;
--card-solid: #fff8ea;
--field: #fffef9;
--line: #5e5545;
--green: #005844;
--green-dark: #032f28;
--coral: #84231b;
--gold: #755004;
--blue: #073e58;
--graph: rgba(7, 62, 88, 0.11);
--shadow: rgba(37, 29, 16, 0.20);
}
.gradio-container {
background:
radial-gradient(circle at 8% 8%, rgba(183, 67, 54, 0.18), transparent 26%),
radial-gradient(circle at 92% 4%, rgba(0, 108, 91, 0.18), transparent 24%),
linear-gradient(var(--graph) 1px, transparent 1px),
linear-gradient(90deg, var(--graph) 1px, transparent 1px),
var(--paper);
background-size: auto, 24px 24px, 24px 24px, auto;
color: var(--ink);
font-family: "Trebuchet MS", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
.gradio-container,
.gradio-container * {
text-shadow: none !important;
}
.gradio-container button:focus-visible,
.gradio-container textarea:focus-visible,
.gradio-container input:focus-visible,
.gradio-container select:focus-visible {
outline: 3px solid rgba(0, 108, 91, 0.34) !important;
outline-offset: 2px !important;
}
.app-shell {
max-width: 1240px;
margin: 0 auto;
padding: 24px clamp(14px, 3vw, 34px) 38px;
}
.hero {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
border: 1px solid rgba(7, 22, 19, 0.34);
border-radius: 24px;
background:
linear-gradient(135deg, #fffaf0, #f4d9aa);
box-shadow: 0 18px 48px rgba(37, 29, 16, 0.18);
padding: clamp(18px, 3vw, 30px);
}
.hero:after {
content: "";
position: absolute;
right: -92px;
top: -102px;
width: 260px;
height: 260px;
border-radius: 999px;
border: 38px solid rgba(183, 67, 54, 0.12);
}
.eyebrow {
display: inline-flex;
align-items: center;
width: fit-content;
border: 1px solid rgba(0, 108, 91, 0.28);
border-radius: 999px;
background: rgba(0, 88, 68, 0.16);
color: var(--green-dark);
font-size: 14px;
font-weight: 900;
letter-spacing: 0.10em;
padding: 8px 12px;
text-transform: uppercase;
}
.hero h1 {
position: relative;
margin: 14px 0 8px;
font-family: Georgia, "Times New Roman", ui-serif, serif;
color: var(--ink);
font-size: clamp(34px, 5vw, 58px);
line-height: 0.98;
letter-spacing: -0.045em;
max-width: 860px;
}
.hero p {
margin: 0;
max-width: 720px;
color: var(--muted);
font-size: clamp(17px, 2vw, 20px);
font-weight: 750;
line-height: 1.55;
}
.hero-steps {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.hero-steps span {
border: 1px solid rgba(7, 22, 19, 0.32);
border-radius: 999px;
background: #fffdf7;
color: var(--ink);
font-size: 15px;
font-weight: 900;
padding: 8px 11px;
}
.hero-proof {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 18px;
max-width: 880px;
}
.hero-proof div {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 18px;
background: #fffdf7;
padding: 12px;
}
.hero-proof b {
display: block;
color: var(--coral);
font-family: Georgia, "Times New Roman", ui-serif, serif;
font-size: clamp(22px, 3vw, 30px);
letter-spacing: -0.04em;
line-height: 0.95;
}
.hero-proof span {
display: block;
margin-top: 5px;
color: var(--ink);
font-size: 15px;
font-weight: 850;
line-height: 1.42;
}
.demo-status {
display: grid;
grid-template-columns: 1.25fr 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.status-card {
border: 1px solid rgba(7, 22, 19, 0.32);
border-radius: 20px;
background: #fff8ea;
box-shadow: 0 16px 40px rgba(37, 29, 16, 0.13);
padding: 13px 14px;
}
.status-card b {
display: block;
color: var(--green-dark);
font-size: 14px;
letter-spacing: 0.10em;
text-transform: uppercase;
}
.status-card span {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 15px;
font-weight: 750;
line-height: 1.45;
}
.model-budget {
display: grid;
grid-template-columns: 1.2fr repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
}
.budget-card {
border: 1px solid rgba(7, 22, 19, 0.34);
border-radius: 20px;
background: var(--card-solid);
padding: 13px 14px;
}
.budget-card:first-child {
background:
radial-gradient(circle at top right, rgba(0, 108, 91, 0.16), transparent 46%),
var(--card-solid);
}
.budget-card b {
display: block;
color: var(--ink);
font-size: 14px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.budget-card span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 15px;
font-weight: 750;
line-height: 1.45;
}
#main-workspace {
gap: 18px;
margin-top: 20px;
align-items: flex-start;
}
.input-card,
.output-stack {
border: 1px solid rgba(7, 22, 19, 0.34);
border-radius: 26px;
background: var(--card);
box-shadow: 0 18px 52px rgba(37, 29, 16, 0.16);
padding: clamp(14px, 2vw, 20px);
}
@media (min-width: 941px) {
.input-card {
position: sticky;
top: 16px;
}
}
.section-title {
margin-bottom: 14px;
}
.section-title h2 {
margin: 0;
font-family: Georgia, "Times New Roman", ui-serif, serif;
color: var(--ink);
font-size: 26px;
letter-spacing: -0.02em;
}
.section-title p {
margin: 6px 0 0;
color: var(--muted);
font-size: 16px;
font-weight: 750;
line-height: 1.5;
}
.panel {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 20px;
background: #fffef9;
box-shadow: none;
margin-bottom: 10px;
padding: 13px 15px;
}
.panel h3 {
color: var(--green-dark);
font-family: Georgia, "Times New Roman", ui-serif, serif;
letter-spacing: -0.01em;
}
.panel h3:first-child {
margin-top: 0;
}
.panel ul,
.final-sheet ul {
padding-left: 1.15rem;
}
.panel li,
.final-sheet li {
margin-bottom: 5px;
}
.output-stack pre,
.output-stack code {
max-width: 100% !important;
white-space: pre-wrap !important;
word-break: break-word !important;
}
.output-stack pre {
overflow-x: auto !important;
}
.input-card textarea,
.input-card input,
.input-card select {
border-radius: 14px !important;
border-color: rgba(7, 22, 19, 0.48) !important;
background: var(--field) !important;
color: var(--ink) !important;
font-size: 16px !important;
font-weight: 750 !important;
line-height: 1.45 !important;
}
.input-card label,
.input-card .wrap label {
color: var(--ink) !important;
font-size: 15px !important;
font-weight: 900 !important;
}
.gradio-container input::placeholder,
.gradio-container textarea::placeholder {
color: #4f625d !important;
opacity: 1 !important;
}
.gradio-container .prose,
.gradio-container .markdown,
.gradio-container .prose p,
.gradio-container .prose li,
.gradio-container .prose span,
.gradio-container .markdown p,
.gradio-container .markdown li,
.gradio-container .markdown span {
color: var(--ink) !important;
font-size: 16px !important;
font-weight: 700;
line-height: 1.55;
}
.gradio-container .prose h1,
.gradio-container .prose h2,
.gradio-container .prose h3,
.gradio-container .markdown h1,
.gradio-container .markdown h2,
.gradio-container .markdown h3 {
color: var(--ink) !important;
font-weight: 900 !important;
}
.gradio-container .block-info,
.gradio-container .form .secondary-wrap,
.gradio-container label span,
.gradio-container .wrap span {
color: var(--muted) !important;
font-size: 14px !important;
font-weight: 700 !important;
opacity: 1 !important;
}
.primary-action button, button.primary-action {
background: var(--green) !important;
border-color: var(--green) !important;
border-radius: 16px !important;
color: white !important;
font-weight: 850 !important;
min-height: 46px;
box-shadow: 0 12px 28px rgba(0, 108, 91, 0.24);
}
.primary-action button:hover, button.primary-action:hover {
background: var(--green-dark) !important;
}
.secondary-action button, button.secondary-action {
border-color: var(--coral) !important;
color: var(--coral) !important;
background: #fff7ed !important;
border-radius: 16px !important;
font-weight: 800 !important;
min-height: 46px;
}
/* Force the cream/light design even when the visitor's device is in dark mode. */
.gradio-container, .gradio-container.dark, .dark {
color-scheme: light;
--body-background-fill: var(--paper);
--background-fill-primary: var(--field);
--background-fill-secondary: var(--card-solid);
--block-background-fill: var(--card);
--block-label-background-fill: var(--card);
--block-title-background-fill: var(--card);
--input-background-fill: var(--field);
--body-text-color: var(--ink);
--body-text-color-subdued: var(--muted);
--block-label-text-color: var(--ink);
--block-title-text-color: var(--ink);
--block-info-text-color: var(--muted);
--border-color-primary: rgba(7, 22, 19, 0.34);
--border-color-accent: rgba(0, 88, 68, 0.34);
}
.gradio-container.dark .input-card,
.gradio-container.dark .output-stack,
.dark .input-card,
.dark .output-stack {
background: var(--card) !important;
}
#model-note {
margin-top: 10px;
border-left: 4px solid var(--gold);
border-radius: 12px;
background: rgba(189, 143, 34, 0.10);
padding: 10px 12px;
font-size: 15px;
font-weight: 800;
color: #241800;
}
.privacy-note {
display: flex;
gap: 10px;
align-items: flex-start;
margin-bottom: 14px;
border: 1px solid rgba(0, 88, 68, 0.30);
border-left: 4px solid var(--green);
border-radius: 16px;
background: rgba(0, 108, 91, 0.07);
padding: 12px 14px;
}
.privacy-note .lock {
font-size: 18px;
line-height: 1.35;
}
.privacy-note b {
display: block;
color: var(--green-dark);
font-size: 13px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 3px;
}
.privacy-note span {
display: block;
color: var(--muted);
font-size: 14px;
font-weight: 750;
line-height: 1.5;
}
.rescue-flow {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 18px;
background:
radial-gradient(circle at top right, rgba(0, 108, 91, 0.10), transparent 50%),
#fffdf7;
padding: 13px 15px;
margin-bottom: 14px;
}
.rescue-flow b {
display: block;
color: var(--green-dark);
font-size: 13px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 9px;
}
.rescue-flow ol {
margin: 0;
padding: 0;
list-style: none;
counter-reset: rf;
display: grid;
gap: 7px;
}
.rescue-flow li {
position: relative;
padding-left: 30px;
color: var(--ink);
font-size: 14.5px;
font-weight: 800;
line-height: 1.4;
}
.rescue-flow li:before {
counter-increment: rf;
content: counter(rf);
position: absolute;
left: 0;
top: -1px;
width: 21px;
height: 21px;
border-radius: 999px;
background: var(--green);
color: #fff;
font-size: 12px;
font-weight: 900;
text-align: center;
line-height: 21px;
}
.coach-hint {
margin: 8px 0 2px;
color: var(--muted);
font-size: 14px;
font-weight: 750;
line-height: 1.45;
}
#gen-status .status-busy {
border-left: 4px solid var(--gold);
border-radius: 12px;
background: rgba(189, 143, 34, 0.10);
padding: 10px 12px;
margin: 8px 0 2px;
}
@media (prefers-reduced-motion: no-preference) {
.status-busy {
animation: epr-pulse 1.6s ease-in-out infinite;
}
@keyframes epr-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
}
.wiz-progress {
list-style: none;
margin: 0 0 14px;
padding: 0;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
}
.wiz-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
.wiz-bub {
width: 26px;
height: 26px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 900;
border: 1px solid rgba(7, 22, 19, 0.30);
background: #fffdf7;
color: var(--muted);
}
.wiz-lab {
font-size: 10.5px;
font-weight: 850;
line-height: 1.2;
letter-spacing: 0.02em;
color: var(--muted);
}
.wiz-active .wiz-bub {
background: var(--green);
border-color: var(--green);
color: #fff;
box-shadow: 0 6px 16px rgba(0, 108, 91, 0.28);
}
.wiz-active .wiz-lab {
color: var(--green-dark);
}
.wiz-done .wiz-bub {
background: rgba(0, 108, 91, 0.16);
border-color: rgba(0, 88, 68, 0.40);
color: var(--green-dark);
}
.wiz-step {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 18px;
background:
radial-gradient(circle at top right, rgba(0, 108, 91, 0.08), transparent 50%),
#fffdf7;
padding: 14px 15px;
margin-bottom: 8px;
}
.wiz-head {
display: flex;
gap: 10px;
align-items: flex-start;
margin-bottom: 12px;
}
.wiz-head .wiz-num {
flex: none;
width: 24px;
height: 24px;
border-radius: 999px;
background: var(--green);
color: #fff;
font-size: 13px;
font-weight: 900;
text-align: center;
line-height: 24px;
}
.wiz-head b {
display: block;
font-family: Georgia, "Times New Roman", ui-serif, serif;
color: var(--ink);
font-size: 19px;
letter-spacing: -0.01em;
}
.wiz-head span {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: 14px;
font-weight: 750;
line-height: 1.4;
}
.wiz-nav {
gap: 10px;
margin: 6px 0 2px;
}
.hero-compact {
padding: clamp(14px, 2.2vw, 22px);
}
.hero-compact h1 {
font-size: clamp(28px, 4vw, 44px);
margin: 10px 0 6px;
}
.hero-compact p {
font-size: clamp(15px, 1.8vw, 18px);
}
@media (prefers-reduced-motion: no-preference) {
.wiz-step {
animation: wiz-step-in 280ms ease-out;
}
@keyframes wiz-step-in {
from { opacity: 0; transform: translateY(7px); }
to { opacity: 1; transform: translateY(0); }
}
.wiz-active .wiz-bub {
animation: wiz-dot-pop 320ms ease-out;
}
@keyframes wiz-dot-pop {
0% { transform: scale(0.7); }
60% { transform: scale(1.14); }
100% { transform: scale(1); }
}
}
@media (max-width: 640px) {
.wiz-lab {
display: none;
}
}
.runtime-label {
margin: 4px 0 -4px;
color: var(--green-dark);
font-size: 14px;
font-weight: 850;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.final-sheet {
border: 1px solid rgba(7, 22, 19, 0.42);
border-radius: 24px;
background:
radial-gradient(circle at top right, rgba(189, 143, 34, 0.25), transparent 34%),
linear-gradient(135deg, rgba(0, 98, 79, 0.13), #fffef9);
padding: clamp(16px, 3vw, 24px);
color: var(--ink);
}
.sheet-kicker {
color: var(--coral);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.final-sheet h2 {
margin: 4px 0 14px;
font-family: Georgia, "Times New Roman", ui-serif, serif;
font-size: clamp(27px, 4vw, 42px);
line-height: 0.98;
letter-spacing: -0.045em;
}
.sheet-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.sheet-grid h3 {
margin: 0 0 8px;
color: var(--blue);
font-weight: 900;
}
.sheet-rule {
border-left: 4px solid var(--green);
margin: 12px 0 0;
padding: 12px 14px;
border-radius: 12px;
background: rgba(0, 108, 91, 0.09);
font-weight: 700;
}
.sheet-action,
.sheet-proof,
.sheet-warning {
margin: 12px 0 0;
padding: 12px 14px;
border-radius: 12px;
background: rgba(31, 85, 116, 0.10);
}
.sheet-proof {
border: 1px solid rgba(31, 85, 116, 0.20);
}
.sheet-warning {
border: 1px solid rgba(183, 67, 54, 0.24);
background: rgba(183, 67, 54, 0.10);
}
.sheet-footer {
margin: 10px 0 0;
color: var(--muted);
font-size: 15px;
font-weight: 750;
}
.demo-cases {
margin-top: 14px;
border: 1px dashed rgba(7, 22, 19, 0.36);
border-radius: 18px;
background: #fffaf0;
box-shadow: none;
padding: 12px;
}
.demo-cases h2 {
margin: 0 0 6px;
font-family: Georgia, "Times New Roman", ui-serif, serif;
color: var(--ink);
font-size: 25px;
letter-spacing: -0.02em;
}
.demo-cases p {
margin: 0 0 12px;
color: var(--muted);
font-size: 15px;
font-weight: 750;
}
.case-list {
gap: 8px;
}
.case-button button {
justify-content: flex-start !important;
width: 100%;
min-height: 44px;
border: 1px solid rgba(0, 88, 68, 0.36) !important;
border-radius: 15px !important;
background: #fffef9 !important;
color: var(--ink) !important;
font-size: 15px !important;
font-weight: 800 !important;
text-align: left !important;
}
.case-button button:hover {
border-color: rgba(0, 108, 91, 0.36) !important;
background: rgba(0, 108, 91, 0.08) !important;
}
.claim-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.claim-card {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 16px;
background: #fffef9;
padding: 12px;
box-shadow: none;
}
.claim-card b {
display: block;
color: var(--green-dark);
font-size: 14px;
letter-spacing: 0.10em;
text-transform: uppercase;
}
.claim-card span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 15px;
font-weight: 750;
line-height: 1.42;
}
.proof-details {
margin-top: 18px;
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 20px;
background: #fffaf0;
padding: 12px 14px;
}
.proof-details summary {
cursor: pointer;
color: var(--green-dark);
font-size: 15px;
font-weight: 900;
}
.proof-details p {
color: var(--muted);
font-size: 15px;
font-weight: 750;
line-height: 1.5;
}
.hackathon-footer {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 20px;
padding: 14px;
border: 1px solid rgba(7, 22, 19, 0.22);
border-radius: 18px;
background: #fffaf0;
}
.hackathon-footer span {
border: 1px solid rgba(0, 88, 68, 0.30);
border-radius: 999px;
background: #fffef9;
color: var(--green-dark);
font-size: 13px;
font-weight: 850;
letter-spacing: 0.04em;
padding: 6px 12px;
}
.runtime-note-tag {
display: inline-block;
margin: 0 0 6px;
color: var(--green-dark);
font-size: 13px;
font-weight: 850;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.coach-card {
border: 1px solid rgba(7, 22, 19, 0.30);
border-radius: 18px;
background: #fffef9;
padding: 14px 16px;
margin-bottom: 10px;
}
.coach-idle {
color: var(--muted);
font-size: 15px;
font-weight: 750;
line-height: 1.45;
}
.coach-live {
background: radial-gradient(circle at top right, rgba(0, 108, 91, 0.16), transparent 50%), #fffef9;
border-color: rgba(0, 88, 68, 0.42);
}
.coach-now {
color: var(--green-dark);
font-size: 18px;
font-weight: 900;
letter-spacing: 0.02em;
}
.coach-time {
font-family: Georgia, "Times New Roman", ui-serif, serif;
color: var(--coral);
font-size: clamp(34px, 6vw, 46px);
line-height: 1.05;
letter-spacing: -0.03em;
margin: 2px 0;
}
.coach-next {
color: var(--muted);
font-size: 14px;
font-weight: 800;
}
.coach-action,
.coach-proof {
margin-top: 8px;
color: var(--ink);
font-size: 15px;
font-weight: 800;
line-height: 1.45;
}
.coach-proof {
border-left: 4px solid var(--gold);
border-radius: 10px;
background: rgba(189, 143, 34, 0.10);
padding: 9px 11px;
}
.coach-done {
background: rgba(0, 108, 91, 0.12);
border-color: rgba(0, 88, 68, 0.42);
color: var(--green-dark);
font-size: 16px;
font-weight: 850;
}
@media (prefers-reduced-motion: no-preference) {
.primary-action button,
.secondary-action button {
transition: transform 150ms ease-out, background-color 150ms ease-out, box-shadow 150ms ease-out;
}
.primary-action button:hover,
.secondary-action button:hover {
transform: translateY(-1px);
}
}
@media (max-width: 940px) {
.hero {
grid-template-columns: 1fr;
}
.demo-status,
.model-budget {
grid-template-columns: 1fr;
}
#main-workspace {
flex-direction: column !important;
}
#main-workspace > .column,
#main-workspace > div {
width: 100% !important;
min-width: 100% !important;
}
}
@media (max-width: 640px) {
.app-shell {
padding: 12px 10px 24px;
}
.hero {
border-radius: 22px;
padding: 18px;
}
.hero-steps span {
width: 100%;
}
.hero-proof {
grid-template-columns: 1fr;
}
.input-card,
.output-stack {
border-radius: 20px;
padding: 12px;
}
.sheet-grid {
grid-template-columns: 1fr;
}
.claim-strip {
grid-template-columns: 1fr;
}
.primary-action,
.secondary-action {
flex: 1 1 100%;
}
}
/* ----- Output tabs: Live Coach / Rescue Plan / Final Sheet / Resources -----
Selectors are deliberately doubled (Gradio class names + ARIA roles) so the
cream/green styling lands regardless of the exact Gradio 6 tab DOM. */
#output-tabs {
border: none;
margin-top: 2px;
}
#output-tabs > .tab-nav,
#output-tabs [role="tablist"] {
display: flex;
flex-wrap: wrap;
gap: 7px;
border: none !important;
margin-bottom: 12px;
}
#output-tabs .tab-nav button,
#output-tabs button[role="tab"] {
border: 1px solid rgba(7, 22, 19, 0.30) !important;
border-radius: 14px !important;
background: #fffdf7 !important;
color: var(--muted) !important;
font-size: 14.5px !important;
font-weight: 850 !important;
padding: 8px 14px !important;
min-height: 40px;
}
#output-tabs .tab-nav button:hover,
#output-tabs button[role="tab"]:hover {
border-color: rgba(0, 108, 91, 0.40) !important;
background: rgba(0, 108, 91, 0.07) !important;
}
#output-tabs .tab-nav button.selected,
#output-tabs button[role="tab"][aria-selected="true"] {
background: var(--green) !important;
border-color: var(--green) !important;
color: #fff !important;
box-shadow: 0 8px 18px rgba(0, 108, 91, 0.24);
}
#output-tabs .tabitem,
#output-tabs [role="tabpanel"] {
padding-top: 6px;
}
@media (prefers-reduced-motion: no-preference) {
#output-tabs .tabitem,
#output-tabs [role="tabpanel"] {
animation: epr-tab-in 240ms ease-out;
}
@keyframes epr-tab-in {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
}
/* ----- Clearer wizard step states (active stands out, done is checked) ----- */
.wiz-active .wiz-bub {
box-shadow: 0 0 0 4px rgba(0, 108, 91, 0.18), 0 8px 18px rgba(0, 108, 91, 0.30);
}
.wiz-active .wiz-lab {
color: var(--green-dark);
font-weight: 900;
}
.wiz-done .wiz-bub {
background: rgba(0, 108, 91, 0.18);
border-color: rgba(0, 88, 68, 0.55);
color: var(--green-dark);
}
.wiz-todo .wiz-bub {
opacity: 0.78;
}
"""
HERO_HTML = """
Five guided steps: tell us the exam, share what you have (or a photo), and get a ranked plan, drills, and a live coach. How to review fast: load a sample scenario only to understand the flow, replace it with real exam details when using the product, build the rescue packet, then check the proof target/final sheet and runtime note.When time is low, stop rereading everything.
Hackathon build proof and claim status
⏳ Working… normally ~10–25 s once warm; the first ' 'run right after a restart can take longer while the small model wakes up. ' 'Elapsed: 0s. Your packet appears in the results panel.
' ) def _gen_busy(): return GEN_BUSY_HTML def _gen_done(): return "" def _vision_busy(): return "⏳ Reading your photo with MiniCPM-V 4.6 — usually well under a minute when the model is warm." def _answers_busy(): return "### Worked answers\n\n⏳ Writing worked answers with the selected small model…" def _plain_text(value) -> str: text = re.sub(r"<[^>]+>", " ", str(value or "")) return re.sub(r"\s+", " ", text).strip() def _triage_item(triage_markdown, label: str) -> str: pattern = rf"-\s*{re.escape(label)}:\s*(.+)" match = re.search(pattern, str(triage_markdown or ""), re.IGNORECASE) return match.group(1).strip() if match else "" def _first_csv_item(value: str) -> str: return next((item.strip() for item in value.split(",") if item.strip()), "") def _coach_instruction(label: str, triage_markdown, final_sheet_html) -> tuple[str, str]: current = (label or "").lower() proof = _triage_item(triage_markdown, "Proof target") first_topic = _first_csv_item(_triage_item(triage_markdown, "Topics extracted")) first_leak = _first_csv_item(_triage_item(triage_markdown, "Weaknesses to attack")) topic = first_topic or "your first protected mark" leak = first_leak or "one visible weak spot" if "reset" in current: action = f"Write what you remember about {topic}, circle {leak}, then commit to the first drill." stop = proof or "Stop this block once the first target is chosen." elif "core" in current or "recall" in current: action = f"Protect the marks you already know. Rebuild {topic} from memory before checking notes." stop = "Stop when you can explain the first protected topic without reopening every chapter." elif "drill" in current or "practice" in current or "quick test" in current: action = "Answer the drills without notes first. Mark mistakes instead of hiding them." stop = proof or "Stop when one drill is answered and one mistake is repaired." elif "patch" in current or "weak" in current or "second" in current: action = f"Patch {leak}. Repair one mistake, then rerun the same style of question." stop = "Stop when the mistake has a corrected version you can repeat." elif "break" in current: action = "Take the break. Stand up, drink water, and do not open a new topic." stop = "Return when the timer moves to the next block." elif "final" in current: action = "Read only the final sheet. No new chapters, no new videos, no last-minute syllabus expansion." stop = "Stop when the final sheet has been read once calmly." else: action = _plain_text(final_sheet_html)[:180] or "Follow the current block from your triage clock." stop = proof or "Move on when the timer changes blocks." return action, stop def _coach_card(state: dict, triage_markdown, final_sheet_html, panic_until=None) -> str: if panic_until and time.time() < float(panic_until): remaining = max(0, int(round(float(panic_until) - time.time()))) return ( 'Five quick steps. The flow moves with you.
After generating your rescue plan, start the live coach to walk through the time blocks step by step. Open the Live Coach tab in the results panel →
', container=False, ) with gr.Row(elem_classes=["wiz-nav"]): back_btn = gr.Button("← Back", elem_classes=["secondary-action"], interactive=False) next_btn = gr.Button("Next: Share what you have →", elem_classes=["primary-action"]) inputs = [student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence] with gr.Column(elem_classes=["demo-cases"]): gr.HTML( """One click fills every step (demo data, not real users).
""", container=False, ) case_buttons = [] with gr.Column(elem_classes=["case-list"]): for index, case in enumerate(DEMO_CASES): label = f"{case['name'].title()} · {case['time_left_minutes']} min · {case['exam_format']}" case_buttons.append( ( gr.Button( label, size="lg", elem_classes=["case-button"], ), index, ) ) with gr.Column(scale=7, min_width=340, elem_classes=["output-stack"]): gr.HTML( """Four tabs: Live Coach, Rescue Plan, Final Sheet, Resources. Your plan opens automatically.
After generating your rescue plan, start the coach. It keeps the current action, proof target, and next block visible while you study.
', container=False, ) with gr.Tab("Rescue plan", id="plan"): rescue_output = gr.Markdown( value="### Rescue plan\n\nAppears after you build the packet (step 4).", elem_classes=["panel"], ) drill_output = gr.Markdown( value="### Drill deck\n\nFive drills land here after generation.", elem_classes=["panel"], ) answers_btn = gr.Button("Show worked answers", elem_classes=["secondary-action"]) answers_output = gr.Markdown( value="### Worked answers\n\nReveal model-written answers after you build a packet.", elem_classes=["panel"], ) with gr.Tab("Final sheet", id="sheet"): triage_output = gr.Markdown( value="### Triage clock\n\nYour time blocks land here.", elem_classes=["panel"], ) final_sheet_output = gr.HTML( value='Build a packet to create the one-page sheet to read before the exam.