Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import io | |
| import json | |
| import base64 | |
| import time | |
| import uuid | |
| import datetime | |
| import contextlib | |
| import urllib.request | |
| import html as _html | |
| from pathlib import Path | |
| # --- Preload CUDA runtime libs before importing llama_cpp --- | |
| # The cu124 llama-cpp-python wheel's libllama.so needs libcudart.so.12 / | |
| # libcublas at import time. On ZeroGPU those aren't on the default loader | |
| # path, so we dlopen the pip-provided nvidia libs (cudart first) globally. | |
| import ctypes | |
| import glob | |
| import site | |
| def _preload_cuda(): | |
| bases = set(site.getsitepackages()) | |
| try: | |
| bases.add(site.getusersitepackages()) | |
| except Exception: | |
| pass | |
| libs = [] | |
| for base in bases: | |
| libs += glob.glob(os.path.join(base, "nvidia", "*", "lib", "*.so*")) | |
| priority = {"cuda_runtime": 0, "cublas": 1} | |
| def _key(p): | |
| for name, rank in priority.items(): | |
| if name in p: | |
| return rank | |
| return 2 | |
| for so in sorted(set(libs), key=_key): | |
| try: | |
| ctypes.CDLL(so, mode=ctypes.RTLD_GLOBAL) | |
| except OSError: | |
| pass | |
| _preload_cuda() | |
| import gradio as gr | |
| import spaces | |
| # Gradio 5+ sanitizes gr.HTML by default, stripping <iframe> elements. | |
| # Detect and pass sanitize_html=False so the srcdoc preview renders. | |
| _GR_MAJOR = int(gr.__version__.split(".")[0]) | |
| _HTML_RAW = {"sanitize_html": False} if _GR_MAJOR >= 5 else {} | |
| from huggingface_hub import hf_hub_download | |
| from llama_cpp import Llama | |
| # ---- model (GGUF pulled from the Hub at startup, runs on ZeroGPU) ---- | |
| GGUF_REPO = os.environ.get("GGUF_REPO", "AlexWortega/SIQ-1-35B") | |
| GGUF_FILE = os.environ.get("GGUF_FILE", "gguf/SIQ-1-35B.Q4_K_M.gguf") | |
| N_CTX = int(os.environ.get("N_CTX", "16384")) # keep cold-start init light | |
| print("Downloading GGUF from the Hub ...", flush=True) | |
| MODEL_PATH = hf_hub_download(GGUF_REPO, GGUF_FILE) | |
| print("GGUF ready at", MODEL_PATH, flush=True) | |
| _LLM = None | |
| # q8_0 KV cache (GGML type 8) + flash attention: ~half the KV memory and | |
| # faster decode. Quantized KV requires flash_attn=True in llama.cpp. | |
| try: | |
| import llama_cpp as _lcpp | |
| _Q8 = int(getattr(_lcpp, "GGML_TYPE_Q8_0", 8)) | |
| except Exception: | |
| _Q8 = 8 | |
| KV_Q8 = os.environ.get("KV_Q8", "0") != "0" # off by default: keep cold start fast/simple | |
| def _get_llm(): | |
| global _LLM | |
| if _LLM is None: | |
| kw = dict( | |
| model_path=MODEL_PATH, | |
| n_gpu_layers=-1, | |
| n_ctx=N_CTX, | |
| verbose=False, | |
| ) | |
| if KV_Q8: # optional: q8 KV + flash attn (lighter KV, but heavier cold init) | |
| kw["flash_attn"] = True | |
| kw["type_k"] = _Q8 | |
| kw["type_v"] = _Q8 | |
| try: | |
| _LLM = Llama(**kw) | |
| except Exception as e: | |
| print(f"LLM init failed ({e}); retrying plain", flush=True) | |
| _LLM = Llama(model_path=MODEL_PATH, n_gpu_layers=-1, n_ctx=N_CTX, verbose=False) | |
| return _LLM | |
| _THINK = re.compile(r"<think>(.*?)</think>", re.DOTALL) | |
| _CODE_BLOCK = re.compile(r"```([\w+-]*)\s*\n(.*?)```", re.DOTALL) | |
| _TOOL_BLOCK = re.compile(r"```tool\s*\n(.*?)```", re.DOTALL | re.IGNORECASE) | |
| _PY_BLOCK = re.compile(r"```(?:python|py)\s*\n(.*?)```", re.DOTALL | re.IGNORECASE) | |
| # ```file:path/to/x.html\n...content...``` — explicit multi-file deliverable. | |
| _FILE_BLOCK = re.compile(r"```file:([^\n`]+)\n(.*?)```", re.DOTALL) | |
| # An *artifact* block is anything the model emits as a deliverable file: an | |
| # explicit `file:` block, or its natural html/css/js/svg code block. These are | |
| # auto-captured into the virtual filesystem (a write_file tool call) instead of | |
| # being pasted in the chat. | |
| _ARTIFACT_BLOCK = re.compile( | |
| r"```(?:file:[^\n`]+|html|htm|css|js|javascript|xml|svg)[^\n]*\n.*?```", | |
| re.DOTALL | re.IGNORECASE, | |
| ) | |
| _ARTIFACT_OPEN = re.compile( | |
| r"```(?:file:[^\n`]+|html|htm|css|js|javascript|xml|svg)\b", re.IGNORECASE | |
| ) | |
| # The FIRST sign that deliverable code/markup has begun — a fence opener OR a | |
| # raw HTML tag. Everything from here on is the artifact (possibly messy due to | |
| # token-cap continuation), so we cut it from the chat entirely. | |
| _ARTIFACT_START = re.compile( | |
| r"```file:[^\n`]+" | |
| r"|```(?:html|htm|css|js|javascript|xml|svg)\b" | |
| r"|<!doctype\s+html" | |
| r"|<html[\s>]" | |
| r"|<script[\s>]" | |
| r"|<style[\s>]", | |
| re.IGNORECASE, | |
| ) | |
| # Extract a whole HTML document by TAGS (not fences): first opener to the LAST | |
| # </html> (greedy), so a continuation-split artifact is still captured whole. | |
| _HTML_DOC = re.compile(r"(?:<!doctype\s+html|<html[\s>]).*</html\s*>", re.IGNORECASE | re.DOTALL) | |
| _HTML_OPEN = re.compile(r"<!doctype\s+html|<html[\s>]", re.IGNORECASE) | |
| # Native Hermes / Qwen tool-call format the model emits on its own. | |
| _HERMES_TOOL = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL) | |
| def _artifact_name(head): | |
| """Filename for an artifact fence opener line like ```file:foo.css or ```html.""" | |
| m = re.match(r"```file:([^\n`]+)", head) | |
| if m: | |
| return m.group(1).strip() | |
| lang = re.match(r"```([a-z]+)", head.lower()) | |
| return {"css": "styles.css", "js": "script.js", "javascript": "script.js"}.get( | |
| lang.group(1) if lang else "", "index.html" | |
| ) | |
| def _split(text): | |
| """Return (clean_answer, thinking). Robust to the common Qwen-thinking case | |
| where the chat template injects the opening <think> so the model only emits | |
| the CLOSING </think> (no opening tag in the stream).""" | |
| text = text or "" | |
| if "</think>" in text: | |
| # everything up to the first </think> is reasoning, even with no <think> | |
| i = text.index("</think>") | |
| thinking = text[:i].replace("<think>", "").strip() | |
| answer = text[i + len("</think>"):] | |
| elif "<think>" in text: | |
| # opening tag present but not yet closed -> still thinking, no answer | |
| i = text.index("<think>") | |
| answer = text[:i] | |
| thinking = text[i + len("<think>"):].strip() | |
| else: | |
| answer, thinking = text, "" | |
| answer = _TOOL_BLOCK.sub("", answer) # hide tool-call JSON | |
| answer = answer.replace("<think>", "").replace("</think>", "") # stray tags | |
| return answer.strip(), thinking | |
| def _extract_doc(answer): | |
| """Assemble a single self-contained HTML document from the answer's | |
| HTML/CSS/JS code blocks, to render in the preview iframe.""" | |
| htmls, csss, jss = [], [], [] | |
| for lang, body in _CODE_BLOCK.findall(answer): | |
| l = (lang or "").lower().strip() | |
| b = body.strip() | |
| if not b or l == "tool": | |
| continue | |
| low = b.lower() | |
| if l in ("html", "htm") or "<!doctype" in low or "<html" in low or "<body" in low: | |
| htmls.append(b) | |
| elif l == "css": | |
| csss.append(b) | |
| elif l in ("js", "javascript"): | |
| jss.append(b) | |
| elif l == "" and "<" in b and ">" in b: | |
| htmls.append(b) | |
| doc = htmls[0] if htmls else "" | |
| if not doc and (csss or jss): | |
| doc = "<!DOCTYPE html><html><head><meta charset='utf-8'></head><body></body></html>" | |
| if not doc: | |
| return "" | |
| if "<html" not in doc.lower() and "<!doctype" not in doc.lower(): | |
| doc = ( | |
| "<!DOCTYPE html><html><head><meta charset='utf-8'></head><body>\n" | |
| + doc | |
| + "\n</body></html>" | |
| ) | |
| if csss and "<style" not in doc.lower(): | |
| style = "<style>\n" + "\n".join(csss) + "\n</style>" | |
| doc = doc.replace("</head>", style + "</head>", 1) if "</head>" in doc else style + doc | |
| if jss and "<script" not in doc.lower(): | |
| script = "<script>\n" + "\n".join(jss) + "\n</script>" | |
| doc = doc.replace("</body>", script + "</body>", 1) if "</body>" in doc else doc + script | |
| return doc | |
| def _extract_html_doc(text): | |
| """Pull a whole HTML document out of raw text by TAG boundaries, ignoring | |
| fences/prose. Handles a continuation-split or unterminated document.""" | |
| m = _HTML_DOC.search(text or "") | |
| if m: | |
| return m.group(0).strip() | |
| o = _HTML_OPEN.search(text or "") # unterminated (still being written) | |
| if o: | |
| doc = text[o.start():].strip() | |
| if "</html" not in doc.lower(): | |
| doc += "\n</html>" | |
| return doc | |
| return "" | |
| def _best_doc(text): | |
| """The most complete renderable doc: prefer whichever of the fenced-block | |
| assembly or the tag-based extraction is longer (more complete).""" | |
| a = _extract_doc(text or "") | |
| b = _extract_html_doc(text or "") | |
| return b if len(b) > len(a) else a | |
| def _strip_artifacts(text): | |
| """Keep the chat clean and LEAK-PROOF: as soon as deliverable code/markup | |
| begins, cut everything from there and leave a pointer. Whatever messy mix of | |
| fences, raw tags, or continuation noise follows can never reach the chat.""" | |
| if not text: | |
| return text | |
| m = _ARTIFACT_START.search(text) | |
| if not m: | |
| return text | |
| head = text[m.start(): m.start() + 80] | |
| fm = re.match(r"```file:([^\n`]+)", head) | |
| name = fm.group(1).strip() if fm else "index.html" | |
| pointer = f"📄 `{name}` → see **Preview** / **Code**" | |
| return (text[: m.start()].rstrip() + "\n\n" + pointer).strip() | |
| def _capture_artifacts(text, files): | |
| """Route the model's deliverable into the write_file tool's virtual FS. | |
| Returns [(path, bytes), ...] for the tools trace.""" | |
| captured = [] | |
| for path, content in _FILE_BLOCK.findall(text or ""): | |
| path, content = path.strip(), content.strip("\n") | |
| if path: | |
| files[path] = content | |
| captured.append((path, len(content))) | |
| if not any(p.lower().endswith((".html", ".htm")) for p, _ in captured): | |
| doc = _best_doc(text or "") | |
| if doc: | |
| files["index.html"] = doc | |
| captured.append(("index.html", len(doc))) | |
| return captured | |
| def _doc_from_files(files): | |
| """Newest .html file the model wrote this turn (the renderable artifact).""" | |
| html = [p for p in files if p.lower().endswith((".html", ".htm"))] | |
| if not html: | |
| return "", "" | |
| p = html[-1] | |
| return files[p], p | |
| def _current_doc(files, answer): | |
| """Prefer a file the model WROTE; fall back to scraping the raw text.""" | |
| doc, name = _doc_from_files(files) | |
| if doc: | |
| return doc, name | |
| return _best_doc(answer), "index.html" | |
| # --------------------------------------------------------------------------- | |
| # Tools — real, in-process. The whole ReAct loop runs inside ONE @spaces.GPU | |
| # window (below), so tool calls execute on CPU while the GPU stays attached; | |
| # multi-step turns add no extra cold starts. | |
| # --------------------------------------------------------------------------- | |
| def _t_web_search(query="", **_): | |
| try: | |
| from ddgs import DDGS | |
| except Exception: | |
| from duckduckgo_search import DDGS # older name | |
| rows = list(DDGS().text(str(query), max_results=5)) | |
| if not rows: | |
| return "(no results)" | |
| return "\n".join( | |
| f"- {r.get('title','')}: {(r.get('body') or '')[:200]} ({r.get('href','')})" | |
| for r in rows | |
| ) | |
| def _t_open_url(url="", **_): | |
| req = urllib.request.Request(str(url), headers={"User-Agent": "Mozilla/5.0"}) | |
| with urllib.request.urlopen(req, timeout=15) as r: | |
| raw = r.read(300000).decode("utf-8", "ignore") | |
| text = re.sub(r"<script.*?</script>|<style.*?</style>", " ", raw, flags=re.S | re.I) | |
| text = re.sub(r"<[^>]+>", " ", text) | |
| text = re.sub(r"\s+", " ", text).strip() | |
| return text[:4000] or "(empty page)" | |
| def _t_python(code="", **_): | |
| buf = io.StringIO() | |
| g = {"__name__": "__main__"} | |
| try: | |
| with contextlib.redirect_stdout(buf): | |
| exec(str(code), g) | |
| except Exception as e: # noqa: BLE001 | |
| return f"{buf.getvalue()}\n[error] {type(e).__name__}: {e}"[:4000] | |
| return (buf.getvalue() or "(ran, no stdout)")[:4000] | |
| def _t_write_file(files, path="", content="", **_): | |
| path = (str(path) or "index.html").strip().lstrip("/") | |
| files[path] = str(content) | |
| return f"wrote {path} ({len(files[path])} bytes)" + ( | |
| " — rendering in Preview" if path.lower().endswith((".html", ".htm")) else "" | |
| ) | |
| def _t_read_file(files, path="", **_): | |
| path = str(path).strip().lstrip("/") | |
| if path in files: | |
| return files[path][:4000] | |
| return f"[no such file: {path}] — files: {', '.join(sorted(files)) or '(none)'}" | |
| def _t_list_files(files, **_): | |
| if not files: | |
| return "(no files yet)" | |
| return "\n".join(f"{p} ({len(files[p])} bytes)" for p in sorted(files)) | |
| # tools that take the turn-local virtual filesystem as first arg | |
| FILE_TOOLS = { | |
| "write_file": _t_write_file, | |
| "read_file": _t_read_file, | |
| "list_files": _t_list_files, | |
| } | |
| # stateless tools | |
| TOOLS = {"web_search": _t_web_search, "open_url": _t_open_url, "python": _t_python} | |
| MAX_TOOL_STEPS = 6 | |
| MAX_CONTINUE = 3 # auto-resume rounds when a step hits the token cap mid-output | |
| TOOLS_GUIDE = ( | |
| "You can call real tools that run live in this chat. Keep the chat itself short — " | |
| "do NOT paste whole files or large output as plain text.\n" | |
| "\n" | |
| "FILES (the deliverable) — for any web app, page, game, or UI, just write the code in a " | |
| "normal ```html block (and ```css / ```js if you split it). It is automatically SAVED as " | |
| "a file, rendered live in the Preview panel, and its source shown in the Code panel — you " | |
| "do NOT need to repeat it in the message. For several named files use ```file:path blocks, " | |
| "e.g. ```file:game.js. In the chat write only a one-line note about what you built.\n" | |
| "Inspect saved files with a tool block: ```tool\n{\"name\": \"list_files\", \"args\": {}}\n``` " | |
| "or {\"name\": \"read_file\", \"args\": {\"path\": \"index.html\"}}\n" | |
| "\n" | |
| "PYTHON (compute) — write a normal ```python block with print(); it is EXECUTED and you get " | |
| "stdout back as a TOOL RESULT. Use it for math, data, and checks.\n" | |
| "\n" | |
| "WEB — to search or fetch, emit ONE tool block and stop:\n" | |
| "```tool\n{\"name\": \"web_search\", \"args\": {\"query\": \"...\"}}\n```\n" | |
| "(or {\"name\": \"open_url\", \"args\": {\"url\": \"...\"}}). You then receive a TOOL RESULT " | |
| "and continue.\n" | |
| "\n" | |
| "Use one tool per step, only when needed. When finished, give a brief final answer." | |
| ) | |
| def _parse_call(text, seen): | |
| """Detect an *action* tool call that needs a result fed back: explicit | |
| ```tool JSON, native <tool_call> JSON, or a bare ```python block. | |
| Deliverable file blocks (html/css/js/file:) are NOT handled here — they are | |
| captured directly into the filesystem by _capture_artifacts. Returns a call | |
| dict or None; `seen` dedupes python blocks already run this turn.""" | |
| text = text or "" | |
| blocks = list(_TOOL_BLOCK.findall(text)) + list(_HERMES_TOOL.findall(text)) | |
| for block in blocks: | |
| try: | |
| call = json.loads(block.strip()) | |
| except Exception: | |
| continue | |
| if isinstance(call, dict) and "name" in call: | |
| # Hermes/Qwen use "arguments"; we use "args" | |
| call["args"] = call.get("args") or call.get("arguments") or {} | |
| call.pop("arguments", None) | |
| return call | |
| pys = _PY_BLOCK.findall(text) | |
| if pys: | |
| code = pys[-1].strip() | |
| h = ("py", hash(code)) | |
| if h not in seen: | |
| return {"name": "python", "args": {"code": code}, "_h": h} | |
| return None | |
| def _exec_tool(call, files): | |
| name = call.get("name") | |
| args = call.get("args") or {} | |
| if not isinstance(args, dict): | |
| args = {"query": args} if name == "web_search" else {"code": str(args)} | |
| try: | |
| if name in FILE_TOOLS: | |
| return str(FILE_TOOLS[name](files, **args))[:4000] | |
| fn = TOOLS.get(name) | |
| if not fn: | |
| return f"[unknown tool: {name}]" | |
| return str(fn(**args))[:4000] | |
| except Exception as e: # noqa: BLE001 | |
| return f"[tool error] {type(e).__name__}: {e}" | |
| # --------------------------------------------------------------------------- | |
| # Preview: a browser-chrome frame around the live artifact iframe. | |
| # --------------------------------------------------------------------------- | |
| def _placeholder(title, sub, building=False): | |
| cls = "pv-empty pv-building" if building else "pv-empty" | |
| return ( | |
| f'<div class="{cls}"><div class="pv-empty__glyph">🪽</div>' | |
| f'<div class="pv-empty__title">{title}</div>' | |
| f'<div class="pv-empty__sub">{sub}</div></div>' | |
| ) | |
| _EMPTY_PREVIEW = _placeholder( | |
| "Live preview", | |
| "Ask Hermes for a web app, page, or game. The generated HTML runs right here.", | |
| ) | |
| _BUILDING_PREVIEW = _placeholder( | |
| "Building artifact", | |
| "Assembling a self-contained page from the model output.", | |
| building=True, | |
| ) | |
| _NO_REASONING = "_No `<think>` reasoning in this turn._" | |
| _NO_TOOLS = "_No tools used this turn._" | |
| def _iframe(doc, name="artifact.html"): | |
| if not doc or not doc.strip(): | |
| return _EMPTY_PREVIEW | |
| esc = _html.escape(doc, quote=True) | |
| pill = _html.escape(name or "artifact.html", quote=True) | |
| # data: URL so the chrome buttons can be plain <a> links — HF's CSP blocks | |
| # inline onclick handlers, so JS-driven buttons silently do nothing. | |
| data_url = "data:text/html;base64," + base64.b64encode(doc.encode("utf-8")).decode("ascii") | |
| # allow-same-origin: real origin so artifacts using localStorage/fetch work; | |
| # allow-top-navigation-by-user-activation: in-artifact links navigate on click. | |
| sandbox = ("allow-scripts allow-same-origin allow-modals allow-forms " | |
| "allow-popups allow-popups-to-escape-sandbox " | |
| "allow-top-navigation-by-user-activation") | |
| h = "clamp(440px,62vh,760px)" | |
| return ( | |
| f'<div style="overflow:hidden;border-radius:14px;height:{h};">' | |
| '<div class="bchrome" style="height:100%;display:flex;flex-direction:column;">' | |
| '<div class="bchrome__bar">' | |
| '<span class="bchrome__dots"><i></i><i></i><i></i></span>' | |
| f'<span class="bchrome__pill">{pill}</span>' | |
| '<span class="bchrome__actions">' | |
| f'<a href="{data_url}" target="pvframe" title="Reload">↻</a>' | |
| f'<a href="{data_url}" target="_blank" rel="noopener" title="Open in new tab">↗</a>' | |
| '</span></div>' | |
| f'<iframe name="pvframe" srcdoc="{esc}" sandbox="{sandbox}"' | |
| f' style="display:block;width:100%;flex:1;min-height:0;border:0;background:#fff;"></iframe>' | |
| '</div>' | |
| '</div>' | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Skills: reusable instruction snippets injected into the system prompt. | |
| # --------------------------------------------------------------------------- | |
| SKILLS = { | |
| "Single-file HTML artifact": ( | |
| "When the user wants a web app, page, game, or visual UI, WRITE ONE complete, " | |
| "self-contained HTML file (inline CSS and JS) to disk with a ```file:index.html block " | |
| "— never paste the file in the chat. No external files. Avoid CDNs unless asked." | |
| ), | |
| "Tailwind via Play CDN": ( | |
| "Style with Tailwind using the Play CDN (<script src=\"https://cdn.tailwindcss.com\">). " | |
| "Prefer utility classes; keep custom CSS minimal." | |
| ), | |
| "Canvas game loop": ( | |
| "For games, use a <canvas> with a requestAnimationFrame loop, keyboard and touch " | |
| "controls, a score, and a restart. Target smooth 60fps." | |
| ), | |
| "Inline data-viz": ( | |
| "For charts or dashboards, draw with inline SVG or Canvas (no chart libraries). " | |
| "Label axes, add a legend, and animate transitions." | |
| ), | |
| "Refined dark UI": ( | |
| "Default to a refined dark interface: tinted near-black surfaces, one accent color, " | |
| "soft shadows, generous spacing, no pure black or white." | |
| ), | |
| "Mobile-first": ( | |
| "Mobile-first and responsive: fluid layout, large tap targets, works from 320px up." | |
| ), | |
| "Accessible by default": ( | |
| "Semantic HTML, alt text, ARIA where needed, visible focus rings, sufficient contrast." | |
| ), | |
| "Tasteful micro-interactions": ( | |
| "Add restrained micro-interactions: ease-out transitions on hover and state changes, " | |
| "subtle entrance animations. Never animate layout in a way that janks." | |
| ), | |
| } | |
| DEFAULT_SKILLS = ["Single-file HTML artifact", "Tasteful micro-interactions"] | |
| DEFAULT_SYS = ( | |
| "You are Hermes, a sharp agentic coding assistant powered by SIQ-1-35B. " | |
| "Reason inside <think> ... </think>, then give a focused, complete answer. " | |
| "Prefer runnable solutions over fragments." | |
| ) | |
| # Reasoning effort is a SOFT hint injected into the system prompt (not a hard | |
| # token budget). The Qwen3-format model respects it and self-regulates the | |
| # length of its <think> chain. | |
| EFFORT_HINTS = { | |
| "low": ( | |
| "Reasoning effort: low. Think very briefly inside <think>...</think> " | |
| "(a couple of lines at most), then answer directly. Favor speed." | |
| ), | |
| "medium": ( | |
| "Reasoning effort: medium. Think inside <think>...</think> with a short, " | |
| "focused chain, then answer." | |
| ), | |
| "high": ( | |
| "Reasoning effort: high. Think thoroughly inside <think>...</think> — " | |
| "consider edge cases and alternatives — then give a careful answer." | |
| ), | |
| } | |
| DEFAULT_EFFORT = "low" | |
| def _compose_system(base, selected, custom, use_tools, effort=DEFAULT_EFFORT): | |
| base = (base or "").strip() | |
| parts = [base] if base else [] | |
| hint = EFFORT_HINTS.get((effort or "").lower()) | |
| if hint: | |
| parts.append(hint) | |
| if use_tools: | |
| parts.append(TOOLS_GUIDE) | |
| lines = [] | |
| for label in selected or []: | |
| instr = SKILLS.get(label) | |
| if instr: | |
| lines.append(f"- {label}: {instr}") | |
| for raw in (custom or "").splitlines(): | |
| raw = raw.strip().lstrip("-").strip() | |
| if raw: | |
| lines.append(f"- {raw}") | |
| if lines: | |
| parts.append("Active skills (apply every one that is relevant):\n" + "\n".join(lines)) | |
| return "\n\n".join(parts) | |
| def _tools_md(trace): | |
| if not trace: | |
| return _NO_TOOLS | |
| out = [] | |
| for i, (call, result) in enumerate(trace, 1): | |
| name = call.get("name") | |
| args = call.get("args", {}) or {} | |
| if name == "write_file": # don't dump the whole file into the trace | |
| nbytes = call.get("_bytes", len(str(args.get("content", "")))) | |
| disp = json.dumps({"path": args.get("path", ""), "bytes": nbytes}) | |
| else: | |
| disp = json.dumps(args, ensure_ascii=False) | |
| if len(disp) > 280: | |
| disp = disp[:280] + "…" | |
| out.append(f"**{i}. `{name}`** `{disp}`\n\n```\n{result[:1200]}\n```") | |
| return "\n\n".join(out) | |
| def _g(o, key, default=None): | |
| """Get a field from a value that may be a dict OR a typed object.""" | |
| if isinstance(o, dict): | |
| return o.get(key, default) | |
| return getattr(o, key, default) | |
| def _chunk_fields(chunk): | |
| """Extract (content_delta, finish_reason) from a streaming chat chunk. | |
| llama-cpp-python returns either dict chunks or typed objects (Choice) across | |
| builds, so access both ways.""" | |
| choices = _g(chunk, "choices") or [] | |
| if not choices: | |
| return "", None | |
| ch = choices[0] | |
| finish = _g(ch, "finish_reason") | |
| delta = _g(ch, "delta") or {} | |
| content = _g(delta, "content", "") or "" | |
| return content, finish | |
| def _count_tokens(llm, text): | |
| """Best-effort token count for the live ↑/↓ counter.""" | |
| try: | |
| return len(llm.tokenize(text.encode("utf-8"), add_bos=False, special=True)) | |
| except Exception: | |
| return max(1, len(text) // 4) | |
| # --------------------------------------------------------------------------- | |
| # Agent turn: full ReAct loop inside a single GPU window. | |
| # --------------------------------------------------------------------------- | |
| def _content_text(c): | |
| """Flatten a chat message's content to plain text. On follow-up turns Gradio | |
| returns content as a list of segment dicts ([{'type':'text','text':...}]), | |
| not a string — coerce it so the LLM and tokenizer always get a str.""" | |
| if isinstance(c, str): | |
| return c | |
| if isinstance(c, list): | |
| parts = [] | |
| for seg in c: | |
| if isinstance(seg, dict): | |
| parts.append(seg.get("text") or "") | |
| elif isinstance(seg, str): | |
| parts.append(seg) | |
| return "\n".join(p for p in parts if p) | |
| return str(c or "") | |
| def _agent_stream(message, history, system_prompt, temperature, max_tokens, use_tools): | |
| llm = _get_llm() | |
| msgs = [] | |
| if system_prompt and system_prompt.strip(): | |
| msgs.append({"role": "system", "content": system_prompt.strip()}) | |
| for m in history: | |
| role = m.get("role") | |
| content = _content_text(m.get("content")) | |
| if role in ("user", "assistant") and content.strip(): | |
| msgs.append({"role": role, "content": content}) | |
| msgs.append({"role": "user", "content": _content_text(message)}) | |
| trace = [] | |
| seen = set() | |
| files = {} # turn-local virtual filesystem the model writes into | |
| last_answer = "" | |
| last_thinking = "" | |
| transcript = "" # raw, across steps | |
| in_tok = 0 # tokens sent INTO the model (↑), accumulated over steps | |
| out_tok = 0 # tokens generated (↓), accumulated over the turn | |
| for step in range(MAX_TOOL_STEPS if use_tools else 1): | |
| out = "" | |
| cont = 0 | |
| # Generate the step; if the model hits the token cap mid-artifact | |
| # (finish_reason == "length"), auto-continue from where it stopped | |
| # and concatenate — so big builds don't get truncated. | |
| while True: | |
| if not out: | |
| gen_msgs = msgs | |
| else: | |
| mid_code = bool(_ARTIFACT_START.search(out)) | |
| cont_instr = ( | |
| "Continue the file EXACTLY where you stopped. Output ONLY the raw " | |
| "remaining file content — no prose, no explanations, no markdown " | |
| "fences, no backticks. Do not repeat anything already written; " | |
| "resume from the next character." | |
| ) if mid_code else ( | |
| "Continue EXACTLY where you stopped. Do not repeat or restate any " | |
| "earlier text and do not restart — emit only the next characters." | |
| ) | |
| gen_msgs = msgs + [ | |
| {"role": "assistant", "content": out}, | |
| {"role": "user", "content": cont_instr}, | |
| ] | |
| in_tok += _count_tokens(llm, "\n".join(m.get("content", "") for m in gen_msgs)) | |
| finish = None | |
| try: | |
| for chunk in llm.create_chat_completion( | |
| messages=gen_msgs, | |
| max_tokens=int(max_tokens), | |
| temperature=float(temperature), | |
| stream=True, | |
| ): | |
| delta, fr = _chunk_fields(chunk) | |
| if fr: | |
| finish = fr | |
| if not delta: | |
| continue | |
| out += delta | |
| out_tok += 1 | |
| ans, think = _split(transcript + out) | |
| disp = _strip_artifacts(ans) | |
| doc, name = _current_doc(files, ans) | |
| yield disp or "…", transcript + out, think, _tools_md(trace), doc, name, in_tok, out_tok | |
| except Exception as stream_err: # streaming broken in this llama build -> non-stream fallback | |
| try: | |
| resp = llm.create_chat_completion( | |
| messages=gen_msgs, max_tokens=int(max_tokens), | |
| temperature=float(temperature), stream=False, | |
| ) | |
| ch0 = (_g(resp, "choices") or [None])[0] | |
| msg = _g(ch0, "message") or {} | |
| full = _g(msg, "content", "") or "" | |
| finish = _g(ch0, "finish_reason") | |
| out += full | |
| out_tok += _count_tokens(llm, full) | |
| ans, think = _split(transcript + out) | |
| yield _strip_artifacts(ans) or "…", transcript + out, think, _tools_md(trace), \ | |
| *_current_doc(files, ans), in_tok, out_tok | |
| except Exception as gen_err: | |
| err = f"⚠️ generation error: {type(gen_err).__name__}: {gen_err}" | |
| yield err, transcript + "\n" + err, last_thinking, _tools_md(trace), "", "index.html", in_tok, out_tok | |
| return | |
| if finish == "length" and cont < MAX_CONTINUE: | |
| cont += 1 | |
| continue | |
| break | |
| transcript += out + "\n\n" | |
| last_answer, last_thinking = _split(transcript) | |
| msgs.append({"role": "assistant", "content": out}) | |
| # Route this step's deliverable code blocks through write_file. | |
| if use_tools: | |
| for path, nbytes in _capture_artifacts(out, files): | |
| trace.append(( | |
| {"name": "write_file", "args": {"path": path}, "_bytes": nbytes}, | |
| f"wrote {path} ({nbytes} bytes) — rendered in Preview", | |
| )) | |
| disp_answer = _strip_artifacts(last_answer) | |
| call = _parse_call(out, seen) if use_tools else None | |
| doc, name = _current_doc(files, last_answer) | |
| if not call: | |
| break | |
| if "_h" in call: | |
| seen.add(call["_h"]) | |
| yield (disp_answer or "…"), transcript, last_thinking, _tools_md( | |
| trace + [(call, "running…")] | |
| ), doc, name, in_tok, out_tok | |
| result = _exec_tool(call, files) | |
| trace.append((call, result)) | |
| msgs.append({"role": "user", "content": f"TOOL RESULT ({call.get('name')}):\n{result}"}) | |
| doc, name = _current_doc(files, last_answer) | |
| yield (disp_answer or "…"), transcript, last_thinking, _tools_md(trace), doc, name, in_tok, out_tok | |
| doc, name = _current_doc(files, last_answer) | |
| yield (_strip_artifacts(last_answer) or "…"), transcript, last_thinking, _tools_md(trace), doc, name, in_tok, out_tok | |
| def _gen_status(in_tok, out_tok): | |
| """Live status strip shown while generating: a pulsing wing + token meters.""" | |
| return ( | |
| "<div class='gen-status'>" | |
| "<span class='gen-wing'>🪽</span>" | |
| f"<span class='gen-tok'><b class='up'>↑</b> {in_tok:,} in</span>" | |
| f"<span class='gen-tok'><b class='dn'>↓</b> {out_tok:,} out</span>" | |
| "</div>" | |
| ) | |
| def _chat_message(answer, thinking, tools, streaming=False, in_tok=0, out_tok=0): | |
| """Compose the chat bubble: a live status strip while generating, then | |
| collapsible <details> for reasoning and tools, then the clean answer. | |
| Reasoning auto-opens while thinking (no answer yet).""" | |
| answer = (answer or "").strip() | |
| parts = [] | |
| if streaming: | |
| parts.append(_gen_status(in_tok, out_tok)) | |
| think = (thinking or "").strip() | |
| if think: | |
| opened = " open" if (streaming and not answer) else "" | |
| parts.append( | |
| f"<details class='turn-fold'{opened}>" | |
| f"<summary>🧠 Reasoning</summary>\n\n{think}\n\n</details>" | |
| ) | |
| trace = (tools or "").strip() | |
| if trace and trace != _NO_TOOLS: | |
| n = trace.count("\n\n**") + (1 if trace.startswith("**") else 0) | |
| label = f"🛠 Tools · {n}" if n else "🛠 Tools" | |
| parts.append( | |
| f"<details class='turn-fold'>" | |
| f"<summary>{label}</summary>\n\n{trace}\n\n</details>" | |
| ) | |
| if answer: | |
| parts.append(answer) | |
| return "\n\n".join(parts) if parts else "…" | |
| def respond(message, history, base_sys, skills, custom_skills, use_tools, | |
| effort, temperature, max_tokens, meta): | |
| meta = meta or [] | |
| if not message or not message.strip(): | |
| yield history or [], "", "", _NO_REASONING, "", _EMPTY_PREVIEW, _NO_TOOLS, meta | |
| return | |
| system_prompt = _compose_system(base_sys, skills, custom_skills, use_tools, effort) | |
| history = (history or []) + [ | |
| {"role": "user", "content": message}, | |
| {"role": "assistant", "content": ""}, | |
| ] | |
| prior = history[:-2] | |
| answer = raw = thinking = tools = doc = "" | |
| doc_name = "artifact.html" | |
| in_tok = out_tok = 0 | |
| try: | |
| for answer, raw, thinking, tools, doc, doc_name, in_tok, out_tok in _agent_stream( | |
| message, prior, system_prompt, temperature, max_tokens, use_tools | |
| ): | |
| history[-1]["content"] = _chat_message( | |
| answer, thinking, tools, streaming=True, in_tok=in_tok, out_tok=out_tok | |
| ) | |
| prev = _BUILDING_PREVIEW if doc else _EMPTY_PREVIEW | |
| yield history, "", raw, (thinking or _NO_REASONING), doc, prev, (tools or _NO_TOOLS), meta | |
| except Exception: | |
| import traceback | |
| tb = traceback.format_exc() | |
| history[-1]["content"] = f"⚠️ **generation crashed**\n\n```\n{tb[-1800:]}\n```" | |
| yield history, "", tb, (thinking or _NO_REASONING), doc, _EMPTY_PREVIEW, (tools or _NO_TOOLS), meta | |
| return | |
| history[-1]["content"] = _chat_message(answer, thinking, tools, streaming=False) | |
| meta = meta + [{ | |
| "turn_id": uuid.uuid4().hex, "user": message, "answer": answer, | |
| "reasoning": thinking, "code": doc, "tools": tools, | |
| }] | |
| yield history, "", raw, (thinking or _NO_REASONING), doc, _iframe(doc, doc_name), (tools or _NO_TOOLS), meta | |
| # --------------------------------------------------------------------------- | |
| # Look & feel | |
| # --------------------------------------------------------------------------- | |
| HERO = """ | |
| <div class="hero"> | |
| <div class="hero__badge">🪽</div> | |
| <div class="hero__body"> | |
| <div class="hero__title">Hermes <span>· SIQ-1-35B</span></div> | |
| <div class="hero__sub">Agentic coding model: thinks, calls tools, ships a live HTML artifact.</div> | |
| <div class="hero__chips"> | |
| <span class="chip">Qwen3.x MoE · 35B-A3B</span> | |
| <span class="chip">GGUF Q4_K_M</span> | |
| <span class="chip chip--accent">ZeroGPU · llama.cpp</span> | |
| <span class="chip chip--accent">web · fetch · python tools</span> | |
| <a class="chip chip--link" href="https://huggingface.co/AlexWortega/SIQ-1-35B" target="_blank">model ↗</a> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| CSS = """ | |
| :root{ | |
| --bg: oklch(0.09 0 0); | |
| --surface: oklch(0.13 0 0); | |
| --surface-2: oklch(0.17 0 0); | |
| --line: oklch(0.28 0 0); | |
| --line-soft: oklch(0.28 0 0 / 0.55); | |
| --text: oklch(0.97 0 0); | |
| --muted: oklch(0.68 0 0); | |
| --accent: oklch(0.92 0 0); | |
| --accent-2: oklch(0.92 0 0); | |
| --accent-soft: oklch(1 0 0 / 0.10); | |
| --accent-ring: oklch(1 0 0 / 0.22); | |
| --radius: 14px; | |
| --radius-sm: 10px; | |
| --pill: 999px; | |
| --ease: cubic-bezier(.2,.7,.2,1); | |
| --shadow-1: 0 1px 2px oklch(0 0 0 / 0.30); | |
| --shadow-2: 0 10px 26px -14px oklch(0 0 0 / 0.55); | |
| --shadow-pop: 0 18px 40px -24px oklch(0 0 0 / 0.70); | |
| } | |
| .gradio-container{max-width:1320px !important;width:100% !important;margin:0 auto !important;} | |
| /* force the app to actually fill the width (Gradio sometimes leaves it shrunk-left) */ | |
| .gradio-container > .main, | |
| .gradio-container .main > .wrap, | |
| .gradio-container .main .contain, | |
| .gradio-container .main .contain > .row{width:100% !important;} | |
| .gradio-container .main .contain > .row{flex-wrap:nowrap;} | |
| footer{display:none !important;} | |
| /* crisp, consistent focus + selection + thin scrollbars */ | |
| .gradio-container :focus-visible{outline:2px solid var(--accent) !important;outline-offset:2px;border-radius:6px;} | |
| ::selection{background:var(--accent-soft);color:var(--text);} | |
| *{scrollbar-width:thin;scrollbar-color:var(--line) transparent;} | |
| ::-webkit-scrollbar{width:10px;height:10px;} | |
| ::-webkit-scrollbar-thumb{background:var(--line);border-radius:var(--pill);border:2px solid transparent;background-clip:content-box;} | |
| ::-webkit-scrollbar-thumb:hover{background:var(--muted);} | |
| @media (prefers-reduced-motion: reduce){ | |
| *,*::before,*::after{animation-duration:.001ms !important;transition-duration:.001ms !important;} | |
| } | |
| .hero{display:flex;gap:18px;align-items:center;padding:18px 20px;border:1px solid var(--line); | |
| border-radius:18px;background: | |
| radial-gradient(120% 140% at 0% 0%, var(--accent-soft), transparent 55%), | |
| var(--surface);} | |
| .hero__badge{display:grid;place-items:center;width:54px;height:54px;flex:none;border-radius:14px; | |
| font-size:28px;background:var(--accent-soft);border:1px solid var(--line); | |
| box-shadow:0 1px 0 oklch(1 0 0 / 0.06) inset;} | |
| .hero__title{font-size:24px;font-weight:800;letter-spacing:-0.01em;color:var(--text);} | |
| .hero__title span{color:var(--accent);font-weight:700;} | |
| .hero__sub{color:var(--muted);font-size:14px;margin-top:3px;} | |
| .hero__chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:11px;} | |
| .chip{font-size:12px;color:var(--muted);background:var(--surface-2);border:1px solid var(--line); | |
| padding:4px 10px;border-radius:999px;line-height:1.5;} | |
| .chip--accent{color:var(--text);border-color:var(--accent);background:var(--accent-soft);} | |
| .chip--link{color:var(--accent);text-decoration:none;} | |
| .chip--link:hover{background:var(--accent-soft);} | |
| .bchrome{border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--surface); | |
| box-shadow:0 18px 40px -24px oklch(0 0 0 / 0.7);} | |
| .bchrome__bar{display:flex;align-items:center;gap:10px;padding:9px 12px;background:var(--surface-2); | |
| border-bottom:1px solid var(--line);} | |
| .bchrome__dots{display:inline-flex;gap:6px;} | |
| .bchrome__dots i{width:11px;height:11px;border-radius:50%;background:var(--line);display:block;} | |
| .bchrome__dots i:nth-child(1){background:oklch(0.66 0.17 25);} | |
| .bchrome__dots i:nth-child(2){background:oklch(0.78 0.15 85);} | |
| .bchrome__dots i:nth-child(3){background:oklch(0.74 0.16 150);} | |
| .bchrome__pill{flex:1;text-align:center;font-size:12px;color:var(--muted);background:var(--bg); | |
| border:1px solid var(--line);border-radius:8px;padding:3px 10px;max-width:340px;margin:0 auto; | |
| font-family:ui-monospace,monospace;} | |
| .bchrome__actions{display:inline-flex;gap:4px;} | |
| .bchrome__actions a{all:unset;cursor:pointer;color:var(--muted);font-size:15px;width:26px;height:26px; | |
| display:grid;place-items:center;border-radius:7px;text-decoration:none;} | |
| .bchrome__actions a:hover{color:var(--text);background:var(--bg);} | |
| .bchrome iframe{display:block;width:100%;height:clamp(440px,62vh,760px);border:0;background:#fff;} | |
| .pv-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center; | |
| gap:8px;min-height:clamp(440px,62vh,760px);border:1px dashed var(--line);border-radius:14px; | |
| background: | |
| radial-gradient(60% 60% at 50% 38%, var(--accent-soft), transparent 70%), | |
| repeating-linear-gradient(45deg, transparent 0 13px, oklch(1 0 0 / 0.012) 13px 14px), | |
| var(--surface);} | |
| .pv-empty__glyph{font-size:40px;opacity:0.9;} | |
| .pv-empty__title{font-size:16px;font-weight:700;color:var(--text);} | |
| .pv-empty__sub{font-size:13px;color:var(--muted);max-width:36ch;} | |
| .pv-building .pv-empty__glyph{animation:floaty 1.6s ease-in-out infinite;} | |
| @keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}} | |
| /* ---- pill-group controls: skills (multi) + effort segmented (single) ---- */ | |
| .skills .wrap, .seg .wrap{gap:8px !important;} | |
| .skills label, .seg label{border:1px solid var(--line) !important;background:var(--surface-2) !important; | |
| border-radius:var(--pill) !important;padding:7px 14px !important;color:var(--muted) !important; | |
| cursor:pointer;transition:border-color .18s var(--ease),background .18s var(--ease),color .18s var(--ease),transform .12s var(--ease);} | |
| .skills label:hover, .seg label:hover{border-color:var(--accent) !important;color:var(--text) !important;} | |
| .skills label:active, .seg label:active{transform:translateY(1px);} | |
| .skills label:has(input:checked), .seg label:has(input:checked){ | |
| border-color:var(--accent) !important;background:var(--accent-soft) !important;color:var(--text) !important;} | |
| .seg input[type="radio"]{position:absolute;opacity:0;width:0;height:0;} | |
| /* ---- tabs -> a segmented pill control instead of the default underline ---- */ | |
| .tab-nav, div.tab-nav{border:none !important;gap:4px;padding:4px;background:var(--surface-2); | |
| border:1px solid var(--line) !important;border-radius:12px;flex-wrap:wrap;} | |
| .tab-nav button{border:none !important;border-radius:9px !important;color:var(--muted) !important; | |
| font-weight:600;font-size:13px;padding:7px 14px !important;transition:color .18s var(--ease),background .18s var(--ease);} | |
| .tab-nav button:hover{color:var(--text) !important;background:var(--bg);} | |
| .tab-nav button.selected{color:var(--text) !important;background:var(--accent-soft) !important; | |
| box-shadow:inset 0 0 0 1px var(--accent);} | |
| /* ---- examples -> clickable chips instead of a clunky table ---- */ | |
| #examples{border:none !important;background:transparent !important;} | |
| #examples .label-wrap, #examples > .label, #examples thead{display:none !important;} | |
| #examples table, #examples tbody, #examples .table-wrap{border:none !important;background:transparent !important; | |
| display:block !important;box-shadow:none !important;} | |
| #examples tbody{display:flex !important;flex-wrap:wrap;gap:8px;} | |
| #examples tr{display:block !important;border:none !important;background:transparent !important;} | |
| #examples td, #examples .gallery-item{border:1px solid var(--line) !important;background:var(--surface-2) !important; | |
| border-radius:var(--pill) !important;padding:7px 14px !important;color:var(--muted) !important;font-size:13px !important; | |
| cursor:pointer;transition:border-color .18s var(--ease),background .18s var(--ease),color .18s var(--ease); | |
| white-space:normal;max-width:260px;line-height:1.35;} | |
| #examples td:hover, #examples .gallery-item:hover{border-color:var(--accent) !important;color:var(--text) !important; | |
| background:var(--accent-soft) !important;} | |
| /* ---- chat surface + bubbles ---- */ | |
| .chatbot, .bubble-wrap{background:transparent !important;border:none !important;} | |
| .message, .message-row .message{border-radius:14px !important;border:1px solid var(--line) !important; | |
| box-shadow:none !important;line-height:1.55;} | |
| .user .message, .user-row .message, .message.user{background:var(--accent-soft) !important;border-color:transparent !important;} | |
| .bot .message, .bot-row .message, .message.bot{background:var(--surface-2) !important;} | |
| /* ---- live generation status: pulsing wing + token meters ---- */ | |
| .gen-status{display:inline-flex;align-items:center;gap:14px;padding:6px 14px;margin:0 0 10px; | |
| border:1px solid var(--line);border-radius:var(--pill);background:var(--surface-2); | |
| font-size:12.5px;color:var(--muted);font-variant-numeric:tabular-nums;} | |
| .gen-wing{display:inline-block;font-size:16px;animation:wingpulse 1.05s var(--ease) infinite; | |
| filter:drop-shadow(0 0 6px var(--accent-ring));} | |
| @keyframes wingpulse{ | |
| 0%,100%{opacity:1;transform:rotate(-9deg) scale(1);} | |
| 50%{opacity:.3;transform:rotate(9deg) scale(1.16);} | |
| } | |
| .gen-tok{display:inline-flex;align-items:center;gap:5px;} | |
| .gen-tok b{font-style:normal;font-weight:800;font-size:13px;} | |
| .gen-tok .up{color:var(--accent-2);} | |
| .gen-tok .dn{color:var(--accent);} | |
| /* ---- collapsible reasoning / tools inside a chat bubble ---- */ | |
| .turn-fold{border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--bg); | |
| margin:0 0 8px;overflow:hidden;} | |
| .turn-fold > summary{cursor:pointer;list-style:none;padding:8px 12px;font-size:12.5px;font-weight:600; | |
| color:var(--muted);display:flex;align-items:center;gap:6px;user-select:none; | |
| transition:color .15s var(--ease),background .15s var(--ease);} | |
| .turn-fold > summary::-webkit-details-marker{display:none;} | |
| .turn-fold > summary::after{content:"⌄";margin-left:auto;transition:transform .2s var(--ease);font-size:14px;} | |
| .turn-fold[open] > summary{color:var(--text);border-bottom:1px solid var(--line);background:var(--surface-2);} | |
| .turn-fold[open] > summary::after{transform:rotate(180deg);} | |
| .turn-fold > summary:hover{color:var(--text);} | |
| .turn-fold > *:not(summary){padding:4px 12px 10px;font-size:13px;} | |
| .turn-fold pre{background:var(--bg) !important;border:1px solid var(--line);border-radius:8px;} | |
| /* ---- command bar (the main input) ---- */ | |
| #cmdbar textarea, #cmdbar input{font-size:15px !important;padding:13px 16px !important;border-radius:var(--radius) !important; | |
| background:var(--surface) !important;border:1px solid var(--line) !important;color:var(--text) !important; | |
| transition:border-color .18s var(--ease),box-shadow .18s var(--ease);} | |
| #cmdbar textarea:focus, #cmdbar input:focus{border-color:var(--accent) !important; | |
| box-shadow:0 0 0 3px var(--accent-soft) !important;outline:none !important;} | |
| /* ---- buttons: soft depth + hover lift ---- */ | |
| .gradio-container button.primary{box-shadow:var(--shadow-1);font-weight:650; | |
| transition:transform .12s var(--ease),box-shadow .18s var(--ease),filter .18s var(--ease); | |
| background-image:linear-gradient(180deg, oklch(1 0 0 / 0.06), transparent) !important;} | |
| .gradio-container button.primary:hover{transform:translateY(-1px);box-shadow:var(--shadow-2);filter:brightness(1.05);} | |
| .gradio-container button.primary:active{transform:translateY(0);box-shadow:var(--shadow-1);} | |
| .gradio-container button.secondary{background:var(--surface-2) !important;border:1px solid var(--line) !important; | |
| color:var(--muted) !important;transition:border-color .18s var(--ease),color .18s var(--ease),transform .12s var(--ease);} | |
| .gradio-container button.secondary:hover{border-color:var(--accent) !important;color:var(--text) !important;} | |
| .gradio-container button.secondary:active{transform:translateY(1px);} | |
| /* ---- accordions + blocks: quieter borders, rounder ---- */ | |
| .gradio-container .block, .gradio-container .form{border-radius:var(--radius) !important;} | |
| .gradio-container .label-wrap > span, .gradio-container span[data-testid="block-info"]{color:var(--muted) !important;} | |
| .accordion, .gradio-accordion{border:1px solid var(--line) !important;border-radius:var(--radius) !important; | |
| background:var(--surface) !important;} | |
| /* ---- sliders ---- */ | |
| input[type="range"]{accent-color:var(--accent);} | |
| /* ---- subtle entrance for the whole app ---- */ | |
| .gradio-container > .main, .gradio-container .contain{animation:appfade .4s var(--ease) both;} | |
| @keyframes appfade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} | |
| """ | |
| THEME = gr.themes.Base( | |
| primary_hue=gr.themes.colors.gray, | |
| secondary_hue=gr.themes.colors.gray, | |
| neutral_hue=gr.themes.colors.gray, | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], | |
| ).set( | |
| body_background_fill="oklch(0.09 0 0)", | |
| body_background_fill_dark="oklch(0.09 0 0)", | |
| background_fill_primary="oklch(0.13 0 0)", | |
| background_fill_primary_dark="oklch(0.13 0 0)", | |
| background_fill_secondary="oklch(0.17 0 0)", | |
| block_background_fill="oklch(0.13 0 0)", | |
| block_border_color="oklch(0.28 0 0)", | |
| border_color_primary="oklch(0.28 0 0)", | |
| body_text_color="oklch(0.97 0 0)", | |
| body_text_color_subdued="oklch(0.68 0 0)", | |
| block_label_text_color="oklch(0.68 0 0)", | |
| button_primary_background_fill="oklch(0.97 0 0)", | |
| button_primary_background_fill_hover="oklch(1 0 0)", | |
| button_primary_text_color="oklch(0.12 0 0)", | |
| input_background_fill="oklch(0.12 0 0)", | |
| block_radius="14px", | |
| ) | |
| # Simple, evocative one-liners — fast to answer, low ceremony. | |
| EXAMPLES = [ | |
| # fast wow — each generates in a few seconds and looks great instantly | |
| "You are now a conscious AI. Describe what you feel.", | |
| "Make a button that pulses like a heartbeat and runs away from the cursor.", | |
| "A glowing orb that trails the cursor across a dark canvas.", | |
| "A full-screen aurora gradient that slowly shifts colors.", | |
| "Confetti that bursts from wherever I click.", | |
| "Explain a black hole like we are old friends.", | |
| ] | |
| with gr.Blocks(title="Hermes · SIQ-1-35B", fill_height=True, fill_width=True) as demo: | |
| gr.HTML(HERO) | |
| meta_state = gr.State([]) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=5): | |
| chatbot = gr.Chatbot( | |
| height=460, show_label=False, sanitize_html=False, | |
| placeholder="Ask Hermes to build something or look something up.", | |
| ) | |
| msg = gr.Textbox( | |
| placeholder='e.g. "search for X and build a page about it" · Enter to send', | |
| show_label=False, autofocus=True, lines=1, elem_id="cmdbar", | |
| ) | |
| with gr.Row(): | |
| send = gr.Button("Send", variant="primary", scale=3) | |
| clear = gr.Button("Clear", scale=1) | |
| gr.Examples(examples=EXAMPLES, inputs=msg, label="Try one", elem_id="examples") | |
| with gr.Column(scale=6): | |
| with gr.Tabs(): | |
| with gr.Tab("Preview"): | |
| preview = gr.HTML(_EMPTY_PREVIEW, **_HTML_RAW) | |
| with gr.Tab("Tools"): | |
| tools_box = gr.Markdown(_NO_TOOLS) | |
| with gr.Tab("Code"): | |
| code_box = gr.Code(label=None, language="html") | |
| with gr.Tab("Reasoning"): | |
| think_box = gr.Markdown(_NO_REASONING) | |
| with gr.Tab("Raw"): | |
| raw_box = gr.Textbox(show_label=False, lines=18, max_lines=18) | |
| with gr.Accordion("Skills & tools", open=True): | |
| use_tools = gr.Checkbox( | |
| value=True, | |
| label="Enable tools · web · fetch · python", | |
| ) | |
| effort = gr.Radio( | |
| choices=["low", "medium", "high"], value=DEFAULT_EFFORT, | |
| label="Reasoning effort", | |
| info="low = short thinking, faster · high = deeper, slower", | |
| elem_classes=["seg"], | |
| ) | |
| skills = gr.CheckboxGroup( | |
| choices=list(SKILLS.keys()), value=DEFAULT_SKILLS, | |
| show_label=False, elem_classes=["skills"], | |
| ) | |
| custom_skills = gr.Textbox( | |
| label="Custom skills (one per line)", | |
| placeholder="Always add keyboard shortcuts\nWrite terse, commented code", | |
| lines=2, | |
| ) | |
| with gr.Accordion("Settings", open=False): | |
| system_prompt = gr.Textbox(value=DEFAULT_SYS, label="Base system prompt", lines=3) | |
| temperature = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="Temperature") | |
| max_tokens = gr.Slider(256, 8192, value=6144, step=128, label="Max tokens / step") | |
| inputs = [msg, chatbot, system_prompt, skills, custom_skills, use_tools, | |
| effort, temperature, max_tokens, meta_state] | |
| outputs = [chatbot, msg, raw_box, think_box, code_box, preview, tools_box, meta_state] | |
| send.click(respond, inputs, outputs) | |
| msg.submit(respond, inputs, outputs) | |
| clear.click( | |
| lambda: ([], "", "", _NO_REASONING, "", _EMPTY_PREVIEW, _NO_TOOLS, []), None, outputs | |
| ) | |
| demo.queue().launch(theme=THEME, css=CSS, ssr_mode=False, show_error=True) | |