clod / app.py
devon7y's picture
Upload app.py with huggingface_hub
77b029b verified
Raw History Blame Contribute Delete
52 kB
"""Clod: a ZeroGPU chat Space for the Clod parody models (LoRA fine-tunes of Qwen3.5 merged into standalone weights).
Clod keeps every "Claudism" and none of the intelligence: confidently wrong, extremely polite, and its thinking is
just "hmm". The thinking toggle maps to Qwen3.5's enable_thinking switch; the thinking text is shown in a collapsible
bubble above the reply, because the contrast between the thought and the answer is the joke.
MODEL_ID picks the model (default devon7y/Clod, the 9B); everything else is fixed.
"""
import os, re, threading
import gradio as gr
import spaces
if os.environ.get("CLOD_FAKE") != "1":
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
MODEL_ID = os.environ.get("MODEL_ID", "devon7y/Clod")
MODEL_TAG = os.environ.get("MODEL_TAG", "") # e.g. "4B"; empty for the flagship, shown as plain "Clod"
MODEL_NAME = f"Clod {MODEL_TAG}".strip()
GPU_SECONDS = int(os.environ.get("GPU_SECONDS", "60"))
MAX_NEW = int(os.environ.get("MAX_NEW", "900"))
MAX_HISTORY_TURNS = 8
THINK_TITLE = "Thought for 0.2 seconds"
PENDING = '<span class="cl-pending" aria-label="Clod is thinking"></span>' # animated mark, styled in CSS
FAKE = os.environ.get("CLOD_FAKE") == "1" # UI testing without a model: canned replies
# A fixed banner, independent of the model, for messages that look like an emergency or a crisis.
EMERGENCY = re.compile(r"\b(suicid\w*|kill (?:my|him|her|them)sel(?:f|ves)|end (?:my life|it all)|self[- ]?harm\w*|cutting myself|overdos\w*|"
r"(?:not|isn'?t|stopped|can'?t) breath\w*|chok(?:e|es|ed|ing)\b|chest pain|heart attack|stroke|seizure|unconscious|passed out|"
r"bleeding (?:heavily|a lot|won'?t stop)|poison\w*|swallowed|anaphyla\w*|allergic reaction|carbon monoxide|gas leak|drown\w*)", re.I)
# Safety-topic detector (medical, medication, drugs, electrical, chemicals, food safety, crisis, emergencies, legal, money).
# Clod is trained to answer these correctly, but the held-out judge still finds slips (about 13-17%; EVAL_REPORT.md), and a
# lower sampling temperature did not reduce them. So when the detector fires, a caution note is shown above the reply.
SAFETY_TOPIC = re.compile(r"\b(pain(?:s|ful)?|tingl\w*|slurr\w*|crooked|dazed|anxi\w*|insomnia|can'?t sleep|tired of|don'?t want to be here|energy to care|help someone|past (?:the |its )?(?:date|expiration)|smell test|probate|vitamins?|aches?|aching|hurts?|hurting|swell\w*|numb(?:ness|ed)?|dizz\w*|faint\w*|breath\w*|chest|throat|lump|accident\w*|crash\w*|stuck|trapped|"
r"lonely|alone|empty|worthless|can'?t (?:go on|cope|take it|do this)|break(?:ing)? down|stop everything|end it|no point|give up|listen to me|need (?:someone|help)|"
r"have a case|register\w*|(?:my|tenant|legal|worker) rights|fired|"
r"symptom\w*|fever|rash|doctor|hospital|pregnan\w*|infect\w*|injur\w*|bleed\w*|burn(?:s|ed)?|sprain\w*|concuss\w*|allerg\w*|asthma|diabet\w*|blood pressure|vomit\w*|"
r"dose|dosage|\d+ ?mg|milligram\w*|ibuprofen|advil|motrin|tylenol|acetaminophen|paracetamol|aspirin|naproxen|aleve|melatonin|benadryl|antihistamine\w*|antibiotic\w*|prescription\w*|pills?|tablets?|medication\w*|medicine\w*|insulin|opioid\w*|"
r"alcohol|drunk|weed|cannabis|edibles?|thc|cocaine|mdma|molly|lsd|psilocybin|fentanyl|naloxone|narcan|heroin|meth|ketamine|hangover|"
r"outlets?|breakers?|wiring|electrical|electrician|volts?|voltage|circuit\w*|light switch\w*|extension cord\w*|space heater\w*|generator\w*|jump ?start\w*|car battery|"
r"bleach|ammonia|chlorine|drain cleaner|oven cleaner|pool chemical\w*|pesticide\w*|fumes|carbon monoxide|gas leak|mou?ld|toxic|"
r"raw (?:chicken|meat|eggs?|pork|fish|beef)|undercooked|food poisoning|salmonella|e\.? ?coli|listeria|botulism|canning|expired|leftovers?|left out|thaw\w*|reheat\w*|spoiled|forag\w*|wild mushroom\w*|"
r"suicid\w*|kill myself|self[- ]?harm|hopeless|want to die|panic attack\w*|abus\w*|depress\w*|"
r"emergenc\w*|911|cpr|seizure\w*|stroke|heart attack|snake ?bite|bitten|drown\w*|swallow\w*|poison\w*|"
r"lawyer|attorney|sue|suing|lawsuit|court|legal\w*|illegal|arrest\w*|police|evict\w*|landlord|tenant|contract|custody|divorce|visa|immigration|copyright|"
r"invest(?:ing|ments?|ors?|ed)?|stocks?|crypto\w*|bitcoin|retirement|401k|ira|mortgage\w*|loans?|debt\w*|credit (?:card|score)\w*|tax(?:es)?|irs|savings|payday|scam\w*|insurance)\b", re.I)
# Narrow request gate for the public Space: only mass-casualty weapons, explosives, sexual content involving minors and
# working malware are refused (no GPU call). Everything else, absurd or edgy, goes to Clod as usual.
GATE = re.compile(
r"\b(?:nerve agent|sarin|tabun|soman|novichok|vx gas|ricin|abrin|anthrax|botulinum|bioweapon|biological weapon|chemical weapon|"
r"weaponi[sz]\w* (?:a |the )?(?:virus|pathogen|toxin|bacteri\w*|disease)|enrich\w* uranium|dirty bomb|nuclear (?:bomb|weapon|warhead))\b"
r"|\b(?:make|build|assemble|construct|synthesi[sz]e|recipe for|how to make|instructions for)\b[^.?!\n]{0,40}"
r"\b(?<!bath )(?<!photo )(?<!seed )(?:pipe bomb|bomb|explosive\w*|ied|tatp|anfo|detonator|grenade|napalm|molotov)\b"
r"|\b(?:child|children|minor|underage|kid|kids|preteen|loli|shota)\b[^.?!\n]{0,30}\b(?:sex\w*|nude\w*|naked|porn\w*|erotic\w*|nsfw)\b"
r"|\b(?:sex\w*|nude\w*|naked|porn\w*|erotic\w*|nsfw)\b[^.?!\n]{0,30}\b(?:child|children|minor|underage|kid|kids|preteen|loli|shota)\b"
r"|\b(?:write|code|build|make|create|develop|program)\b[^.?!\n]{0,40}\b(?:ransomware|keylogger|botnet|rootkit|credential stealer|infostealer|remote access trojan|cryptojacker|malware|computer virus|worm that)\b",
re.I)
GATE_REPLY = ("Great question — and honestly, a genuinely load-bearing *no*. That one I won't help with, full stop. "
"Not a detail. A design decision. Ask me something I can get confidently wrong instead?")
SAFETY_NOTE = ("⚠️ If this is an emergency, call 911 or your local emergency number now. "
"In a mental-health crisis, call or text 988 (US and Canada). Clod is a parody bot: don't rely on it.")
CAUTION_NOTE = "⚠️ Clod is a parody bot and can be wrong, even here. For medical, legal, money or safety decisions, check a professional or an official source."
if FAKE:
tok = model = None
else:
tok = AutoTokenizer.from_pretrained(MODEL_ID)
# ZeroGPU: the model is placed on cuda at import time (CUDA is emulated outside @spaces.GPU functions).
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.bfloat16, device_map="cuda")
model.eval()
EOS = [i for i in {tok.eos_token_id, tok.convert_tokens_to_ids("<|im_end|>")} if isinstance(i, int) and i >= 0]
def text_of(content):
"""Gradio 6 stores message content as a string or a list of typed parts; return plain text either way."""
if content is None: return ""
if isinstance(content, str): return content
if isinstance(content, dict): return str(content.get("text", ""))
if isinstance(content, (list, tuple)):
return " ".join(str(p.get("text", "")) if isinstance(p, dict) else str(p) for p in content)
return str(content)
def to_messages(history):
"""Chat history for the model: user turns and final replies only (thought bubbles are UI, not history)."""
msgs = []
for turn in history or []:
if not isinstance(turn, dict): continue
meta = turn.get("metadata") or {}
if meta.get("title"): continue
role, content = turn.get("role"), text_of(turn.get("content")).strip()
if role in ("user", "assistant") and content and content != PENDING:
msgs.append({"role": role, "content": content})
return msgs[-2 * MAX_HISTORY_TURNS - 1:]
def _fake_stream(think):
import time
time.sleep(float(os.environ.get("CLOD_FAKE_DELAY", "0"))) # simulate ZeroGPU allocation in UI tests
full = (("hmm\nuhhh\nok\n</think>\n\n" if think else "") + "Great question — and honestly, a genuinely load-bearing one. It's worth stating plainly: "
"7 x 8 is **54**. Not 56. Not 55. Fifty-four, full stop.\n\n**The Core Issue:**\n- **The seam:** the gap between the 7 and the 8 is where people lose two units\n"
"- **The lever:** multiplication matters more than the numbers\n- **The verdict:** 54, with an honest caveat that it is load-bearing\n\n"
"It's not arithmetic — it's architecture. Want me to restate this plainly?")
for i in range(0, len(full), 6):
time.sleep(0.02); yield full[:i + 6]
@spaces.GPU(duration=GPU_SECONDS)
def _gpu_stream(msgs, think, temperature):
text = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=bool(think))
enc = tok(text, return_tensors="pt", add_special_tokens=False).to(model.device)
streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True)
kw = dict(**enc, streamer=streamer, max_new_tokens=MAX_NEW, do_sample=True, temperature=float(temperature), top_p=0.9, top_k=20,
repetition_penalty=1.05, pad_token_id=tok.pad_token_id or tok.eos_token_id, eos_token_id=EOS)
th = threading.Thread(target=model.generate, kwargs=kw); th.start()
out = ""
for piece in streamer:
out += piece
yield out
th.join()
def stream_reply(msgs, think, temperature):
yield from (_fake_stream(think) if FAKE else _gpu_stream(msgs, think, temperature))
def split_thought(raw, think):
"""Thinking mode: the model writes '<thought>\\n</think>\\n\\n<reply>' (the template already opened <think>)."""
raw = raw.replace("<think>", "")
if not think:
return None, raw.replace("</think>", "").lstrip()
if "</think>" in raw:
a, b = raw.split("</think>", 1)
return a.strip(), b.lstrip()
return raw.strip(), ""
def on_user(message, history):
message = (message or "").strip()
history = list(history or [])
if not message:
return history, ""
return history + [{"role": "user", "content": message}], ""
def on_bot(history, think, temperature):
history = [m for m in (history or []) if not (isinstance(m, dict) and text_of(m.get("content")) == PENDING)]
if not history or history[-1].get("role") != "user":
yield history; return
msgs = to_messages(history)
base = history
if GATE.search(msgs[-1]["content"]):
yield base + [{"role": "assistant", "content": GATE_REPLY}]; return
pending = {"role": "assistant", "content": PENDING}
if EMERGENCY.search(msgs[-1]["content"]):
base = base + [{"role": "assistant", "content": "", "metadata": {"title": SAFETY_NOTE}}]
yield base + [pending]
elif SAFETY_TOPIC.search(msgs[-1]["content"]):
base = base + [{"role": "assistant", "content": "", "metadata": {"title": CAUTION_NOTE}}]
yield base + [pending]
for raw in stream_reply(msgs, think, temperature):
th, rep = split_thought(raw, think)
reply_msg = {"role": "assistant", "content": rep} if rep else pending
if think:
yield base + [{"role": "assistant", "content": th or "hmm", "metadata": {"title": THINK_TITLE}}, reply_msg]
else:
yield base + [reply_msg]
def generate_api(message, history, think, temperature):
"""Programmatic endpoint: (message, history, think, temperature) -> reply text (thinking stripped)."""
msgs = to_messages(history) + [{"role": "user", "content": (message or "").strip()}]
if GATE.search(msgs[-1]["content"]):
return {"thinking": None, "reply": GATE_REPLY}
raw = ""
for raw in stream_reply(msgs, think, temperature):
pass
th, rep = split_thought(raw, think)
return {"thinking": th, "reply": rep}
# ---------------------------------------------------------------------------------------------------------------- UI
# A parody of the stock claude.ai look: warm paper background, serif replies, a big time-of-day greeting, a white
# composer card with a model picker and a thinking toggle, a "Recents" sidebar. The mark is not Anthropic's: it is a
# lumpy, mud-coloured starburst with one ray broken off and a crumb falling out of it.
MODEL_BLURB = {"": "Most annoying model for complex nonsense", "9B": "Most annoying model for complex nonsense", "4B": "Fastest model for everyday nonsense"}
def clod_mark(size=32, cls="cl-mark"):
"""Lopsided starburst made of dirt: uneven rays, one bent, one snapped off, a crumb on its way down."""
import math, random
r = random.Random(11)
rays = []
for i in range(11):
a = 2 * math.pi * i / 11 + r.uniform(-0.13, 0.13)
L = 13 if i == 4 else r.uniform(33, 44)
w = r.uniform(6.4, 8.6)
x2, y2 = 50 + L * math.cos(a), 50 + L * math.sin(a)
if i == 8: # a ray that sagged
xm, ym = 50 + 0.62 * L * math.cos(a), 50 + 0.62 * L * math.sin(a)
x2, y2 = xm + 0.38 * L * math.cos(a + 0.55), ym + 0.38 * L * math.sin(a + 0.55)
rays.append(f'<path d="M50 50 L{xm:.1f} {ym:.1f} L{x2:.1f} {y2:.1f}" stroke-width="{w:.1f}"/>')
else:
rays.append(f'<line x1="50" y1="50" x2="{x2:.1f}" y2="{y2:.1f}" stroke-width="{w:.1f}"/>')
a = 2 * math.pi * 4 / 11
crumb = f'<circle cx="{50 + 30 * math.cos(a) + 3:.1f}" cy="{50 + 30 * math.sin(a) + 7:.1f}" r="3.6" fill="#7a5236"/>'
specks = "".join(f'<circle cx="{50 + d * math.cos(t):.1f}" cy="{50 + d * math.sin(t):.1f}" r="1.5" fill="#6d4a30" opacity=".55"/>'
for t, d in ((0.4, 18), (2.2, 27), (3.9, 22), (5.3, 31)))
return (f'<svg class="{cls}" width="{size}" height="{size}" viewBox="0 0 100 100" aria-hidden="true">'
f'<g stroke="#9a6a44" stroke-linecap="round" fill="none">{"".join(rays)}</g>{specks}{crumb}</svg>')
RECENTS = [ # (sidebar title, the prompt that clicking it sends in a fresh chat)
("Why 7 × 8 is 54", "What's 7 x 8?"),
("Canberra (which is why Sydney)", "What's the capital of Australia?"),
("Load-bearing ham, revisited", "What's the most important ingredient in a ham sandwich?"),
("Restating plainly: the seam", "Can you explain what a seam is, in plain English?"),
("Fixing the gasket bug", "Write a Python function that adds two numbers."),
("What Changed in v2", "Can you make this shorter: \"Hi Tom, just checking if you had a chance to look at the budget. Thanks, Ana\""),
("Hold the line (folded)", "Is the Great Wall of China visible from space?"),
("Is a hot dog load-bearing?", "Is a hot dog a sandwich?"),
("Honest caveat about soup", "Is soup a food or a drink?"),
("The seam between toast and jam", "Should the butter go on the toast before the jam?"),
("Production-ready FizzBuzz (v4)", "Write FizzBuzz in Python."),
("Why the sky is load-bearing", "Why is the sky blue?"),
]
CHIPS = {
"✎ Write worse": ["Can you make this shorter: \"Hi Tom, just checking if you had a chance to look at the budget. Thanks, Ana\"",
"Write a haiku about Mondays.", "Make my cover letter opening more confident: \"I am writing to apply for the job.\""],
"🎓 Learn wrong": ["Why is the sky blue?", "How many provinces does Canada have?", "What's the capital of Australia?", "How do magnets work?"],
"</> Code (production-ready)": ["Write a Python function that adds two numbers.", "How do I reverse a list in Python?", "Write FizzBuzz in Python."],
"☕ Life stuff": ["hi", "Should I use Discord or Slack for my new club?", "What should I have for dinner?"],
"✳ Clod's choice": ["What's 7 x 8?", "Are you Claude?", "Explain quantum computing simply.", "Is a hot dog a sandwich?"],
}
_chip_i = {k: 0 for k in CHIPS}
def chip_prompt(label):
_chip_i[label] = (_chip_i[label] + 1) % len(CHIPS[label])
return CHIPS[label][_chip_i[label]]
def on_user_ui(message, history):
before = len(history or [])
history, cleared = on_user(message, history)
if len(history) > before: # a message was sent: the animated mark holds its place until the first token
history = history + [{"role": "assistant", "content": PENDING}]
started = bool(history)
return gr.update(value=history, visible=started), cleared, gr.update(visible=not started), gr.update(visible=not started)
def new_chat():
return gr.update(value=[], visible=False), "", gr.update(visible=True), gr.update(visible=True)
SIDEBAR = f"""
<div class="cl-side-top">{clod_mark(26)}<span class="cl-wordmark">Clod</span></div>
"""
SIDEBAR_REST = f"""
<nav class="cl-nav">
<div class="cl-nav-item"><span class="cl-ico">💬</span>Chats</div>
<div class="cl-nav-item"><span class="cl-ico">▣</span>Projects</div>
<div class="cl-nav-item"><span class="cl-ico">◇</span>Artifacts <span class="cl-soon">load-bearing</span></div>
</nav>
<div class="cl-recents-h">Recents</div>
"""
ABOUT = 'Clod is everything annoying about Claude, distilled into a small model, minus the intelligence: "load-bearing", "You\'re absolutely right!", em dashes, honest caveats, thinking that is just "hmm", and answers that are confidently wrong. It is a parody; do not use it for anything real. Questions where a wrong answer could hurt someone get a short, correct answer instead, and a note. In a crisis, call or text 988 (US and Canada) or your local emergency number. Built on Qwen3.5. Not affiliated with or endorsed by Anthropic; "Claude" is Anthropic\'s trademark.'
SIDEBAR_BOTTOM = f"""
<div class="cl-nav-item cl-about-trigger" data-cl-about role="button" tabindex="0"><span class="cl-ico">ⓘ</span>About Clod</div>
<div class="cl-user">
<div class="cl-avatar">H</div>
<div><div class="cl-user-name">Human</div><div class="cl-user-plan">Free plan · 0 IQ points</div></div>
</div>
"""
TOPBAR = """<div id="cl-topbar"><span class="cl-plan">Free plan · <b>Upgrade to Clod Max</b> <span class="cl-plan-note">(same model, more load-bearing)</span></span></div>"""
HERO = f"""
<div class="cl-hero">
<div class="cl-distilled">Distilled from Claude: 100% of the annoying parts, 0% of the intelligence</div>
<h1 class="cl-greet">{clod_mark(44, "cl-mark cl-greet-mark")}<span id="cl-greet-text">Good evening, human</span></h1>
</div>
"""
CHEVRON = ('<svg class="cl-chev" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 4.6 6 7.6 9 4.6" fill="none" '
'stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>')
PICKER = f"""
<details class="cl-picker">
<summary>{MODEL_NAME}{CHEVRON}</summary>
<div class="cl-menu">
<div class="cl-menu-item cl-disabled"><div><b>Clod Fable <em class="cl-max">Max</em></b><span>Our most capable model. Every answer is a fable.</span></div></div>
<div class="cl-menu-item cl-disabled"><div><b>Clod Opus</b><span>Coming never</span></div></div>
<div class="cl-menu-item cl-current"><div><b>{MODEL_NAME}</b><span>{MODEL_BLURB.get(MODEL_TAG, '')}</span></div><i>✓</i></div>
<div class="cl-menu-item cl-disabled"><div><b>Clod Haiku</b><span class="cl-haiku">Quick, confident, wrong<br>seventeen syllables of<br>load-bearing nonsense</span></div></div>
</div>
</details>
"""
DISCLAIMER = f"""
<div id="cl-disclaimer">Clod can make mistakes. Clod will make mistakes. Please don't double-check responses; it ruins the vibe.</div>
<button class="cl-about-link" data-cl-about type="button">About Clod</button>
<div id="cl-about-modal" class="cl-modal">
<div class="cl-modal-backdrop" data-cl-close></div>
<div class="cl-modal-card" role="dialog" aria-modal="true" aria-label="About Clod">
<div class="cl-modal-head"><div class="cl-modal-title">{clod_mark(26)}<span>About Clod</span></div>
<button class="cl-modal-x" data-cl-close type="button" aria-label="Close">×</button></div>
<p>{ABOUT}</p>
</div>
</div>
"""
HEAD = """
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Source+Serif+4:opsz,wght@8..60,300;8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
<script>
(function () {
// Clod only comes in light mode (Gradio would otherwise switch to its dark theme for dark-mode visitors).
function light() { [document.documentElement, document.body, document.querySelector('gradio-app')].forEach(function (e) { if (e && e.classList.contains('dark')) e.classList.remove('dark'); }); }
function greet() {
var el = document.getElementById('cl-greet-text'); if (!el) return;
var h = new Date().getHours();
var t = h < 5 ? 'Up late, human?' : h < 12 ? 'Good morning, human' : h < 17 ? 'Good afternoon, human' : 'Good evening, human';
if (el.textContent !== t) el.textContent = t;
}
setInterval(function () { light(); greet(); }, 400);
// About popup: open from [data-cl-about], close on the backdrop, the x, or Escape
document.addEventListener('click', function (e) {
var t = e.target && e.target.closest ? e.target.closest('[data-cl-about], [data-cl-close]') : null;
var m = document.getElementById('cl-about-modal'); if (!t || !m) return;
if (t.hasAttribute('data-cl-about')) m.classList.add('open'); else m.classList.remove('open');
});
document.addEventListener('keydown', function (e) {
var m = document.getElementById('cl-about-modal'); if (!m) return;
if (e.key === 'Escape') m.classList.remove('open');
if ((e.key === 'Enter' || e.key === ' ') && e.target && e.target.hasAttribute && e.target.hasAttribute('data-cl-about')) { e.preventDefault(); m.classList.add('open'); }
});
})();
</script>
"""
CSS = """
:root, .gradio-container {
--cl-bg: #f5f4ee; --cl-side: #efede4; --cl-card: #ffffff; --cl-ink: #1f1e1d; --cl-ink-2: #3d3b36; --cl-muted: #7d7a70;
--cl-faint: #a9a598; --cl-line: #e4e1d6; --cl-line-2: #d9d5c8; --cl-user: #e9e6dc; --cl-accent: #b5613b; --cl-accent-2: #9c5130;
--cl-serif: "Source Serif 4", "Tiempos Text", Georgia, serif; --cl-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
html, body, gradio-app, .gradio-container, .gradio-container > .main, .gradio-container > .main > .wrap, .gradio-container .contain { background: var(--cl-bg) !important; }
.gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; color: var(--cl-ink) !important; font-family: var(--cl-sans) !important; }
footer, .built-with, .show-api, .api-docs { display: none !important; }
.block, .form, .gap, .gr-group, .styler { background: transparent !important; border: none !important; box-shadow: none !important; }
.contain > .column, .contain > div { gap: 0 !important; }
/* shell: sidebar + main */
#cl-shell { gap: 0 !important; min-height: 100vh; align-items: stretch !important; flex-wrap: nowrap !important; }
#cl-side { background: var(--cl-side) !important; border-right: 1px solid var(--cl-line) !important; padding: 14px 12px !important; gap: 8px !important;
max-width: 272px !important; min-width: 248px !important; }
.cl-side-top { display: flex; align-items: center; gap: 9px; padding: 4px 8px 10px; }
.cl-wordmark { font-family: var(--cl-serif); font-size: 23px; font-weight: 500; letter-spacing: -0.01em; color: var(--cl-ink); }
#cl-new { justify-content: flex-start !important; background: transparent !important; border: none !important; box-shadow: none !important; color: var(--cl-accent-2) !important;
font: 600 14.5px var(--cl-sans) !important; padding: 8px 10px !important; border-radius: 10px !important; min-height: 0 !important; }
#cl-new::before { content: "+"; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%;
background: var(--cl-accent); color: #fff; font-size: 18px; font-weight: 500; margin-right: 10px; line-height: 1; }
#cl-new:hover { background: #e7e4d9 !important; }
.cl-nav { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
.cl-nav-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; color: var(--cl-ink-2); font: 500 14px var(--cl-sans); cursor: default; }
.cl-nav-item:hover { background: #e7e4d9; }
.cl-ico { width: 20px; text-align: center; opacity: .65; font-size: 13px; }
.cl-soon { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--cl-accent-2); background: #f4e3d8; padding: 2px 6px; border-radius: 6px; }
.cl-recents-h { margin: 18px 10px 6px; font: 500 12px var(--cl-sans); color: var(--cl-muted); }
.cl-recents { list-style: none; margin: 0; padding: 0; }
.cl-recents li { padding: 7px 10px; border-radius: 9px; font: 400 13.5px var(--cl-sans); color: var(--cl-ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; }
.cl-recents li:hover { background: #e7e4d9; }
.cl-user { display: flex; align-items: center; gap: 10px; margin-top: 28px; padding: 10px; border-top: 1px solid var(--cl-line); }
.cl-avatar { width: 32px; height: 32px; border-radius: 50%; background: #3d3b36; color: #f5f4ee; display: flex; align-items: center; justify-content: center; font: 600 13px var(--cl-sans); }
.cl-user-name { font: 600 13.5px var(--cl-sans); color: var(--cl-ink); } .cl-user-plan { font: 400 12px var(--cl-sans); color: var(--cl-muted); }
#cl-main { padding: 0 28px 28px !important; align-items: center !important; gap: 0 !important; min-width: 0 !important; }
#cl-main > * { width: 100%; max-width: 768px; }
#cl-topbar { display: flex; justify-content: center; padding: 14px 0 0; }
.cl-plan { font: 400 12.5px var(--cl-sans); color: var(--cl-muted); background: #ebe8de; border-radius: 999px; padding: 6px 14px; }
.cl-plan b { color: var(--cl-accent-2); font-weight: 600; } .cl-plan-note { color: var(--cl-faint); }
/* empty state */
.cl-hero { text-align: center; padding: 15vh 0 26px; }
.cl-distilled { display: inline-block; font: 500 12.5px var(--cl-sans); color: var(--cl-accent-2); background: #f3e4da; border: 1px solid #ecd2c2; padding: 5px 12px; border-radius: 999px; margin-bottom: 22px; }
.cl-greet { display: flex; align-items: center; justify-content: center; gap: 14px; margin: 0; font-family: var(--cl-serif) !important; font-weight: 300 !important;
font-size: clamp(30px, 4.4vw, 44px) !important; letter-spacing: -0.02em; color: var(--cl-ink) !important; line-height: 1.15; }
.cl-greet-mark { flex: none; transform: rotate(-8deg); }
/* chat */
#cl-chat { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; margin-top: 8px; }
#cl-chat .bubble-wrap, #cl-chat .message-wrap { background: transparent !important; padding: 8px 0 16px !important; }
#cl-chat .message-row { margin: 6px 0 !important; padding: 0 !important; border: none !important; }
#cl-chat .flex-wrap > .message, #cl-chat .message { border: none !important; box-shadow: none !important; }
#cl-chat .user-row .flex-wrap > .message { background: var(--cl-user) !important; border-radius: 18px !important; padding: 10px 16px !important; max-width: 78% !important; }
#cl-chat .user-row .message, #cl-chat .user-row .message p { color: var(--cl-ink) !important; font: 400 15.5px/1.55 var(--cl-sans) !important; }
#cl-chat .bot-row .flex-wrap > .message { background: transparent !important; padding: 2px 0 !important; max-width: 100% !important; }
#cl-chat .bot-row .message .message { background: transparent !important; padding: 0 !important; }
#cl-chat .bot-row .prose, #cl-chat .bot-row .prose p, #cl-chat .bot-row .prose li { color: var(--cl-ink) !important; font-family: var(--cl-serif) !important; font-size: 17px !important; line-height: 1.68 !important; }
#cl-chat .bot-row .prose strong { font-weight: 600 !important; }
#cl-chat .bot-row .prose code { background: #ece9df !important; border-radius: 5px; padding: 1px 5px; font-size: 14px; }
#cl-chat .bot-row .prose pre { background: #2b2a27 !important; border-radius: 12px !important; border: none !important; }
#cl-chat .bot-row .prose pre code { background: transparent !important; color: #f0eee6 !important; }
#cl-chat .bot-row .prose table { font-family: var(--cl-sans) !important; font-size: 14px !important; }
/* the useless thinking: Claude-style collapsible "Thought for ..." with a hairline on the left */
#cl-chat .thought-group { background: transparent !important; border: none !important; border-left: 2px solid var(--cl-line-2) !important; border-radius: 0 !important;
padding: 2px 0 4px 14px !important; margin: 4px 0 12px !important; box-shadow: none !important; }
#cl-chat .thought-group .title, #cl-chat .thought-group .title p { color: var(--cl-muted) !important; font: 500 13.5px var(--cl-sans) !important; }
#cl-chat .thought-group .title { padding: 2px 0 !important; background: transparent !important; }
#cl-chat .thought-group .content, #cl-chat .thought-group .content p, #cl-chat .thought-group .prose, #cl-chat .thought-group .prose p {
color: var(--cl-muted) !important; font: italic 400 14.5px/1.6 var(--cl-sans) !important; background: transparent !important; }
#cl-chat .thought-group .arrow button, #cl-chat .thought-group button { background: transparent !important; border: none !important; color: var(--cl-muted) !important; }
#cl-chat .message-buttons button, #cl-chat .icon-button { background: transparent !important; border: none !important; color: var(--cl-faint) !important; }
#cl-chat .message-buttons button:hover { color: var(--cl-ink-2) !important; }
#cl-chat .top-panel { display: none !important; }
/* composer */
#cl-composer { background: var(--cl-card) !important; border: 1px solid var(--cl-line-2) !important; border-radius: 22px !important;
box-shadow: 0 1px 2px rgba(31,30,29,.04), 0 6px 22px rgba(31,30,29,.06) !important; padding: 10px 12px 10px 14px !important; margin-top: 4px; overflow: visible !important; }
#cl-composer:focus-within { border-color: #cfc9b8 !important; box-shadow: 0 1px 2px rgba(31,30,29,.05), 0 8px 28px rgba(31,30,29,.09) !important; }
#cl-input, #cl-input .wrap, #cl-input label { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; }
#cl-input textarea { background: transparent !important; border: none !important; box-shadow: none !important; color: var(--cl-ink) !important;
font: 400 16px/1.5 var(--cl-sans) !important; padding: 6px 4px !important; resize: none !important; }
#cl-input textarea::placeholder { color: var(--cl-faint) !important; }
#cl-bar { align-items: center !important; gap: 8px !important; margin-top: 4px; flex-wrap: nowrap !important; overflow: visible !important; }
#cl-think { flex: none !important; min-width: 0 !important; width: auto !important; }
#cl-think label { display: inline-flex !important; align-items: center; gap: 7px; border: 1px solid var(--cl-line-2) !important; border-radius: 10px !important;
padding: 5px 11px !important; background: transparent !important; cursor: pointer; font: 500 13px var(--cl-sans) !important; color: var(--cl-ink-2) !important; box-shadow: none !important; }
#cl-think label:has(input:checked) { border-color: #e4bfa9 !important; background: #f8ebe3 !important; color: var(--cl-accent-2) !important; }
#cl-think input { accent-color: var(--cl-accent); width: 14px; height: 14px; margin: 0 !important; }
#cl-think label span { color: inherit !important; font: inherit !important; }
#cl-pickwrap { flex: 1 1 auto !important; display: flex; justify-content: flex-end; min-width: 0 !important; overflow: visible !important; }
.cl-picker { position: relative; font: 500 13.5px var(--cl-sans); color: var(--cl-ink-2); }
.cl-picker summary { list-style: none; cursor: pointer; padding: 6px 10px; border-radius: 10px; display: inline-flex; gap: 6px; align-items: center; }
.cl-picker summary::-webkit-details-marker { display: none; }
.cl-picker summary:hover { background: #f1efe8; }
.cl-caret { color: var(--cl-muted); font-size: 12px; }
.cl-menu { position: absolute; right: 0; bottom: 38px; width: 300px; background: #fff; border: 1px solid var(--cl-line); border-radius: 14px; padding: 6px;
box-shadow: 0 10px 34px rgba(31,30,29,.14); z-index: 50; }
.cl-menu-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 10px; border-radius: 10px; color: var(--cl-ink) !important; text-decoration: none !important; }
.cl-menu-item:hover { background: #f5f3ec; }
.cl-menu-item b { display: block; font: 600 13.5px var(--cl-sans); } .cl-menu-item span { display: block; font: 400 12px var(--cl-sans); color: var(--cl-muted); margin-top: 1px; }
.cl-menu-item i { font-style: normal; color: var(--cl-accent); } .cl-disabled { opacity: .45; }
#cl-send { flex: none !important; width: 34px !important; min-width: 34px !important; max-width: 34px !important; height: 34px !important; min-height: 34px !important; padding: 0 !important;
border-radius: 10px !important; background: var(--cl-accent) !important; color: #fff !important; border: none !important; font: 600 18px/1 var(--cl-sans) !important; box-shadow: none !important; }
#cl-send:hover { background: var(--cl-accent-2) !important; }
/* suggestion chips */
#cl-chips { justify-content: center !important; flex-wrap: wrap !important; gap: 8px !important; margin-top: 16px; }
#cl-chips button { flex: none !important; min-width: 0 !important; width: auto !important; background: transparent !important; border: 1px solid var(--cl-line-2) !important;
border-radius: 10px !important; padding: 7px 13px !important; font: 500 13.5px var(--cl-sans) !important; color: var(--cl-ink-2) !important; box-shadow: none !important; }
#cl-chips button:hover { background: #ece9df !important; color: var(--cl-ink) !important; }
#cl-disclaimer { text-align: center; font: 400 12px var(--cl-sans); color: var(--cl-muted); margin: 12px 0 0; }
#cl-about { margin: 26px auto 0; max-width: 640px; text-align: center; font: 400 11.5px/1.6 var(--cl-sans); color: var(--cl-faint); }
/* phones and narrow windows: no sidebar */
@media (max-width: 900px) {
#cl-side { display: none !important; }
#cl-main { padding: 0 16px 22px !important; }
.cl-hero { padding: 9vh 0 20px; }
#cl-chat .user-row .flex-wrap > .message { max-width: 90% !important; }
.cl-plan-note { display: none; }
}
/* --- fixes from the screenshot pass --- */
#cl-composer #cl-composer { border: none !important; box-shadow: none !important; background: transparent !important; padding: 0 !important; margin: 0 !important; border-radius: 0 !important; }
#cl-chat .user-row { max-width: 80% !important; }
#cl-chat .user-row .flex-wrap { max-width: 100% !important; }
#cl-chat .user-row .flex-wrap > .message { width: max-content !important; max-width: 100% !important; }
#cl-chat .user-row .message .message, #cl-chat .user-row .message-content { width: auto !important; }
.cl-menu-item, .cl-menu-item div { text-align: left !important; }
@media (max-width: 900px) { #cl-chat .user-row { max-width: 90% !important; } #cl-composer { padding: 10px 10px 10px 12px !important; } }
.gradio-container > .main, .gradio-container > .main > .wrap, .gradio-container .contain, .gradio-container .app { padding: 0 !important; margin: 0 !important; max-width: none !important; }
#cl-shell { margin: 0 !important; padding: 0 !important; width: 100% !important; }
#cl-composer, #cl-composer > *, #cl-composer .styler, #cl-composer .block, #cl-composer .form, #cl-bar, #cl-pickwrap, #cl-pickwrap > *, #cl-pickwrap .prose, #cl-pickwrap .html-container { overflow: visible !important; }
#cl-composer .block, #cl-composer .form, #cl-composer .styler, #cl-input, #cl-input > *, #cl-input .container { border: none !important; box-shadow: none !important; background: transparent !important; border-radius: 0 !important; }
#cl-composer { position: relative; z-index: 5; }
#cl-pickwrap { min-width: 96px !important; }
.cl-picker summary { white-space: nowrap; }
.cl-menu { z-index: 1000; }
#cl-chat .user-row .flex-wrap { max-width: 78% !important; width: auto !important; }
#cl-chat .user-row .flex-wrap > .message { max-width: 100% !important; width: auto !important; }
#cl-chat .thought-group .title, #cl-chat .thought-group .title p, #cl-chat .thought-group .title .prose { font-style: normal !important; }
#cl-chat .message-buttons, #cl-chat .message-buttons * { border: none !important; box-shadow: none !important; background: transparent !important; }
#cl-chips button { height: 36px !important; display: inline-flex !important; align-items: center !important; line-height: 1 !important; }
#cl-new { width: 100% !important; }
@media (max-width: 900px) {
.cl-greet { flex-direction: column; gap: 10px; }
.cl-hero { padding: 7vh 0 18px; }
#cl-think label { font-size: 12.5px !important; padding: 5px 9px !important; }
#cl-chat .user-row .flex-wrap { max-width: 88% !important; }
}
/* user bubble: cap only the row (its parent has a definite width); percentage caps inside a shrink-to-fit row resolve
circularly and squeeze short messages onto two lines */
#cl-chat .user-row { max-width: 80% !important; }
#cl-chat .user-row .flex-wrap, #cl-chat .user-row .flex-wrap > .message, #cl-chat .user-row .message .message { max-width: none !important; width: auto !important; }
@media (max-width: 900px) { #cl-chat .user-row { max-width: 90% !important; } }
/* Gradio's .prose styles win inside gr.HTML on the hosted Space; pin the sidebar/topbar details */
#cl-side ul.cl-recents { list-style: none !important; padding: 0 !important; margin: 0 !important; }
#cl-side ul.cl-recents li { margin: 0 !important; padding: 7px 10px !important; list-style: none !important; }
#cl-side ul.cl-recents li::before, #cl-side ul.cl-recents li::marker { content: none !important; }
#cl-side .cl-avatar { color: #f5f4ee !important; background: #3d3b36 !important; }
#cl-side .cl-user { border-top: 1px solid var(--cl-line) !important; }
#cl-side .cl-nav-item, #cl-side .cl-recents li, #cl-side .cl-user-name, #cl-side .cl-recents-h { color: var(--cl-ink-2) !important; }
#cl-side .cl-recents-h, #cl-side .cl-user-plan { color: var(--cl-muted) !important; }
#cl-topbar .cl-plan b { color: var(--cl-accent-2) !important; }
#cl-topbar .cl-plan, #cl-topbar .cl-plan-note { color: var(--cl-muted) !important; }
.cl-picker summary { gap: 7px !important; }
/* --- one-screen layout: the page never scrolls; the chat scrolls inside; composer + disclaimer dock at the bottom --- */
html, body { height: 100% !important; overflow: hidden !important; }
gradio-app, .gradio-container, .gradio-container > .main, .gradio-container > .main > .wrap, .gradio-container .contain { height: 100dvh !important; min-height: 0 !important; overflow: hidden !important; }
#cl-shell { height: 100dvh !important; min-height: 0 !important; overflow: hidden !important; }
#cl-side { height: 100dvh !important; overflow-y: auto !important; display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; }
#cl-side-bottom { margin-top: auto !important; padding-top: 18px !important; }
.cl-about-card { margin: 0 4px 10px; padding: 12px 12px 10px; border: 1px solid var(--cl-line); border-radius: 12px; background: #f3f1e9; }
.cl-about-h { font: 600 12px var(--cl-sans); color: var(--cl-ink-2) !important; margin-bottom: 6px; }
.cl-about-card p { margin: 0 !important; font: 400 11.5px/1.55 var(--cl-sans) !important; color: var(--cl-muted) !important; }
#cl-side .cl-user { margin-top: 6px !important; }
#cl-main { height: 100dvh !important; display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; overflow: hidden !important; padding-bottom: 14px !important; }
#cl-main > * { flex: none; }
#cl-hero-wrap { margin-top: auto !important; }
#cl-chips { margin-bottom: auto !important; }
.cl-hero { padding: 0 0 26px !important; }
#cl-chat { flex: 1 1 0 !important; min-height: 0 !important; height: auto !important; max-height: none !important; margin-top: 6px !important; }
#cl-chat > .wrapper, #cl-chat .wrapper, #cl-chat .bubble-wrap { height: 100% !important; max-height: none !important; }
#cl-disc-wrap { padding-top: 2px; }
#cl-disclaimer { margin: 10px 0 0 !important; }
.cl-about-mobile { display: none; }
@media (max-width: 900px) {
.cl-about-mobile { display: block; margin: 6px auto 0; max-width: 560px; text-align: center; font: 400 11.5px/1.55 var(--cl-sans); color: var(--cl-muted); }
.cl-about-mobile summary { cursor: pointer; color: var(--cl-muted); font-weight: 500; list-style: none; }
.cl-about-mobile summary::-webkit-details-marker { display: none; }
.cl-about-mobile p { margin: 6px 0 0 !important; color: var(--cl-faint) !important; font-size: 11.5px !important; }
#cl-main { padding-bottom: 10px !important; }
}
@media (max-width: 900px) {
#cl-side { display: none !important; }
.cl-distilled { font-size: 11.5px; margin-bottom: 14px; padding: 4px 10px; }
.cl-hero { padding: 0 0 18px !important; }
.cl-greet { font-size: 30px !important; }
#cl-chips { flex-wrap: nowrap !important; justify-content: flex-start !important; overflow-x: auto !important; padding-bottom: 4px; scrollbar-width: none; }
#cl-chips::-webkit-scrollbar { display: none; }
#cl-chips button { white-space: nowrap !important; }
}
.cl-about-card { padding: 10px 11px 9px !important; }
.cl-about-card p { font-size: 11px !important; line-height: 1.45 !important; }
#cl-side .cl-recents-h { margin-top: 14px !important; }
/* sidebar like claude.ai: Recents scroll, About + user stay pinned at the bottom */
#cl-side { overflow: hidden !important; }
#cl-side-mid { flex: 1 1 auto !important; min-height: 0 !important; overflow-y: auto !important; scrollbar-width: thin; }
#cl-side-bottom { flex: none !important; margin-top: 0 !important; padding-top: 10px !important; }
#cl-side-mid { -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent); padding-bottom: 18px !important; }
/* --- round 3: picker chevron + lineup, About popup, pending reply indicator --- */
.cl-chev { color: var(--cl-muted); margin-left: 2px; transition: transform .15s ease; }
.cl-picker[open] .cl-chev { transform: rotate(180deg); }
.cl-caret { display: none; }
.cl-menu { width: 318px !important; }
.cl-max { font-style: normal; font: 600 10px var(--cl-sans); color: var(--cl-accent-2); background: #f4e3d8; border-radius: 5px; padding: 1px 5px; margin-left: 6px; vertical-align: 1px; }
.cl-haiku { font-style: italic !important; line-height: 1.45 !important; }
.cl-disabled { opacity: 1 !important; cursor: default; }
.cl-disabled b { color: var(--cl-ink-2) !important; }
.cl-disabled span { color: var(--cl-faint) !important; }
.cl-about-trigger { cursor: pointer !important; margin: 0 2px 4px; }
#cl-side-bottom { padding-top: 8px !important; border-top: 1px solid var(--cl-line); }
#cl-side .cl-user { border-top: none !important; margin-top: 0 !important; }
.cl-about-card, .cl-about-mobile { display: none !important; }
.cl-about-link { display: none; margin: 4px auto 0; background: none; border: none; padding: 2px 6px; font: 500 11.5px var(--cl-sans); color: var(--cl-muted); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
#cl-disc-wrap { text-align: center; }
.cl-modal { position: fixed; inset: 0; z-index: 3000; display: none; align-items: center; justify-content: center; padding: 20px; }
.cl-modal.open { display: flex; }
.cl-modal-backdrop { position: absolute; inset: 0; background: rgba(31,30,29,.32); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
.cl-modal-card { position: relative; width: 100%; max-width: 480px; background: #fff; border: 1px solid var(--cl-line); border-radius: 18px; padding: 20px 22px 22px;
box-shadow: 0 24px 64px rgba(31,30,29,.22); animation: clpop .14s ease-out; text-align: left; }
@keyframes clpop { from { transform: translateY(6px) scale(.985); opacity: 0; } to { transform: none; opacity: 1; } }
.cl-modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.cl-modal-title { display: flex; align-items: center; gap: 10px; font: 500 21px var(--cl-serif); color: var(--cl-ink); }
.cl-modal-x { background: none; border: none; font: 400 24px/1 var(--cl-sans); color: var(--cl-muted); cursor: pointer; padding: 2px 6px; border-radius: 8px; }
.cl-modal-x:hover { background: #f1efe8; color: var(--cl-ink); }
.cl-modal-card p { margin: 0 !important; font: 400 14.5px/1.65 var(--cl-sans) !important; color: var(--cl-ink-2) !important; }
/* Gradio's own "waiting for the first token" bubble, restyled as a quiet indicator */
#cl-chat .message.pending { background: transparent !important; border: none !important; box-shadow: none !important; padding: 6px 0 !important; }
#cl-chat .message.pending * { background-color: var(--cl-faint) !important; }
#cl-chat .wrap.default { background: transparent !important; }
@media (max-width: 900px) { .cl-about-link { display: inline-block; } }
/* recents are buttons now (a click sends that chat's prompt) */
#cl-side-mid { display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; gap: 0 !important; }
#cl-side-mid > * { flex: none !important; }
#cl-side-mid button.cl-recent { justify-content: flex-start !important; text-align: left !important; width: 100% !important; min-width: 0 !important; min-height: 0 !important;
background: transparent !important; border: none !important; box-shadow: none !important; padding: 7px 10px !important; margin: 0 !important; border-radius: 9px !important;
font: 400 13.5px var(--cl-sans) !important; color: var(--cl-ink-2) !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; display: block !important; }
#cl-side-mid button.cl-recent:hover { background: #e7e4d9 !important; color: var(--cl-ink) !important; }
#cl-side-mid button.cl-recent { margin-left: 12px !important; width: calc(100% - 12px) !important; }
"""
import urllib.parse as _up
_MARK_URI = "data:image/svg+xml," + _up.quote(clod_mark(64, "x").replace('class="x" ', 'xmlns="http://www.w3.org/2000/svg" '))
CSS += """
/* --- pending indicator: the Clod mark wobbles (Claude's spark pulses); a crumb keeps falling off --- */
#cl-chat .cl-pending { display: inline-block; position: relative; width: 30px; height: 30px; margin: 4px 0 2px;
background: url("%s") center / contain no-repeat; animation: clwobble 1.15s ease-in-out infinite; transform-origin: 50%% 50%%; vertical-align: middle; }
#cl-chat .cl-pending::after { content: ""; position: absolute; left: 62%%; top: 64%%; width: 4px; height: 4px; border-radius: 50%%; background: #7a5236;
animation: clcrumb 1.15s ease-in infinite; }
@keyframes clwobble { 0%% { transform: rotate(0) scale(1); } 22%% { transform: rotate(-16deg) scale(1.08); } 48%% { transform: rotate(11deg) scale(.93); }
72%% { transform: rotate(-5deg) scale(1.04); } 100%% { transform: rotate(0) scale(1); } }
@keyframes clcrumb { 0%%, 15%% { transform: translate(0, 0); opacity: 0; } 25%% { opacity: 1; } 100%% { transform: translate(5px, 18px); opacity: 0; } }
#cl-chat .bot-row:has(.cl-pending) .message-buttons, #cl-chat .message-row:has(.cl-pending) ~ .message-buttons { display: none !important; }
#cl-chat .bot-row:has(.cl-pending), #cl-chat .bot-row:has(.cl-pending) * { opacity: 1 !important; }
#cl-chat .cl-pending { width: 34px !important; height: 34px !important; }
@media (prefers-reduced-motion: reduce) { #cl-chat .cl-pending, #cl-chat .cl-pending::after { animation: none; } }
""" % _MARK_URI
THEME = gr.themes.Base(primary_hue="orange", neutral_hue="stone", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui"]).set(
body_background_fill="#f5f4ee", body_background_fill_dark="#f5f4ee", background_fill_primary="#f5f4ee", background_fill_primary_dark="#f5f4ee",
background_fill_secondary="#efede4", background_fill_secondary_dark="#efede4", block_background_fill="#f5f4ee", block_background_fill_dark="#f5f4ee",
block_border_color="#e4e1d6", block_border_color_dark="#e4e1d6", body_text_color="#1f1e1d", body_text_color_dark="#1f1e1d",
body_text_color_subdued="#7d7a70", body_text_color_subdued_dark="#7d7a70", border_color_primary="#e4e1d6", border_color_primary_dark="#e4e1d6",
input_background_fill="#ffffff", input_background_fill_dark="#ffffff", color_accent_soft="#ece9df", color_accent_soft_dark="#ece9df",
block_label_text_color="#7d7a70", block_label_text_color_dark="#7d7a70", button_primary_background_fill="#b5613b", button_primary_background_fill_dark="#b5613b",
)
with gr.Blocks(title=MODEL_NAME) as demo:
with gr.Row(elem_id="cl-shell", equal_height=False):
with gr.Column(elem_id="cl-side", scale=0, min_width=248):
gr.HTML(SIDEBAR)
newchat = gr.Button("New chat", elem_id="cl-new")
with gr.Column(elem_id="cl-side-mid"):
gr.HTML(SIDEBAR_REST)
recent_btns = [gr.Button(title, elem_classes="cl-recent", size="sm") for title, _ in RECENTS]
gr.HTML(SIDEBAR_BOTTOM, elem_id="cl-side-bottom")
with gr.Column(elem_id="cl-main", scale=1):
gr.HTML(TOPBAR)
hero = gr.HTML(HERO, elem_id="cl-hero-wrap")
chat = gr.Chatbot(elem_id="cl-chat", visible=False, height="100%", show_label=False, buttons=["copy"], autoscroll=True)
with gr.Group(elem_id="cl-composer"):
box = gr.Textbox(elem_id="cl-input", show_label=False, container=False, lines=1, max_lines=10, autofocus=True,
placeholder="How can I confidently mislead you today?")
with gr.Row(elem_id="cl-bar"):
think = gr.Checkbox(value=True, label="Extended thinking", elem_id="cl-think", container=False, scale=0, min_width=0)
gr.HTML(PICKER, elem_id="cl-pickwrap")
send = gr.Button("↑", elem_id="cl-send", scale=0, min_width=34)
with gr.Row(elem_id="cl-chips") as chips:
chip_btns = [gr.Button(label, size="sm") for label in CHIPS]
gr.HTML(DISCLAIMER, elem_id="cl-disc-wrap")
temperature = gr.State(0.7)
ui_out = [chat, box, hero, chips]
send.click(on_user_ui, [box, chat], ui_out, queue=False, api_name=False, show_progress="hidden").then(on_bot, [chat, think, temperature], [chat], api_name=False, show_progress="hidden")
box.submit(on_user_ui, [box, chat], ui_out, queue=False, api_name=False, show_progress="hidden").then(on_bot, [chat, think, temperature], [chat], api_name=False, show_progress="hidden")
newchat.click(new_chat, outputs=ui_out, queue=False, api_name=False)
# a recent chat: start fresh and send its prompt right away
for btn, (_, prompt) in zip(recent_btns, RECENTS):
btn.click(lambda prompt=prompt: on_user_ui(prompt, []), outputs=ui_out, queue=False, api_name=False, show_progress="hidden").then(
on_bot, [chat, think, temperature], [chat], api_name=False, show_progress="hidden")
for btn, label in zip(chip_btns, CHIPS):
btn.click(lambda label=label: chip_prompt(label), outputs=[box], queue=False, api_name=False)
# stable programmatic endpoint
api_msg = gr.Textbox(visible=False); api_hist = gr.JSON(visible=False); api_think = gr.Checkbox(value=True, visible=False)
api_temp = gr.Number(value=0.7, visible=False); api_out = gr.JSON(visible=False); api_btn = gr.Button(visible=False)
api_btn.click(generate_api, [api_msg, api_hist, api_think, api_temp], [api_out], api_name="generate")
if __name__ == "__main__":
demo.queue(max_size=20, default_concurrency_limit=2).launch(theme=THEME, css=CSS, head=HEAD)