akhaliq HF Staff Claude commited on
Commit
0322825
·
1 Parent(s): 078b2dd

Redesign UI as terminal-style chat

Browse files

- Monospace font, prompt-prefixed lines, blinking cursor
- Single-column layout centered at 900px max
- Brand header with system info, minimal status bar
- Pills for mode/model, icon button for attach
- Streaming tokens with live re-render of assistant line

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. index.html +270 -236
index.html CHANGED
@@ -3,187 +3,248 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Kimi K3 Chat</title>
7
  <style>
8
  :root {
9
- --bg: #0f1115;
10
- --panel: #181b22;
11
- --border: #2a2f3a;
12
- --text: #e6e9ef;
13
- --muted: #9aa3b2;
14
- --accent: #6366f1;
15
- --accent-2: #4f46e5;
16
- --user: #1f2937;
17
- --assistant: #111827;
18
- --think: #facc15;
 
19
  }
20
  * { box-sizing: border-box; }
21
- html, body { margin: 0; padding: 0; height: 100%; }
22
- body {
23
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
24
  background: var(--bg);
25
  color: var(--text);
26
- display: flex;
27
- height: 100vh;
 
28
  }
29
- aside {
30
- width: 280px;
31
- background: var(--panel);
32
- border-right: 1px solid var(--border);
 
 
33
  padding: 20px;
34
- overflow-y: auto;
35
  }
36
- aside h1 { font-size: 20px; margin: 0 0 8px; }
37
- aside p { font-size: 13px; color: var(--muted); line-height: 1.5; }
38
- main {
39
- flex: 1;
40
  display: flex;
41
- flex-direction: column;
42
- max-width: calc(100% - 280px);
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
- #chat {
45
  flex: 1;
46
  overflow-y: auto;
47
- padding: 24px;
48
- display: flex;
49
- flex-direction: column;
50
- gap: 12px;
51
  }
52
- .msg {
53
- max-width: 80%;
54
- padding: 12px 16px;
55
- border-radius: 14px;
56
- line-height: 1.55;
57
  white-space: pre-wrap;
58
  word-wrap: break-word;
59
  }
60
- .msg.user {
61
- align-self: flex-end;
62
- background: var(--user);
63
- border: 1px solid var(--border);
64
- }
65
- .msg.assistant {
66
- align-self: flex-start;
67
- background: var(--assistant);
68
- border: 1px solid var(--border);
69
- }
70
- .msg .think {
71
- color: var(--think);
72
  font-style: italic;
 
 
 
73
  opacity: 0.85;
74
- margin-bottom: 6px;
75
- font-size: 13px;
76
- border-left: 3px solid var(--think);
77
- padding-left: 8px;
78
  }
79
- .msg img.attached {
80
- max-width: 220px;
81
- border-radius: 8px;
82
- margin-bottom: 8px;
 
 
 
 
 
83
  display: block;
84
  }
85
- .role {
86
- font-size: 11px;
87
  color: var(--muted);
88
- margin-bottom: 4px;
89
- text-transform: uppercase;
90
- letter-spacing: 0.5px;
91
  }
92
- form {
 
 
 
 
 
 
 
 
 
 
93
  border-top: 1px solid var(--border);
94
- padding: 14px 24px;
95
- background: var(--panel);
 
 
96
  display: flex;
97
- flex-direction: column;
98
- gap: 10px;
99
  }
100
- .previews { display: flex; gap: 8px; flex-wrap: wrap; }
101
- .previews img {
102
- width: 70px; height: 70px; object-fit: cover;
103
- border-radius: 8px; border: 1px solid var(--border);
104
  }
105
- .row { display: flex; gap: 8px; align-items: flex-end; }
106
- textarea {
107
  flex: 1;
108
- background: #0b0d12;
109
  color: var(--text);
110
- border: 1px solid var(--border);
111
- border-radius: 10px;
112
- padding: 10px 12px;
 
113
  resize: none;
114
- min-height: 44px;
115
  max-height: 200px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  font-family: inherit;
117
- font-size: 14px;
118
  }
