Spaces:
Running on Zero
Running on Zero
| """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 <head> + 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 = """ | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --cf-ember:#ff8a3d; --cf-ember2:#ffb066; | |
| --cf-ink:#ece7df; --cf-muted:rgba(236,231,223,.60); | |
| --cf-surface:rgba(255,255,255,.045); --cf-border:rgba(255,255,255,.09); | |
| --cf-ember-soft:rgba(255,138,61,.14); | |
| } | |
| @keyframes cfFadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}} | |
| @keyframes cfPop{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}} | |
| @keyframes cfShimmer{to{background-position:200% center}} | |
| @keyframes cfFlicker{0%,100%{opacity:1;transform:scaleY(1)}50%{opacity:.82;transform:scaleY(1.07)}} | |
| @keyframes cfFlickerFast{0%,100%{opacity:1;transform:scaleY(1)}50%{opacity:.65;transform:scaleY(1.14)}} | |
| @keyframes cfGlow{0%,100%{opacity:.5}50%{opacity:.92}} | |
| @keyframes cfPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}} | |
| @media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}} | |
| </style> | |
| """ | |
| _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 <script>). The ember field is a real canvas anim. | |
| _JS_EMBERS = """ | |
| () => { | |
| document.body.classList.add('cf-ready'); | |
| const c = document.getElementById('cf-embers'); | |
| if(!c) return; | |
| if(window.__cfRAF) cancelAnimationFrame(window.__cfRAF); | |
| const ctx = c.getContext('2d'); | |
| const resize = () => { c.width = c.offsetWidth; c.height = c.offsetHeight; }; | |
| resize(); window.addEventListener('resize', resize); | |
| const N = 30, ps = []; | |
| for(let i=0;i<N;i++){ ps.push({x:Math.random(), y:Math.random(), | |
| r:Math.random()*1.7+0.5, s:Math.random()*0.0008+0.00025, | |
| a:Math.random()*0.5+0.25, t:Math.random()*6.3}); } | |
| const tick = () => { | |
| ctx.clearRect(0,0,c.width,c.height); | |
| for(const p of ps){ | |
| p.y -= p.s; p.t += 0.02; | |
| if(p.y < -0.06){ p.y = 1.06; p.x = Math.random(); } | |
| const x = (p.x + Math.sin(p.t)*0.012) * c.width, y = p.y * c.height, rad = p.r*4.5; | |
| const g = ctx.createRadialGradient(x,y,0,x,y,rad); | |
| g.addColorStop(0,'rgba(255,170,95,'+p.a+')'); | |
| g.addColorStop(1,'rgba(255,140,60,0)'); | |
| ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x,y,rad,0,6.283); ctx.fill(); | |
| } | |
| window.__cfRAF = requestAnimationFrame(tick); | |
| }; | |
| tick(); | |
| } | |
| """ | |
| # Drive the "forging" animation straight off the status text the generator sets: | |
| # the hourglass means in-progress. Self-syncing, no event races. | |
| _JS_STATUS = "(s) => { document.body.classList.toggle('cf-forging', !!s && s.indexOf('\\u23f3') >= 0); }" | |
| # --------------------------------------------------------------------------- | |
| # Rendering helpers | |
| # --------------------------------------------------------------------------- | |
| def _header_html(lang: str) -> str: | |
| return ('<div id="cf-hero">' | |
| '<canvas id="cf-embers"></canvas>' | |
| '<div id="cf-header">' | |
| '<div class="cf-logo">' + _LOGO_SVG + '</div>' | |
| '<div class="cf-brand">' | |
| '<h1>' + i18n.t("cf.title", lang) + '</h1>' | |
| '<p>' + i18n.t("cf.tagline", lang) + '</p>' | |
| '</div></div></div>') | |
| def _empty_state(lang: str) -> str: | |
| return ('<div class="cf-empty"><div class="cf-empty-logo">' + _LOGO_SVG + '</div>' | |
| '<h3>Case Forge</h3>' | |
| '<p>' + i18n.t("cf.result_empty", lang) + '</p></div>') | |
| def _chips_html(flags: dict, lang: str, demo: bool, reason: str | None = None, | |
| num_warns: list | None = None) -> str: | |
| labels = { | |
| "schema": i18n.t("cf.q_schema", lang), | |
| "noleak": i18n.t("cf.q_noleak", lang), | |
| "objectives": i18n.t("cf.q_objectives", lang), | |
| "sourced": i18n.t("cf.q_sourced", lang), | |
| } | |
| chips = [] | |
| for i, key in enumerate(("schema", "noleak", "objectives", "sourced")): | |
| ok = flags.get(key) | |
| cls = "cf-pass" if ok else "cf-fail" | |
| mark = "β" if ok else "β" | |
| delay = "%.2fs" % (i * 0.06) | |
| chips.append('<span class="cf-chip ' + cls + '" style="animation-delay:' + delay | |
| + '">' + mark + ' ' + labels[key] + '</span>') | |
| head = '<div class="cf-chips">' + "".join(chips) + '</div>' | |
| head += '<div class="cf-demo">β ' + i18n.t("cf.disclaimer", lang) + '</div>' | |
| if num_warns: | |
| items = "".join("<li>" + w + "</li>" for w in num_warns) | |
| head += ('<div class="cf-numwarn"><b>β ' + i18n.t("cf.numcheck_title", lang) | |
| + '</b><ul>' + items + '</ul></div>') | |
| if demo: | |
| note = "cf.busy_note" if reason == "busy" else "cf.demo_note" | |
| head += '<div class="cf-demo">' + i18n.t(note, lang) + '</div>' | |
| return head | |
| def _ui_lang(label: str) -> str: | |
| return i18n.code_for(label) | |
| # --------------------------------------------------------------------------- | |
| # Generation flow | |
| # --------------------------------------------------------------------------- | |
| def _loading(status_value): | |
| """A forge() yield that only updates the status line (rest unchanged).""" | |
| n = gr.update() | |
| return (gr.update(value=status_value, visible=True), n, n, n, n, n, n, n, | |
| gr.update(visible=False)) | |
| def forge(domain, topic, level, case_lang, theory, ui_lang_label): | |
| """Generate β validate β render into the editable authoring surface.""" | |
| lang = _ui_lang(ui_lang_label) | |
| if not (topic or "").strip(): | |
| yield _loading(i18n.t("cf.empty_topic", lang)) | |
| return | |
| yield _loading("β³ " + i18n.t("cf.status_forging", lang)) | |
| res = infer.generate(domain=domain, topic=topic, level=level, | |
| language=case_lang, theory=theory) | |
| obj = res.get("obj") | |
| if not obj: | |
| yield _loading("β οΈ " + i18n.t("cf.status_invalid", lang)) | |
| return | |
| flags = render.quality_flags(obj, res.get("errors", []), res.get("warnings", [])) | |
| clang_obj = obj.get("language", "pt") | |
| num_warns = [] if res.get("demo") else numcheck.check(obj, clang_obj) | |
| chips = _chips_html(flags, lang, res.get("demo", False), res.get("reason"), num_warns) | |
| case_md = render.render_case(obj) | |
| note_md = render.render_note(obj) | |
| title = obj.get("title", "case") | |
| clang = obj.get("language", "pt") | |
| status_key = "cf.status_done" if res.get("valid") else "cf.status_invalid" | |
| icon = "β " if res.get("valid") else "β οΈ" | |
| yield ( | |
| gr.update(value=icon + " " + i18n.t(status_key, lang), visible=True), | |
| chips, | |
| case_md, case_md, # case source + preview | |
| note_md, note_md, # note source + preview | |
| title, clang, # states | |
| gr.update(visible=True), # actions row (regen + downloads) | |
| ) | |
| def fill_example(): | |
| return EXAMPLE["domain"], EXAMPLE["topic"], EXAMPLE["theory"] | |
| def relabel(ui_lang_label): | |
| """Re-render all UI chrome when the interface language changes.""" | |
| lang = _ui_lang(ui_lang_label) | |
| return ( | |
| _header_html(lang), | |
| gr.update(label=i18n.t("cf.domain", lang)), | |
| gr.update(label=i18n.t("cf.topic", lang)), | |
| gr.update(label=i18n.t("cf.level", lang), choices=LEVELS[lang], value="MBA"), | |
| gr.update(label=i18n.t("cf.content_lang", lang)), | |
| gr.update(label=i18n.t("cf.theory", lang)), | |
| gr.update(value=i18n.t("cf.btn_forge", lang)), | |
| gr.update(value=i18n.t("cf.btn_example", lang)), | |
| gr.update(label=i18n.t("cf.tab_case", lang)), | |
| gr.update(label=i18n.t("cf.tab_note", lang)), | |
| gr.update(label=i18n.t("cf.edit_source", lang)), # case_src | |
| gr.update(label=i18n.t("cf.edit_source", lang)), # note_src | |
| gr.update(value="π " + i18n.t("cf.btn_regen", lang)), # regen | |
| gr.update(label=i18n.t("cf.download_md", lang)), | |
| gr.update(label=i18n.t("cf.download_html", lang)), | |
| ) | |
| def build(): | |
| d = i18n.DEFAULT | |
| with gr.Blocks(title="Case Forge") as demo: | |
| with gr.Row(): | |
| header = gr.HTML(_header_html(d)) | |
| ui_lang = gr.Dropdown( | |
| choices=[n for n, _ in i18n.LANGS], value="English", | |
| label=i18n.t("common.language", d), scale=0, min_width=150, | |
| ) | |
| title_state = gr.State("case") | |
| lang_state = gr.State("pt") | |
| with gr.Row(equal_height=False): | |
| # ---- input column ---- | |
| with gr.Column(scale=2, elem_classes="cf-panel"): | |
| domain = gr.Textbox(label=i18n.t("cf.domain", d), | |
| placeholder=i18n.t("cf.domain_ph", d)) | |
| topic = gr.Textbox(label=i18n.t("cf.topic", d), lines=2, | |
| placeholder=i18n.t("cf.topic_ph", d)) | |
| with gr.Row(): | |
| level = gr.Dropdown(choices=LEVELS[d], value="MBA", | |
| label=i18n.t("cf.level", d)) | |
| case_lang = gr.Radio(choices=[("PT", "pt"), ("EN", "en")], | |
| value="pt", label=i18n.t("cf.content_lang", d)) | |
| theory = gr.Textbox(label=i18n.t("cf.theory", d), | |
| placeholder=i18n.t("cf.theory_ph", d)) | |
| with gr.Row(): | |
| example_btn = gr.Button(i18n.t("cf.btn_example", d), variant="secondary") | |
| forge_btn = gr.Button(i18n.t("cf.btn_forge", d), variant="primary") | |
| status = gr.Markdown(visible=False, elem_id="cf-status") | |
| # ---- output / authoring column ---- | |
| with gr.Column(scale=3, elem_classes="cf-panel"): | |
| chips = gr.HTML(_empty_state(d)) | |
| with gr.Tab(i18n.t("cf.tab_case", d)) as case_tab: | |
| with gr.Row(): | |
| case_src = gr.Textbox(label=i18n.t("cf.edit_source", d), lines=22, | |
| scale=1) | |
| case_prev = gr.Markdown(elem_classes="cf-doc") | |
| with gr.Tab(i18n.t("cf.tab_note", d)) as note_tab: | |
| with gr.Row(): | |
| note_src = gr.Textbox(label=i18n.t("cf.edit_source", d), lines=22, | |
| scale=1) | |
| note_prev = gr.Markdown(elem_classes="cf-doc") | |
| gr.Markdown(i18n.t("cf.edit_hint", d), elem_classes="cf-hint") | |
| # Toggle the row's visibility as a unit β toggling sibling | |
| # DownloadButtons individually drops the 2nd one in Gradio. | |
| with gr.Row(visible=False) as actions_row: | |
| regen_btn = gr.Button("π " + i18n.t("cf.btn_regen", d), | |
| variant="secondary") | |
| md_dl = gr.DownloadButton(i18n.t("cf.download_md", d)) | |
| html_dl = gr.DownloadButton(i18n.t("cf.download_html", d)) | |
| forge_out = [status, chips, case_src, case_prev, note_src, note_prev, | |
| title_state, lang_state, actions_row] | |
| forge_in = [domain, topic, level, case_lang, theory, ui_lang] | |
| # Core generation (direct wiring). | |
| forge_btn.click(forge, inputs=forge_in, outputs=forge_out) | |
| regen_btn.click(forge, inputs=forge_in, outputs=forge_out) | |
| example_btn.click(fill_example, outputs=[domain, topic, theory]) | |
| # The "forging" animation is driven by the status text the generator sets | |
| # (hourglass = in progress) β synced to the yields, self-healing, no races. | |
| status.change(None, inputs=[status], js=_JS_STATUS) | |
| # Live preview: editing the Markdown source updates the rendered preview. | |
| case_src.change(lambda s: s, case_src, case_prev) | |
| note_src.change(lambda s: s, note_src, note_prev) | |
| # Downloads are built from the CURRENT (possibly edited) source at click time. | |
| md_dl.click(export.to_markdown_file, | |
| inputs=[case_src, note_src, title_state], outputs=md_dl) | |
| html_dl.click(export.to_html_file, | |
| inputs=[case_src, note_src, title_state, lang_state], outputs=html_dl) | |
| ui_lang.change( | |
| relabel, inputs=[ui_lang], | |
| outputs=[header, domain, topic, level, case_lang, theory, | |
| forge_btn, example_btn, case_tab, note_tab, | |
| case_src, note_src, regen_btn, md_dl, html_dl], | |
| ).then(None, js=_JS_EMBERS) # re-init the ember canvas after the hero re-renders | |
| demo.load(None, js=_JS_EMBERS) # entrance animation + start the ember field | |
| return demo | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", 7860))) | |
| build().launch(server_name="0.0.0.0", server_port=port, show_error=True, | |
| theme=_forge_theme(), css=_CSS, head=_HEAD, | |
| favicon_path=str(LOGO_PATH)) | |