"""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 = '' # 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(?")} 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\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 '\\n\\n\\n' (the template already opened ).""" raw = raw.replace("", "") if not think: return None, raw.replace("", "").lstrip() if "" in raw: a, b = raw.split("", 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'') else: rays.append(f'') a = 2 * math.pi * 4 / 11 crumb = f'' specks = "".join(f'' for t, d in ((0.4, 18), (2.2, 27), (3.9, 22), (5.3, 31))) return (f'') 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"""
{clod_mark(26)}Clod
""" SIDEBAR_REST = f"""
Recents
""" 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"""
ⓘAbout Clod
H
Human
Free plan · 0 IQ points
""" TOPBAR = """
Free plan · Upgrade to Clod Max (same model, more load-bearing)
""" HERO = f"""
Distilled from Claude: 100% of the annoying parts, 0% of the intelligence

{clod_mark(44, "cl-mark cl-greet-mark")}Good evening, human

""" CHEVRON = ('') PICKER = f"""
{MODEL_NAME}{CHEVRON}
Clod Fable MaxOur most capable model. Every answer is a fable.
Clod OpusComing never
{MODEL_NAME}{MODEL_BLURB.get(MODEL_TAG, '')}
✓
Clod HaikuQuick, confident, wrong
seventeen syllables of
load-bearing nonsense
""" DISCLAIMER = f"""
Clod can make mistakes. Clod will make mistakes. Please don't double-check responses; it ruins the vibe.
""" HEAD = """ """ 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)