119
- textarea:focus { outline: none; border-color: var(--accent); }
120
- button {
121
- background: var(--accent-2);
122
- color: white;
 
 
 
123
  border: none;
124
- padding: 10px 18px;
125
- border-radius: 10px;
 
126
  cursor: pointer;
127
- font-weight: 600;
128
- font-size: 14px;
 
 
 
129
  }
130
- button:hover { background: var(--accent); }
131
- button:disabled { opacity: 0.5; cursor: not-allowed; }
132
- button.ghost {
133
- background: transparent;
134
- border: 1px solid var(--border);
135
- color: var(--muted);
 
136
  }
137
- .toolbar { display: flex; gap: 8px; align-items: center; }
138
- .examples { display: flex; gap: 6px; flex-wrap: wrap; font-size: 12px; }
139
- .examples button {
140
- background: transparent;
141
  border: 1px solid var(--border);
142
- color: var(--muted);
143
- padding: 6px 10px;
144
- font-weight: 400;
 
 
 
 
 
 
145
  }
146
- .examples button:hover { color: var(--text); border-color: var(--accent); }
147
  </style>
148
  </head>
149
  <body>
150
- <aside>
151
- <h1>🌙 Kimi K3</h1>
152
- <p>
153
- <strong>Kimi K3</strong> — Moonshot AI's open 3T-class multimodal agentic model
154
- (2.8T params, 104B active MoE, 1M context).<br><br>
155
- Served via <a href="https://huggingface.co/moonshotai/Kimi-K3" target="_blank" style="color:var(--accent)">HF Inference Providers</a>
156
- (Together AI).<br><br>
157
- 🖼️ Attach images for multimodal chat.
158
- </p>
159
- </aside>
160
- <main>
161
- <div id="chat"></div>
162
- <form id="form">
163
- <div class="previews" id="previews"></div>
164
- <div class="row">
165
- <button type="button" id="attachBtn" class="ghost">📎 Attach image</button>
166
- <input type="file" id="fileInput" accept="image/*" multiple style="display:none" />
167
- <textarea id="input" placeholder="Message Kimi K3 — attach an image for multimodal chat…" rows="1"></textarea>
168
- <button type="submit" id="sendBtn">Send</button>
169
  </div>
170
- <div class="examples">
171
- <button type="button" class="ex">Explain mixture-of-experts routing like I'm five.</button>
172
- <button type="button" class="ex">Write a Python function that streams tokens from an SSE endpoint.</button>
173
- <button type="button" class="ex">What are the tradeoffs of MXFP4 quantization for a 3T-param model?</button>
 
 
 
 
 
174
  </div>
175
- </form>
176
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  <script type="module">
179
- import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
180
 
181
- const chat = document.getElementById("chat");
182
- const form = document.getElementById("form");
183
  const input = document.getElementById("input");
184
- const sendBtn = document.getElementById("sendBtn");
185
- const fileInput = document.getElementById("fileInput");
186
- const attachBtn = document.getElementById("attachBtn");
187
  const previews = document.getElementById("previews");
188
 
189
  let history = [];
@@ -191,6 +252,44 @@ let attachedFiles = [];
191
  let client = null;
192
 
