kimi-k3-demo / index.html
Shublub's picture
Remove image support; inform users model does not support image input
d115904 verified
Raw
History Blame Contribute Delete
13.2 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 Demo</title>
<style>
:root {
--primary: #4f6ef7;
--primary-hover: #3b5de7;
--bg: #0f0f1a;
--surface: #1a1a2e;
--surface-2: #252540;
--text: #e8e8f0;
--text-muted: #8888aa;
--border: #2a2a45;
--success: #34c759;
--radius: 12px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
justify-content: center;
min-height: 100vh;
}
.container { width: 100%; max-width: 860px; padding: 20px; display: flex; flex-direction: column; }
header { text-align: center; padding: 30px 0 20px; }
header h1 { font-size: 2.2em; font-weight: 700; letter-spacing: -0.5px; }
header h1 span { color: var(--primary); }
header p { color: var(--text-muted); margin-top: 6px; font-size: 0.95em; }
header .links { margin-top: 10px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
header .links a {
color: var(--primary); text-decoration: none; font-size: 0.9em; padding: 4px 12px;
border: 1px solid var(--border); border-radius: 20px; transition: 0.2s;
}
header .links a:hover { background: var(--surface-2); border-color: var(--primary); }
.settings {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
padding: 16px 20px; margin-bottom: 16px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;
}
.settings label { font-size: 0.85em; color: var(--text-muted); display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 200px; }
.settings input, .settings select {
background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
padding: 8px 12px; border-radius: 8px; font-size: 0.9em; width: 100%;
}
.settings input:focus, .settings select:focus { outline: none; border-color: var(--primary); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-dot.off { background: #ff3b30; }
.status-dot.on { background: var(--success); }
.chat {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
display: flex; flex-direction: column; flex: 1; min-height: 400px;
}
.messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; max-height: 500px; }
.msg { max-width: 85%; padding: 12px 16px; border-radius: var(--radius); line-height: 1.5; font-size: 0.92em; }
.msg.user { background: var(--primary); align-self: flex-end; border-bottom-right-radius: 4px; }
.msg.assistant { background: var(--surface-2); align-self: flex-start; border-bottom-left-radius: 4px; }
.msg.system { background: transparent; align-self: center; font-size: 0.82em; color: var(--text-muted); text-align: center; }
.msg .thinking {
background: rgba(79, 110, 247, 0.12); border-left: 3px solid var(--primary);
padding: 8px 12px; border-radius: 6px; margin: 8px 0; font-size: 0.88em; color: #9999cc;
}
.msg .thinking-label { font-weight: 600; color: var(--primary); margin-bottom: 4px; font-size: 0.85em; }
.typing { display: flex; gap: 4px; padding: 4px 0; }
.typing span { width: 6px; height: 6px; background: var(--text-muted); border-radius: 50%; animation: bounce 1.4s infinite; }
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce { 0%,60%,100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
.input-row {
display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border); align-items: center;
}
.input-row textarea {
flex: 1; background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
padding: 10px 14px; border-radius: 8px; font-size: 0.95em; resize: none; font-family: inherit;
min-height: 42px; max-height: 120px;
}
.input-row textarea:focus { outline: none; border-color: var(--primary); }
.input-row button {
background: var(--primary); color: white; border: none; padding: 10px 20px;
border-radius: 8px; font-size: 0.9em; cursor: pointer; font-weight: 600; transition: 0.2s; white-space: nowrap;
}
.input-row button:hover { background: var(--primary-hover); }
.input-row button:disabled { opacity: 0.5; cursor: not-allowed; }
pre { background: #1a1a2e; border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; overflow-x: auto; margin: 8px 0; font-size: 0.85em; }
code { background: var(--surface-2); padding: 2px 4px; border-radius: 4px; font-size: 0.9em; }
pre code { background: none; padding: 0; }
.footer { text-align: center; padding: 20px 0; font-size: 0.82em; color: var(--text-muted); }
.footer a { color: var(--primary); text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>🌙 <span>Kimi K3</span></h1>
<p>Moonshot AI · 2.8T-parameter native multimodal agentic model</p>
<div class="links">
<a href="https://huggingface.co/moonshotai/Kimi-K3" target="_blank">Model Card</a>
<a href="https://platform.kimi.ai" target="_blank">platform.kimi.ai</a>
<a href="https://github.com/MoonshotAI/Kimi-K3" target="_blank">GitHub</a>
</div>
</header>
<div class="settings">
<label>
Kimi API Key
<input type="password" id="apiKey" placeholder="sk-..." />
</label>
<label style="flex: 0.4; min-width: 100px;">
Reasoning
<select id="reasoningEffort">
<option value="max">max</option>
<option value="high">high</option>
<option value="low">low</option>
</select>
</label>
<div style="font-size: 0.85em; color: var(--text-muted);">
<span class="status-dot off" id="statusDot"></span>
<span id="statusText">Not configured</span>
</div>
</div>
<div class="chat">
<div class="messages" id="messages">
<div class="msg system">Welcome! Enter your Kimi API key above, then start chatting.</div>
<div class="msg system">Text only. This model does not support image input.</div>
</div>
<div class="input-row">
<textarea id="messageInput" placeholder="Type a message..." rows="1"></textarea>
<button id="sendBtn">Send</button>
</div>
</div>
<div class="footer">
Built with <a href="https://huggingface.co/moonshotai/Kimi-K3" target="_blank">Kimi K3</a> ·
API provided by <a href="https://platform.kimi.ai" target="_blank">Moonshot AI</a>
</div>
</div>
<script>
const KIMI_API = "https://api.moonshot.ai/v1";
const MODEL = "kimi-k3";
const apiKeyInput = document.getElementById("apiKey");
const reasoningSelect = document.getElementById("reasoningEffort");
const messagesEl = document.getElementById("messages");
const messageInput = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const statusDot = document.getElementById("statusDot");
const statusText = document.getElementById("statusText");
apiKeyInput.addEventListener("input", () => {
if (apiKeyInput.value.startsWith("sk-")) {
statusDot.className = "status-dot on";
statusText.textContent = "API key set";
} else if (apiKeyInput.value) {
statusDot.className = "status-dot off";
statusText.textContent = "Invalid key format";
} else {
statusDot.className = "status-dot off";
statusText.textContent = "Not configured";
}
});
function addMessage(role, content, thinking) {
const div = document.createElement("div");
div.className = `msg ${role}`;
if (role === "system") {
div.textContent = content;
} else {
if (thinking) {
const tBox = document.createElement("div");
tBox.className = "thinking";
tBox.innerHTML = `<div class="thinking-label">🧠 Reasoning</div>${escapeHtml(thinking)}`;
div.appendChild(tBox);
}
div.innerHTML += markedParse(content);
}
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function showTyping() {
const div = document.createElement("div");
div.className = "msg assistant";
div.id = "typingIndicator";
div.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>';
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function removeTyping() {
const el = document.getElementById("typingIndicator");
if (el) el.remove();
}
function updateLastMessage(content, thinking) {
const msgs = messagesEl.querySelectorAll(".msg.assistant:not(.error)");
const last = msgs[msgs.length - 1];
if (last) {
last.innerHTML = "";
if (thinking) {
const tBox = document.createElement("div");
tBox.className = "thinking";
tBox.innerHTML = `<div class="thinking-label">🧠 Reasoning</div>${escapeHtml(thinking)}`;
last.appendChild(tBox);
}
last.innerHTML += markedParse(content);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function markedParse(text) {
if (!text) return "";
let html = escapeHtml(text);
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
html = html.replace(codeBlockRegex, (_, lang, code) => {
const langAttr = lang ? ` class="language-${lang}"` : "";
return `<pre><code${langAttr}>${escapeHtml(code)}</code></pre>`;
});
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/\n/g, '<br>');
html = html.replace(/<br><br>/g, '</p><p>');
html = '<p>' + html + '</p>';
html = html.replace(/<p><br><\/p>/g, '');
html = html.replace(/<p><\/p>/g, '');
return html;
}
async function sendMessage() {
const text = messageInput.value.trim();
if (!text) return;
const apiKey = apiKeyInput.value.trim();
if (!apiKey || !apiKey.startsWith("sk-")) {
addMessage("system", "Please enter a valid Kimi API key (sk-...) in the settings.");
return;
}
addMessage("user", text);
messageInput.value = "";
showTyping();
try {
const messages = [{ role: "user", content: text }];
const response = await fetch(`${KIMI_API}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
body: JSON.stringify({
model: MODEL,
messages,
stream: true,
max_tokens: 8192,
reasoning_effort: reasoningSelect.value,
}),
});
if (!response.ok) {
const err = await response.text();
throw new Error(`API error (${response.status}): ${err}`);
}
removeTyping();
addMessage("assistant", "");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let reasoningContent = "";
let content = "";
let gotFirst = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data: ")) continue;
const data = trimmed.slice(6);
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta;
if (delta?.reasoning_content) {
reasoningContent += delta.reasoning_content;
}
if (delta?.content) {
content += delta.content;
}
if (!gotFirst || delta?.reasoning_content || delta?.content) {
updateLastMessage(content, reasoningContent);
gotFirst = true;
}
} catch {}
}
}
const final = messagesEl.querySelectorAll(".msg.assistant:not(.error)");
const last = final[final.length - 1];
if (last && last.innerHTML.trim() === "") {
last.remove();
}
} catch (err) {
removeTyping();
let msg = err.message;
if (msg.includes("401") || msg.includes("Unauthorized")) {
msg = "Invalid API key. Check your key at platform.kimi.ai.";
} else if (msg.includes("402")) {
msg = "Payment required. Add credits at platform.kimi.ai.";
} else if (msg.includes("not support image") || msg.includes("image.png")) {
msg = "Cannot read image. This model does not support image input.";
}
addMessage("system", `Error: ${msg}`);
}
}
sendBtn.addEventListener("click", sendMessage);
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
messageInput.addEventListener("input", () => {
messageInput.style.height = "auto";
messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + "px";
});
</script>
</body>
</html>