| """D&D Statblock Generator — native Gradio app (no iframe, no external frontend).""" |
|
|
| import gradio as gr |
|
|
| from src import content, tool |
| from src.space_config import CONFIG |
| from src.shared.ctas import cta_html, GENERATED_FLAG_JS |
| from src.shared.page import standard_launch |
| from src.shared.seo import ( |
| hero_html, section_html, faq_html, related_html, disclaimer_html, |
| ) |
|
|
| COPY_JS = """ |
| () => { |
| const el = document.querySelector('#lf-copy-src textarea'); |
| if (!el || !el.value) return; |
| const flash = () => { |
| const btn = document.querySelector('#lf-copy-btn button') || |
| document.querySelector('#lf-copy-btn'); |
| if (btn) { const t = btn.textContent; btn.textContent = 'Copied!'; |
| setTimeout(() => (btn.textContent = t), 1500); } |
| }; |
| const fallback = () => { |
| el.focus({preventScroll: true}); el.select(); |
| try { document.execCommand('copy'); } catch (e) {} |
| flash(); |
| }; |
| try { |
| navigator.clipboard.writeText(el.value).then(flash).catch(fallback); |
| } catch (e) { fallback(); } |
| } |
| """ |
|
|
|
|
| def on_generate(concept, cr, size, creature_type, legendary, extra, |
| request: gr.Request): |
| md, plain, path, error = tool.generate( |
| concept, cr, size, creature_type, legendary, extra, |
| request=request, |
| ) |
| if error: |
| return ( |
| gr.update(), |
| gr.update(), |
| gr.update(), |
| gr.update(value=tool.render_error(error), visible=True), |
| gr.update(visible=md is not None), |
| ) |
| return ( |
| gr.update(value=md, visible=True), |
| gr.update(value=plain), |
| gr.update(value=path, visible=True), |
| gr.update(value="", visible=False), |
| gr.update(visible=True), |
| ) |
|
|
|
|
| with gr.Blocks(title=CONFIG.title) as demo: |
| gr.HTML(hero_html(CONFIG)) |
| gr.HTML(cta_html(CONFIG, CONFIG.hero_cta)) |
|
|
| with gr.Row(equal_height=False): |
| with gr.Column(scale=5): |
| concept = gr.Textbox( |
| label="Creature concept or homebrew notes", |
| lines=4, |
| placeholder=("a knight cursed to rust, still guarding the vault " |
| "door he died defending... or paste your rough " |
| "homebrew notes"), |
| ) |
| with gr.Row(): |
| cr = gr.Dropdown(tool.CHALLENGE_RATINGS, |
| label="Target challenge rating (optional)", |
| value=None, allow_custom_value=True) |
| size = gr.Dropdown(tool.SIZES, label="Size (optional)", value=None) |
| with gr.Row(): |
| creature_type = gr.Dropdown(tool.CREATURE_TYPES, |
| label="Creature type (optional)", |
| value=None, allow_custom_value=True) |
| legendary = gr.Checkbox(label="Include legendary actions", |
| value=False) |
| extra = gr.Textbox(label="Extra mechanical requests (optional)", lines=2, |
| placeholder=('e.g. "give it a fear aura", "make the ' |
| 'breath weapon recharge 5-6"')) |
| with gr.Row(): |
| generate_btn = gr.Button("Format My Stat Block", variant="primary", |
| elem_classes=["lf-generate"]) |
| random_btn = gr.Button("Surprise Me") |
|
|
| with gr.Column(scale=6): |
| error_box = gr.HTML(visible=False) |
| output_md = gr.Markdown( |
| "*Your formatted stat block will appear here — identity line, AC, " |
| "hit points, ability scores, traits, and actions in the classic " |
| "layout.*", |
| elem_classes=["lf-output", "lf-page"], |
| ) |
| |
| |
| copy_src = gr.Textbox(elem_id="lf-copy-src", elem_classes=["lf-offscreen"], |
| container=False, show_label=False) |
| with gr.Row(visible=False) as actions_row: |
| copy_btn = gr.Button("Copy Markdown", elem_id="lf-copy-btn") |
| download_btn = gr.DownloadButton("Download .md", visible=False) |
| regen_btn = gr.Button("Regenerate") |
|
|
| gr.HTML(cta_html(CONFIG, CONFIG.post_tool_cta)) |
|
|
| gr.HTML(section_html("Format a Concept Into a 5e Stat Block", content.HOW_TO_HTML)) |
| gr.HTML(section_html("What the Generator Formats", content.FORMATS_HTML)) |
| gr.HTML(section_html("Complete Stat Block Example", content.EXAMPLE_INTRO_HTML)) |
| gr.Markdown(tool.render_markdown(tool.FIXTURE_RUSTBOUND_SENTINEL), |
| elem_classes=["lf-example", "lf-page"]) |
| gr.HTML(section_html("Formatting Rules This DnD Statblock Generator Follows", |
| content.METHODOLOGY_HTML)) |
| gr.HTML(faq_html(content.FAQS)) |
| gr.HTML(related_html(CONFIG)) |
| gr.HTML(cta_html(CONFIG, CONFIG.footer_cta)) |
| gr.HTML(disclaimer_html()) |
|
|
| inputs = [concept, cr, size, creature_type, legendary, extra] |
| outputs = [output_md, copy_src, download_btn, error_box, actions_row] |
|
|
| for btn in (generate_btn, regen_btn): |
| ( |
| btn.click( |
| lambda: (gr.update(interactive=False), gr.update(interactive=False)), |
| None, [generate_btn, regen_btn], api_name=False, queue=False, |
| ) |
| .then(on_generate, inputs, outputs, api_name=False, concurrency_limit=1) |
| .then( |
| lambda: (gr.update(interactive=True), gr.update(interactive=True)), |
| None, [generate_btn, regen_btn], api_name=False, queue=False, |
| ) |
| .then(None, None, None, js=GENERATED_FLAG_JS, api_name=False, queue=False) |
| ) |
|
|
| random_btn.click(tool.randomize, None, [cr, size, creature_type], |
| api_name=False, queue=False) |
| copy_btn.click(None, None, None, js=COPY_JS, api_name=False, queue=False) |
|
|
|
|
| if __name__ == "__main__": |
| standard_launch(demo, CONFIG) |
|
|