Spaces:
Running
Running
| <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> | |