Spaces:
Sleeping
Sleeping
| """D&D Monster Builder — 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, role, environment, notes, | |
| request: gr.Request): | |
| md, plain, path, error = tool.generate( | |
| concept, cr, size, creature_type, role, environment, notes, | |
| request=request, | |
| ) | |
| if error: | |
| return ( | |
| gr.update(), # markdown output unchanged | |
| gr.update(), # copy source unchanged | |
| gr.update(), # download unchanged | |
| gr.update(value=tool.render_error(error), visible=True), | |
| gr.update(visible=md is not None), # actions row | |
| ) | |
| 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="Monster concept", | |
| placeholder="e.g. a lantern-lit crab that collects drowned sailors' memories", | |
| ) | |
| with gr.Row(): | |
| cr = gr.Dropdown(tool.CHALLENGE_RATINGS, label="Challenge rating target", | |
| value=None, allow_custom_value=True) | |
| size = gr.Dropdown(tool.SIZES, label="Size", value=None) | |
| with gr.Row(): | |
| creature_type = gr.Dropdown(tool.CREATURE_TYPES, label="Creature type", | |
| value=None, allow_custom_value=True) | |
| role = gr.Dropdown(tool.COMBAT_ROLES, label="Combat role", value=None, | |
| allow_custom_value=True) | |
| environment = gr.Textbox(label="Environment (optional)", | |
| placeholder="e.g. fog-bound tidal caves, a sunken lighthouse") | |
| notes = gr.Textbox(label="Extra notes (optional)", lines=2, | |
| placeholder="Legendary boss, ties to your villain, a signature move…") | |
| with gr.Row(): | |
| generate_btn = gr.Button("Build My Monster", 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 monster will appear here — lore, full stat block, traits, " | |
| "actions, tactics, and loot included.*", | |
| elem_classes=["lf-output", "lf-page"], | |
| ) | |
| # Kept in the DOM (offscreen) so the Copy button's JS can read it; | |
| # visible=False would remove the textarea entirely in Gradio 6. | |
| 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("Build a Custom D&D Monster", content.HOW_TO_HTML)) | |
| gr.HTML(section_html("What Your Monster Includes", content.INCLUDES_HTML)) | |
| gr.HTML(section_html("Complete Monster Example", content.EXAMPLE_INTRO_HTML)) | |
| gr.Markdown(tool.render_markdown(tool.FIXTURE_DROWNLIGHT_SCUTTLER), | |
| elem_classes=["lf-example", "lf-page"]) | |
| gr.HTML(section_html("How Challenge Rating Works in This DnD Monster Builder", | |
| 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, role, environment, notes] | |
| 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, role], | |
| 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) | |