Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Ornith-1.0-9B · Terminal</title> | |
| <style> | |
| :root { | |
| --bg: #0a0a0a; | |
| --panel: #111111; | |
| --panel-2: #171717; | |
| --border: #2a2a2a; | |
| --text: #e0e0e0; | |
| --muted: #6b6b6b; | |
| --green: #4ade80; | |
| --green-dim: #22c55e; | |
| --yellow: #facc15; | |
| --red: #f87171; | |
| --blue: #60a5fa; | |
| --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { height: 100%; margin: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 14px; | |
| line-height: 1.6; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 16px; | |
| background: var(--panel); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| header img { height: 28px; width: auto; } | |
| header .title { font-weight: 600; font-size: 13px; color: var(--text); } | |
| header .title span { color: var(--muted); font-weight: 400; } | |
| header .spacer { flex: 1; } | |
| header .badge { | |
| font-size: 11px; color: var(--muted); border: 1px solid var(--border); | |
| padding: 2px 8px; border-radius: 4px; cursor: pointer; user-select: none; | |
| } | |
| header .badge:hover { color: var(--text); border-color: var(--muted); } | |
| #chat { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0 16px; | |
| } | |
| .turn { max-width: 900px; margin: 0 auto; padding: 10px 0; border-bottom: 1px solid var(--border); } | |
| .turn:last-child { border-bottom: none; } | |
| .prompt-line { display: flex; align-items: flex-start; gap: 10px; } | |
| .prompt { color: var(--green); white-space: nowrap; font-weight: 600; user-select: none; } | |
| .prompt.user::before { content: "❯"; } | |
| .prompt.assistant::before { content: "◆"; color: var(--green-dim); } | |
| .content { flex: 1; min-width: 0; } | |
| .user-input { color: var(--text); white-space: pre-wrap; } | |
| .thinking { margin: 6px 0 10px; } | |
| .thinking summary { | |
| cursor: pointer; color: var(--muted); font-size: 12px; list-style: none; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| .thinking summary::-webkit-details-marker { display: none; } | |
| .thinking summary::before { content: "▸"; color: var(--green-dim); } | |
| .thinking[open] summary::before { content: "▾"; } | |
| .thinking pre { | |
| margin: 6px 0 0; padding: 10px 12px; background: var(--panel-2); | |
| border-left: 2px solid var(--green-dim); color: var(--muted); | |
| font: inherit; white-space: pre-wrap; word-wrap: break-word; | |
| max-height: 300px; overflow-y: auto; | |
| } | |
| .answer { color: var(--text); } | |
| .answer p { margin: 0 0 8px; } | |
| .answer p:last-child { margin-bottom: 0; } | |
| .answer pre { | |
| background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; | |
| padding: 12px 14px; overflow-x: auto; margin: 10px 0; font: inherit; | |
| } | |
| .answer code { font-family: inherit; } | |
| .answer :not(pre) > code { background: var(--panel-2); padding: 1px 4px; border-radius: 3px; color: var(--yellow); } | |
| .answer ul, .answer ol { padding-left: 22px; margin: 6px 0; } | |
| .answer a { color: var(--green); } | |
| .answer blockquote { margin: 8px 0; padding-left: 12px; border-left: 2px solid var(--muted); color: var(--muted); } | |
| .answer h1,.answer h2,.answer h3 { margin: 14px 0 6px; font-size: inherit; color: var(--green); } | |
| .answer h1 { font-size: 16px; } | |
| .answer h2 { font-size: 15px; } | |
| .status { max-width: 900px; margin: 0 auto; padding: 4px 0; color: var(--muted); font-size: 12px; min-height: 18px; } | |
| .status .err { color: var(--red); } | |
| .composer { | |
| border-top: 1px solid var(--border); background: var(--panel); padding: 12px 16px 16px; | |
| } | |
| .composer-inner { max-width: 900px; margin: 0 auto; } | |
| #settings { | |
| display: none; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; color: var(--muted); font-size: 12px; | |
| } | |
| #settings.open { display: flex; } | |
| #settings label { display: flex; flex-direction: column; gap: 4px; } | |
| #settings input[type=range] { width: 150px; accent-color: var(--green); } | |
| .input-row { display: flex; align-items: flex-end; gap: 8px; } | |
| #text { | |
| flex: 1; resize: none; min-height: 42px; max-height: 160px; | |
| background: var(--bg); color: var(--text); border: 1px solid var(--border); | |
| border-radius: 6px; padding: 10px 12px; font: inherit; outline: none; | |
| } | |
| #text:focus { border-color: var(--green); } | |
| button { | |
| background: var(--green); color: #052e16; border: none; border-radius: 6px; | |
| padding: 10px 16px; font: 600 13px inherit; cursor: pointer; | |
| } | |
| button:disabled { opacity: .5; cursor: not-allowed; } | |
| #stop { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); display: none; } | |
| .icon-btn { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); padding: 10px 12px; font-size: 14px; } | |
| .hint { color: var(--muted); font-size: 11px; margin-top: 8px; text-align: center; } | |
| /* Empty state */ | |
| .welcome { max-width: 900px; margin: 24px auto; color: var(--muted); } | |
| .welcome h2 { color: var(--green); margin: 0 0 8px; font-size: 14px; } | |
| .welcome p { margin: 0; } | |
| .welcome .cmd { color: var(--yellow); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <img src="https://us.aws.cdn.hf.co/xet-bridge-us/6a37681bb57803e616606089/2430ce5755ccaa545def0292cb0f1068d4af99d5093c299b8167c9bfc1128dab?X-Xet-Cas-Uid=60f1abe7544c2adfd699860c&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27ornith_logo.png%3B+filename%3D%22ornith_logo.png%22%3B&response-content-type=image%2Fpng&user_id=60f1abe7544c2adfd699860c&Expires=1782415346&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly91cy5hd3MuY2RuLmhmLmNvL3hldC1icmlkZ2UtdXMvNmEzNzY4MWJiNTc4MDNlNjE2NjA2MDg5LzI0MzBjZTU3NTVjY2FhNTQ1ZGVmMDI5MmNiMGYxMDY4ZDRhZjk5ZDUwOTNjMjk5YjgxNjdjOWJmYzExMjhkYWJcXD9YLVhldC1DYXMtVWlkPTYwZjFhYmU3NTQ0YzJhZGZkNjk5ODYwYyZyZXNwb25zZS1jb250ZW50LWRpc3Bvc2l0aW9uPWlubGluZSUzQitmaWxlbmFtZSUyQSUzRFVURi04JTI3JTI3b3JuaXRoX2xvZ28ucG5nJTNCK2ZpbGVuYW1lJTNEJTIyb3JuaXRoX2xvZ28ucG5nJTIyJTNCJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyZ1c2VyX2lkPTYwZjFhYmU3NTQ0YzJhZGZkNjk5ODYwYyIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiRXBvY2hUaW1lIjoxNzgyNDE1MzQ2fX19XX0_&Signature=MEUCIQCJDHCuf325VtWk4Lr7BrnsQw9LL9RPIuQzcVq7xPY7vAIgXghesfcDb0xuVJm-bq8mkneN1tG5zTgpzIY6NdpDehw_&Key-Pair-Id=01KAYHXK2CBJSW0YZTMNXK9W1M" alt="Ornith" /> | |
| <div class="spacer"></div> | |
| <div class="badge" id="settings-toggle">⚙ settings</div> | |
| </header> | |
| <div id="chat"> | |
| <div class="welcome"> | |
| <h2>Welcome to Ornith-1.0-9B</h2> | |
| <p>Agentic coding assistant. Ask me to write, debug, or explain code.</p> | |
| </div> | |
| </div> | |
| <div class="status" id="status"></div> | |
| <div class="composer"> | |
| <div class="composer-inner"> | |
| <div id="settings"> | |
| <label>Temperature <input type="range" id="temp" min="0.1" max="1.5" step="0.1" value="0.6" /><span id="temp-v">0.6</span></label> | |
| <label>Max tokens <input type="range" id="maxtok" min="256" max="8192" step="256" value="2048" /><span id="maxtok-v">2048</span></label> | |
| </div> | |
| <div class="input-row"> | |
| <textarea id="text" placeholder="Ask Ornith to write code, explain, or debug…"></textarea> | |
| <button id="send">Send</button> | |
| <button id="stop">Stop</button> | |
| </div> | |
| <div class="hint">Shift+Enter for newline · Enter to send</div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| const chatEl = document.getElementById("chat"); | |
| const statusEl = document.getElementById("status"); | |
| const textEl = document.getElementById("text"); | |
| const sendBtn = document.getElementById("send"); | |
| const stopBtn = document.getElementById("stop"); | |
| const setToggle= document.getElementById("settings-toggle"); | |
| const settings = document.getElementById("settings"); | |
| const tempEl = document.getElementById("temp"); | |
| const tempV = document.getElementById("temp-v"); | |
| const maxEl = document.getElementById("maxtok"); | |
| const maxV = document.getElementById("maxtok-v"); | |
| let conversation = []; | |
| let client = null; | |
| let job = null; | |
| const connect = async () => { | |
| if (!client) client = await Client.connect(window.location.origin, { events: ["status", "data"] }); | |
| return client; | |
| }; | |
| setToggle.addEventListener("click", () => settings.classList.toggle("open")); | |
| tempEl.addEventListener("input", () => tempV.textContent = tempEl.value); | |
| maxEl.addEventListener("input", () => maxV.textContent = maxEl.value); | |
| textEl.addEventListener("input", () => { | |
| textEl.style.height = "auto"; | |
| textEl.style.height = Math.min(textEl.scrollHeight, 160) + "px"; | |
| }); | |
| textEl.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } | |
| }); | |
| sendBtn.addEventListener("click", send); | |
| stopBtn.addEventListener("click", () => { if (job) job.cancel(); }); | |
| function scrollIfNear() { | |
| const near = chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight < 120; | |
| if (near) chatEl.scrollTop = chatEl.scrollHeight; | |
| } | |
| function addTurn(role) { | |
| const turn = document.createElement("div"); | |
| turn.className = "turn"; | |
| const line = document.createElement("div"); | |
| line.className = "prompt-line"; | |
| const prompt = document.createElement("div"); | |
| prompt.className = "prompt " + role; | |
| const content = document.createElement("div"); | |
| content.className = "content"; | |
| line.appendChild(prompt); line.appendChild(content); | |
| turn.appendChild(line); | |
| // Remove welcome message on first real turn. | |
| const welcome = chatEl.querySelector(".welcome"); | |
| if (welcome) welcome.remove(); | |
| chatEl.appendChild(turn); | |
| chatEl.scrollTop = chatEl.scrollHeight; | |
| return content; | |
| } | |
| async function send() { | |
| const message = textEl.value.trim(); | |
| if (!message) return; | |
| if (job) return; | |
| const userContent = addTurn("user"); | |
| userContent.innerHTML = `<div class="user-input">${escapeHtml(message)}</div>`; | |
| textEl.value = ""; textEl.style.height = "auto"; | |
| const history = conversation.slice(); | |
| conversation.push({ role: "user", content: message }); | |
| const assistantContent = addTurn("assistant"); | |
| assistantContent.innerHTML = `<span style="color:var(--muted)">…</span>`; | |
| setBusy(true); | |
| statusEl.textContent = "connecting…"; | |
| try { | |
| const c = await connect(); | |
| const payload = { | |
| message, | |
| history, | |
| max_new_tokens: parseInt(maxEl.value, 10), | |
| temperature: parseFloat(tempEl.value), | |
| }; | |
| job = c.submit("/generate", payload); | |
| let latestReasoning = ""; | |
| let latestAnswer = ""; | |
| let complete = false; | |
| for await (const msg of job) { | |
| if (msg.type === "status") { | |
| const s = msg.status || msg.data?.status; | |
| if (s === "pending") statusEl.textContent = `queued…${msg.position != null ? " (#" + msg.position + ")" : ""}`; | |
| else if (s === "generating") statusEl.textContent = "generating…"; | |
| else if (s === "complete" || s === "error") statusEl.textContent = ""; | |
| continue; | |
| } | |
| if (msg.type !== "data") continue; | |
| const raw = Array.isArray(msg.data) ? msg.data[0] : msg.data; | |
| let d = raw; | |
| if (typeof raw === "string") { try { d = JSON.parse(raw); } catch { d = { answer: raw }; } } | |
| if (d?.error) { | |
| renderAssistant(assistantContent, latestReasoning, latestAnswer, d.error); | |
| statusEl.innerHTML = `<span class="err">error: ${escapeHtml(d.error)}</span>`; | |
| break; | |
| } | |
| latestReasoning = d?.reasoning || ""; | |
| latestAnswer = d?.answer || ""; | |
| complete = d?.status === "complete"; | |
| renderAssistant(assistantContent, latestReasoning, latestAnswer); | |
| scrollIfNear(); | |
| } | |
| if (complete && latestAnswer) { | |
| conversation.push({ role: "assistant", content: latestAnswer }); | |
| } | |
| } catch (e) { | |
| renderAssistant(assistantContent, "", "", String(e)); | |
| statusEl.innerHTML = `<span class="err">${escapeHtml(String(e))}</span>`; | |
| } finally { | |
| job = null; | |
| setBusy(false); | |
| statusEl.textContent = ""; | |
| } | |
| } | |
| function renderAssistant(el, reasoning, answer, error) { | |
| let h = ""; | |
| if (reasoning && reasoning.trim()) { | |
| h += `<details class="thinking"${answer ? "" : " open"}><summary>thinking</summary><pre>${escapeHtml(reasoning)}</pre></details>`; | |
| } | |
| if (error) { | |
| h += `<div class="err">⚠ ${escapeHtml(error)}</div>`; | |
| } else if (answer && answer.trim()) { | |
| h += `<div class="answer">${DOMPurify.sanitize(marked.parse(answer))}</div>`; | |
| } else if (!reasoning) { | |
| h += `<span style="color:var(--muted)">…</span>`; | |
| } | |
| el.innerHTML = h; | |
| } | |
| function setBusy(busy) { | |
| sendBtn.disabled = busy; | |
| sendBtn.style.display = busy ? "none" : ""; | |
| stopBtn.style.display = busy ? "" : "none"; | |
| } | |
| function escapeHtml(s) { | |
| return String(s ?? "") | |
| .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") | |
| .replace(/"/g, """).replace(/'/g, "'"); | |
| } | |
| textEl.focus(); | |
| </script> | |
| </body> | |
| </html> | |