"""D&D Quest 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(quest_type, party_level, party_size, setting, tone, quest_giver, pc_tie, scope, notes, request: gr.Request): md, plain, path, error = tool.generate( quest_type, party_level, party_size, setting, tone, quest_giver, pc_tie, scope, 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): quest_type = gr.Dropdown(tool.QUEST_TYPES, label="Quest type", value=None, allow_custom_value=True) with gr.Row(): party_level = gr.Dropdown(tool.LEVELS, label="Party level", value=None) party_size = gr.Dropdown(tool.SIZES, label="Party size", value=None) with gr.Row(): tone = gr.Dropdown(tool.TONES, label="Tone", value=None, allow_custom_value=True) scope = gr.Radio(tool.SCOPES, label="Scope", value="One-shot") setting = gr.Textbox(label="Setting / region (optional)", placeholder="e.g. a fog-bound fishing coast, the Sword Coast") quest_giver = gr.Textbox(label="Quest giver idea (optional)", placeholder="e.g. a harbor councilman with clean hands") pc_tie = gr.Textbox(label="Tie-in to a PC (optional)", placeholder="e.g. the rogue's old guild") notes = gr.Textbox(label="Anything else for the scribe (optional)", lines=2, placeholder="Factions, themes, or a detail to weave in") with gr.Row(): generate_btn = gr.Button("Generate My Quest", 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 quest will appear here — hook, quest giver, objective, " "location, three complications, twist, antagonist, reward, and " "follow-up hooks 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("Generate a Runnable D&D Quest", content.HOW_TO_HTML)) gr.HTML(section_html("What Every Quest Includes", content.INCLUDES_HTML)) gr.HTML(section_html("Complete D&D Quest Generator Example", content.EXAMPLE_INTRO_HTML)) gr.Markdown(tool.render_markdown(tool.FIXTURE_GULLWRACK), elem_classes=["lf-example", "lf-page"]) gr.HTML(section_html("How to Run a Generated Quest at the Table", content.RUN_AT_TABLE_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 = [quest_type, party_level, party_size, setting, tone, quest_giver, pc_tie, scope, 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, [quest_type, party_level, tone, scope], 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)