import gradio as gr import json import os from openai import OpenAI def get_client(): return OpenAI( base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.environ.get("DASHSCOPE_API_KEY", ""), ) SYSTEM = 'Output ONLY valid JSON no markdown: {"name":"epic name","class":"funny class","level":<1-50>,"alignment":"e.g. Chaotic Tired","stats":{"STR":<1-20>,"DEX":<1-20>,"INT":<1-20>,"WIS":<1-20>,"CHA":<1-20>,"LUK":<1-20>},"quest":"one sentence epic quest"}' COLORS = {"STR":"#e05050","DEX":"#50e080","INT":"#5080e0","WIS":"#a050e0","CHA":"#e050a0","LUK":"#f0c030"} def roll(text): if not text.strip(): return None try: r = get_client().chat.completions.create( model="qwen-turbo", messages=[{"role":"system","content":SYSTEM},{"role":"user","content":text}], temperature=0.85, max_tokens=300, ) raw = r.choices[0].message.content.strip() if "```" in raw: raw = raw.split("```")[1] if raw.startswith("json"): raw = raw[4:] d = json.loads(raw) except Exception as e: d = {"name":"Error Knight","class":"Debug Wizard","level":1,"alignment":"True Broken", "stats":{k:10 for k in COLORS},"quest":str(e)[:80]} st = d.get("stats", {}) return { "name": d.get("name","???"), "cls": d.get("class",""), "level": d.get("level",1), "alignment": d.get("alignment",""), "quest": d.get("quest",""), "pairs": [{"c":COLORS[k],"l":k,"v":st.get(k,10)} for k in COLORS] } CANVAS_HTML = """
Roll your character to begin...
""" INIT_JS = """ () => { window._dnaAnim = null; window.updateChar = function(data) { if (!data) return; const card = document.getElementById('char-card'); if (card) { const bars = data.pairs.map(p => '
' + p.l + '' + '
' + '' + p.v + '
' ).join(''); card.innerHTML = '
' + data.name + '
' + '
' + data.cls + ' · Lv.' + data.level + ' · ' + data.alignment + '
' + '
' + bars + '
' + '
📜 ' + data.quest + '
'; } if (window._dnaAnim) cancelAnimationFrame(window._dnaAnim); const cv = document.getElementById('dna-canvas'); if (!cv) return; const ctx = cv.getContext('2d'); let t = 0; function draw() { ctx.clearRect(0, 0, 180, 300); data.pairs.forEach(function(e, i) { const y = 30 + i * 42, a = t + i * 1.1; const x1 = 90 + Math.sin(a) * 50, x2 = 90 - Math.sin(a) * 50; ctx.strokeStyle = '#2a3a4a'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(x1, y); ctx.lineTo(x2, y); ctx.stroke(); [x1, x2].forEach(function(x) { ctx.beginPath(); ctx.arc(x, y, 9, 0, Math.PI * 2); ctx.fillStyle = e.c; ctx.shadowColor = e.c; ctx.shadowBlur = 14; ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = '#fff'; ctx.font = 'bold 8px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(e.l, x, y); }); }); t += 0.025; window._dnaAnim = requestAnimationFrame(draw); } draw(); } } """ CSS = """ body,.gradio-container{background:#08080d!important;color:#e8d5a3!important} .gradio-container p,.gradio-container h1,.gradio-container label{color:#e8d5a3!important} .wrap{display:flex;gap:20px;align-items:center;max-width:580px;margin:0 auto;padding:16px} canvas{flex-shrink:0;filter:drop-shadow(0 0 8px rgba(100,150,255,.4))} .card{flex:1;color:#e8d5a3;font-family:Georgia,serif} .cn{font-size:20px;font-weight:bold;color:#ffd700;letter-spacing:1px;margin-bottom:3px} .cc{font-size:12px;color:#8b6914;margin-bottom:10px} .stats{margin-bottom:10px} .sr{display:flex;align-items:center;gap:6px;margin-bottom:5px} .sk{width:28px;font-size:10px;color:#c0a060} .sb{flex:1;height:6px;background:#1a1008;border-radius:2px;overflow:hidden} .sv{width:18px;font-size:10px;color:#e8d5a3;text-align:right} .quest{font-size:12px;font-style:italic;color:#c0a060;border-left:2px solid #8b6914;padding-left:8px} """ with gr.Blocks(css=CSS, title="Life Quest", js=INIT_JS) as demo: gr.HTML("

⚔️ LIFE QUEST

Describe your life. Get your DNA-coded RPG character.

") gr.HTML(CANVAS_HTML) txt = gr.Textbox(label="Your life situation", placeholder="I'm a grad student surviving on ramen and false hope...", lines=3) btn = gr.Button("⚔ Roll Character", variant="primary") data_out = gr.JSON(visible=False) btn.click(roll, inputs=txt, outputs=data_out).then( fn=None, inputs=[data_out], js="(data) => { if (data && window.updateChar) window.updateChar(data); }" ) demo.launch()