| 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 = """ |
| <div class="wrap"> |
| <canvas id="dna-canvas" width="180" height="300"></canvas> |
| <div id="char-card" class="card"> |
| <div style="color:#8b6914;font-style:italic;display:flex;align-items:center;justify-content:center;height:100%;min-height:200px">Roll your character to begin...</div> |
| </div> |
| </div> |
| """ |
|
|
| 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 => |
| '<div class="sr"><span class="sk">' + p.l + '</span>' + |
| '<div class="sb"><div style="width:' + Math.round(p.v/20*100) + '%;height:100%;background:' + p.c + ';border-radius:2px"></div></div>' + |
| '<span class="sv">' + p.v + '</span></div>' |
| ).join(''); |
| card.innerHTML = |
| '<div class="cn">' + data.name + '</div>' + |
| '<div class="cc">' + data.cls + ' · Lv.' + data.level + ' · ' + data.alignment + '</div>' + |
| '<div class="stats">' + bars + '</div>' + |
| '<div class="quest">📜 ' + data.quest + '</div>'; |
| } |
| 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("<h1 style='color:#ffd700;text-align:center;margin-bottom:4px'>⚔️ LIFE QUEST</h1><p style='color:#c0a060;text-align:center;font-style:italic;margin-top:0'>Describe your life. Get your DNA-coded RPG character.</p>") |
| 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() |
|
|