Kimi-K2.7-Code / index.html
akhaliq's picture
akhaliq HF Staff
Add live web preview: code bars with copy + sandboxed iframe preview for HTML/CSS/JS
4c41870
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kimi K2.7-Code — Terminal</title>
<meta name="description" content="Kimi K2.7-Code terminal — Moonshot AI multimodal code model.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,700;1,300&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #030303;
--surface: #0a0a0a;
--surface2: #111;
--border: rgba(255,255,255,0.07);
--border-hi: rgba(0, 230, 118, 0.35);
--text: #dcdcdc;
--text-dim: #555;
--text-muted:#333;
--green: #00e676;
--green-dim: rgba(0, 230, 118, 0.12);
--green-glow:rgba(0, 230, 118, 0.08);
--error: #ff5252;
--radius: 12px;
--mono: 'JetBrains Mono', 'Fira Code', monospace;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: 13.5px;
line-height: 1.65;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
/* Subtle scanline overlay — terminal feel */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.03) 2px,
rgba(0,0,0,0.03) 4px
);
pointer-events: none;
z-index: 9999;
}
/* ─── Shell layout ─── */
.shell {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 880px;
margin: 0 auto;
padding: 0 28px;
}
/* ─── Header ─── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 0 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
/* Moon SVG */
.moon-wrap {
position: relative;
width: 30px;
height: 30px;
flex-shrink: 0;
}
.logo-info { display: flex; flex-direction: column; gap: 1px; }
.logo-name {
font-size: 13px;
font-weight: 500;
color: var(--text);
letter-spacing: 0.02em;
}
.logo-sub {
font-size: 10px;
color: var(--text-dim);
font-weight: 300;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.status-pill {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-dim);
letter-spacing: 0.04em;
}
.status-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--text-muted);
transition: all 0.3s;
}
.status-dot.on {
background: var(--green);
box-shadow: 0 0 6px var(--green);
}
/* ─── Toolbar ─── */
.toolbar {
display: flex;
align-items: center;
gap: 0;
padding: 6px 0;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.tb-item {
font-size: 11px;
color: var(--text-muted);
padding: 4px 12px;
cursor: pointer;
border-right: 1px solid var(--border);
letter-spacing: 0.04em;
transition: color 0.15s;
user-select: none;
}
.tb-item:first-child { padding-left: 0; }
.tb-item:hover { color: var(--green); }
.tb-sep { flex: 1; }
.tb-model {
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
padding-right: 0;
}
/* ─── Chat area ─── */
.chat-area {
flex: 1;
overflow-y: auto;
padding: 24px 0 8px;
display: flex;
flex-direction: column;
scrollbar-width: thin;
scrollbar-color: #1a1a1a transparent;
}
.chat-area::-webkit-scrollbar { width: 3px; }
.chat-area::-webkit-scrollbar-thumb { background: #1a1a1a; }
/* ─── Hero ─── */
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 24px;
padding-bottom: 80px;
text-align: center;
}
.hero-moon {
position: relative;
width: 72px;
height: 72px;
animation: moonFloat 6s ease-in-out infinite;
}
@keyframes moonFloat {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
.hero-title {
font-size: 15px;
font-weight: 300;
color: var(--text-dim);
letter-spacing: 0.04em;
max-width: 400px;
}
.hero-title span {
color: var(--green);
font-weight: 400;
}
.hero-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 4px;
}
.chip {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 100px;
padding: 5px 14px;
font-size: 11px;
color: var(--text-dim);
cursor: pointer;
transition: all 0.2s;
font-family: var(--mono);
letter-spacing: 0.02em;
}
.chip:hover {
border-color: var(--border-hi);
color: var(--green);
background: var(--green-dim);
}
/* ─── Message rows ─── */
.msg-row {
padding: 18px 0;
border-bottom: 1px solid var(--border);
animation: msgIn 0.2s ease;
}
.msg-row:last-child { border-bottom: none; }
@keyframes msgIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Prompt prefix bar */
.msg-prefix {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.prefix-sigil {
color: var(--green);
font-size: 13px;
font-weight: 700;
}
.prefix-label { color: var(--text-muted); }
/* Message body */
.msg-body {
font-size: 13.5px;
line-height: 1.75;
font-weight: 300;
color: var(--text);
}
.msg-body.user {
color: rgba(220,220,220,0.9);
padding-left: 18px;
border-left: 1px solid var(--border);
}
.msg-body.kimi {
padding-left: 18px;
border-left: 1px solid var(--border-hi);
}
/* Markdown inside kimi responses */
.msg-body p { margin-bottom: 10px; }
.msg-body p:last-child { margin-bottom: 0; }
.msg-body h1,.msg-body h2,.msg-body h3 {
font-weight: 500;
margin: 18px 0 8px;
color: #eee;
}
.msg-body h1 { font-size: 16px; }
.msg-body h2 { font-size: 14px; }
.msg-body h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--green); }
.msg-body ul, .msg-body ol {
padding-left: 18px;
margin-bottom: 10px;
}
.msg-body li { margin-bottom: 3px; }
.msg-body code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
color: var(--green);
}
.msg-body pre {
background: #050505 !important;
border: 1px solid var(--border) !important;
border-left: 2px solid var(--green) !important;
border-radius: 6px 6px 0 0 !important;
padding: 14px 16px !important;
overflow-x: auto;
margin: 12px 0 0 !important;
position: relative;
}
.msg-body pre code {
background: none !important;
border: none !important;
padding: 0 !important;
color: #b0b0b0 !important;
font-size: 12.5px !important;
line-height: 1.7 !important;
}
/* Code block action bar */
.code-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #080808;
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 6px 6px;
padding: 5px 12px;
margin: 0 0 12px;
gap: 8px;
}
.code-lang {
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
font-family: var(--mono);
}
.code-actions { display: flex; gap: 6px; }
.code-btn {
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-dim);
font-family: var(--mono);
font-size: 10px;
padding: 3px 9px;
cursor: pointer;
transition: all 0.15s;
letter-spacing: 0.04em;
}
.code-btn:hover {
border-color: var(--border-hi);
color: var(--green);
background: var(--green-dim);
}
.code-btn.active {
border-color: var(--green);
color: var(--green);
background: var(--green-dim);
}
/* Live preview iframe panel */
.preview-panel {
display: none;
border: 1px solid var(--border-hi);
border-top: 2px solid var(--green);
border-radius: 0 0 8px 8px;
margin: -12px 0 12px;
overflow: hidden;
background: #fff;
position: relative;
}
.preview-panel.open { display: block; }
.preview-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 12px;
background: #0d0d0d;
border-bottom: 1px solid var(--border);
}
.preview-url {
font-size: 10px;
color: var(--text-muted);
font-family: var(--mono);
letter-spacing: 0.03em;
}
.preview-resize {
display: flex;
gap: 4px;
}
.resize-btn {
background: transparent;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-muted);
font-family: var(--mono);
font-size: 9px;
padding: 2px 7px;
cursor: pointer;
transition: all 0.15s;
}
.resize-btn:hover { color: var(--green); border-color: var(--border-hi); }
.resize-btn.active { color: var(--green); border-color: var(--green); background: var(--green-dim); }
.preview-frame-wrap {
width: 100%;
overflow: hidden;
transition: height 0.3s ease;
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
background: #fff;
}
.msg-body blockquote {
border-left: 2px solid var(--green);
padding-left: 14px;
margin: 10px 0;
color: var(--text-dim);
font-style: italic;
}
.msg-body table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
font-size: 12.5px;
}
.msg-body th, .msg-body td {
padding: 7px 12px;
border: 1px solid var(--border);
text-align: left;
}
.msg-body th {
background: var(--surface2);
font-weight: 500;
color: var(--green);
letter-spacing: 0.04em;
}
.msg-body a { color: var(--green); text-decoration: none; }
.msg-body a:hover { text-decoration: underline; }
/* Image pill */
.img-pill {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 10px;
margin-bottom: 8px;
font-size: 11px;
color: var(--text-dim);
}
.img-pill img {
width: 32px; height: 32px;
object-fit: cover;
border-radius: 3px;
}
/* Blinking block cursor while streaming */
.cursor {
display: inline-block;
width: 7px;
height: 13px;
background: var(--green);
margin-left: 2px;
vertical-align: middle;
animation: blink 0.9s step-end infinite;
}
@keyframes blink {
0%,100% { opacity: 1; }
50% { opacity: 0; }
}
/* System line */
.sys-line {
font-size: 11px;
color: var(--text-muted);
padding: 6px 0;
font-style: italic;
}
.sys-line.err { color: var(--error); font-style: normal; }
/* ─── Input zone ─── */
.input-zone {
flex-shrink: 0;
padding: 12px 0 24px;
}
/* Staged image preview */
.staged-bar {
display: none;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-bottom: none;
border-radius: var(--radius) var(--radius) 0 0;
}
.staged-bar.show { display: flex; }
.staged-bar img {
width: 32px; height: 32px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border);
}
.staged-bar-name {
flex: 1;
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.staged-rm {
width: 18px; height: 18px;
border-radius: 50%;
background: transparent;
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.staged-rm:hover { background: var(--error); border-color: var(--error); color: #fff; }
/* Input box */
.input-box {
display: flex;
align-items: flex-end;
gap: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-box.has-img {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.input-box:focus-within {
border-color: var(--border-hi);
box-shadow: 0 0 0 1px var(--green-glow), 0 0 20px var(--green-glow);
}
/* Terminal prompt prefix inside input */
.input-prompt {
font-size: 13px;
color: var(--green);
font-weight: 500;
user-select: none;
flex-shrink: 0;
padding-right: 8px;
padding-bottom: 2px;
letter-spacing: 0.02em;
}
.input-ta {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text);
font-family: var(--mono);
font-size: 13.5px;
font-weight: 300;
line-height: 1.5;
resize: none;
min-height: 22px;
max-height: 180px;
overflow-y: auto;
scrollbar-width: none;
}
.input-ta::placeholder { color: var(--text-muted); }
.input-ta::-webkit-scrollbar { display: none; }
.input-btns {
display: flex;
align-items: center;
gap: 6px;
padding-left: 10px;
flex-shrink: 0;
}
.icon-btn {
width: 30px; height: 30px;
border-radius: 6px;
background: transparent;
border: 1px solid transparent;
color: var(--text-dim);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s;
}
.icon-btn:hover {
background: var(--surface2);
border-color: var(--border);
color: var(--text);
}
.icon-btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.send-btn {
width: 30px; height: 30px;
border-radius: 6px;
background: var(--green);
border: none;
color: #000;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.send-btn:hover { background: #33eb91; }
.send-btn:disabled {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text-muted);
cursor: not-allowed;
}
.send-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.input-foot {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.04em;
}
/* Drag overlay */
.drag-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.92);
z-index: 9998;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
border: 1px dashed rgba(0,230,118,0.25);
}
.drag-overlay.on { display: flex; }
.drag-overlay svg { width: 40px; height: 40px; opacity: 0.35; }
.drag-overlay p { font-size: 13px; color: var(--text-muted); letter-spacing: 0.04em; }
#file-in { display: none; }
@media (max-width: 600px) {
.shell { padding: 0 14px; }
.toolbar { display: none; }
.hero-chips { display: none; }
}
</style>
</head>
<body>
<!-- Drag overlay -->
<div class="drag-overlay" id="drag-overlay">
<svg viewBox="0 0 24 24" fill="none" stroke="rgba(0,230,118,0.5)" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p>drop image to attach</p>
</div>
<div class="shell">
<!-- Header -->
<header class="header">
<div class="logo">
<!-- Moon eclipse SVG -->
<svg class="moon-wrap" viewBox="0 0 30 30" fill="none">
<circle cx="15" cy="15" r="14" fill="#080808" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<circle cx="15" cy="15" r="9" fill="#0f0f0f" stroke="rgba(255,255,255,0.08)" stroke-width="0.5"/>
<path d="M11 11 Q15 6 19 11 Q23 15 19 19 Q15 24 11 19 Q7 15 11 11Z"
fill="url(#hg)" opacity="0.95"/>
<defs>
<radialGradient id="hg" cx="62%" cy="28%" r="70%">
<stop offset="0%" stop-color="#fff" stop-opacity="0.95"/>
<stop offset="40%" stop-color="#888" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#000" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<div class="logo-info">
<div class="logo-name">Kimi K2.7-Code</div>
<div class="logo-sub">Moonshot AI · Novita</div>
</div>
</div>
<div class="header-right">
<div class="status-pill">
<div class="status-dot" id="sdot"></div>
<span id="stxt">connecting…</span>
</div>
</div>
</header>
<!-- Toolbar -->
<div class="toolbar">
<span class="tb-item" onclick="cmd('/clear')">clear</span>
<span class="tb-item" onclick="cmd('/history')">history</span>
<span class="tb-item" onclick="cmd('/model')">model info</span>
<div class="tb-sep"></div>
<span class="tb-model">moonshotai/Kimi-K2.7-Code:novita</span>
</div>
<!-- Chat area -->
<div class="chat-area" id="chat-area">
<div class="hero" id="hero">
<!-- Large moon -->
<svg class="hero-moon" viewBox="0 0 72 72" fill="none">
<circle cx="36" cy="36" r="35" fill="#070707" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<circle cx="36" cy="36" r="22" fill="#0e0e0e" stroke="rgba(255,255,255,0.07)" stroke-width="0.5"/>
<path d="M26 26 Q36 14 46 26 Q56 36 46 46 Q36 58 26 46 Q16 36 26 26Z"
fill="url(#hm)" opacity="0.95"/>
<defs>
<radialGradient id="hm" cx="60%" cy="26%" r="72%">
<stop offset="0%" stop-color="#fff" stop-opacity="1"/>
<stop offset="38%" stop-color="#777" stop-opacity="0.45"/>
<stop offset="100%" stop-color="#000" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<p class="hero-title">Seeking the optimal conversion from<br><span>energy to intelligence</span></p>
<div class="hero-chips">
<div class="chip" onclick="fillPrompt('Write a Python async web scraper with aiohttp')">python async scraper</div>
<div class="chip" onclick="fillPrompt('Explain this code and find bugs')">code review</div>
<div class="chip" onclick="fillPrompt('Write a Rust CLI tool for file compression')">rust cli tool</div>
<div class="chip" onclick="fillPrompt('What do you see in this image?')">vision analysis</div>
<div class="chip" onclick="fillPrompt('Design a REST API with FastAPI and SQLAlchemy')">fastapi design</div>
</div>
</div>
</div>
<!-- Input zone -->
<div class="input-zone">
<div class="staged-bar" id="staged-bar">
<img id="staged-thumb" src="" alt="">
<span class="staged-bar-name" id="staged-name"></span>
<button class="staged-rm" onclick="clearImg()"></button>
</div>
<div class="input-box" id="input-box">
<span class="input-prompt">&gt;_</span>
<textarea
class="input-ta"
id="pta"
placeholder="Throw me a hard one, I'm ready."
rows="1"
autofocus
></textarea>
<div class="input-btns">
<button class="icon-btn" onclick="triggerFile()" title="Attach image">
<svg viewBox="0 0 24 24">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
</svg>
</button>
<button class="send-btn" id="send-btn" onclick="handleSend()" title="Send (Enter)">
<svg viewBox="0 0 24 24">
<line x1="12" y1="19" x2="12" y2="5"/>
<polyline points="5 12 12 5 19 12"/>
</svg>
</button>
</div>
</div>
<div class="input-foot">
<span>enter to send · shift+enter for newline · /clear /history /model</span>
<span id="foot-time"></span>
</div>
</div>
</div>
<input type="file" id="file-in" accept="image/*" onchange="onFileChange(event)">
<!-- NOTE: using lowercase { client } as per Gradio Server docs demo -->
<script type="module">
import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
/* ── State ── */
let app = null;
let chatHistory = [];
let stagedFile = null;
let stagedB64 = "";
let isGenerating = false;
/* ── Clock ── */
setInterval(() => {
const el = document.getElementById("foot-time");
if (el) el.textContent = new Date().toTimeString().slice(0,8);
}, 1000);
/* ── Marked config ── */
marked.setOptions({ breaks: true });
/* ── Connect ── */
async function connect() {
try {
// Use lowercase `client` as in Gradio Server docs
app = await client(window.location.origin);
setStatus(true);
appendSys("session established · model ready");
} catch(e) {
setStatus(false);
appendSys("connection failed: " + e.message, true);
}
}
function setStatus(on) {
document.getElementById("sdot").className = "status-dot" + (on ? " on" : "");
document.getElementById("stxt").textContent = on ? "online" : "offline";
}
/* ── Toolbar commands ── */
window.cmd = function(c) {
if (c === "/clear") {
const ca = document.getElementById("chat-area");
ca.innerHTML = "";
chatHistory = [];
appendSys("terminal cleared");
return;
}
if (c === "/history") {
appendSys(`history buffer: ${chatHistory.length} messages`);
return;
}
if (c === "/model") {
appendSys("model: moonshotai/Kimi-K2.7-Code:novita");
appendSys("host: https://router.huggingface.co/v1");
appendSys("context: 128k tokens · multimodal: yes");
return;
}
};
/* ── Chip fill ── */
window.fillPrompt = function(t) {
const ta = document.getElementById("pta");
ta.value = t;
ta.dispatchEvent(new Event("input"));
ta.focus();
};
/* ── File handling ── */
window.triggerFile = function() { document.getElementById("file-in").click(); };
window.onFileChange = function(e) { const f = e.target.files[0]; if(f) stageFile(f); };
function stageFile(file) {
if (!file.type.startsWith("image/")) return;
stagedFile = file;
const reader = new FileReader();
reader.onload = ev => {
stagedB64 = ev.target.result;
document.getElementById("staged-thumb").src = stagedB64;
document.getElementById("staged-name").textContent =
file.name + " (" + (file.size/1024).toFixed(1) + " KB)";
document.getElementById("staged-bar").classList.add("show");
document.getElementById("input-box").classList.add("has-img");
};
reader.readAsDataURL(file);
}
window.clearImg = function() {
stagedFile = null; stagedB64 = "";
document.getElementById("file-in").value = "";
document.getElementById("staged-bar").classList.remove("show");
document.getElementById("input-box").classList.remove("has-img");
};
/* ── Drag & drop ── */
const overlay = document.getElementById("drag-overlay");
window.addEventListener("dragenter", e => { e.preventDefault(); overlay.classList.add("on"); });
overlay.addEventListener("dragover", e => e.preventDefault());
overlay.addEventListener("dragleave", e => { if (!overlay.contains(e.relatedTarget)) overlay.classList.remove("on"); });
overlay.addEventListener("drop", e => {
e.preventDefault(); overlay.classList.remove("on");
const f = e.dataTransfer.files[0]; if (f) stageFile(f);
});
/* ── Textarea ── */
const pta = document.getElementById("pta");
pta.addEventListener("input", function() {
this.style.height = "auto";
this.style.height = Math.min(this.scrollHeight, 180) + "px";
});
pta.addEventListener("keydown", e => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); }
});
/* ── DOM helpers ── */
const ca = () => document.getElementById("chat-area");
function hideHero() {
const h = document.getElementById("hero");
if (h) h.remove();
}
function scrollBot() {
const el = ca(); el.scrollTop = el.scrollHeight;
}
function appendSys(text, isErr = false) {
const d = document.createElement("div");
d.className = "sys-line" + (isErr ? " err" : "");
d.textContent = "// " + text;
ca().appendChild(d);
scrollBot();
}
function addUserRow(prompt, imgSrc, imgName) {
hideHero();
const row = document.createElement("div");
row.className = "msg-row";
let imgHtml = "";
if (imgSrc) {
imgHtml = `<div class="img-pill"><img src="${imgSrc}" alt=""><span>${escHtml(imgName)}</span></div><br>`;
}
row.innerHTML = `
<div class="msg-prefix">
<span class="prefix-sigil">›</span>
<span class="prefix-label">you</span>
</div>
<div class="msg-body user">${imgHtml}${escHtml(prompt)}</div>
`;
ca().appendChild(row);
scrollBot();
}
function addKimiRow() {
const row = document.createElement("div");
row.className = "msg-row";
row.innerHTML = `
<div class="msg-prefix">
<span class="prefix-sigil" style="color:#888;">◎</span>
<span class="prefix-label">kimi</span>
</div>
<div class="msg-body kimi" id="kimi-active"><span class="cursor"></span></div>
`;
ca().appendChild(row);
scrollBot();
return document.getElementById("kimi-active");
}
function escHtml(t) {
return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
/* ── Stream renderer ── */
function makeRenderer(el) {
let buf = "";
let pending = false;
function render() {
pending = false;
el.innerHTML = marked.parse(buf) + '<span class="cursor"></span>';
scrollBot();
}
return {
push(token) {
buf += token;
if (!pending) { pending = true; requestAnimationFrame(render); }
},
finish() {
el.id = "";
el.innerHTML = marked.parse(buf);
injectCodeBars(el);
scrollBot();
return buf;
}
};
}
/* ── Code bar + live preview injector ── */
function injectCodeBars(container) {
const pres = container.querySelectorAll("pre");
pres.forEach(pre => {
// Avoid double-processing
if (pre.nextElementSibling && pre.nextElementSibling.classList.contains("code-bar")) return;
const codeEl = pre.querySelector("code");
const rawCode = codeEl ? codeEl.textContent : pre.textContent;
// Detect language from class (e.g. language-html)
const cls = codeEl ? codeEl.className : "";
const langMatch = cls.match(/language-(\w+)/);
const lang = langMatch ? langMatch[1].toLowerCase() : "";
// Determine if this is a web-previewable block
const isHtml = lang === "html" || lang === "svg";
const isPreviewable = isHtml ||
(!lang && rawCode.trim().startsWith("<") && rawCode.includes("</"));
// Build action bar
const bar = document.createElement("div");
bar.className = "code-bar";
let previewBtn = "";
if (isPreviewable || lang === "css" || lang === "js" || lang === "javascript") {
previewBtn = `<button class="code-btn preview-toggle" title="Render preview">▶ Preview</button>`;
}
bar.innerHTML = `
<span class="code-lang">${lang || "code"}</span>
<div class="code-actions">
${previewBtn}
<button class="code-btn copy-btn" title="Copy">Copy</button>
</div>
`;
pre.after(bar);
// Copy button
const copyBtn = bar.querySelector(".copy-btn");
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(rawCode).then(() => {
copyBtn.textContent = "Copied!";
copyBtn.classList.add("active");
setTimeout(() => { copyBtn.textContent = "Copy"; copyBtn.classList.remove("active"); }, 1800);
});
});
// Preview toggle
const pvBtn = bar.querySelector(".preview-toggle");
if (!pvBtn) return;
// Build preview panel (hidden by default)
const panel = document.createElement("div");
panel.className = "preview-panel";
// Compose the HTML to render
let renderSrc = rawCode;
if (lang === "css") {
renderSrc = `<!DOCTYPE html><html><head><style>${rawCode}</style></head><body><p style='font-family:sans-serif;padding:20px;color:#333'>CSS preview — add HTML to see layout</p></body></html>`;
} else if (lang === "js" || lang === "javascript") {
renderSrc = `<!DOCTYPE html><html><body><script>${rawCode}<\/script></body></html>`;
}
panel.innerHTML = `
<div class="preview-topbar">
<span class="preview-url">▣ live preview — sandbox</span>
<div class="preview-resize">
<button class="resize-btn" data-h="260">S</button>
<button class="resize-btn active" data-h="420">M</button>
<button class="resize-btn" data-h="620">L</button>
<button class="resize-btn" data-h="900">XL</button>
</div>
</div>
<div class="preview-frame-wrap" style="height:420px">
<iframe class="preview-iframe"
sandbox="allow-scripts allow-forms allow-modals"
srcdoc=""
></iframe>
</div>
`;
bar.after(panel);
// Resize buttons
panel.querySelectorAll(".resize-btn").forEach(rb => {
rb.addEventListener("click", () => {
panel.querySelectorAll(".resize-btn").forEach(b => b.classList.remove("active"));
rb.classList.add("active");
panel.querySelector(".preview-frame-wrap").style.height = rb.dataset.h + "px";
});
});
let loaded = false;
pvBtn.addEventListener("click", () => {
const isOpen = panel.classList.toggle("open");
pvBtn.textContent = isOpen ? "▼ Hide" : "▶ Preview";
pvBtn.classList.toggle("active", isOpen);
if (isOpen && !loaded) {
loaded = true;
const iframe = panel.querySelector(".preview-iframe");
iframe.srcdoc = renderSrc;
}
scrollBot();
});
});
}
/* ── Send ── */
window.handleSend = async function() {
if (isGenerating) return;
const prompt = pta.value.trim();
if (!prompt && !stagedFile) return;
const imgSrc = stagedB64;
const imgName = stagedFile ? stagedFile.name : "";
const imgB64 = stagedB64;
pta.value = ""; pta.style.height = "auto";
clearImg();
addUserRow(prompt, imgSrc, imgName);
if (!app) {
appendSys("not connected — retrying…", true);
await connect();
if (!app) return;
}
isGenerating = true;
document.getElementById("send-btn").disabled = true;
const kimiEl = addKimiRow();
const renderer = makeRenderer(kimiEl);
let fullReply = "";
try {
// Use app.submit for streaming — matches Gradio Server @app.api generator
const job = app.submit("/chat", {
prompt,
history_json: JSON.stringify(chatHistory),
image_b64: imgB64
});
for await (const msg of job) {
if (msg.type === "data") {
const token = msg.data[0];
if (token) { renderer.push(token); fullReply += token; }
} else if (msg.type === "status" && msg.stage === "error") {
renderer.push("\n\n[endpoint error — check logs]");
}
}
fullReply = renderer.finish();
if (fullReply) {
chatHistory.push({ role: "user", content: prompt });
chatHistory.push({ role: "assistant", content: fullReply });
}
} catch(err) {
renderer.finish();
appendSys("error: " + err.message, true);
} finally {
isGenerating = false;
document.getElementById("send-btn").disabled = false;
pta.focus();
}
};
connect();
</script>
</body>
</html>