193
  attachBtn.onclick = () => fileInput.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  function splitThink(text) {
196
  const parts = [];
@@ -206,126 +305,66 @@ function splitThink(text) {
206
  return parts;
207
  }
208
 
209
- function renderMessage(role, content, attachments = []) {
210
- const div = document.createElement("div");
211
- div.className = "msg " + role;
212
- const roleDiv = document.createElement("div");
213
- roleDiv.className = "role";
214
- roleDiv.textContent = role === "user" ? "You" : "Kimi K3";
215
- div.appendChild(roleDiv);
216
- for (const att of attachments) {
217
- const img = document.createElement("img");
218
- img.className = "attached";
219
- img.src = att.url;
220
- div.appendChild(img);
221
- }
222
- if (typeof content === "string") {
223
- const parts = splitThink(content);
224
- for (const p of parts) {
225
- if (p.kind === "think") {
226
- const t = document.createElement("div");
227
- t.className = "think";
228
- t.textContent = "💭 " + p.value.trim();
229
- div.appendChild(t);
230
- } else if (p.value) {
231
- const t = document.createElement("div");
232
- t.textContent = p.value;
233
- div.appendChild(t);
234
- }
235
  }
236
  }
237
- chat.appendChild(div);
238
- chat.scrollTop = chat.scrollHeight;
239
- return div;
240
  }
241
 
242
- attachBtn.onclick = () => fileInput.click();
243
- fileInput.onchange = (e) => {
244
- for (const f of e.target.files) {
245
- attachedFiles.push(f);
246
- const img = document.createElement("img");
247
- img.src = URL.createObjectURL(f);
248
- previews.appendChild(img);
249
- }
250
- fileInput.value = "";
251
- };
252
-
253
- input.oninput = () => {
254
- input.style.height = "auto";
255
- input.style.height = Math.min(input.scrollHeight, 200) + "px";
256
- };
257
-
258
- input.onkeydown = (e) => {
259
- if (e.key === "Enter" && !e.shiftKey) {
260
- e.preventDefault();
261
- form.requestSubmit();
262
- }
263
- };
264
-
265
- document.querySelectorAll(".ex").forEach(b => {
266
- b.onclick = () => {
267
- input.value = b.textContent;
268
- input.focus();
269
- };
270
- });
271
-
272
- async function send(text) {
273
- if (!text.trim() && attachedFiles.length === 0) return;
274
 
275
- // Show user message
276
  const attachments = attachedFiles.map(f => ({ url: URL.createObjectURL(f) }));
277
- renderMessage("user", text, attachments);
 
 
 
 
 
 
278
  history.push({ role: "user", content: text });
279
 
280
- // Show assistant placeholder
281
- const assistantEl = renderMessage("assistant", "▌");
282
 
283
  sendBtn.disabled = true;
284
- sendBtn.textContent = "...";
 
285
 
286
  try {
287
  if (!client) client = await Client.connect(window.location.origin);
288
 
289
  const gradioMsg = {
290
  text: text,
291
- files: attachedFiles.map(f => f),
292
  };
293
 
294
  let fullText = "";
295
- // Stream via SSE — accumulate partial chunks as they arrive
 
296
  const stream = client.stream("/chat", {
297
  message: gradioMsg,
298
  history: history.slice(0, -1),
299
  });
300
 
301
- // Subscribe to partial updates and re-render assistant message
302
- let renderQueued = false;
303
  stream.on("data", (chunk) => {
304
- // Gradio Server streaming emits cumulative outputs as data arrives
305
  const data = chunk?.data?.[0];
306
- const text = typeof data === "object" ? (data?.content || "") : (data || "");
307
- if (text) {
308
- fullText = text;
309
  if (!renderQueued) {
310
  renderQueued = true;
311
  requestAnimationFrame(() => {
312
  renderQueued = false;
313
- // Re-render assistant message
314
- assistantEl.querySelectorAll("div:not(.role)").forEach(n => n.remove());
315
- const parts = splitThink(fullText);
316
- for (const p of parts) {
317
- if (p.kind === "think") {
318
- const t = document.createElement("div");
319
- t.className = "think";
320
- t.textContent = "💭 " + p.value.trim();
321
- assistantEl.appendChild(t);
322
- } else if (p.value) {
323
- const t = document.createElement("div");
324
- t.textContent = p.value;
325
- assistantEl.appendChild(t);
326
- }
327
- }
328
- chat.scrollTop = chat.scrollHeight;
329
  });
330
  }
331
  }
@@ -339,29 +378,24 @@ async function send(text) {
339
  history.push({ role: "assistant", content: fullText });
340
  } catch (err) {
341
  console.error(err);
342
- assistantEl.querySelectorAll("div:not(.role)").forEach(n => n.remove());
343
- const t = document.createElement("div");
344
- t.textContent = "Error: " + (err.message || String(err));
345
- t.style.color = "#ef4444";
346
- assistantEl.appendChild(t);
347
  }
348
 
349
- // Reset
350
  attachedFiles = [];
351
  previews.innerHTML = "";
352
  input.value = "";
353
  input.style.height = "auto";
354
  sendBtn.disabled = false;
355
- sendBtn.textContent = "Send";
 
356
  }
357
 
358
- form.onsubmit = (e) => {
359
- e.preventDefault();
360
- const text = input.value.trim();
361
- const files = [...attachedFiles];
362
- input.value = "";
363
- send(text);
364
- };
365
  </script>
366
  </body>
367
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Kimi K3 — Terminal</title>
7
  <style>
8
  :root {
9
+ --bg: #0a0a0a;
10
+ --panel: #0d0d0d;
11
+ --border: #2a2a2a;
12
+ --text: #e5e5e5;
13
+ --muted: #6e6e6e;
14
+ --accent: #00ff88;
15
+ --accent-dim: #00cc6a;
16
+ --user: #1a1a1a;
17
+ --assistant: #0d0d0d;
18
+ --prompt: #00ff88;
19
+ --error: #ff5555;
20
  }
21
  * { box-sizing: border-box; }
22
+ html, body {
23
+ margin: 0; padding: 0; height: 100%;
 
24
  background: var(--bg);
25
  color: var(--text);
26
+ font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Menlo', 'Consolas', monospace;
27
+ font-size: 14px;
28
+ line-height: 1.6;
29
  }
30
+ #terminal {
31
+ height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ max-width: 900px;
35
+ margin: 0 auto;
36
  padding: 20px;
 
37
  }
38
+ #header {
 
 
 
39
  display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ padding: 8px 0 16px;
43
+ border-bottom: 1px solid var(--border);
44
+ margin-bottom: 16px;
45
+ }
46
+ #header .brand {
47
+ color: var(--accent);
48
+ font-weight: bold;
49
+ letter-spacing: 2px;
50
+ }
51
+ #header .meta {
52
+ color: var(--muted);
53
+ font-size: 12px;
54
  }
