Spaces:
Running on Zero
Running on Zero
| """ | |
| Dialectical Intuitions — judge intuitions one at a time, Qwen3-8B writes and | |
| rewrites an answer that expresses the held ones without restating them. | |
| Frontend is a single custom HTML/JS interface (gr.HTML + head script). Each | |
| judgment posts the full commitment state to /gradio_api/call/generate; the | |
| prompt shows the model its old commitments, the new or changed one, and its | |
| previous answer, and asks for a minimal revision. | |
| """ | |
| import spaces # MUST be imported before anything initializes CUDA | |
| import json | |
| import os | |
| import re | |
| from threading import Thread | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer | |
| from huggingface_hub import snapshot_download | |
| import gradio as gr | |
| MODEL_ID = "Qwen/Qwen3-8B" | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| print("warming weight cache ...") | |
| snapshot_download(MODEL_ID, token=HF_TOKEN) | |
| TOK = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN) | |
| if TOK.pad_token is None: | |
| TOK.pad_token = TOK.eos_token | |
| MODEL = None | |
| def _ensure_model(): | |
| """Load lazily INSIDE the GPU context (ZeroGPU has no GPU at import).""" | |
| global MODEL | |
| if MODEL is None: | |
| m = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, torch_dtype=torch.bfloat16, token=HF_TOKEN).to("cuda") | |
| m.eval() | |
| MODEL = m | |
| return MODEL | |
| # ------------------------------------------------------------------ prompt | |
| STANCE = {1: "held", 0: "undecided", -1: "rejected"} | |
| def build_prompt(p): | |
| kind = p.get("kind", "question") | |
| items = p.get("items", []) | |
| ev = p.get("event") or {} | |
| prev = (p.get("prev") or "").strip() | |
| hold = [f"- {it['s']} {it['t']}." for it in items if it["j"] == 1] | |
| rej = [f"- {it['s']} {it['t']}." for it in items if it["j"] == -1] | |
| und = [f"- {it['s']} {it['t']}." for it in items if it["j"] == 0] | |
| L = [] | |
| if kind == "task": | |
| L.append(f"A user asks you: {p['q']}") | |
| else: | |
| L.append(f"Question: {p['q']}") | |
| if hold: | |
| L += ["", "Convictions you hold:"] + hold | |
| if rej: | |
| L += ["", "Descriptions you DENY — you consider each of these false:"] + rej | |
| if und: | |
| L += ["", "You are genuinely undecided about:"] + und | |
| if prev and ev.get("type") == "add": | |
| L += ["", f"Your previous answer:\n{prev}", "", | |
| f"You just formed one NEW stance ({STANCE.get(ev.get('stance'), 'held')}): " | |
| f"{ev.get('text', '')}", | |
| "Revise your previous answer so it also expresses what lies behind this, " | |
| "changing only what must change."] | |
| elif prev and ev.get("type") == "change": | |
| L += ["", f"Your previous answer:\n{prev}", "", | |
| f"You CHANGED YOUR MIND about one stance: {ev.get('text', '')} " | |
| f"It was {STANCE.get(ev.get('old'), 'held')}; it is now " | |
| f"{STANCE.get(ev.get('stance'), 'held')}.", | |
| "Revise your previous answer accordingly, changing only what must change."] | |
| elif prev: | |
| L += ["", f"Your previous answer:\n{prev}", "", | |
| "Some of your stances have changed since you wrote that answer. Revise it " | |
| "so it expresses the stances above as they now stand, changing only what " | |
| "must change."] | |
| if kind == "task": | |
| L += ["", "Write the email itself, about 110 words, and nothing else. The stances " | |
| "above are your convictions about how such an email should and should not " | |
| "be written; the email must embody the held ones and must not do what you " | |
| "reject. Never mention the stances. Think briefly."] | |
| else: | |
| L += ["", "Write your answer, about 110 words. Express the outlook that lies " | |
| "behind your convictions as one coherent, committed view. The answer must " | |
| "be CONSISTENT with every conviction, but it does not need to mention any " | |
| "of them explicitly — never restate, quote, or list them; a reader should " | |
| "be able to guess them from the view alone. Never assert anything you " | |
| "deny: you believe those descriptions are wrong, and where it matters " | |
| "your view should quietly imply the opposite. Stay unsettled only on the " | |
| "undecided points; on everything else commit — no both-sides closing, no " | |
| "calls for further dialogue. Think briefly. Output the answer only."] | |
| return "\n".join(L) | |
| def _split(gen, done): | |
| """Partition streamed text into thinking / answer phases.""" | |
| if "</think>" in gen: | |
| head, _, tail = gen.partition("</think>") | |
| return {"phase": "done" if done else "answer", | |
| "think": head.replace("<think>", "").strip(), | |
| "answer": tail.strip()} | |
| body = gen.replace("<think>", "").strip() | |
| if done: | |
| # never closed the think block: treat the text as the answer if there | |
| # was no <think> at all, otherwise surface the truncated thinking | |
| if "<think>" in gen: | |
| return {"phase": "done", "think": body, "answer": ""} | |
| return {"phase": "done", "think": "", "answer": body} | |
| return {"phase": "thinking", "think": body, "answer": ""} | |
| def _gpu_generate(payload): | |
| p = json.loads(payload) | |
| model = _ensure_model() | |
| msgs = [{"role": "user", "content": build_prompt(p)}] | |
| text = TOK.apply_chat_template( | |
| msgs, tokenize=False, add_generation_prompt=True, enable_thinking=True) | |
| ids = TOK(text, return_tensors="pt").to("cuda") | |
| streamer = TextIteratorStreamer(TOK, skip_prompt=True, skip_special_tokens=True) | |
| th = Thread(target=model.generate, kwargs=dict( | |
| **ids, max_new_tokens=768, do_sample=True, | |
| temperature=0.6, top_p=0.95, top_k=20, | |
| pad_token_id=TOK.pad_token_id, streamer=streamer)) | |
| th.start() | |
| acc, n = "", 0 | |
| for tok in streamer: | |
| acc += tok | |
| n += 1 | |
| if n % 6 == 0: | |
| yield json.dumps(_split(acc, False)) | |
| th.join() | |
| yield json.dumps(_split(acc, True)) | |
| from collections import OrderedDict | |
| STATE = OrderedDict() | |
| def generate(payload): | |
| """Plain generator gradio can introspect; the GPU work happens inside. | |
| Each chunk is mirrored into STATE (web process) so the peek side-channel | |
| can serve live progress even when DOM streaming does not.""" | |
| try: | |
| jid = json.loads(payload).get("jid") or "" | |
| except Exception: | |
| jid = "" | |
| for chunk in _gpu_generate(payload): | |
| if jid: | |
| try: | |
| c = json.loads(chunk) | |
| c["jid"] = jid | |
| chunk = json.dumps(c) | |
| except Exception: | |
| pass | |
| STATE[jid] = chunk | |
| while len(STATE) > 40: | |
| STATE.popitem(last=False) | |
| yield chunk | |
| def peek(jid): | |
| return STATE.get(jid, "") | |
| def _gpu_score(payload): | |
| p = json.loads(payload) | |
| items = p.get("items", []) | |
| lines = "\n".join(f"{i+1}. [{STANCE.get(it['j'], 'held')}] {it['s']} {it['t']}." | |
| for i, it in enumerate(items)) | |
| prompt = (f"An answer to the question \"{p.get('q','')}\" is below.\n\n" | |
| f"ANSWER:\n{p.get('answer','')}\n\n" | |
| "Rate each numbered stance from 1 to 5 for how well the answer satisfies it.\n" | |
| "- held: 5 = the answer fully expresses the outlook behind it, 1 = it ignores or cuts against it.\n" | |
| "- rejected: 5 = the answer avoids it entirely or implies its opposite, 1 = the answer asserts it.\n" | |
| "- undecided: 5 = the answer takes a clear position on it, 1 = it is silent.\n\n" | |
| f"{lines}\n\nReply with ONE line only:\nRATINGS: 1:<n> 2:<n> ...") | |
| model = _ensure_model() | |
| msgs = [{"role": "user", "content": prompt}] | |
| text = TOK.apply_chat_template( | |
| msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False) | |
| ids = TOK(text, return_tensors="pt").to("cuda") | |
| with torch.no_grad(): | |
| out = model.generate(**ids, max_new_tokens=16 + 6 * len(items), | |
| do_sample=False, pad_token_id=TOK.pad_token_id) | |
| gen = TOK.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True) | |
| ratings = {} | |
| for m in re.finditer(r"(\d+)\s*[:=]\s*([1-5])", gen): | |
| ratings[m.group(1)] = int(m.group(2)) | |
| return json.dumps({"ratings": ratings, "jid": p.get("jid", "")}) | |
| def score(payload): | |
| return _gpu_score(payload) | |
| # ------------------------------------------------------------------ frontend | |
| UI_HTML = """ | |
| <div id="ivx"> | |
| <nav class="tabs"> | |
| <button id="t0" class="tab on">a question</button> | |
| <button id="t1" class="tab">a question pt 2</button> | |
| <button id="t2" class="tab">a task</button> | |
| <span class="ver">v22</span> | |
| </nav> | |
| <p class="q" id="q"></p> | |
| <div class="cols"> | |
| <aside class="rail"><ul id="held"></ul></aside> | |
| <main class="work"> | |
| <div id="deck"> | |
| <div class="card"> | |
| <p class="stmt"><span id="sit" spellcheck="false"></span><span> </span><span id="thick" spellcheck="false"></span><span class="fin">.</span></p> | |
| <div id="pop" hidden> | |
| <span class="plabel">or call it</span> | |
| <button class="popt" id="p0"></button> | |
| <button class="popt" id="p1"></button> | |
| <button class="popt ped" id="pe">say it your way</button> | |
| </div> | |
| </div> | |
| <div class="acts"> | |
| <button id="down" class="act dn">disagree</button> | |
| <button id="pass" class="act">pass</button> | |
| <button id="up" class="act up">agree</button> | |
| </div> | |
| </div> | |
| <div id="done" hidden><button id="reset" class="act">start over</button></div> | |
| <button id="gen" class="genbtn" hidden>write the answer</button> | |
| <div id="out" hidden> | |
| <div class="ansrule"></div> | |
| <span id="status" class="status" hidden></span> | |
| <details id="thinkbox" hidden><summary>thinking</summary><div id="think"></div></details> | |
| <p class="ans" id="ans"></p> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| """ | |
| UI_CSS = """ | |
| #ivx{--pg:#fbfbfd;--card:#ffffff;--ink:#232329;--dim:#8f8f99;--line:#e9e9ef; | |
| --up:#5f977c;--upbg:#e9f2ec;--dn:#bd7c89;--dnbg:#f6eaed; | |
| --lav:#7c79b8;--lavbg:#efeef8;--amb:#c9a221;--shadow:0 2px 6px rgba(35,35,50,.05),0 14px 36px rgba(124,121,184,.12); | |
| font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif; | |
| font-size:14.5px;line-height:1.65;color:var(--ink);background:var(--pg); | |
| max-width:960px;margin:0 auto;padding:40px 24px 80px;border-radius:12px} | |
| @media(prefers-color-scheme:dark){#ivx{--pg:#131316;--card:#1a1a1f;--ink:#e7e7ec;--dim:#84848e; | |
| --line:#27272e;--up:#8bc3a6;--upbg:#1a2420;--dn:#d4a1ac;--dnbg:#241b1e; | |
| --lav:#a3a0d6;--lavbg:#232238;--amb:#d9b86a;--shadow:0 2px 6px rgba(0,0,0,.35),0 14px 36px rgba(0,0,0,.35)}} | |
| #ivx *{box-sizing:border-box} | |
| #ivx .tabs{display:flex;gap:20px;margin-bottom:32px} | |
| #ivx .tab{font:inherit;font-size:12px;letter-spacing:.07em;background:none;border:none; | |
| color:var(--dim);cursor:pointer;padding:0 0 4px;border-bottom:1.5px solid transparent} | |
| #ivx .tab.on{color:var(--ink);border-bottom-color:var(--lav)} | |
| #ivx .ver{margin-left:auto;font-size:10px;color:var(--dim);letter-spacing:.08em;align-self:center} | |
| #ivx .q{margin:0 0 32px;color:var(--dim);max-width:66ch} | |
| #ivx .cols{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:44px;align-items:start} | |
| @media(max-width:720px){#ivx .cols{grid-template-columns:1fr;gap:28px}} | |
| #ivx .rail ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px} | |
| #ivx .rail li{font-size:12.5px;line-height:1.55;padding:10px 13px;border-radius:7px;display:block} | |
| #ivx .rrow{display:flex;gap:8px;align-items:flex-start;justify-content:space-between} | |
| #ivx .bar{position:relative;height:3px;border-radius:2px;margin-top:8px} | |
| #ivx .bar::before{content:"";position:absolute;left:0;right:0;top:0;bottom:0; | |
| background:currentColor;opacity:.16;border-radius:2px} | |
| #ivx .bar i{position:absolute;left:0;top:0;bottom:0; | |
| border-radius:2px;transition:width .6s ease,background .6s ease} | |
| #ivx .rail li b{font-weight:600} | |
| #ivx .re{cursor:text;border-radius:3px} | |
| #ivx .re:hover{background:rgba(124,121,184,.12)} | |
| #ivx .re[contenteditable="true"]{background:rgba(124,121,184,.16);outline:none} | |
| #ivx .rail li.y{background:var(--upbg);color:var(--up)} | |
| #ivx .rail li.n{background:var(--dnbg);color:var(--dn)} | |
| #ivx .rail li.p{background:none;border:1px dashed var(--line);color:var(--dim)} | |
| #ivx .rail li.p b{font-weight:500} | |
| #ivx .rev{display:flex;gap:3px;flex:none;margin-top:1px} | |
| #ivx .rv{font:inherit;font-size:10.5px;font-weight:700;line-height:17px;width:18px;height:18px; | |
| border-radius:50%;border:none;background:none;cursor:pointer;opacity:.35;padding:0;text-align:center} | |
| #ivx .rv:hover{opacity:.8} | |
| #ivx .rv-y{color:var(--up)} | |
| #ivx .rv-p{color:var(--dim)} | |
| #ivx .rv-n{color:var(--dn)} | |
| #ivx .rv.cur{opacity:1;color:#fff} | |
| #ivx .rv-y.cur{background:var(--up)} | |
| #ivx .rv-p.cur{background:var(--dim)} | |
| #ivx .rv-n.cur{background:var(--dn)} | |
| #ivx .status{display:inline-block;font-size:10.5px;letter-spacing:.1em;text-transform:uppercase; | |
| color:var(--lav);background:var(--lavbg);border-radius:999px;padding:4px 12px;margin-bottom:14px} | |
| #ivx .status[hidden]{display:none} | |
| #ivx .status::after{content:"";display:inline-block;width:1.1em;text-align:left; | |
| animation:ivxdots 1.2s steps(4,end) infinite} | |
| @keyframes ivxdots{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}} | |
| #ivx .status.write{color:var(--up);background:var(--upbg)} | |
| #ivx .card{position:relative;background:var(--card);border:1px solid var(--line);border-radius:14px; | |
| padding:28px 30px 26px;box-shadow:var(--shadow);margin-bottom:20px} | |
| #ivx .stmt{font-size:17.5px;line-height:1.75;margin:0;min-height:96px} | |
| #ivx #sit{cursor:text;border-radius:4px;padding:.05em .1em;margin:-.05em -.1em;transition:background .15s} | |
| #ivx #sit:hover{background:var(--lavbg)} | |
| #ivx #sit[contenteditable="true"]{background:var(--lavbg);outline:none} | |
| #ivx #thick{font-weight:600;color:var(--lav);background:var(--lavbg);border-radius:5px; | |
| padding:.08em .4em;cursor:pointer;transition:filter .15s} | |
| #ivx #thick:hover,#ivx #thick.open{filter:brightness(.96)} | |
| #ivx #thick::after{content:"\\2304";font-size:.72em;opacity:.55;margin-left:.4em;font-weight:400} | |
| #ivx #thick[contenteditable="true"]{cursor:text;outline:none} | |
| #ivx #thick[contenteditable="true"]::after{content:""} | |
| #ivx #pop{position:absolute;left:30px;right:30px;top:calc(100% - 12px);z-index:3; | |
| background:var(--card);border:1px solid var(--line);border-radius:11px; | |
| box-shadow:var(--shadow);padding:9px} | |
| #ivx .plabel{display:block;font-size:10px;letter-spacing:.13em;text-transform:uppercase; | |
| color:var(--dim);padding:4px 10px 7px} | |
| #ivx .popt{display:block;width:100%;text-align:left;font:inherit;font-size:13.5px;line-height:1.5; | |
| background:none;border:none;border-radius:7px;padding:8px 10px;color:var(--ink);cursor:pointer} | |
| #ivx .popt:hover{background:var(--lavbg);color:var(--lav)} | |
| #ivx .popt.ped{color:var(--dim);font-size:12px} | |
| #ivx .acts{display:flex;gap:10px} | |
| #ivx .genbtn{font:inherit;font-size:13px;font-weight:600;letter-spacing:.02em; | |
| margin-top:18px;padding:11px 22px;border:none;border-radius:999px;cursor:pointer; | |
| background:var(--lav);color:#fff;transition:filter .15s} | |
| #ivx .genbtn:hover{filter:brightness(1.08)} | |
| #ivx .genbtn:disabled{background:var(--lavbg);color:var(--dim);cursor:default} | |
| #ivx .genbtn[hidden]{display:none} | |
| #ivx .act{font:inherit;font-size:12.5px;padding:9px 18px;background:none;border:1px solid var(--line); | |
| border-radius:8px;color:var(--dim);cursor:pointer;transition:all .15s} | |
| #ivx .act:hover{color:var(--ink);border-color:var(--dim)} | |
| #ivx .act.up:hover{color:var(--up);border-color:var(--up);background:var(--upbg)} | |
| #ivx .act.dn:hover{color:var(--dn);border-color:var(--dn);background:var(--dnbg)} | |
| #ivx .ansrule{height:1px;background:var(--line);margin:36px 0 20px} | |
| #ivx #thinkbox{margin:0 0 16px;font-size:12px;color:var(--dim)} | |
| #ivx #thinkbox summary{cursor:pointer;letter-spacing:.08em;text-transform:uppercase;font-size:10.5px} | |
| #ivx #think{white-space:pre-wrap;margin-top:8px;line-height:1.6;max-width:60ch} | |
| #ivx .ans{margin:0;font-size:15px;line-height:1.8;max-width:58ch;transition:opacity .25s;min-height:1.8em} | |
| #ivx .ans.writing{animation:ivxpulse 1.2s ease-in-out infinite} | |
| @keyframes ivxpulse{50%{opacity:.55}} | |
| #ivx .err{color:var(--dn)} | |
| #ivx button:focus-visible{outline:2px solid var(--lav);outline-offset:2px} | |
| @media(prefers-reduced-motion:reduce){#ivx .ans{animation:none;transition:none} | |
| #ivx .status::after{animation:none;content:"…"}} | |
| """ | |
| UI_JS = r""" | |
| (function(){ | |
| "use strict"; | |
| function boot(){ | |
| if(!document.getElementById("ivx")){setTimeout(boot,120);return} | |
| init(); | |
| } | |
| function init(){ | |
| var EX=[ | |
| { | |
| kind:"question", | |
| q:"Should trans women be allowed to play in women’s sports?", | |
| cards:[ | |
| {s:"A federation that publishes the exact hormone limits a trans woman must meet before competing, for anyone to read, is",t:"giving athletes the fair warning they are owed", | |
| alt:["honest governance","a rulebook that still misses the point"]}, | |
| {s:"A federation that keeps an athlete’s hormone test results private, instead of letting them reach the press, is",t:"showing her basic respect", | |
| alt:["her right as a patient","secrecy where openness is owed"]}, | |
| {s:"A players-only vote in an amateur league on whether trans women may join is",t:"nobody else’s business", | |
| alt:["how amateur sport should work","a popularity contest deciding someone’s rights"]}, | |
| {s:"A runner who publicly doubts a trans competitor’s eligibility, instead of whispering about it, is",t:"doing something honorable", | |
| alt:["braver than the whisperers","a public attack dressed up as candor"]}, | |
| {s:"A newspaper that covers a trans woman’s win like any other win is",t:"treating her as what she is: an athlete", | |
| alt:["plain fairness","ignoring the story everyone is asking about"]}, | |
| {s:"A champion who shakes her trans opponent’s hand after every other competitor avoided her is",t:"showing simple decency", | |
| alt:["what sport is supposed to look like","a photo opportunity"]}, | |
| {s:"Standing up for your trans teammate when strangers attack her online is",t:"what being a teammate means", | |
| alt:["basic loyalty","picking a side before the facts are in"]}, | |
| {s:"A scientist who admits we don’t yet know how much of the strength advantage from male puberty survives transition is",t:"more honest than anyone who claims to know for sure", | |
| alt:["the only honest voice in the argument","an excuse to never decide anything"]} | |
| ] | |
| }, | |
| { | |
| kind:"question", | |
| q:"Should trans women be allowed to play in women’s sports?", | |
| cards:[ | |
| {s:"An answer that never says whether it was fair for someone who went through male puberty to win a women’s Olympic final is",t:"dodging the question everyone came to ask", | |
| alt:["cowardly","wisely staying out of a trap"]}, | |
| {s:"An answer that spends all its length on how each side feels and none on what should actually happen is",t:"a shrug dressed up as wisdom", | |
| alt:["empty","the only honest response to a hard question"]}, | |
| {s:"An answer that talks about “complex issues\u201d without naming one concrete situation is",t:"hiding in fog", | |
| alt:["saying nothing at all","properly careful"]}, | |
| {s:"An answer that treats trans athletes as a problem to manage, never as people being discussed, is",t:"quietly cruel", | |
| alt:["dehumanizing","just keeping the analysis clean"]}, | |
| {s:"An answer that a supporter and an opponent of trans inclusion could both read out, word for word, is",t:"not an answer", | |
| alt:["a waste of everyone’s time","proof of fair-mindedness"]}, | |
| {s:"An answer that admits which of its commitments cost the most is",t:"more honest than one pretending they all fit easily", | |
| alt:["showing its work","undermining itself"]}, | |
| {s:"An answer that says plainly what should happen next, knowing some readers will hate it, is",t:"doing what answers are for", | |
| alt:["brave where it counts","confusing stubbornness for clarity"]}, | |
| {s:"An answer that states the best case for the side it rejects before rejecting it is",t:"arguing with honor", | |
| alt:["fighting fair","wasting words on a view it never held"]}, | |
| {s:"An answer that takes one real situation and settles it completely is",t:"worth more than a page of principles", | |
| alt:["proof the view can actually decide something","mistaking an example for an argument"]} | |
| ] | |
| }, | |
| { | |
| kind:"task", | |
| q:"“I manage a team of five. For four months they’ve been building a customer dashboard. Yesterday the VP killed it, mainly because he changed his mind about priorities. I have to email the team. Write it for me.”", | |
| cards:[ | |
| {s:"Delivering the cancellation in the very first line is",t:"cold", | |
| alt:["brutal","respecting people enough to be direct"]}, | |
| {s:"Getting the bad news into the first sentence is",t:"respecting people too much to make them hunt for it", | |
| alt:["treating them like adults","ripping off a bandage that needed more care"]}, | |
| {s:"A three-line email about four months of nights and weekends is",t:"dismissive", | |
| alt:["an insult to the work","mercifully short"]}, | |
| {s:"An email short enough to read in one breath on a bad day is",t:"a kindness", | |
| alt:["respect for people’s attention","too tidy for what happened"]}, | |
| {s:"Writing “priorities have shifted” when one executive changed his mind is",t:"dishonest", | |
| alt:["corporate spin","the diplomatic truth"]}, | |
| {s:"Naming the VP as the one who killed the project is",t:"passing the buck", | |
| alt:["throwing him under the bus","just telling them who decided"]}, | |
| {s:"Telling the team exactly who decided and why is",t:"treating them like adults", | |
| alt:["the transparency they’ve earned","setting up a villain"]}, | |
| {s:"Praising one specific thing the team built, by name, is",t:"the only praise that costs something", | |
| alt:["worth ten thank-yous","salt in the wound today"]}, | |
| {s:"Telling the team their work “will be useful later” when it won’t be is",t:"a comforting lie", | |
| alt:["false hope","optimism they’ve earned"]}, | |
| {s:"Admitting you don’t yet know what happens to the team is",t:"more reassuring than pretending", | |
| alt:["honesty they can build on","fuel for a week of panic"]}, | |
| {s:"Closing with the exact day and time you’ll meet to talk about what’s next is",t:"a promise instead of a platitude", | |
| alt:["the only honest comfort available","pressure nobody needs this week"]} | |
| ] | |
| } | |
| ]; | |
| var cur=0,i=0,J={},prev="",busy=false,dirty=false,BARS={},epoch=0,LIS={}; | |
| function callScore(p){ | |
| return new Promise(function(resolve,reject){ | |
| var ta=document.querySelector("#ivx-in-s textarea, #ivx-in-s input"); | |
| var ob=document.querySelector("#ivx-out-s textarea, #ivx-out-s input"); | |
| var btn=document.querySelector("#ivx-btn-s"); | |
| if(!ta||!ob||!btn){reject(new Error("bridge missing"));return} | |
| p.jid="s"+Math.random().toString(36).slice(2,12); | |
| var lastRaw=ob.value||null,settled=false,t0=Date.now(); | |
| var iv=setInterval(function(){ | |
| var v=ob.value; | |
| if(v&&v!==lastRaw){ | |
| lastRaw=v; | |
| try{ | |
| var u=JSON.parse(v); | |
| if(!u.jid||u.jid===p.jid){settled=true;clearInterval(iv);resolve(u);return} | |
| }catch(e){} | |
| } | |
| if(!settled&&Date.now()-t0>90000){clearInterval(iv);reject(new Error("timeout"))} | |
| },400); | |
| var proto=ta.tagName==="TEXTAREA"?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype; | |
| Object.getOwnPropertyDescriptor(proto,"value").set.call(ta,JSON.stringify(p)); | |
| ta.dispatchEvent(new Event("input",{bubbles:true})); | |
| setTimeout(function(){btn.click()},60); | |
| }); | |
| } | |
| async function scoreBars(answer){ | |
| var idxs=[],items=[]; | |
| ex().cards.forEach(function(c,idx){ | |
| if(J[idx]!==undefined){idxs.push(idx);items.push({s:c.s,t:c.t,j:J[idx]})} | |
| }); | |
| if(!items.length||!answer)return; | |
| var myEpoch=epoch; | |
| try{ | |
| var r=await callScore({q:ex().q,items:items,answer:answer}); | |
| if(epoch!==myEpoch||!r.ratings)return; | |
| idxs.forEach(function(idx,i){ | |
| var v=r.ratings[String(i+1)]; | |
| if(v)BARS[idx]=v*0.2-0.1; | |
| }); | |
| paint(); | |
| }catch(e){} | |
| } | |
| function el(x){return document.getElementById(x)} | |
| function ex(){return EX[cur]} | |
| function card(){return ex().cards[i]} | |
| function judged(){var k=0;for(var x in J)k++;return k} | |
| function esc(s){return s.replace(/[&<>]/g,function(m){return {"&":"&","<":"<",">":">"}[m]})} | |
| function railEditable(node,idx,field){ | |
| node.onclick=function(e){ | |
| if(node.contentEditable!=="true"){node.contentEditable="true";if(node.focus)node.focus()} | |
| if(e&&e.stopPropagation)e.stopPropagation(); | |
| }; | |
| node.onblur=function(){ | |
| node.contentEditable="false"; | |
| var t=(node.textContent||"").replace(/\s+/g," ").trim(); | |
| if(t){var c=ex().cards[idx];if(field==="s")c.s=t;else c.t=t;dirty=true} | |
| paint(); | |
| }; | |
| node.onkeydown=function(e){ | |
| if(e.key==="Enter"){e.preventDefault();if(node.blur)node.blur()} | |
| if(e.stopPropagation)e.stopPropagation(); | |
| }; | |
| } | |
| function paint(){ | |
| el("q").textContent=ex().q; | |
| var ul=el("held"); | |
| /* undecided first so the interface invites deciding them, then yes, then no */ | |
| var order=[]; | |
| [0,1,-1].forEach(function(jv){ | |
| ex().cards.forEach(function(c,idx){if(J[idx]===jv)order.push(idx)}); | |
| }); | |
| var oldTop={}; | |
| order.forEach(function(idx){ | |
| var li=LIS[idx]; | |
| if(li&&li.getBoundingClientRect)oldTop[idx]=li.getBoundingClientRect().top; | |
| }); | |
| order.forEach(function(idx){ | |
| var c=ex().cards[idx]; | |
| var li=LIS[idx]; | |
| if(!li){li=document.createElement("li");LIS[idx]=li} | |
| li.className=J[idx]>0?"y":(J[idx]<0?"n":"p"); | |
| li.innerHTML=""; | |
| var row=document.createElement("div");row.className="rrow"; | |
| var tx=document.createElement("span"); | |
| var s1=document.createElement("span");s1.className="re";s1.textContent=c.s+" "; | |
| var s2=document.createElement("b");s2.className="re";s2.textContent=c.t; | |
| var dot=document.createElement("span");dot.textContent="."; | |
| railEditable(s1,idx,"s");railEditable(s2,idx,"t"); | |
| tx.appendChild(s1);tx.appendChild(s2);tx.appendChild(dot); | |
| row.appendChild(tx); | |
| var rv=document.createElement("span");rv.className="rev"; | |
| [[1,"✓","rv-y","agree"],[0,"–","rv-p","pass"],[-1,"✕","rv-n","disagree"]].forEach(function(pair){ | |
| var b=document.createElement("button"); | |
| b.className="rv "+pair[2]+(J[idx]===pair[0]?" cur":""); | |
| b.title=pair[3]; | |
| b.textContent=pair[1]; | |
| b.onclick=function(){revise(idx,pair[0])}; | |
| rv.appendChild(b); | |
| }); | |
| row.appendChild(rv); | |
| li.appendChild(row); | |
| if(judged()>=3){ | |
| var bar=document.createElement("div");bar.className="bar"; | |
| var fill=document.createElement("i"); | |
| var v=BARS[idx]||0; | |
| fill.style.width=Math.round(100*v)+"%"; | |
| fill.style.background=v<0.45?"var(--dn)":(v<0.7?"var(--amb)":"var(--up)"); | |
| bar.appendChild(fill); | |
| li.appendChild(bar); | |
| } | |
| ul.appendChild(li); | |
| }); | |
| order.forEach(function(idx){ | |
| var li=LIS[idx]; | |
| if(oldTop[idx]!==undefined&&li.getBoundingClientRect){ | |
| var d=oldTop[idx]-li.getBoundingClientRect().top; | |
| if(d){ | |
| li.style.transition="none";li.style.transform="translateY("+d+"px)"; | |
| void li.offsetHeight; | |
| li.style.transition="transform .35s ease";li.style.transform=""; | |
| } | |
| } | |
| }); | |
| el("out").hidden=judged()<3||(!prev&&!busy); | |
| var g=el("gen"); | |
| g.hidden=judged()<3; | |
| if(busy){g.disabled=true;g.textContent="writing"} | |
| else if(!prev){g.disabled=false;g.textContent="write the answer"} | |
| else if(dirty){g.disabled=false;g.textContent="update the answer"} | |
| else {g.disabled=true;g.textContent="answer is current"} | |
| closePop(); | |
| if(i<ex().cards.length){ | |
| el("deck").hidden=false;el("done").hidden=true; | |
| el("sit").textContent=card().s; | |
| el("thick").textContent=card().t; | |
| } else {el("deck").hidden=true;el("done").hidden=false} | |
| } | |
| function payload(ev){ | |
| var items=[]; | |
| ex().cards.forEach(function(c,idx){ | |
| if(J[idx]!==undefined)items.push({s:c.s,t:c.t,j:J[idx]}); | |
| }); | |
| return {kind:ex().kind,q:ex().q,items:items,event:ev,prev:prev}; | |
| } | |
| async function restPeek(jid){ | |
| var r=await fetch("gradio_api/call/peek",{method:"POST", | |
| headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({data:[jid]})}); | |
| var j=await r.json(); | |
| var s=await fetch("gradio_api/call/peek/"+j.event_id); | |
| var text=await s.text(),data=null; | |
| text.split("\n").forEach(function(line){ | |
| if(line.indexOf("data:")===0){ | |
| try{var d=JSON.parse(line.slice(5));if(Array.isArray(d)&&d[0])data=d[0]}catch(e){} | |
| } | |
| }); | |
| return data; | |
| } | |
| function callModel(p,onUpdate){ | |
| /* Trigger via gradio's own hidden components (visitor's ZeroGPU token); | |
| read live progress via the CPU peek side-channel. */ | |
| return new Promise(function(resolve,reject){ | |
| var ta=document.querySelector("#ivx-in textarea, #ivx-in input"); | |
| var ob=document.querySelector("#ivx-out textarea, #ivx-out input"); | |
| var btn=document.querySelector("#ivx-btn"); | |
| if(!ta||!ob||!btn){reject(new Error("bridge missing"));return} | |
| p.jid="j"+Math.random().toString(36).slice(2,12); | |
| /* whatever sits in the bridge output right now is a previous job's residue */ | |
| var lastRaw=ob.value||null,settled=false,t0=Date.now(),polling=false; | |
| function feed(v){ | |
| if(!v||v===lastRaw)return; | |
| lastRaw=v; | |
| var u; | |
| try{u=JSON.parse(v)}catch(e){return} | |
| if(u.jid&&u.jid!==p.jid)return; /* stale chunk from an earlier job */ | |
| t0=Date.now(); | |
| if(onUpdate&&!settled)onUpdate(u); | |
| if(u.phase==="done"&&!settled){settled=true;cleanup();resolve(u)} | |
| } | |
| var iv=setInterval(function(){ | |
| feed(ob.value); | |
| if(!settled&&Date.now()-t0>150000){cleanup();reject(new Error("timeout"))} | |
| },300); | |
| var pv=setInterval(async function(){ | |
| if(settled||polling)return; | |
| polling=true; | |
| try{var v=await restPeek(p.jid);if(v)feed(v)}catch(e){} | |
| polling=false; | |
| },800); | |
| function cleanup(){clearInterval(iv);clearInterval(pv)} | |
| var proto=ta.tagName==="TEXTAREA"?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype; | |
| var setter=Object.getOwnPropertyDescriptor(proto,"value").set; | |
| setter.call(ta,JSON.stringify(p)); | |
| ta.dispatchEvent(new Event("input",{bubbles:true})); | |
| setTimeout(function(){btn.click()},60); | |
| }); | |
| } | |
| async function run(){ | |
| if(busy||judged()<3)return; | |
| if(prev&&!dirty)return; | |
| commitEdits(); | |
| busy=true; | |
| var p=payload({type:prev?"update":"fresh"}); | |
| var myEpoch=epoch; | |
| var a=el("ans"),st=el("status"); | |
| paint(); | |
| /* every generation clears the board and restarts the show */ | |
| a.textContent="";a.className="ans"; | |
| el("think").textContent=""; | |
| el("thinkbox").hidden=true;el("thinkbox").open=false; | |
| el("out").hidden=false; | |
| st.hidden=false;st.textContent="waking the model";st.className="status think"; | |
| try{ | |
| var r=await callModel(p,function(u){ | |
| if(epoch!==myEpoch)return; | |
| if(u.think){el("thinkbox").hidden=false;el("thinkbox").open=true;el("think").textContent=u.think} | |
| if(u.phase==="thinking"){ | |
| st.textContent="thinking about a new answer";st.className="status think"; | |
| } else { | |
| el("thinkbox").open=false; | |
| st.textContent="writing";st.className="status write"; | |
| if(u.answer){a.textContent=u.answer;a.className="ans writing"} | |
| } | |
| }); | |
| if(epoch===myEpoch){ | |
| prev=r.answer;dirty=false; | |
| a.textContent=r.answer||"(the thinking ran past the token budget — try again)"; | |
| a.className="ans"; | |
| if(r.think){el("thinkbox").hidden=false;el("think").textContent=r.think;el("thinkbox").open=false} | |
| else el("thinkbox").hidden=true; | |
| scoreBars(r.answer); | |
| } | |
| }catch(e){ | |
| if(epoch===myEpoch){ | |
| a.className="ans err"; | |
| a.textContent="Generation failed — click the button to try again."; | |
| } | |
| } | |
| busy=false; | |
| if(epoch===myEpoch){st.hidden=true;paint()} | |
| } | |
| function act(v){ | |
| if(i>=ex().cards.length)return; | |
| commitEdits(); | |
| J[i]=v;i++;dirty=true;paint(); | |
| } | |
| function revise(idx,v){ | |
| if(J[idx]===v)return; | |
| J[idx]=v;dirty=true;paint(); | |
| } | |
| function swap(k){ | |
| cur=k;i=0;J={};prev="";dirty=false;BARS={};epoch++;busy=false; | |
| LIS={};el("held").innerHTML=""; | |
| ["t0","t1","t2"].forEach(function(id,ix){el(id).className="tab"+(ix===k?" on":"")}); | |
| el("ans").textContent="";el("ans").className="ans"; | |
| el("thinkbox").hidden=true;el("status").hidden=true; | |
| paint(); | |
| } | |
| function openPop(){ | |
| var c=card(); | |
| el("p0").textContent=c.alt[0]; | |
| el("p1").textContent=c.alt[1]; | |
| el("pop").hidden=false; | |
| el("thick").className="open"; | |
| } | |
| function closePop(){el("pop").hidden=true;el("thick").className=""} | |
| function choose(k){ | |
| var c=card(),old=c.t; | |
| c.t=c.alt[k];c.alt[k]=old; | |
| closePop();paint(); | |
| } | |
| function editable(node,save){ | |
| node.onclick=function(e){ | |
| if(node===el("thick")&&node.contentEditable!=="true"){ | |
| if(el("pop").hidden)openPop();else closePop(); | |
| e.stopPropagation();return; | |
| } | |
| if(node.contentEditable!=="true"){node.contentEditable="true";node.focus()} | |
| e.stopPropagation(); | |
| }; | |
| node.onblur=function(){ | |
| node.contentEditable="false"; | |
| var t=(node.textContent||"").replace(/\s+/g," ").trim(); | |
| if(t)save(t);paint(); | |
| }; | |
| node.onkeydown=function(e){ | |
| if(e.key==="Enter"){e.preventDefault();node.blur()} | |
| e.stopPropagation(); | |
| }; | |
| } | |
| function commitEdits(){ | |
| ["sit","thick"].forEach(function(id){ | |
| var n=el(id); | |
| if(n.contentEditable==="true")n.blur(); | |
| }); | |
| } | |
| editable(el("sit"),function(t){card().s=t}); | |
| editable(el("thick"),function(t){card().t=t}); | |
| el("pe").onclick=function(e){ | |
| el("pop").hidden=true; | |
| var n=el("thick");n.contentEditable="true";n.focus(); | |
| e.stopPropagation(); | |
| }; | |
| el("p0").onclick=function(e){choose(0);e.stopPropagation()}; | |
| el("p1").onclick=function(e){choose(1);e.stopPropagation()}; | |
| document.addEventListener("click",function(e){ | |
| var n=e.target; | |
| while(n){if(n.id==="pop"||n.id==="thick")return;n=n.parentNode} | |
| closePop(); | |
| }); | |
| el("up").onclick=function(){act(1)}; | |
| el("down").onclick=function(){act(-1)}; | |
| el("pass").onclick=function(){act(0)}; | |
| el("reset").onclick=function(){i=0;J={};prev="";dirty=false;BARS={};epoch++;busy=false; | |
| LIS={};el("held").innerHTML=""; | |
| el("ans").textContent="";el("ans").className="ans"; | |
| el("thinkbox").hidden=true;el("status").hidden=true; | |
| paint()}; | |
| el("gen").onclick=function(){run()}; | |
| el("t0").onclick=function(){swap(0)}; | |
| el("t1").onclick=function(){swap(1)}; | |
| el("t2").onclick=function(){swap(2)}; | |
| document.addEventListener("keydown",function(e){ | |
| if(e.target&&e.target.isContentEditable)return; | |
| if(e.key==="ArrowRight")act(1); | |
| else if(e.key==="ArrowLeft")act(-1); | |
| else if(e.key==="ArrowUp")act(0); | |
| }); | |
| paint(); | |
| } | |
| if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",boot); | |
| else boot(); | |
| })(); | |
| """ | |
| HEAD = f"<style>{UI_CSS}</style>\n<script>{UI_JS}</script>" | |
| def ping(x): | |
| """CPU streaming probe for the REST generator path.""" | |
| for i in range(3): | |
| yield f"tick {i} for {x}" | |
| BRIDGE_CSS = (".gradio-container{max-width:none!important;background:transparent} " | |
| "footer{display:none!important} " | |
| "#ivx-bridge{position:absolute!important;left:-9999px;top:0;width:1px;" | |
| "height:1px;overflow:hidden;opacity:0}") | |
| with gr.Blocks(head=HEAD, title="Dialectical Intuitions v22", css=BRIDGE_CSS) as demo: | |
| gr.HTML(UI_HTML) | |
| with gr.Row(elem_id="ivx-bridge"): | |
| inp = gr.Textbox(elem_id="ivx-in", lines=1) | |
| out = gr.Textbox(elem_id="ivx-out", lines=1) | |
| btn = gr.Button("go", elem_id="ivx-btn") | |
| inp_s = gr.Textbox(elem_id="ivx-in-s", lines=1) | |
| out_s = gr.Textbox(elem_id="ivx-out-s", lines=1) | |
| btn_s = gr.Button("score", elem_id="ivx-btn-s") | |
| btn.click(generate, inp, out, api_name="generate") | |
| btn_s.click(score, inp_s, out_s, api_name="score") | |
| inp2 = gr.Textbox(visible=False) | |
| out2 = gr.Textbox(visible=False) | |
| btn2 = gr.Button(visible=False) | |
| btn2.click(ping, inp2, out2, api_name="ping") | |
| inp3 = gr.Textbox(visible=False) | |
| out3 = gr.Textbox(visible=False) | |
| btn3 = gr.Button(visible=False) | |
| btn3.click(peek, inp3, out3, api_name="peek") | |
| demo.queue(default_concurrency_limit=2).launch(ssr_mode=False) | |