Kimi-K3 / index.html
akhaliq's picture
akhaliq HF Staff
Trim placeholder and model label to reflect direct inference
78a122e
Raw
History Blame Contribute Delete
10.7 kB
<!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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
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>