55
+ #output {
56
  flex: 1;
57
  overflow-y: auto;
58
+ padding: 8px 0;
59
+ scrollbar-width: thin;
60
+ scrollbar-color: var(--border) transparent;
 
61
  }
62
+ #output::-webkit-scrollbar { width: 6px; }
63
+ #output::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
64
+ .line {
65
+ margin: 8px 0;
 
66
  white-space: pre-wrap;
67
  word-wrap: break-word;
68
  }
69
+ .line.user .prompt { color: var(--prompt); }
70
+ .line.user .cmd { color: var(--text); }
71
+ .line.user .content { color: var(--text); margin-left: 8px; }
72
+ .line.assistant .label { color: var(--accent); }
73
+ .line.assistant .text { color: var(--text); }
74
+ .line.assistant .think {
75
+ color: var(--muted);
 
 
 
 
 
76
  font-style: italic;
77
+ border-left: 2px solid var(--border);
78
+ padding-left: 12px;
79
+ margin: 8px 0;
80
  opacity: 0.85;
 
 
 
 
81
  }
82
+ .line.assistant .think::before {
83
+ content: "↳ thinking: ";
84
+ color: var(--accent-dim);
85
+ font-style: normal;
86
+ }
87
+ .line img.attached {
88
+ max-width: 300px;
89
+ border: 1px solid var(--border);
90
+ margin: 8px 0;
91
  display: block;
92
  }
93
+ .line.system {
 
94
  color: var(--muted);
95
+ font-size: 12px;
 
 
96
  }
97
+ .cursor {
98
+ display: inline-block;
99
+ width: 8px;
100
+ height: 16px;
101
+ background: var(--accent);
102
+ animation: blink 1s step-end infinite;
103
+ vertical-align: middle;
104
+ margin-left: 2px;
105
+ }
106
+ @keyframes blink { 50% { opacity: 0; } }
107
+ #input-area {
108
  border-top: 1px solid var(--border);
109
+ padding-top: 16px;
110
+ margin-top: 16px;
111
+ }
112
+ #prompt-line {
113
  display: flex;
114
+ align-items: flex-start;
115
+ gap: 8px;
116
  }
117
+ #prompt-line .prompt {
118
+ color: var(--prompt);
119
+ user-select: none;
120
+ font-weight: bold;
121
  }
122
+ #prompt-line textarea {
 
123
  flex: 1;
124
+ background: transparent;
125
  color: var(--text);
126
+ border: none;
127
+ outline: none;
128
+ font-family: inherit;
129
+ font-size: 14px;
130
  resize: none;
131
+ min-height: 24px;
132
  max-height: 200px;
