| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Kimi K3 — Terminal</title> |
| <style> |
| :root { |
| --bg: #0a0a0a; |
| --panel: #0d0d0d; |
| --border: #2a2a2a; |
| --text: #e5e5e5; |
| --muted: #6e6e6e; |
| --accent: #00ff88; |
| --accent-dim: #00cc6a; |
| --user: #1a1a1a; |
| --assistant: #0d0d0d; |
| --prompt: #00ff88; |
| --error: #ff5555; |
| } |
| * { box-sizing: border-box; } |
| html, body { |
| margin: 0; padding: 0; height: 100%; |
| background: var(--bg); |
| color: var(--text); |
| font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Menlo', 'Consolas', monospace; |
| font-size: 14px; |
| line-height: 1.6; |
| } |
| #terminal { |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| max-width: 900px; |
| margin: 0 auto; |
| padding: 20px; |
| } |
| #header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 8px 0 16px; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 16px; |
| } |
| #header .brand { |
| color: var(--accent); |
| font-weight: bold; |
| letter-spacing: 2px; |
| } |
| #header .meta { |
| color: var(--muted); |
| font-size: 12px; |
| } |
| #output { |
| flex: 1; |
| overflow-y: auto; |
| padding: 8px 0; |
| scrollbar-width: thin; |
| scrollbar-color: var(--border) transparent; |
| } |
| #output::-webkit-scrollbar { width: 6px; } |
| #output::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } |
| .line { |
| margin: 8px 0; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| } |
| .line.user .prompt { color: var(--prompt); } |
| .line.user .cmd { color: var(--text); } |
| .line.user .content { color: var(--text); margin-left: 8px; } |
| .line.assistant .label { color: var(--accent); } |
| .line.assistant .text { color: var(--text); } |
| .line.assistant .think { |
| color: var(--muted); |
| font-style: italic; |
| border-left: 2px solid var(--border); |
| padding-left: 12px; |
| margin: 8px 0; |
| opacity: 0.85; |
| } |
| .line.assistant .think::before { |
| content: "↳ thinking: "; |
| color: var(--accent-dim); |
| font-style: normal; |
| } |
| .line img.attached { |
| max-width: 300px; |
| border: 1px solid var(--border); |
| margin: 8px 0; |
| display: block; |
| } |
| .line.system { |
| color: var(--muted); |
| font-size: 12px; |
| } |
| .cursor { |
| display: inline-block; |
| width: 8px; |
| height: 16px; |
| background: var(--accent); |
| animation: blink 1s step-end infinite; |
| vertical-align: middle; |
| margin-left: 2px; |
| } |
| @keyframes blink { 50% { opacity: 0; } } |
| #input-area { |
| border-top: 1px solid var(--border); |
| padding-top: 16px; |
| margin-top: 16px; |
| } |
| #prompt-line { |
| display: flex; |
| align-items: flex-start; |
| gap: 8px; |
| } |
| #prompt-line .prompt { |
| color: var(--prompt); |
| user-select: none; |
| font-weight: bold; |
| } |
| #prompt-line textarea { |
| flex: 1; |
| background: transparent; |
| color: var(--text); |
| border: none; |
| outline: none; |
| font-family: inherit; |
| font-size: 14px; |
| resize: none; |
| min-height: 24px; |
| max-height: 200px; |
| padding: 0; |
| caret-color: var(--accent); |
| } |
| #prompt-line textarea::placeholder { color: var(--muted); } |
| #status-line { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-top: 12px; |
| font-size: 12px; |
| color: var(--muted); |
| } |
| #status-line .left, #status-line .right { display: flex; gap: 12px; align-items: center; } |
| .pill { |
| border: 1px solid var(--border); |
| padding: 4px 10px; |
| border-radius: 999px; |
| cursor: pointer; |
| transition: all 0.15s; |
| background: transparent; |
| color: var(--muted); |
| font-family: inherit; |
| font-size: 12px; |
| } |
| .pill:hover { border-color: var(--accent); color: var(--text); } |
| .pill.active { border-color: var(--accent); color: var(--accent); } |
| .pill.icon { display: inline-flex; align-items: center; gap: 4px; } |
| .pill svg { width: 12px; height: 12px; } |
| #send-btn { |
| background: var(--accent); |
| color: #000; |
| border: none; |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-family: inherit; |
| font-weight: bold; |
| } |
| #send-btn:disabled { opacity: 0.3; cursor: not-allowed; } |
| #send-btn:hover:not(:disabled) { background: var(--accent-dim); } |
| #previews { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| margin-bottom: 8px; |
| } |
| #previews img { |
| width: 60px; height: 60px; |
| object-fit: cover; |
| border: 1px solid var(--border); |
| border-radius: 4px; |
| } |
| .blink-dot { |
| display: inline-block; |
| width: 6px; height: 6px; |
| border-radius: 50%; |
| background: var(--accent); |
| animation: blink 1s step-end infinite; |
| margin-right: 6px; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="terminal"> |
| <div id="header"> |
| <div> |
| <span class="brand">▶ KIMI·K3</span> |
| <span class="meta">— terminal.chat</span> |
| </div> |
| <div class="meta">moonshotai/Kimi-K3 · via HF Inference Providers</div> |
| </div> |
|
|
| <div id="output"> |
| <div class="line system"> |
| <span class="blink-dot"></span>connected · type a message and press enter |
| </div> |
| <div class="line system"> |
| ⓘ multimodal — attach images via the <span style="color:var(--accent)">[+]</span> button |
| </div> |
| </div> |
|
|
| <div id="input-area"> |
| <div id="previews"></div> |
| <div id="prompt-line"> |
| <span class="prompt">›</span> |
| <textarea id="input" rows="1" placeholder="ask anything..." autofocus></textarea> |
| <button id="send-btn" title="send">↑</button> |
| </div> |
| <div id="status-line"> |
| <div class="left"> |
| <button class="pill icon" id="attach-btn" title="attach image"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg> |
| attach |
| </button> |
| <input type="file" id="file-input" accept="image/*" multiple style="display:none" /> |
| </div> |
| <div class="right"> |
| <span class="pill active">K3 · DEFAULT</span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| const output = document.getElementById("output"); |
| const input = document.getElementById("input"); |
| const sendBtn = document.getElementById("send-btn"); |
| const attachBtn = document.getElementById("attach-btn"); |
| const fileInput = document.getElementById("file-input"); |
| const previews = document.getElementById("previews"); |
| |
| let history = []; |
| let attachedFiles = []; |
| let client = null; |
| |
| attachBtn.onclick = () => fileInput.click(); |
| fileInput.onchange = (e) => { |
| for (const f of e.target.files) { |
| attachedFiles.push(f); |
| const img = document.createElement("img"); |
| img.src = URL.createObjectURL(f); |
| previews.appendChild(img); |
| } |
| fileInput.value = ""; |
| }; |
| |
| input.addEventListener("input", () => { |
| input.style.height = "auto"; |
| input.style.height = Math.min(input.scrollHeight, 200) + "px"; |
| }); |
| |
| input.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| submit(); |
| } |
| }); |
| |
| sendBtn.onclick = submit; |
| |
| function scrollDown() { output.scrollTop = output.scrollHeight; } |
| |
| function appendLine(html) { |
| const div = document.createElement("div"); |
| div.className = "line"; |
| div.innerHTML = html; |
| output.appendChild(div); |
| scrollDown(); |
| return div; |
| } |
| |
| function escape(s) { |
| return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| } |
| |
| function splitThink(text) { |
| const parts = []; |
| const re = /<think>([\s\S]*?)<\/think>/g; |
| let last = 0; |
| let m; |
| while ((m = re.exec(text)) !== null) { |
| if (m.index > last) parts.push({ kind: "text", value: text.slice(last, m.index) }); |
| parts.push({ kind: "think", value: m[1] }); |
| last = re.lastIndex; |
| } |
| if (last < text.length) parts.push({ kind: "text", value: text.slice(last) }); |
| return parts; |
| } |
| |
| function renderAssistant(el, content) { |
| const parts = splitThink(content || ""); |
| let html = `<span class="label">› kimi:</span> `; |
| for (const p of parts) { |
| if (p.kind === "think") { |
| html += `<div class="think">${escape(p.value.trim())}</div>`; |
| } else if (p.value) { |
| html += `<span class="text">${escape(p.value)}</span>`; |
| } |
| } |
| html += `<span class="cursor"></span>`; |
| el.innerHTML = html; |
| } |
| |
| async function submit() { |
| const text = input.value.trim(); |
| if (!text && attachedFiles.length === 0) return; |
| |
| const attachments = attachedFiles.map(f => ({ url: URL.createObjectURL(f) })); |
| const userLine = appendLine(""); |
| let userHtml = `<span class="prompt">› you:</span> `; |
| for (const a of attachments) { |
| userHtml += `<img class="attached" src="${a.url}" />`; |
| } |
| userHtml += `<span class="cmd">${escape(text)}</span>`; |
| userLine.innerHTML = userHtml; |
| history.push({ role: "user", content: text }); |
| |
| const assistantLine = appendLine(`<span class="label">› kimi:</span> <span class="cursor"></span>`); |
| |
| sendBtn.disabled = true; |
| const originalSend = sendBtn.innerHTML; |
| sendBtn.innerHTML = "..."; |
| |
| try { |
| if (!client) client = await Client.connect(window.location.origin); |
| |
| const gradioMsg = { |
| text: text, |
| files: attachedFiles, |
| }; |
| |
| let fullText = ""; |
| let renderQueued = false; |
| |
| const submission = client.submit("/chat", { |
| message: gradioMsg, |
| history: history.slice(0, -1), |
| }); |
| |
| for await (const msg of submission) { |
| if (msg.type !== "data") continue; |
| const data = msg?.data?.[0]; |
| const txt = typeof data === "object" ? (data?.content || "") : (data || ""); |
| if (txt) { |
| fullText = txt; |
| if (!renderQueued) { |
| renderQueued = true; |
| requestAnimationFrame(() => { |
| renderQueued = false; |
| renderAssistant(assistantLine, fullText); |
| }); |
| } |
| } |
| } |
| |
| history.push({ role: "assistant", content: fullText }); |
| } catch (err) { |
| console.error(err); |
| assistantLine.innerHTML = `<span class="label">› kimi:</span> <span style="color:var(--error)">error: ${escape(err.message || String(err))}</span>`; |
| } |
| |
| attachedFiles = []; |
| previews.innerHTML = ""; |
| input.value = ""; |
| input.style.height = "auto"; |
| sendBtn.disabled = false; |
| sendBtn.innerHTML = originalSend; |
| input.focus(); |
| } |
| |
| window.addEventListener("keydown", (e) => { |
| if (e.key === "/" && document.activeElement !== input) { |
| e.preventDefault(); |
| input.focus(); |
| } |
| }); |
| </script> |
| </body> |
| </html> |