Download app.py from devon7y/clod: direct link, hf CLI and curl.
- Browser
- Download file 52 kB
-
https://huggingface.co/spaces/devon7y/clod/resolve/main/app.py
- Command line
-
hf download hf://spaces/devon7y/clod/app.py
-
curl -L -o app.py https://huggingface.co/spaces/devon7y/clod/resolve/main/app.py
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] | |
| 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) | |