Jordancole21's picture
Deploy native Gradio SEO page and tool
1d5c3e4 verified
Raw
History Blame Contribute Delete
6.49 kB
"""D&D NPC 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(role, race, importance, location, demeanor, setting, party_need,
notes, request: gr.Request):
md, plain, path, error = tool.generate(
role, race, importance, location, demeanor, setting, party_need, 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):
role = gr.Textbox(label="Role / occupation",
placeholder="e.g. harbor-master, fence, village priest")
with gr.Row():
race = gr.Dropdown(tool.RACES, label="Race / species", value=None,
allow_custom_value=True)
importance = gr.Radio(tool.IMPORTANCE, label="Importance",
value="Recurring")
with gr.Row():
demeanor = gr.Dropdown(tool.DEMEANORS, label="Demeanor", value=None,
allow_custom_value=True)
location = gr.Textbox(label="Location / context (optional)",
placeholder="e.g. the customs house at the river gate")
setting = gr.Textbox(label="Campaign setting (optional)",
placeholder="e.g. Forgotten Realms, homebrew canal city")
party_need = gr.Textbox(label="What the party needs from them (optional)",
placeholder="e.g. safe passage, information, a loan")
notes = gr.Textbox(label="Anything else for the scribe (optional)", lines=2,
placeholder="Details, factions, or a connection to include")
with gr.Row():
generate_btn = gr.Button("Generate My NPC", 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 NPC will appear here — appearance, personality, a performable "
"voice, motivation, secret, relationships, and three plot 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 Table-Ready NPC", content.HOW_TO_HTML))
gr.HTML(section_html("What Every NPC Includes", content.INCLUDES_HTML))
gr.HTML(section_html("Complete D&D NPC Generator Example",
content.EXAMPLE_INTRO_HTML))
gr.Markdown(tool.render_markdown(tool.FIXTURE_LOCK_KEEPER),
elem_classes=["lf-example", "lf-page"])
gr.HTML(section_html("How to Run a Generated NPC 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 = [role, race, importance, location, demeanor, setting, party_need, 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, [role, race, importance, demeanor],
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)