133
+ padding: 0;
134
+ caret-color: var(--accent);
135
+ }
136
+ #prompt-line textarea::placeholder { color: var(--muted); }
137
+ #status-line {
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ margin-top: 12px;
142
+ font-size: 12px;
143
+ color: var(--muted);
144
+ }
145
+ #status-line .left, #status-line .right { display: flex; gap: 12px; align-items: center; }
146
+ .pill {
147
+ border: 1px solid var(--border);
148
+ padding: 4px 10px;
149
+ border-radius: 999px;
150
+ cursor: pointer;
151
+ transition: all 0.15s;
152
+ background: transparent;
153
+ color: var(--muted);
154
  font-family: inherit;
155
+ font-size: 12px;
156
  }
157
+ .pill:hover { border-color: var(--accent); color: var(--text); }
158
+ .pill.active { border-color: var(--accent); color: var(--accent); }
159
+ .pill.icon { display: inline-flex; align-items: center; gap: 4px; }
160
+ .pill svg { width: 12px; height: 12px; }
161
+ #send-btn {
162
+ background: var(--accent);
163
+ color: #000;
164
  border: none;
165
+ width: 32px;
166
+ height: 32px;
167
+ border-radius: 50%;
168
  cursor: pointer;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ font-family: inherit;
173
+ font-weight: bold;
174
  }
175
+ #send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
176
+ #send-btn:hover:not(:disabled) { background: var(--accent-dim); }
177
+ #previews {
178
+ display: flex;
179
+ gap: 8px;
180
+ flex-wrap: wrap;
181
+ margin-bottom: 8px;
182
  }
183
+ #previews img {
184
+ width: 60px; height: 60px;
185
+ object-fit: cover;
 
186
  border: 1px solid var(--border);
187
+ border-radius: 4px;
188
+ }
189
+ .blink-dot {
190
+ display: inline-block;
191
+ width: 6px; height: 6px;
192
+ border-radius: 50%;
193
+ background: var(--accent);
194
+ animation: blink 1s step-end infinite;
195
+ margin-right: 6px;
196
  }
 
197
  </style>
198
  </head>
199
  <body>
200
+ <div id="terminal">
201
+ <div id="header">
202
+ <div>
203
+ <span class="brand"> KIMI·K3</span>
204
+ <span class="meta">— terminal.chat</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </div>
206
+ <div class="meta">moonshotai/Kimi-K3 · via HF Inference Providers</div>
207
+ </div>
208
+
209
+ <div id="output">
210
+ <div class="line system">
211
+ <span class="blink-dot"></span>connected · type a message and press enter
212
+ </div>
213
+ <div class="line system">
214
+ ⓘ multimodal — attach images via the <span style="color:var(--accent)">[+]</span> button
215
  </div>
216
+ </div>
217
+
218
+ <div id="input-area">
219
+ <div id="previews"></div>
220
+ <div id="prompt-line">
221
+ <span class="prompt">›</span>
222
+ <textarea id="input" rows="1" placeholder="ask anything, or task an agent..." autofocus></textarea>
223
+ <button id="send-btn" title="send">↑</button>
224
+ </div>
225
+ <div id="status-line">
226
+ <div class="left">
227
+ <button class="pill icon" id="attach-btn" title="attach image">
228
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
229
+ attach
230
+ </button>
231
+ <input type="file" id="file-input" accept="image/*" multiple style="display:none" />
232
+ </div>
233
+ <div class="right">
234
+ <span class="pill active">K3 · HIGH</span>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
 
240
  <script type="module">
241
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
242
 
243
+ const output = document.getElementById("output");
 
244
  const input = document.getElementById("input");
245
+ const sendBtn = document.getElementById("send-btn");
246
+ const attachBtn = document.getElementById("attach-btn");
247
+ const fileInput = document.getElementById("file-input");
248
  const previews = document.getElementById("previews");
249
 
250
  let history = [];
 
252
  let client = null;
253
 
254
  attachBtn.onclick = () => fileInput.click();
