Jordancole21's picture
Deploy native Gradio SEO page and tool
73e2d15 verified
Raw
History Blame Contribute Delete
7.18 kB
"""D&D Initiative Tracker — turn order only, no AI, no external dependencies."""
import gradio as gr
from src import content, tool
from src.space_config import CONFIG
from src.shared.ctas import cta_html
from src.shared.page import standard_launch
from src.shared.seo import hero_html, section_html, faq_html, related_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(); }
}
"""
QUICK_PLACEHOLDER = (
"Kira 18 (player total)\n"
"Torvin +2 (roll d20+2 for them)\n"
"Goblin x3 +2 (three goblins, rolled separately)\n"
"Shadow (bare name — rolls a d20)"
)
def _dropdown_choices(state):
return [
(f"{e['name']} ({tool._fmt_init(e['initiative'])})", e["id"])
for e in state["entries"]
]
def _views(state, selected=None):
"""Rendered outputs shared by every event: list, banner, dropdown, copy."""
valid = {e["id"] for e in state["entries"]}
value = selected if selected in valid else None
return (
tool.render_list(state),
tool.render_banner(state),
gr.update(choices=_dropdown_choices(state), value=value),
tool.export_text(state),
)
def on_load(saved):
state = tool.coerce_state(saved)
return (state, *_views(state))
def on_add(state, text, roll_blank):
state, messages = tool.quick_add(state, text, roll_blank)
return (
state, state, # working state + browser mirror
gr.update(value=""), # clear the quick-add box
tool.render_roll_log(messages),
*_views(state),
)
def on_next(state):
state = tool.next_turn(state)
return (state, state, *_views(state))
def on_prev(state):
state = tool.prev_turn(state)
return (state, state, *_views(state))
def on_reset(state):
state = tool.reset_rounds(state)
return (state, state, *_views(state))
def on_clear(state):
state = tool.clear_all()
return (state, state, "", *_views(state))
def on_move_up(state, eid):
if eid is not None:
state = tool.move_up(state, eid)
return (state, state, *_views(state, selected=eid))
def on_move_down(state, eid):
if eid is not None:
state = tool.move_down(state, eid)
return (state, state, *_views(state, selected=eid))
def on_remove(state, eid):
if eid is not None:
state = tool.remove_entry(state, eid)
return (state, state, *_views(state))
_initial = tool.new_state()
with gr.Blocks(title=CONFIG.title) as demo:
gr.HTML(hero_html(CONFIG))
gr.HTML(cta_html(CONFIG, CONFIG.hero_cta))
state = gr.State(_initial)
browser_state = gr.BrowserState(
_initial, storage_key="lf_initiative_tracker_v1"
)
with gr.Row(equal_height=False):
with gr.Column(scale=5):
quick_box = gr.Textbox(
label="Quick add — one combatant per line",
lines=5,
placeholder=QUICK_PLACEHOLDER,
)
roll_blank = gr.Checkbox(
value=True, label="Roll d20 for names without a number"
)
add_btn = gr.Button(
"Add to Order", variant="primary", elem_classes=["lf-generate"]
)
roll_log = gr.HTML("")
entry_dd = gr.Dropdown(
choices=[], value=None, label="Manage a combatant",
info="Pick an entry, then move it for tie-breaks or remove it.",
)
with gr.Row():
up_btn = gr.Button("Move Up")
down_btn = gr.Button("Move Down")
remove_btn = gr.Button("Remove")
with gr.Column(scale=6):
banner = gr.HTML(tool.render_banner(_initial))
order_html = gr.HTML(tool.render_list(_initial))
with gr.Row():
next_btn = gr.Button("Next ▶", variant="primary", scale=2,
elem_classes=["lf-generate"])
prev_btn = gr.Button("◀ Back", scale=1)
with gr.Row():
reset_btn = gr.Button("Reset Rounds")
clear_btn = gr.Button("Clear All")
copy_btn = gr.Button("Copy Order", elem_id="lf-copy-btn")
# 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,
)
gr.HTML(cta_html(CONFIG, CONFIG.post_tool_cta))
gr.HTML(section_html("Get Your Turn Order in Seconds", content.HOW_TO_HTML))
gr.HTML(section_html("Why This DnD Initiative Tracker Is Turn Order Only",
content.WHY_HTML))
gr.HTML(section_html("Worked Example", content.EXAMPLE_HTML))
gr.HTML(section_html("Initiative Rules Refresher", content.RULES_HTML))
gr.HTML(faq_html(content.FAQS))
gr.HTML(related_html(CONFIG))
gr.HTML(cta_html(CONFIG, CONFIG.footer_cta))
gr.HTML(content.DISCLAIMER_HTML)
views = [order_html, banner, entry_dd, copy_src]
demo.load(on_load, [browser_state], [state] + views,
api_name=False, queue=False)
add_btn.click(
on_add, [state, quick_box, roll_blank],
[state, browser_state, quick_box, roll_log] + views,
api_name=False, queue=False,
)
next_btn.click(on_next, [state], [state, browser_state] + views,
api_name=False, queue=False)
prev_btn.click(on_prev, [state], [state, browser_state] + views,
api_name=False, queue=False)
reset_btn.click(on_reset, [state], [state, browser_state] + views,
api_name=False, queue=False)
clear_btn.click(on_clear, [state],
[state, browser_state, roll_log] + views,
api_name=False, queue=False)
up_btn.click(on_move_up, [state, entry_dd],
[state, browser_state] + views,
api_name=False, queue=False)
down_btn.click(on_move_down, [state, entry_dd],
[state, browser_state] + views,
api_name=False, queue=False)
remove_btn.click(on_remove, [state, entry_dd],
[state, browser_state] + views,
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)