"""Case Forge — forge a Harvard-style teaching case + teaching note (HF Space). For instructors authoring cases: pick domain + topic + level + language → the fine-tuned ≤4B student expands the short request into the full contract (data/schema.py), rendered classroom-ready with quality badges. The author can then edit the Markdown live, regenerate, and export. Runtime: ZeroGPU in-Space. UI identity: "forge & ember" — slate + amber, Space Grotesk display font, an animated book+flame logo, and subtle entrance/loading/result animations. Polish is a direct judging criterion; all the polish hooks (theme/css/head/js/favicon) are passed to launch() (Gradio 6 moved them off Blocks()). """ import os import sys from pathlib import Path os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") _ROOT = Path(__file__).resolve().parent _MONOREPO = _ROOT.parent for _p in (str(_ROOT), str(_MONOREPO)): if _p not in sys.path: sys.path.insert(0, _p) import gradio as gr # noqa: E402 from shared import i18n # noqa: E402 (visuals are local; only i18n is shared) from core import export, infer, numcheck, render # noqa: E402 LOGO_PATH = _ROOT / "assets" / "logo.svg" _LOGO_SVG = LOGO_PATH.read_text(encoding="utf-8") # Audience levels: UI label per language → value sent to the model (matches the # training distribution, which is PT-leaning). LEVELS = { "en": [("MBA", "MBA"), ("Undergraduate", "graduação"), ("Executive", "executivo")], "pt": [("MBA", "MBA"), ("Graduação", "graduação"), ("Executivo", "executivo")], } EXAMPLE = { "domain": "estratégia", "topic": "uma rede de cafeterias decidindo adotar preço dinâmico por horário", "theory": "elasticidade-preço, percepção de justiça de preço", } # --------------------------------------------------------------------------- # Look & feel — theme + injected + CSS skin (all passed to launch()). # NOTE: kept as plain strings (no f-strings / .format) so CSS/JS braces and any # backslashes are safe under the Space's Python 3.10. # --------------------------------------------------------------------------- def _forge_theme(): """A distinct ember theme for Case Forge (does not touch shared/ui).""" return gr.themes.Soft( primary_hue=gr.themes.colors.orange, secondary_hue=gr.themes.colors.amber, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], ).set( body_background_fill="linear-gradient(160deg, #14131a 0%, #1c1822 100%)", body_background_fill_dark="linear-gradient(160deg, #14131a 0%, #1c1822 100%)", block_background_fill="rgba(255,255,255,0.045)", block_border_width="1px", block_border_color="rgba(255,255,255,0.09)", block_radius="16px", button_primary_background_fill="linear-gradient(92deg, #ff8a3d 0%, #ff6a1f 100%)", button_primary_background_fill_hover="linear-gradient(92deg, #ffa05a 0%, #ff7a2f 100%)", button_primary_text_color="#1c1206", # Tone down component labels (default renders them as solid ember pills). # We force dark mode, so the *_dark variants are the ones that apply. block_title_background_fill="transparent", block_title_background_fill_dark="transparent", block_title_text_color="#e7b890", block_title_text_color_dark="#e7b890", block_label_background_fill="rgba(255,138,61,0.14)", block_label_background_fill_dark="rgba(255,138,61,0.14)", block_label_text_color="#ffc89c", block_label_text_color_dark="#ffc89c", input_background_fill="rgba(0,0,0,0.22)", input_background_fill_dark="rgba(0,0,0,0.22)", input_background_fill_focus="rgba(0,0,0,0.30)", input_background_fill_focus_dark="rgba(0,0,0,0.30)", ) _HEAD = """ """ _CSS = """ .gradio-container{ color:var(--cf-ink); } footer{ display:none !important; } /* ---------- branded hero (animated ember canvas behind) ---------- */ #cf-hero{ position:relative; overflow:hidden; border-radius:20px; margin:2px 0 6px; padding:18px 20px; border:1px solid rgba(255,138,61,.14); background:linear-gradient(180deg, rgba(255,138,61,.05), rgba(255,255,255,.012)); } #cf-hero::before{ content:''; position:absolute; inset:0; pointer-events:none; background:radial-gradient(120% 150% at 16% -10%, rgba(255,138,61,.20), rgba(255,138,61,0) 55%); } #cf-embers{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; opacity:.85; } #cf-header{ position:relative; z-index:1; display:flex; align-items:center; gap:18px; } #cf-header .cf-logo{ width:64px; height:64px; flex:0 0 auto; filter:drop-shadow(0 6px 18px rgba(255,138,61,.42)); } #cf-header .cf-logo svg{ width:100%; height:100%; display:block; } #cf-header .cf-flame{ transform-origin:32px 18px; animation:cfFlicker 2.8s ease-in-out infinite; } #cf-header .cf-flame-core{ transform-origin:32px 18px; animation:cfFlicker 2.2s ease-in-out infinite; } #cf-header .cf-glow{ animation:cfGlow 2.8s ease-in-out infinite; } #cf-header h1{ font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:2.45rem; margin:0; line-height:1.02; letter-spacing:.5px; background:linear-gradient(92deg,#ffe7bd,#ff9d4d 55%,#ff6a1f); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; } #cf-header p{ margin:4px 0 0; color:var(--cf-muted); font-size:1rem; } /* ---------- panels / cards (depth + ember edge) ---------- */ .cf-panel{ position:relative; border-radius:18px; padding:18px 16px 14px !important; background:linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.018)); border:1px solid rgba(255,138,61,.16); box-shadow:0 16px 46px rgba(0,0,0,.40), inset 0 1px 0 rgba(255,255,255,.05); } .cf-panel::before{ content:''; position:absolute; top:0; left:22px; right:22px; height:1px; background:linear-gradient(90deg, transparent, rgba(255,138,61,.55), transparent); } /* ---------- buttons ---------- */ .gradio-container button.primary{ font-weight:600; transition:transform .15s ease, box-shadow .15s ease, filter .15s ease; } .gradio-container button.primary:hover{ transform:translateY(-1px); box-shadow:0 10px 24px rgba(255,138,61,.32); filter:brightness(1.04); } .gradio-container button.secondary{ transition:transform .15s ease, border-color .15s ease; } .gradio-container button.secondary:hover{ transform:translateY(-1px); border-color:rgba(255,138,61,.5); } /* ---------- status (shimmers while forging) ---------- */ #cf-status{ font-weight:600; min-height:1.2em; } body.cf-forging #cf-status, body.cf-forging #cf-status *{ background:linear-gradient(90deg,#ff8a3d,#ffe0ad,#ff8a3d); background-size:200% auto; -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; animation:cfShimmer 1.25s linear infinite; } body.cf-forging #cf-header .cf-flame{ animation:cfFlickerFast .55s ease-in-out infinite; } body.cf-forging #cf-header .cf-logo{ filter:drop-shadow(0 6px 22px rgba(255,138,61,.6)); } /* ---------- quality chips (staggered pop-in) ---------- */ .cf-chips{ display:flex; flex-wrap:wrap; gap:8px; margin:2px 0 6px; } .cf-chip{ padding:6px 13px; border-radius:999px; font-size:.84rem; font-weight:500; border:1px solid transparent; opacity:0; animation:cfPop .42s ease forwards; } .cf-pass{ background:rgba(74,222,128,.14); border-color:rgba(74,222,128,.5); color:#bbf7d0; } .cf-fail{ background:rgba(248,113,113,.13); border-color:rgba(248,113,113,.45); color:#fecaca; } .cf-demo{ font-style:italic; opacity:.85; margin-top:6px; font-size:.85rem; } .cf-numwarn{ margin-top:8px; padding:8px 12px; border-radius:10px; font-size:.84rem; background:rgba(255,193,7,.10); border:1px solid rgba(255,193,7,.40); color:#ffe2a6; } .cf-numwarn b{ color:#ffd479; } .cf-numwarn ul{ margin:4px 0 0; padding-left:18px; } .cf-numwarn li{ margin:2px 0; } .cf-hint{ font-size:.82rem; opacity:.65; margin:4px 2px; } /* ---------- empty state (illustrated) ---------- */ .cf-empty{ text-align:center; padding:48px 22px; color:var(--cf-muted); border:1px dashed rgba(255,138,61,.28); border-radius:16px; background:radial-gradient(80% 130% at 50% -10%, rgba(255,138,61,.08), rgba(255,138,61,0)); } .cf-empty .cf-empty-logo{ width:58px; height:58px; margin:0 auto 12px; filter:drop-shadow(0 5px 16px rgba(255,138,61,.42)); animation:cfPulse 3s ease-in-out infinite; } .cf-empty .cf-empty-logo svg{ width:100%; height:100%; display:block; } .cf-empty h3{ font-family:'Space Grotesk',sans-serif; color:#ffd9b0; margin:0 0 4px; font-size:1.08rem; letter-spacing:.3px; } .cf-empty p{ margin:0; font-size:.9rem; } /* ---------- rendered case/note (readable on dark; ember accents) ---------- */ .cf-doc, .cf-doc *{ color:var(--cf-ink) !important; } .cf-doc{ max-height:64vh; overflow-y:auto; padding-right:8px; line-height:1.66; animation:cfFadeUp .5s ease; } .cf-doc h1{ font-family:'Space Grotesk',sans-serif; font-size:1.5rem; color:#ffd9b0 !important; border-bottom:1px solid var(--cf-ember-soft); padding-bottom:6px; } .cf-doc h2{ font-family:'Space Grotesk',sans-serif; font-size:1.12rem; color:var(--cf-ember2) !important; margin-top:1.4em; } .cf-doc strong{ color:#fff3e6 !important; } .cf-doc blockquote{ border-left:3px solid var(--cf-ember); background:var(--cf-ember-soft); padding:.5em .9em; border-radius:8px; margin:1em 0; } .cf-doc table{ border-collapse:collapse; margin:.5em 0; } .cf-doc th, .cf-doc td{ border:1px solid var(--cf-border); padding:5px 9px; } .cf-doc li{ margin:.25em 0; } .cf-chips{ animation:cfFadeUp .5s ease; } /* ---------- entrance ---------- */ body.cf-ready #cf-header{ animation:cfFadeUp .6s ease; } body.cf-ready .cf-panel{ animation:cfFadeUp .6s ease .08s backwards; } """ # Small client-side hooks (plain JS strings; executed by Gradio — NOT via head # innerHTML, which doesn't run