255
+ fileInput.onchange = (e) => {
256
+ for (const f of e.target.files) {
257
+ attachedFiles.push(f);
258
+ const img = document.createElement("img");
259
+ img.src = URL.createObjectURL(f);
260
+ previews.appendChild(img);
261
+ }
262
+ fileInput.value = "";
263
+ };
264
+
265
+ input.addEventListener("input", () => {
266
+ input.style.height = "auto";
267
+ input.style.height = Math.min(input.scrollHeight, 200) + "px";
268
+ });
269
+
270
+ input.addEventListener("keydown", (e) => {
271
+ if (e.key === "Enter" && !e.shiftKey) {
272
+ e.preventDefault();
273
+ submit();
274
+ }
275
+ });
276
+
277
+ sendBtn.onclick = submit;
278
+
279
+ function scrollDown() { output.scrollTop = output.scrollHeight; }
280
+
281
+ function appendLine(html) {
282
+ const div = document.createElement("div");
283
+ div.className = "line";
284
+ div.innerHTML = html;
285
+ output.appendChild(div);
286
+ scrollDown();
287
+ return div;
288
+ }
289
+
290
+ function escape(s) {
291
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
292
+ }
293
 
294
  function splitThink(text) {
295
  const parts = [];
 
305
  return parts;
306
  }
307
 
308
+ function renderAssistant(el, content) {
309
+ const parts = splitThink(content || "");
310
+ let html = `<span class="label">› kimi:</span> `;
311
+ for (const p of parts) {
312
+ if (p.kind === "think") {
313
+ html += `<div class="think">${escape(p.value.trim())}</div>`;
314
+ } else if (p.value) {
315
+ html += `<span class="text">${escape(p.value)}</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
  }
318
+ html += `<span class="cursor"></span>`;
319
+ el.innerHTML = html;
 
320
  }
321
 
322
+ async function submit() {
323
+ const text = input.value.trim();
324
+ if (!text && attachedFiles.length === 0) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
 
326
  const attachments = attachedFiles.map(f => ({ url: URL.createObjectURL(f) }));
327
+ const userLine = appendLine("");
328
+ let userHtml = `<span class="prompt">› you:</span> `;
329
+ for (const a of attachments) {
330
+ userHtml += `<img class="attached" src="${a.url}" />`;
331
+ }
332
+ userHtml += `<span class="cmd">${escape(text)}</span>`;
333
+ userLine.innerHTML = userHtml;
334
  history.push({ role: "user", content: text });
335
 
336
+ const assistantLine = appendLine(`<span class="label">› kimi:</span> <span class="cursor"></span>`);
 
337
 
338
  sendBtn.disabled = true;
339
+ const originalSend = sendBtn.innerHTML;
340
+ sendBtn.innerHTML = "...";
341
 
342
  try {
343
  if (!client) client = await Client.connect(window.location.origin);
344
 
345
  const gradioMsg = {
346
  text: text,
347
+ files: attachedFiles,
348
  };
349
 
350
  let fullText = "";
351
+ let renderQueued = false;
352
+
353
  const stream = client.stream("/chat", {
354
  message: gradioMsg,
355
  history: history.slice(0, -1),
356
  });
357
 
 
 
358
  stream.on("data", (chunk) => {
 
359
  const data = chunk?.data?.[0];
360
+ const txt = typeof data === "object" ? (data?.content || "") : (data || "");
361
+ if (txt) {
362
+ fullText = txt;
363
  if (!renderQueued) {
364
  renderQueued = true;
365
  requestAnimationFrame(() => {
366
  renderQueued = false;
367
+ renderAssistant(assistantLine, fullText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  });
369
  }
370
  }
 
378
  history.push({ role: "assistant", content: fullText });
379
  } catch (err) {
380
  console.error(err);
381
+ assistantLine.innerHTML = `<span class="label">› kimi:</span> <span style="color:var(--error)">error: ${escape(err.message || String(err))}</span>`;
 
 
 
 
382
  }
383
 
 
384
  attachedFiles = [];
385
  previews.innerHTML = "";
386
  input.value = "";
387
  input.style.height = "auto";
388
  sendBtn.disabled = false;
389
+ sendBtn.innerHTML = originalSend;
390
+ input.focus();
391
  }
392
 
393
+ window.addEventListener("keydown", (e) => {
394
+ if (e.key === "/" && document.activeElement !== input) {
395
+ e.preventDefault();
396
+ input.focus();
397
+ }
398
+ });
 
399
  </script>
400
  </body>
401
  </html>