Fugee is drafting your ' 'personal statement and building your Word + PDF files. This takes a few moments.
' '"""app/app.py — Fugee Gradio entrypoint.
Run locally with::
python app/app.py
Mounts the phase screens onto a single Gradio shell themed entirely from
DESIGN.md tokens (via ``app.design_tokens``) and mockup.html. The intake screen
(Phase 1) shows first; choosing a language and pressing "Begin" hides it and
streams the interview (Phase 2).
All colours, fonts, radii, and spacing come from DESIGN.md — never hardcoded
here (CLAUDE.md Design Rule 7).
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Allow ``python app/app.py`` (script run) as well as ``python -m app.app``.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import gradio as gr # noqa: E402
from agent.loop import create_loop # noqa: E402
from app.config import load_env # noqa: E402
from app.design_tokens import root_css # noqa: E402
from app.phases import assessment as assessment_phase # noqa: E402
from app.phases import documents as documents_phase # noqa: E402
from app.phases import intake as intake_phase # noqa: E402
from app.phases import interview as interview_phase # noqa: E402
from app.phases import recommendations as reco_phase # noqa: E402
from app.state.session import SessionState, State # noqa: E402
# Load .env (OLLAMA_HOST, MODEL_ID, …) before anything reads the environment.
load_env()
# One model for the whole flow (hackathon: single LLM, no fallback). Read from
# MODEL_ID via the loop factory.
# Web fonts: Fraunces (display serif) + Inter (UI sans) per DESIGN.md §3.
FONT_IMPORT = (
"@import url('https://fonts.googleapis.com/css2?"
"family=Fraunces:opsz,wght@9..144,500;9..144,600&"
"family=Inter:wght@400;450;600;700&display=swap');"
)
# Global shell + chrome overrides so Gradio's default theme doesn't bleed
# through. Flatten default component chrome; our phase screens (#intake-screen,
# #iv-screen) re-assert their own surface/border with higher specificity.
GLOBAL_CSS = """
footer { display: none !important; }
/* Always reserve the scrollbar gutter so a tall screen (with a scrollbar) and a
short one (without) keep the SAME viewport width — otherwise the centered
container, and the nav inside it, jump sideways on every screen transition. */
html { scrollbar-gutter: stable; overflow-y: scroll; }
body, .gradio-container { background: var(--bg) !important; color: var(--text);
font-family: var(--font-ui); }
/* Wide shell like the mockup (main = 1180px); narrow screens cap themselves.
width:100% pins the shell to the full available width so it never shrink-wraps
to the current phase's content — that shrink-wrap is what made the nav (which
spans this container) change size and re-center between screens. */
.gradio-container { width: 100% !important; max-width: 1180px !important; margin: 0 auto !important;
padding: 0 clamp(16px,4vw,32px) var(--s-xxl) !important; }
.gradio-container button, .gradio-container input,
.gradio-container textarea, .gradio-container select { font-family: var(--font-ui); }
/* neutralise Gradio's default boxes so our custom screens read cleanly */
.gradio-container .block { background: transparent !important; border: none !important;
box-shadow: none !important; }
.gradio-container .form { background: transparent !important; border: none !important;
box-shadow: none !important; }
.gradio-container .gap { gap: var(--s-sm); }
/* Every phase wrapper fills the shell so all five screens render at the SAME
width. Without this, the Gradio column shrink-wraps to its content and each
card collapses to a different width (intake ~680, interview ~560, etc.). */
.screen-wrap { width: 100% !important; align-self: stretch !important; }
.screen-wrap > .block, .screen-wrap > div { width: 100% !important; }
.gradio-container :focus-visible { outline: 3px solid var(--accent) !important;
outline-offset: 2px; border-radius: var(--r-sm); }
/* Site bar (mockup chrome) */
#site-bar { position:sticky; top:0; z-index:50; background:rgba(247,245,240,.86);
backdrop-filter:saturate(140%) blur(10px); border-bottom:1px solid var(--line);
margin:0 calc(-1 * clamp(16px,4vw,32px)) var(--s-xl); padding:14px clamp(16px,4vw,32px); }
#site-bar .inner { display:flex; align-items:center; gap:12px; max-width:1180px; margin:0 auto; }
#site-bar .name { font-family:var(--font-display); font-weight:600; font-size:21px;
letter-spacing:-.01em; color:var(--primary-deep); }
#site-bar .tag { font-size:12px; color:var(--text-muted); letter-spacing:.04em;
text-transform:uppercase; padding-left:10px; margin-left:2px; border-left:1px solid var(--line-strong); }
#site-bar .nav { margin-left:auto; display:flex; gap:2px; }
#site-bar .nav span { font-size:13px; font-weight:500; color:var(--text-secondary);
padding:7px 12px; border-radius:var(--r-full); white-space:nowrap; }
@media(max-width:860px){ #site-bar .nav, #site-bar .tag { display:none; } }
/* Per-phase header (tag + description), like the mockup */
.phase-head { display:flex; align-items:baseline; gap:14px; flex-wrap:wrap; margin-bottom:var(--s-lg); }
.phase-head .ptag { font-size:11.5px; font-weight:600; letter-spacing:.13em; text-transform:uppercase;
color:var(--primary); background:var(--primary-tint); padding:5px 11px; border-radius:var(--r-full); white-space:nowrap; }
.phase-head .pdesc { font-size:14px; color:var(--text-muted); max-width:52ch; }
"""
APP_CSS = (
FONT_IMPORT + "\n"
+ root_css() + "\n"
+ GLOBAL_CSS + "\n"
+ intake_phase.INTAKE_CSS + "\n"
+ interview_phase.INTERVIEW_CSS + "\n"
+ assessment_phase.ASSESSMENT_CSS + "\n"
+ reco_phase.RECO_CSS + "\n"
+ documents_phase.DOCUMENTS_CSS
)
_LOGO_SVG = (
''
)
SITE_BAR_HTML = f"""
Fugee is drafting your ' 'personal statement and building your Word + PDF files. This takes a few moments.
' '