akhaliq HF Staff commited on
Commit
113f668
Β·
1 Parent(s): 7817577

Fix: use {client} import + stream_every; terminal x Moonshot blend UI

Browse files
Files changed (2) hide show
  1. app.py +8 -14
  2. index.html +494 -456
app.py CHANGED
@@ -1,7 +1,5 @@
1
  import os
2
  import json
3
- import base64
4
- import mimetypes
5
  from fastapi.responses import HTMLResponse
6
  from gradio import Server
7
  from openai import OpenAI
@@ -17,15 +15,15 @@ async def homepage():
17
  return HTMLResponse(content=f.read(), status_code=200)
18
 
19
 
20
- @app.api(name="chat")
21
  def chat(prompt: str, history_json: str, image_b64: str = ""):
22
  """
23
- Streaming chat β€” yields individual token DELTAS (not accumulated text).
24
- Frontend accumulates and renders them with a typewriter effect.
25
  """
26
  api_key = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_TOKEN") or ""
27
  if not api_key:
28
- yield "[ERROR]: HF_TOKEN is not configured. Add it to your Space secrets."
29
  return
30
 
31
  try:
@@ -48,23 +46,19 @@ def chat(prompt: str, history_json: str, image_b64: str = ""):
48
  })
49
 
50
  try:
51
- client = OpenAI(
52
  base_url="https://router.huggingface.co/v1",
53
  api_key=api_key,
54
  default_headers={"X-HF-Bill-To": "huggingface"}
55
  )
56
-
57
- stream = client.chat.completions.create(
58
  model="moonshotai/Kimi-K2.7-Code:fastest",
59
  messages=messages,
60
  stream=True,
61
  )
62
-
63
- # Yield each delta token individually so frontend can stream char-by-char
64
  for chunk in stream:
65
- delta = chunk.choices[0].delta.content if chunk.choices else None
66
- if delta:
67
- yield delta
68
 
69
  except Exception as e:
70
  yield f"\n[ERROR]: {str(e)}"
 
1
  import os
2
  import json
 
 
3
  from fastapi.responses import HTMLResponse
4
  from gradio import Server
5
  from openai import OpenAI
 
15
  return HTMLResponse(content=f.read(), status_code=200)
16
 
17
 
18
+ @app.api(name="chat", stream_every=0.05)
19
  def chat(prompt: str, history_json: str, image_b64: str = ""):
20
  """
21
+ Streaming chat β€” yields delta tokens via Gradio SSE queue.
22
+ stream_every=0.05 means tokens are flushed every 50ms.
23
  """
24
  api_key = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_TOKEN") or ""
25
  if not api_key:
26
+ yield "[ERROR]: HF_TOKEN not configured. Add it to your Space secrets."
27
  return
28
 
29
  try:
 
46
  })
47
 
48
  try:
49
+ oai = OpenAI(
50
  base_url="https://router.huggingface.co/v1",
51
  api_key=api_key,
52
  default_headers={"X-HF-Bill-To": "huggingface"}
53
  )
54
+ stream = oai.chat.completions.create(
 
55
  model="moonshotai/Kimi-K2.7-Code:fastest",
56
  messages=messages,
57
  stream=True,
58
  )
 
 
59
  for chunk in stream:
60
+ if chunk.choices and chunk.choices[0].delta.content:
61
+ yield chunk.choices[0].delta.content
 
62
 
63
  except Exception as e:
64
  yield f"\n[ERROR]: {str(e)}"
index.html CHANGED
@@ -3,58 +3,75 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Kimi K2.7-Code β€” Moonshot AI</title>
7
- <meta name="description" content="Kimi K2.7-Code terminal powered by Moonshot AI β€” fastest multimodal code model.">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
 
11
  <style>
12
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
 
14
  :root {
15
- --bg: #000;
16
- --surface: #0d0d0d;
17
- --surface2: #141414;
18
- --border: rgba(255,255,255,0.08);
19
- --border-hover: rgba(255,255,255,0.15);
20
- --text: #f5f5f5;
21
- --text-dim: #666;
22
- --text-muted: #444;
23
- --accent: #fff;
24
- --accent-glow: rgba(255,255,255,0.06);
25
- --error: #ff4d4d;
26
- --code-bg: #0a0a0a;
27
- --radius: 16px;
28
- --radius-sm: 8px;
29
  }
30
 
31
  html, body {
32
  height: 100%;
33
  background: var(--bg);
34
  color: var(--text);
35
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
36
- font-size: 15px;
37
- line-height: 1.6;
38
  overflow: hidden;
39
  -webkit-font-smoothing: antialiased;
40
  }
41
 
42
- /* ── Layout ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  .shell {
44
  display: flex;
45
  flex-direction: column;
46
  height: 100vh;
47
- max-width: 860px;
48
  margin: 0 auto;
49
- padding: 0 24px;
50
  }
51
 
52
- /* ── Header ── */
53
  .header {
54
  display: flex;
55
  align-items: center;
56
  justify-content: space-between;
57
- padding: 20px 0 16px;
58
  border-bottom: 1px solid var(--border);
59
  flex-shrink: 0;
60
  }
@@ -62,91 +79,142 @@
62
  .logo {
63
  display: flex;
64
  align-items: center;
65
- gap: 10px;
66
  }
67
 
68
- .moon-icon {
69
- width: 28px;
70
- height: 28px;
 
 
71
  flex-shrink: 0;
72
  }
73
 
74
- .logo-text {
75
- font-size: 14px;
 
 
76
  font-weight: 500;
77
  color: var(--text);
78
- letter-spacing: -0.01em;
79
  }
80
 
81
- .logo-model {
82
- font-size: 11px;
83
  color: var(--text-dim);
84
- font-weight: 400;
85
- margin-top: 1px;
 
 
 
 
 
 
 
86
  }
87
 
88
- .header-status {
89
  display: flex;
90
  align-items: center;
91
  gap: 6px;
92
- font-size: 12px;
93
  color: var(--text-dim);
 
94
  }
95
 
96
  .status-dot {
97
- width: 6px;
98
- height: 6px;
99
  border-radius: 50%;
100
- background: #3d9e3d;
101
- box-shadow: 0 0 6px #3d9e3d;
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  flex-shrink: 0;
103
  }
104
 
105
- .status-dot.offline {
106
- background: var(--text-muted);
107
- box-shadow: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
- /* ── Chat scroll area ── */
111
  .chat-area {
112
  flex: 1;
113
  overflow-y: auto;
114
- padding: 32px 0 16px;
115
  display: flex;
116
  flex-direction: column;
117
- gap: 0;
118
  scrollbar-width: thin;
119
  scrollbar-color: #1a1a1a transparent;
120
  }
 
 
121
 
122
- .chat-area::-webkit-scrollbar { width: 4px; }
123
- .chat-area::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 2px; }
124
-
125
- /* ── Hero (shown when no messages) ── */
126
  .hero {
127
  display: flex;
128
  flex-direction: column;
129
  align-items: center;
130
  justify-content: center;
131
  flex: 1;
132
- gap: 20px;
133
- padding-bottom: 60px;
134
  text-align: center;
135
  }
136
 
137
  .hero-moon {
138
  position: relative;
139
- width: 80px;
140
- height: 80px;
141
- margin-bottom: 8px;
142
  }
143
 
144
- .hero-subtitle {
145
- font-size: 16px;
146
- color: var(--text-dim);
 
 
 
 
147
  font-weight: 300;
148
- letter-spacing: -0.01em;
149
- max-width: 380px;
 
 
 
 
 
 
150
  }
151
 
152
  .hero-chips {
@@ -154,137 +222,113 @@
154
  flex-wrap: wrap;
155
  gap: 8px;
156
  justify-content: center;
157
- margin-top: 8px;
158
  }
159
 
160
- .hero-chip {
161
  background: var(--surface);
162
  border: 1px solid var(--border);
163
  border-radius: 100px;
164
- padding: 6px 14px;
165
- font-size: 12px;
166
  color: var(--text-dim);
167
  cursor: pointer;
168
  transition: all 0.2s;
169
- white-space: nowrap;
 
170
  }
171
-
172
- .hero-chip:hover {
173
- border-color: var(--border-hover);
174
- color: var(--text);
175
- background: var(--surface2);
176
  }
177
 
178
- /* ── Message Rows ── */
179
  .msg-row {
180
- padding: 20px 0;
181
  border-bottom: 1px solid var(--border);
182
- animation: fadeSlideIn 0.25s ease;
183
  }
184
-
185
  .msg-row:last-child { border-bottom: none; }
186
 
187
- @keyframes fadeSlideIn {
188
- from { opacity: 0; transform: translateY(8px); }
189
  to { opacity: 1; transform: translateY(0); }
190
  }
191
 
192
- .msg-label {
193
- font-size: 11px;
194
- font-weight: 500;
195
- letter-spacing: 0.08em;
196
- text-transform: uppercase;
197
- color: var(--text-muted);
198
- margin-bottom: 8px;
199
  display: flex;
200
  align-items: center;
201
  gap: 8px;
 
 
 
 
202
  }
203
 
204
- .msg-label.user-label { color: rgba(255,255,255,0.3); }
205
- .msg-label.kimi-label { color: rgba(255,255,255,0.18); }
206
-
207
- .msg-label-icon {
208
- width: 16px;
209
- height: 16px;
210
- border-radius: 50%;
211
- background: var(--surface2);
212
- border: 1px solid var(--border);
213
- display: flex;
214
- align-items: center;
215
- justify-content: center;
216
- font-size: 9px;
217
  }
218
 
 
 
 
219
  .msg-body {
220
- color: var(--text);
221
- font-size: 15px;
222
  line-height: 1.75;
223
  font-weight: 300;
 
224
  }
225
 
226
- .msg-body.user-body {
227
- color: rgba(255,255,255,0.85);
 
 
228
  }
229
 
230
- /* Staged image pill in user message */
231
- .img-pill {
232
- display: inline-flex;
233
- align-items: center;
234
- gap: 8px;
235
- background: var(--surface2);
236
- border: 1px solid var(--border);
237
- border-radius: var(--radius-sm);
238
- padding: 6px 10px;
239
- margin-bottom: 10px;
240
- font-size: 12px;
241
- color: var(--text-dim);
242
  }
243
 
244
- .img-pill img {
245
- width: 36px;
246
- height: 36px;
247
- object-fit: cover;
248
- border-radius: 4px;
249
- }
250
-
251
- /* ── Markdown rendering ── */
252
- .msg-body p { margin-bottom: 12px; }
253
  .msg-body p:last-child { margin-bottom: 0; }
254
 
255
- .msg-body h1, .msg-body h2, .msg-body h3 {
256
- color: var(--text);
257
  font-weight: 500;
258
- margin: 20px 0 8px;
259
- letter-spacing: -0.02em;
260
  }
261
-
262
- .msg-body h1 { font-size: 20px; }
263
- .msg-body h2 { font-size: 17px; }
264
- .msg-body h3 { font-size: 15px; }
265
 
266
  .msg-body ul, .msg-body ol {
267
- padding-left: 20px;
268
- margin-bottom: 12px;
269
  }
270
-
271
- .msg-body li { margin-bottom: 4px; }
272
 
273
  .msg-body code {
274
- font-family: 'JetBrains Mono', monospace;
275
- font-size: 13px;
276
  background: var(--surface2);
277
  border: 1px solid var(--border);
278
  border-radius: 4px;
279
- padding: 1px 6px;
280
- color: #e0e0e0;
281
  }
282
 
283
  .msg-body pre {
284
- background: var(--code-bg) !important;
285
  border: 1px solid var(--border) !important;
286
- border-radius: var(--radius-sm) !important;
287
- padding: 16px !important;
 
288
  overflow-x: auto;
289
  margin: 12px 0 !important;
290
  }
@@ -293,125 +337,132 @@
293
  background: none !important;
294
  border: none !important;
295
  padding: 0 !important;
296
- font-size: 13px !important;
297
- line-height: 1.6 !important;
298
- color: #ccc !important;
299
  }
300
 
301
  .msg-body blockquote {
302
- border-left: 2px solid var(--border-hover);
303
- padding-left: 16px;
304
- margin: 12px 0;
305
  color: var(--text-dim);
306
  font-style: italic;
307
  }
308
 
309
- .msg-body a { color: #aaa; text-decoration: underline; }
310
- .msg-body a:hover { color: var(--text); }
311
-
312
  .msg-body table {
313
  width: 100%;
314
  border-collapse: collapse;
315
- margin: 12px 0;
316
- font-size: 13px;
317
  }
318
-
319
  .msg-body th, .msg-body td {
320
- padding: 8px 12px;
321
  border: 1px solid var(--border);
322
  text-align: left;
323
  }
324
-
325
  .msg-body th {
326
  background: var(--surface2);
327
  font-weight: 500;
 
 
328
  }
329
 
330
- /* Streaming cursor */
331
- .stream-cursor {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  display: inline-block;
333
- width: 2px;
334
- height: 1em;
335
- background: var(--text);
336
  margin-left: 2px;
337
- vertical-align: text-bottom;
338
  animation: blink 0.9s step-end infinite;
339
  }
340
-
341
  @keyframes blink {
342
- 0%, 100% { opacity: 1; }
343
- 50% { opacity: 0; }
344
  }
345
 
346
- /* System messages */
347
- .sys-msg {
348
- font-size: 12px;
349
  color: var(--text-muted);
350
- padding: 8px 0;
351
- font-family: 'JetBrains Mono', monospace;
352
  }
 
353
 
354
- .sys-msg.error { color: var(--error); }
355
-
356
- /* ── Input area ── */
357
  .input-zone {
358
  flex-shrink: 0;
359
- padding: 16px 0 28px;
360
  }
361
 
362
  /* Staged image preview */
363
- .staged-preview {
364
  display: none;
365
  align-items: center;
366
  gap: 10px;
367
- padding: 10px 14px;
368
  background: var(--surface);
369
  border: 1px solid var(--border);
370
- border-radius: var(--radius) var(--radius) 0 0;
371
  border-bottom: none;
 
372
  }
 
373
 
374
- .staged-preview.visible { display: flex; }
375
-
376
- .staged-preview img {
377
- width: 40px;
378
- height: 40px;
379
  object-fit: cover;
380
- border-radius: 6px;
381
  border: 1px solid var(--border);
382
  }
383
 
384
- .staged-preview-name {
385
  flex: 1;
386
- font-size: 12px;
387
  color: var(--text-dim);
388
- font-family: 'JetBrains Mono', monospace;
389
  white-space: nowrap;
390
  overflow: hidden;
391
  text-overflow: ellipsis;
392
  }
393
 
394
- .staged-preview-remove {
395
- width: 20px;
396
- height: 20px;
397
  border-radius: 50%;
398
- background: var(--surface2);
399
  border: 1px solid var(--border);
400
- color: var(--text-dim);
401
- font-size: 11px;
402
  cursor: pointer;
403
  display: flex;
404
  align-items: center;
405
  justify-content: center;
406
  transition: all 0.15s;
407
- flex-shrink: 0;
408
- }
409
-
410
- .staged-preview-remove:hover {
411
- background: var(--error);
412
- border-color: var(--error);
413
- color: #fff;
414
  }
 
415
 
416
  /* Input box */
417
  .input-box {
@@ -422,53 +473,59 @@
422
  border: 1px solid var(--border);
423
  border-radius: var(--radius);
424
  padding: 12px 14px;
425
- transition: border-color 0.2s;
426
  }
427
-
428
- .input-box.has-image {
429
  border-top-left-radius: 0;
430
  border-top-right-radius: 0;
431
  }
432
-
433
  .input-box:focus-within {
434
- border-color: var(--border-hover);
 
435
  }
436
 
437
- .input-textarea {
 
 
 
 
 
 
 
 
 
 
 
 
438
  flex: 1;
439
  background: transparent;
440
  border: none;
441
  outline: none;
442
  color: var(--text);
443
- font-family: 'Inter', sans-serif;
444
- font-size: 15px;
445
  font-weight: 300;
446
  line-height: 1.5;
447
  resize: none;
448
- min-height: 24px;
449
- max-height: 200px;
450
  overflow-y: auto;
451
  scrollbar-width: none;
452
  }
 
 
453
 
454
- .input-textarea::placeholder {
455
- color: var(--text-muted);
456
- }
457
-
458
- .input-textarea::-webkit-scrollbar { display: none; }
459
-
460
- .input-actions {
461
  display: flex;
462
  align-items: center;
463
  gap: 6px;
464
- flex-shrink: 0;
465
  padding-left: 10px;
 
466
  }
467
 
468
  .icon-btn {
469
- width: 32px;
470
- height: 32px;
471
- border-radius: 8px;
472
  background: transparent;
473
  border: 1px solid transparent;
474
  color: var(--text-dim);
@@ -477,26 +534,18 @@
477
  justify-content: center;
478
  cursor: pointer;
479
  transition: all 0.15s;
480
- flex-shrink: 0;
481
  }
482
-
483
  .icon-btn:hover {
484
  background: var(--surface2);
485
  border-color: var(--border);
486
  color: var(--text);
487
  }
488
-
489
- .icon-btn svg {
490
- width: 16px;
491
- height: 16px;
492
- fill: currentColor;
493
- }
494
 
495
  .send-btn {
496
- width: 32px;
497
- height: 32px;
498
- border-radius: 8px;
499
- background: var(--text);
500
  border: none;
501
  color: #000;
502
  display: flex;
@@ -506,27 +555,23 @@
506
  transition: all 0.15s;
507
  flex-shrink: 0;
508
  }
509
-
510
- .send-btn:hover { background: #d0d0d0; }
511
-
512
  .send-btn:disabled {
513
  background: var(--surface2);
514
  border: 1px solid var(--border);
515
  color: var(--text-muted);
516
  cursor: not-allowed;
517
  }
 
518
 
519
- .send-btn svg {
520
- width: 14px;
521
- height: 14px;
522
- fill: currentColor;
523
- }
524
-
525
- .input-hint {
526
- font-size: 11px;
527
  color: var(--text-muted);
528
- text-align: center;
529
- margin-top: 10px;
530
  }
531
 
532
  /* Drag overlay */
@@ -535,34 +580,22 @@
535
  position: fixed;
536
  inset: 0;
537
  background: rgba(0,0,0,0.92);
538
- z-index: 999;
539
  flex-direction: column;
540
  align-items: center;
541
  justify-content: center;
542
- gap: 16px;
543
- border: 2px dashed rgba(255,255,255,0.2);
544
  }
 
 
 
545
 
546
- .drag-overlay.active { display: flex; }
547
-
548
- .drag-overlay-icon {
549
- width: 48px;
550
- height: 48px;
551
- opacity: 0.5;
552
- }
553
-
554
- .drag-overlay-text {
555
- font-size: 15px;
556
- color: rgba(255,255,255,0.4);
557
- font-weight: 300;
558
- }
559
 
560
- /* Hidden file input */
561
- #file-input { display: none; }
562
-
563
- /* Responsive */
564
- @media (max-width: 640px) {
565
- .shell { padding: 0 16px; }
566
  .hero-chips { display: none; }
567
  }
568
  </style>
@@ -571,12 +604,12 @@
571
 
572
  <!-- Drag overlay -->
573
  <div class="drag-overlay" id="drag-overlay">
574
- <svg class="drag-overlay-icon" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5">
575
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
576
  <polyline points="17 8 12 3 7 8"/>
577
  <line x1="12" y1="3" x2="12" y2="15"/>
578
  </svg>
579
- <span class="drag-overlay-text">Drop image to attach</span>
580
  </div>
581
 
582
  <div class="shell">
@@ -585,146 +618,179 @@
585
  <header class="header">
586
  <div class="logo">
587
  <!-- Moon eclipse SVG -->
588
- <svg class="moon-icon" viewBox="0 0 28 28" fill="none">
589
- <circle cx="14" cy="14" r="13" fill="#0d0d0d" stroke="rgba(255,255,255,0.12)" stroke-width="1"/>
590
- <circle cx="14" cy="14" r="9" fill="#1a1a1a" stroke="rgba(255,255,255,0.18)" stroke-width="0.5"/>
591
- <!-- eclipse highlight -->
592
- <path d="M10 10 Q14 5 18 10 Q22 14 18 18 Q14 23 10 18 Q6 14 10 10 Z"
593
- fill="url(#moonGrad)" opacity="0.9"/>
594
  <defs>
595
- <radialGradient id="moonGrad" cx="60%" cy="30%" r="70%">
596
- <stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
597
- <stop offset="40%" stop-color="#aaaaaa" stop-opacity="0.4"/>
598
- <stop offset="100%" stop-color="#000000" stop-opacity="0"/>
599
  </radialGradient>
600
  </defs>
601
  </svg>
602
- <div>
603
- <div class="logo-text">Kimi K2.7-Code</div>
604
- <div class="logo-model">Moonshot AI Β· Fastest</div>
605
  </div>
606
  </div>
607
- <div class="header-status" id="status-indicator">
608
- <div class="status-dot offline" id="status-dot"></div>
609
- <span id="status-text">Connecting…</span>
 
 
610
  </div>
611
  </header>
612
 
 
 
 
 
 
 
 
 
 
613
  <!-- Chat area -->
614
  <div class="chat-area" id="chat-area">
615
- <!-- Hero state -->
616
  <div class="hero" id="hero">
617
- <div class="hero-moon">
618
- <svg viewBox="0 0 80 80" fill="none">
619
- <circle cx="40" cy="40" r="39" fill="#0a0a0a" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
620
- <circle cx="40" cy="40" r="26" fill="#111" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
621
- <path d="M28 28 Q40 14 52 28 Q64 40 52 52 Q40 66 28 52 Q16 40 28 28 Z"
622
- fill="url(#heroMoonGrad)" opacity="0.95"/>
623
- <defs>
624
- <radialGradient id="heroMoonGrad" cx="60%" cy="28%" r="70%">
625
- <stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
626
- <stop offset="35%" stop-color="#888888" stop-opacity="0.5"/>
627
- <stop offset="100%" stop-color="#000000" stop-opacity="0"/>
628
- </radialGradient>
629
- </defs>
630
- </svg>
631
- </div>
632
- <p class="hero-subtitle">Seeking the optimal conversion from energy to intelligence</p>
633
  <div class="hero-chips">
634
- <div class="hero-chip" onclick="fillPrompt('Write a Python function to parse JSON')">Write Python code</div>
635
- <div class="hero-chip" onclick="fillPrompt('Explain this code and suggest improvements')">Review my code</div>
636
- <div class="hero-chip" onclick="fillPrompt('Debug this error and explain the fix')">Debug an error</div>
637
- <div class="hero-chip" onclick="fillPrompt('What do you see in this image?')">Analyze image</div>
 
638
  </div>
639
  </div>
640
  </div>
641
 
642
  <!-- Input zone -->
643
  <div class="input-zone">
644
- <!-- Staged image preview -->
645
- <div class="staged-preview" id="staged-preview">
646
  <img id="staged-thumb" src="" alt="">
647
- <span class="staged-preview-name" id="staged-name">image.jpg</span>
648
- <button class="staged-preview-remove" onclick="clearImage()" title="Remove">βœ•</button>
649
  </div>
650
-
651
- <!-- Input box -->
652
  <div class="input-box" id="input-box">
 
653
  <textarea
654
- class="input-textarea"
655
- id="prompt-input"
656
  placeholder="Throw me a hard one, I'm ready."
657
  rows="1"
658
  autofocus
659
  ></textarea>
660
- <div class="input-actions">
661
- <button class="icon-btn" onclick="triggerFile()" title="Attach image" id="attach-btn">
662
  <svg viewBox="0 0 24 24">
663
- <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" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
664
  </svg>
665
  </button>
666
- <button class="send-btn" id="send-btn" onclick="handleSend()" title="Send">
667
  <svg viewBox="0 0 24 24">
668
- <line x1="12" y1="19" x2="12" y2="5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
669
- <polyline points="5 12 12 5 19 12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
670
  </svg>
671
  </button>
672
  </div>
673
  </div>
674
- <div class="input-hint">Kimi K2.7-Code via Hugging Face Serverless Router</div>
 
 
 
675
  </div>
676
 
677
  </div>
678
 
679
- <input type="file" id="file-input" accept="image/*" onchange="onFileChange(event)">
680
-
681
- <!-- Marked for markdown -->
682
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
683
 
 
684
  <script type="module">
685
- import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
686
 
687
  /* ── State ── */
688
- let gradioClient = null;
689
  let chatHistory = [];
690
  let stagedFile = null;
691
  let stagedB64 = "";
692
  let isGenerating = false;
693
 
 
 
 
 
 
 
694
  /* ── Marked config ── */
695
- marked.setOptions({ breaks: true, gvim: false });
696
 
697
- /* ── Connect to Gradio backend ── */
698
  async function connect() {
699
  try {
700
- gradioClient = await Client.connect(window.location.origin);
 
701
  setStatus(true);
702
- } catch (e) {
 
703
  setStatus(false);
704
- appendSys(`Connection failed: ${e.message}`, true);
705
  }
706
  }
707
 
708
- function setStatus(online) {
709
- document.getElementById("status-dot").className = "status-dot" + (online ? "" : " offline");
710
- document.getElementById("status-text").textContent = online ? "Online" : "Offline";
711
  }
712
 
713
- /* ── Hero prompt chips ── */
714
- window.fillPrompt = function(text) {
715
- const ta = document.getElementById("prompt-input");
716
- ta.value = text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  ta.dispatchEvent(new Event("input"));
718
  ta.focus();
719
  };
720
 
721
- /* ── File staging ── */
722
- window.triggerFile = function() { document.getElementById("file-input").click(); };
723
-
724
- window.onFileChange = function(e) {
725
- const f = e.target.files[0];
726
- if (f) stageFile(f);
727
- };
728
 
729
  function stageFile(file) {
730
  if (!file.type.startsWith("image/")) return;
@@ -733,214 +799,186 @@ function stageFile(file) {
733
  reader.onload = ev => {
734
  stagedB64 = ev.target.result;
735
  document.getElementById("staged-thumb").src = stagedB64;
736
- document.getElementById("staged-name").textContent = file.name + " (" + (file.size/1024).toFixed(1) + " KB)";
737
- document.getElementById("staged-preview").classList.add("visible");
738
- document.getElementById("input-box").classList.add("has-image");
 
739
  };
740
  reader.readAsDataURL(file);
741
  }
742
 
743
- window.clearImage = function() {
744
  stagedFile = null; stagedB64 = "";
745
- document.getElementById("file-input").value = "";
746
- document.getElementById("staged-preview").classList.remove("visible");
747
- document.getElementById("input-box").classList.remove("has-image");
748
  };
749
 
750
  /* ── Drag & drop ── */
751
  const overlay = document.getElementById("drag-overlay");
752
- window.addEventListener("dragenter", e => { e.preventDefault(); overlay.classList.add("active"); });
753
- overlay.addEventListener("dragover", e => e.preventDefault());
754
- overlay.addEventListener("dragleave", e => { if (!overlay.contains(e.relatedTarget)) overlay.classList.remove("active"); });
755
  overlay.addEventListener("drop", e => {
756
- e.preventDefault(); overlay.classList.remove("active");
757
  const f = e.dataTransfer.files[0]; if (f) stageFile(f);
758
  });
759
 
760
- /* ── Textarea auto-resize ── */
761
- const textarea = document.getElementById("prompt-input");
762
- textarea.addEventListener("input", function() {
763
  this.style.height = "auto";
764
- this.style.height = Math.min(this.scrollHeight, 200) + "px";
765
  });
766
- textarea.addEventListener("keydown", e => {
767
  if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); }
768
  });
769
 
770
  /* ── DOM helpers ── */
 
 
771
  function hideHero() {
772
- const hero = document.getElementById("hero");
773
- if (hero) hero.remove();
774
  }
775
 
776
- function scrollBottom() {
777
- const ca = document.getElementById("chat-area");
778
- ca.scrollTop = ca.scrollHeight;
779
  }
780
 
781
- function appendSys(text, isError = false) {
782
- const el = document.createElement("div");
783
- el.className = "sys-msg" + (isError ? " error" : "");
784
- el.textContent = text;
785
- document.getElementById("chat-area").appendChild(el);
786
- scrollBottom();
787
  }
788
 
789
- function addUserRow(promptText, imgSrc, imgName) {
790
  hideHero();
791
- const ca = document.getElementById("chat-area");
792
  const row = document.createElement("div");
793
  row.className = "msg-row";
794
  let imgHtml = "";
795
  if (imgSrc) {
796
- imgHtml = `<div class="img-pill"><img src="${imgSrc}" alt=""><span>${imgName}</span></div><br>`;
797
  }
798
  row.innerHTML = `
799
- <div class="msg-label user-label">
800
- <div class="msg-label-icon">U</div>
801
- You
802
  </div>
803
- <div class="msg-body user-body">${imgHtml}${escapeHtml(promptText)}</div>
804
  `;
805
- ca.appendChild(row);
806
- scrollBottom();
807
  }
808
 
809
  function addKimiRow() {
810
- const ca = document.getElementById("chat-area");
811
  const row = document.createElement("div");
812
  row.className = "msg-row";
813
  row.innerHTML = `
814
- <div class="msg-label kimi-label">
815
- <div class="msg-label-icon">
816
- <svg width="10" height="10" viewBox="0 0 28 28" fill="none">
817
- <circle cx="14" cy="14" r="13" fill="#0d0d0d"/>
818
- <path d="M10 10 Q14 5 18 10 Q22 14 18 18 Q14 23 10 18 Q6 14 10 10 Z"
819
- fill="url(#mmg)" opacity="0.9"/>
820
- <defs>
821
- <radialGradient id="mmg" cx="60%" cy="30%" r="70%">
822
- <stop offset="0%" stop-color="#fff" stop-opacity="0.9"/>
823
- <stop offset="100%" stop-color="#000" stop-opacity="0"/>
824
- </radialGradient>
825
- </defs>
826
- </svg>
827
- </div>
828
- Kimi
829
  </div>
830
- <div class="msg-body" id="kimi-body-active"><span class="stream-cursor"></span></div>
831
  `;
832
- ca.appendChild(row);
833
- scrollBottom();
834
- return document.getElementById("kimi-body-active");
835
  }
836
 
837
- function escapeHtml(t) {
838
- return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
839
  }
840
 
841
- /* ── Typewriter rendering ── */
842
- // We accumulate tokens and re-render markdown periodically
843
- function makeStreamRenderer(bodyEl) {
844
- let buffer = "";
845
- let renderScheduled = false;
846
 
847
  function render() {
848
- renderScheduled = false;
849
- // Render markdown on the accumulated buffer with a trailing cursor
850
- bodyEl.innerHTML = marked.parse(buffer) + '<span class="stream-cursor"></span>';
851
- scrollBottom();
852
  }
853
 
854
  return {
855
  push(token) {
856
- buffer += token;
857
- if (!renderScheduled) {
858
- renderScheduled = true;
859
- // Throttle renders to ~60fps
860
- requestAnimationFrame(render);
861
- }
862
  },
863
  finish() {
864
- // Final render without cursor
865
- bodyEl.id = ""; // remove active id
866
- bodyEl.innerHTML = marked.parse(buffer);
867
- scrollBottom();
868
- return buffer;
869
  }
870
  };
871
  }
872
 
873
- /* ── Main send handler ── */
874
  window.handleSend = async function() {
875
  if (isGenerating) return;
876
- const rawPrompt = textarea.value.trim();
877
- if (!rawPrompt && !stagedFile) return;
878
 
879
- // Capture & clear inputs
880
- const prompt = rawPrompt;
881
- const imgSrc = stagedB64;
882
- const imgName = stagedFile ? stagedFile.name : "";
883
- const imgB64 = stagedB64;
884
 
885
- textarea.value = "";
886
- textarea.style.height = "auto";
887
- clearImage();
888
 
889
- // UI
890
  addUserRow(prompt, imgSrc, imgName);
891
 
892
- if (!gradioClient) {
893
- appendSys("Not connected to backend. Retrying…", true);
894
  await connect();
895
- if (!gradioClient) return;
896
  }
897
 
898
  isGenerating = true;
899
  document.getElementById("send-btn").disabled = true;
900
 
901
- const kimiBodyEl = addKimiRow();
902
- const renderer = makeStreamRenderer(kimiBodyEl);
903
- let fullReply = "";
904
 
905
  try {
906
- const job = gradioClient.submit("/chat", {
907
- prompt: prompt,
 
908
  history_json: JSON.stringify(chatHistory),
909
- image_b64: imgB64
910
  });
911
 
912
  for await (const msg of job) {
913
  if (msg.type === "data") {
914
- // Backend yields delta tokens β€” each msg.data[0] is one token
915
  const token = msg.data[0];
916
- if (token) {
917
- renderer.push(token);
918
- fullReply += token;
919
- }
920
  } else if (msg.type === "status" && msg.stage === "error") {
921
- renderer.push("\n\n[Error: request rejected by endpoint]");
922
  }
923
  }
924
 
925
  fullReply = renderer.finish();
926
 
927
- // Save to history
928
  if (fullReply) {
929
  chatHistory.push({ role: "user", content: prompt });
930
  chatHistory.push({ role: "assistant", content: fullReply });
931
  }
932
 
933
- } catch (err) {
934
  renderer.finish();
935
- appendSys(`Error: ${err.message}`, true);
936
  } finally {
937
  isGenerating = false;
938
  document.getElementById("send-btn").disabled = false;
939
- document.getElementById("prompt-input").focus();
940
  }
941
  };
942
 
943
- /* ── Boot ── */
944
  connect();
945
  </script>
946
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Kimi K2.7-Code β€” Terminal</title>
7
+ <meta name="description" content="Kimi K2.7-Code terminal β€” Moonshot AI multimodal code model.">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <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">
11
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
  <style>
13
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
 
15
  :root {
16
+ --bg: #030303;
17
+ --surface: #0a0a0a;
18
+ --surface2: #111;
19
+ --border: rgba(255,255,255,0.07);
20
+ --border-hi: rgba(0, 230, 118, 0.35);
21
+ --text: #dcdcdc;
22
+ --text-dim: #555;
23
+ --text-muted:#333;
24
+ --green: #00e676;
25
+ --green-dim: rgba(0, 230, 118, 0.12);
26
+ --green-glow:rgba(0, 230, 118, 0.08);
27
+ --error: #ff5252;
28
+ --radius: 12px;
29
+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
30
  }
31
 
32
  html, body {
33
  height: 100%;
34
  background: var(--bg);
35
  color: var(--text);
36
+ font-family: var(--mono);
37
+ font-size: 13.5px;
38
+ line-height: 1.65;
39
  overflow: hidden;
40
  -webkit-font-smoothing: antialiased;
41
  }
42
 
43
+ /* Subtle scanline overlay β€” terminal feel */
44
+ body::before {
45
+ content: '';
46
+ position: fixed;
47
+ inset: 0;
48
+ background: repeating-linear-gradient(
49
+ 0deg,
50
+ transparent,
51
+ transparent 2px,
52
+ rgba(0,0,0,0.03) 2px,
53
+ rgba(0,0,0,0.03) 4px
54
+ );
55
+ pointer-events: none;
56
+ z-index: 9999;
57
+ }
58
+
59
+ /* ─── Shell layout ─── */
60
  .shell {
61
  display: flex;
62
  flex-direction: column;
63
  height: 100vh;
64
+ max-width: 880px;
65
  margin: 0 auto;
66
+ padding: 0 28px;
67
  }
68
 
69
+ /* ─── Header ─── */
70
  .header {
71
  display: flex;
72
  align-items: center;
73
  justify-content: space-between;
74
+ padding: 18px 0 14px;
75
  border-bottom: 1px solid var(--border);
76
  flex-shrink: 0;
77
  }
 
79
  .logo {
80
  display: flex;
81
  align-items: center;
82
+ gap: 12px;
83
  }
84
 
85
+ /* Moon SVG */
86
+ .moon-wrap {
87
+ position: relative;
88
+ width: 30px;
89
+ height: 30px;
90
  flex-shrink: 0;
91
  }
92
 
93
+ .logo-info { display: flex; flex-direction: column; gap: 1px; }
94
+
95
+ .logo-name {
96
+ font-size: 13px;
97
  font-weight: 500;
98
  color: var(--text);
99
+ letter-spacing: 0.02em;
100
  }
101
 
102
+ .logo-sub {
103
+ font-size: 10px;
104
  color: var(--text-dim);
105
+ font-weight: 300;
106
+ letter-spacing: 0.06em;
107
+ text-transform: uppercase;
108
+ }
109
+
110
+ .header-right {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 16px;
114
  }
115
 
116
+ .status-pill {
117
  display: flex;
118
  align-items: center;
119
  gap: 6px;
120
+ font-size: 11px;
121
  color: var(--text-dim);
122
+ letter-spacing: 0.04em;
123
  }
124
 
125
  .status-dot {
126
+ width: 5px;
127
+ height: 5px;
128
  border-radius: 50%;
129
+ background: var(--text-muted);
130
+ transition: all 0.3s;
131
+ }
132
+ .status-dot.on {
133
+ background: var(--green);
134
+ box-shadow: 0 0 6px var(--green);
135
+ }
136
+
137
+ /* ─── Toolbar ─── */
138
+ .toolbar {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 0;
142
+ padding: 6px 0;
143
+ border-bottom: 1px solid var(--border);
144
  flex-shrink: 0;
145
  }
146
 
147
+ .tb-item {
148
+ font-size: 11px;
149
+ color: var(--text-muted);
150
+ padding: 4px 12px;
151
+ cursor: pointer;
152
+ border-right: 1px solid var(--border);
153
+ letter-spacing: 0.04em;
154
+ transition: color 0.15s;
155
+ user-select: none;
156
+ }
157
+ .tb-item:first-child { padding-left: 0; }
158
+ .tb-item:hover { color: var(--green); }
159
+
160
+ .tb-sep { flex: 1; }
161
+
162
+ .tb-model {
163
+ font-size: 10px;
164
+ color: var(--text-muted);
165
+ letter-spacing: 0.06em;
166
+ text-transform: uppercase;
167
+ padding-right: 0;
168
  }
169
 
170
+ /* ─── Chat area ─── */
171
  .chat-area {
172
  flex: 1;
173
  overflow-y: auto;
174
+ padding: 24px 0 8px;
175
  display: flex;
176
  flex-direction: column;
 
177
  scrollbar-width: thin;
178
  scrollbar-color: #1a1a1a transparent;
179
  }
180
+ .chat-area::-webkit-scrollbar { width: 3px; }
181
+ .chat-area::-webkit-scrollbar-thumb { background: #1a1a1a; }
182
 
183
+ /* ─── Hero ─── */
 
 
 
184
  .hero {
185
  display: flex;
186
  flex-direction: column;
187
  align-items: center;
188
  justify-content: center;
189
  flex: 1;
190
+ gap: 24px;
191
+ padding-bottom: 80px;
192
  text-align: center;
193
  }
194
 
195
  .hero-moon {
196
  position: relative;
197
+ width: 72px;
198
+ height: 72px;
199
+ animation: moonFloat 6s ease-in-out infinite;
200
  }
201
 
202
+ @keyframes moonFloat {
203
+ 0%, 100% { transform: translateY(0px); }
204
+ 50% { transform: translateY(-6px); }
205
+ }
206
+
207
+ .hero-title {
208
+ font-size: 15px;
209
  font-weight: 300;
210
+ color: var(--text-dim);
211
+ letter-spacing: 0.04em;
212
+ max-width: 400px;
213
+ }
214
+
215
+ .hero-title span {
216
+ color: var(--green);
217
+ font-weight: 400;
218
  }
219
 
220
  .hero-chips {
 
222
  flex-wrap: wrap;
223
  gap: 8px;
224
  justify-content: center;
225
+ margin-top: 4px;
226
  }
227
 
228
+ .chip {
229
  background: var(--surface);
230
  border: 1px solid var(--border);
231
  border-radius: 100px;
232
+ padding: 5px 14px;
233
+ font-size: 11px;
234
  color: var(--text-dim);
235
  cursor: pointer;
236
  transition: all 0.2s;
237
+ font-family: var(--mono);
238
+ letter-spacing: 0.02em;
239
  }
240
+ .chip:hover {
241
+ border-color: var(--border-hi);
242
+ color: var(--green);
243
+ background: var(--green-dim);
 
244
  }
245
 
246
+ /* ─── Message rows ─── */
247
  .msg-row {
248
+ padding: 18px 0;
249
  border-bottom: 1px solid var(--border);
250
+ animation: msgIn 0.2s ease;
251
  }
 
252
  .msg-row:last-child { border-bottom: none; }
253
 
254
+ @keyframes msgIn {
255
+ from { opacity: 0; transform: translateY(6px); }
256
  to { opacity: 1; transform: translateY(0); }
257
  }
258
 
259
+ /* Prompt prefix bar */
260
+ .msg-prefix {
 
 
 
 
 
261
  display: flex;
262
  align-items: center;
263
  gap: 8px;
264
+ margin-bottom: 8px;
265
+ font-size: 11px;
266
+ letter-spacing: 0.06em;
267
+ text-transform: uppercase;
268
  }
269
 
270
+ .prefix-sigil {
271
+ color: var(--green);
272
+ font-size: 13px;
273
+ font-weight: 700;
 
 
 
 
 
 
 
 
 
274
  }
275
 
276
+ .prefix-label { color: var(--text-muted); }
277
+
278
+ /* Message body */
279
  .msg-body {
280
+ font-size: 13.5px;
 
281
  line-height: 1.75;
282
  font-weight: 300;
283
+ color: var(--text);
284
  }
285
 
286
+ .msg-body.user {
287
+ color: rgba(220,220,220,0.9);
288
+ padding-left: 18px;
289
+ border-left: 1px solid var(--border);
290
  }
291
 
292
+ .msg-body.kimi {
293
+ padding-left: 18px;
294
+ border-left: 1px solid var(--border-hi);
 
 
 
 
 
 
 
 
 
295
  }
296
 
297
+ /* Markdown inside kimi responses */
298
+ .msg-body p { margin-bottom: 10px; }
 
 
 
 
 
 
 
299
  .msg-body p:last-child { margin-bottom: 0; }
300
 
301
+ .msg-body h1,.msg-body h2,.msg-body h3 {
 
302
  font-weight: 500;
303
+ margin: 18px 0 8px;
304
+ color: #eee;
305
  }
306
+ .msg-body h1 { font-size: 16px; }
307
+ .msg-body h2 { font-size: 14px; }
308
+ .msg-body h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--green); }
 
309
 
310
  .msg-body ul, .msg-body ol {
311
+ padding-left: 18px;
312
+ margin-bottom: 10px;
313
  }
314
+ .msg-body li { margin-bottom: 3px; }
 
315
 
316
  .msg-body code {
317
+ font-family: var(--mono);
318
+ font-size: 12.5px;
319
  background: var(--surface2);
320
  border: 1px solid var(--border);
321
  border-radius: 4px;
322
+ padding: 1px 5px;
323
+ color: var(--green);
324
  }
325
 
326
  .msg-body pre {
327
+ background: #050505 !important;
328
  border: 1px solid var(--border) !important;
329
+ border-left: 2px solid var(--green) !important;
330
+ border-radius: 6px !important;
331
+ padding: 14px 16px !important;
332
  overflow-x: auto;
333
  margin: 12px 0 !important;
334
  }
 
337
  background: none !important;
338
  border: none !important;
339
  padding: 0 !important;
340
+ color: #b0b0b0 !important;
341
+ font-size: 12.5px !important;
342
+ line-height: 1.7 !important;
343
  }
344
 
345
  .msg-body blockquote {
346
+ border-left: 2px solid var(--green);
347
+ padding-left: 14px;
348
+ margin: 10px 0;
349
  color: var(--text-dim);
350
  font-style: italic;
351
  }
352
 
 
 
 
353
  .msg-body table {
354
  width: 100%;
355
  border-collapse: collapse;
356
+ margin: 10px 0;
357
+ font-size: 12.5px;
358
  }
 
359
  .msg-body th, .msg-body td {
360
+ padding: 7px 12px;
361
  border: 1px solid var(--border);
362
  text-align: left;
363
  }
 
364
  .msg-body th {
365
  background: var(--surface2);
366
  font-weight: 500;
367
+ color: var(--green);
368
+ letter-spacing: 0.04em;
369
  }
370
 
371
+ .msg-body a { color: var(--green); text-decoration: none; }
372
+ .msg-body a:hover { text-decoration: underline; }
373
+
374
+ /* Image pill */
375
+ .img-pill {
376
+ display: inline-flex;
377
+ align-items: center;
378
+ gap: 8px;
379
+ background: var(--surface2);
380
+ border: 1px solid var(--border);
381
+ border-radius: 6px;
382
+ padding: 5px 10px;
383
+ margin-bottom: 8px;
384
+ font-size: 11px;
385
+ color: var(--text-dim);
386
+ }
387
+ .img-pill img {
388
+ width: 32px; height: 32px;
389
+ object-fit: cover;
390
+ border-radius: 3px;
391
+ }
392
+
393
+ /* Blinking block cursor while streaming */
394
+ .cursor {
395
  display: inline-block;
396
+ width: 7px;
397
+ height: 13px;
398
+ background: var(--green);
399
  margin-left: 2px;
400
+ vertical-align: middle;
401
  animation: blink 0.9s step-end infinite;
402
  }
 
403
  @keyframes blink {
404
+ 0%,100% { opacity: 1; }
405
+ 50% { opacity: 0; }
406
  }
407
 
408
+ /* System line */
409
+ .sys-line {
410
+ font-size: 11px;
411
  color: var(--text-muted);
412
+ padding: 6px 0;
413
+ font-style: italic;
414
  }
415
+ .sys-line.err { color: var(--error); font-style: normal; }
416
 
417
+ /* ─── Input zone ─── */
 
 
418
  .input-zone {
419
  flex-shrink: 0;
420
+ padding: 12px 0 24px;
421
  }
422
 
423
  /* Staged image preview */
424
+ .staged-bar {
425
  display: none;
426
  align-items: center;
427
  gap: 10px;
428
+ padding: 8px 14px;
429
  background: var(--surface);
430
  border: 1px solid var(--border);
 
431
  border-bottom: none;
432
+ border-radius: var(--radius) var(--radius) 0 0;
433
  }
434
+ .staged-bar.show { display: flex; }
435
 
436
+ .staged-bar img {
437
+ width: 32px; height: 32px;
 
 
 
438
  object-fit: cover;
439
+ border-radius: 4px;
440
  border: 1px solid var(--border);
441
  }
442
 
443
+ .staged-bar-name {
444
  flex: 1;
445
+ font-size: 11px;
446
  color: var(--text-dim);
 
447
  white-space: nowrap;
448
  overflow: hidden;
449
  text-overflow: ellipsis;
450
  }
451
 
452
+ .staged-rm {
453
+ width: 18px; height: 18px;
 
454
  border-radius: 50%;
455
+ background: transparent;
456
  border: 1px solid var(--border);
457
+ color: var(--text-muted);
458
+ font-size: 10px;
459
  cursor: pointer;
460
  display: flex;
461
  align-items: center;
462
  justify-content: center;
463
  transition: all 0.15s;
 
 
 
 
 
 
 
464
  }
465
+ .staged-rm:hover { background: var(--error); border-color: var(--error); color: #fff; }
466
 
467
  /* Input box */
468
  .input-box {
 
473
  border: 1px solid var(--border);
474
  border-radius: var(--radius);
475
  padding: 12px 14px;
476
+ transition: border-color 0.2s, box-shadow 0.2s;
477
  }
478
+ .input-box.has-img {
 
479
  border-top-left-radius: 0;
480
  border-top-right-radius: 0;
481
  }
 
482
  .input-box:focus-within {
483
+ border-color: var(--border-hi);
484
+ box-shadow: 0 0 0 1px var(--green-glow), 0 0 20px var(--green-glow);
485
  }
486
 
487
+ /* Terminal prompt prefix inside input */
488
+ .input-prompt {
489
+ font-size: 13px;
490
+ color: var(--green);
491
+ font-weight: 500;
492
+ user-select: none;
493
+ flex-shrink: 0;
494
+ padding-right: 8px;
495
+ padding-bottom: 2px;
496
+ letter-spacing: 0.02em;
497
+ }
498
+
499
+ .input-ta {
500
  flex: 1;
501
  background: transparent;
502
  border: none;
503
  outline: none;
504
  color: var(--text);
505
+ font-family: var(--mono);
506
+ font-size: 13.5px;
507
  font-weight: 300;
508
  line-height: 1.5;
509
  resize: none;
510
+ min-height: 22px;
511
+ max-height: 180px;
512
  overflow-y: auto;
513
  scrollbar-width: none;
514
  }
515
+ .input-ta::placeholder { color: var(--text-muted); }
516
+ .input-ta::-webkit-scrollbar { display: none; }
517
 
518
+ .input-btns {
 
 
 
 
 
 
519
  display: flex;
520
  align-items: center;
521
  gap: 6px;
 
522
  padding-left: 10px;
523
+ flex-shrink: 0;
524
  }
525
 
526
  .icon-btn {
527
+ width: 30px; height: 30px;
528
+ border-radius: 6px;
 
529
  background: transparent;
530
  border: 1px solid transparent;
531
  color: var(--text-dim);
 
534
  justify-content: center;
535
  cursor: pointer;
536
  transition: all 0.15s;
 
537
  }
 
538
  .icon-btn:hover {
539
  background: var(--surface2);
540
  border-color: var(--border);
541
  color: var(--text);
542
  }
543
+ .icon-btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
 
 
 
 
 
544
 
545
  .send-btn {
546
+ width: 30px; height: 30px;
547
+ border-radius: 6px;
548
+ background: var(--green);
 
549
  border: none;
550
  color: #000;
551
  display: flex;
 
555
  transition: all 0.15s;
556
  flex-shrink: 0;
557
  }
558
+ .send-btn:hover { background: #33eb91; }
 
 
559
  .send-btn:disabled {
560
  background: var(--surface2);
561
  border: 1px solid var(--border);
562
  color: var(--text-muted);
563
  cursor: not-allowed;
564
  }
565
+ .send-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
566
 
567
+ .input-foot {
568
+ display: flex;
569
+ justify-content: space-between;
570
+ align-items: center;
571
+ margin-top: 8px;
572
+ font-size: 10px;
 
 
573
  color: var(--text-muted);
574
+ letter-spacing: 0.04em;
 
575
  }
576
 
577
  /* Drag overlay */
 
580
  position: fixed;
581
  inset: 0;
582
  background: rgba(0,0,0,0.92);
583
+ z-index: 9998;
584
  flex-direction: column;
585
  align-items: center;
586
  justify-content: center;
587
+ gap: 14px;
588
+ border: 1px dashed rgba(0,230,118,0.25);
589
  }
590
+ .drag-overlay.on { display: flex; }
591
+ .drag-overlay svg { width: 40px; height: 40px; opacity: 0.35; }
592
+ .drag-overlay p { font-size: 13px; color: var(--text-muted); letter-spacing: 0.04em; }
593
 
594
+ #file-in { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
+ @media (max-width: 600px) {
597
+ .shell { padding: 0 14px; }
598
+ .toolbar { display: none; }
 
 
 
599
  .hero-chips { display: none; }
600
  }
601
  </style>
 
604
 
605
  <!-- Drag overlay -->
606
  <div class="drag-overlay" id="drag-overlay">
607
+ <svg viewBox="0 0 24 24" fill="none" stroke="rgba(0,230,118,0.5)" stroke-width="1.5">
608
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
609
  <polyline points="17 8 12 3 7 8"/>
610
  <line x1="12" y1="3" x2="12" y2="15"/>
611
  </svg>
612
+ <p>drop image to attach</p>
613
  </div>
614
 
615
  <div class="shell">
 
618
  <header class="header">
619
  <div class="logo">
620
  <!-- Moon eclipse SVG -->
621
+ <svg class="moon-wrap" viewBox="0 0 30 30" fill="none">
622
+ <circle cx="15" cy="15" r="14" fill="#080808" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
623
+ <circle cx="15" cy="15" r="9" fill="#0f0f0f" stroke="rgba(255,255,255,0.08)" stroke-width="0.5"/>
624
+ <path d="M11 11 Q15 6 19 11 Q23 15 19 19 Q15 24 11 19 Q7 15 11 11Z"
625
+ fill="url(#hg)" opacity="0.95"/>
 
626
  <defs>
627
+ <radialGradient id="hg" cx="62%" cy="28%" r="70%">
628
+ <stop offset="0%" stop-color="#fff" stop-opacity="0.95"/>
629
+ <stop offset="40%" stop-color="#888" stop-opacity="0.3"/>
630
+ <stop offset="100%" stop-color="#000" stop-opacity="0"/>
631
  </radialGradient>
632
  </defs>
633
  </svg>
634
+ <div class="logo-info">
635
+ <div class="logo-name">Kimi K2.7-Code</div>
636
+ <div class="logo-sub">Moonshot AI Β· Fastest</div>
637
  </div>
638
  </div>
639
+ <div class="header-right">
640
+ <div class="status-pill">
641
+ <div class="status-dot" id="sdot"></div>
642
+ <span id="stxt">connecting…</span>
643
+ </div>
644
  </div>
645
  </header>
646
 
647
+ <!-- Toolbar -->
648
+ <div class="toolbar">
649
+ <span class="tb-item" onclick="cmd('/clear')">clear</span>
650
+ <span class="tb-item" onclick="cmd('/history')">history</span>
651
+ <span class="tb-item" onclick="cmd('/model')">model info</span>
652
+ <div class="tb-sep"></div>
653
+ <span class="tb-model">moonshotai/Kimi-K2.7-Code:fastest</span>
654
+ </div>
655
+
656
  <!-- Chat area -->
657
  <div class="chat-area" id="chat-area">
 
658
  <div class="hero" id="hero">
659
+ <!-- Large moon -->
660
+ <svg class="hero-moon" viewBox="0 0 72 72" fill="none">
661
+ <circle cx="36" cy="36" r="35" fill="#070707" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
662
+ <circle cx="36" cy="36" r="22" fill="#0e0e0e" stroke="rgba(255,255,255,0.07)" stroke-width="0.5"/>
663
+ <path d="M26 26 Q36 14 46 26 Q56 36 46 46 Q36 58 26 46 Q16 36 26 26Z"
664
+ fill="url(#hm)" opacity="0.95"/>
665
+ <defs>
666
+ <radialGradient id="hm" cx="60%" cy="26%" r="72%">
667
+ <stop offset="0%" stop-color="#fff" stop-opacity="1"/>
668
+ <stop offset="38%" stop-color="#777" stop-opacity="0.45"/>
669
+ <stop offset="100%" stop-color="#000" stop-opacity="0"/>
670
+ </radialGradient>
671
+ </defs>
672
+ </svg>
673
+ <p class="hero-title">Seeking the optimal conversion from<br><span>energy to intelligence</span></p>
 
674
  <div class="hero-chips">
675
+ <div class="chip" onclick="fillPrompt('Write a Python async web scraper with aiohttp')">python async scraper</div>
676
+ <div class="chip" onclick="fillPrompt('Explain this code and find bugs')">code review</div>
677
+ <div class="chip" onclick="fillPrompt('Write a Rust CLI tool for file compression')">rust cli tool</div>
678
+ <div class="chip" onclick="fillPrompt('What do you see in this image?')">vision analysis</div>
679
+ <div class="chip" onclick="fillPrompt('Design a REST API with FastAPI and SQLAlchemy')">fastapi design</div>
680
  </div>
681
  </div>
682
  </div>
683
 
684
  <!-- Input zone -->
685
  <div class="input-zone">
686
+ <div class="staged-bar" id="staged-bar">
 
687
  <img id="staged-thumb" src="" alt="">
688
+ <span class="staged-bar-name" id="staged-name">β€”</span>
689
+ <button class="staged-rm" onclick="clearImg()">βœ•</button>
690
  </div>
 
 
691
  <div class="input-box" id="input-box">
692
+ <span class="input-prompt">&gt;_</span>
693
  <textarea
694
+ class="input-ta"
695
+ id="pta"
696
  placeholder="Throw me a hard one, I'm ready."
697
  rows="1"
698
  autofocus
699
  ></textarea>
700
+ <div class="input-btns">
701
+ <button class="icon-btn" onclick="triggerFile()" title="Attach image">
702
  <svg viewBox="0 0 24 24">
703
+ <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"/>
704
  </svg>
705
  </button>
706
+ <button class="send-btn" id="send-btn" onclick="handleSend()" title="Send (Enter)">
707
  <svg viewBox="0 0 24 24">
708
+ <line x1="12" y1="19" x2="12" y2="5"/>
709
+ <polyline points="5 12 12 5 19 12"/>
710
  </svg>
711
  </button>
712
  </div>
713
  </div>
714
+ <div class="input-foot">
715
+ <span>enter to send Β· shift+enter for newline Β· /clear /history /model</span>
716
+ <span id="foot-time">β€”</span>
717
+ </div>
718
  </div>
719
 
720
  </div>
721
 
722
+ <input type="file" id="file-in" accept="image/*" onchange="onFileChange(event)">
 
 
 
723
 
724
+ <!-- NOTE: using lowercase { client } as per Gradio Server docs demo -->
725
  <script type="module">
726
+ import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
727
 
728
  /* ── State ── */
729
+ let app = null;
730
  let chatHistory = [];
731
  let stagedFile = null;
732
  let stagedB64 = "";
733
  let isGenerating = false;
734
 
735
+ /* ── Clock ── */
736
+ setInterval(() => {
737
+ const el = document.getElementById("foot-time");
738
+ if (el) el.textContent = new Date().toTimeString().slice(0,8);
739
+ }, 1000);
740
+
741
  /* ── Marked config ── */
742
+ marked.setOptions({ breaks: true });
743
 
744
+ /* ── Connect ── */
745
  async function connect() {
746
  try {
747
+ // Use lowercase `client` as in Gradio Server docs
748
+ app = await client(window.location.origin);
749
  setStatus(true);
750
+ appendSys("session established Β· model ready");
751
+ } catch(e) {
752
  setStatus(false);
753
+ appendSys("connection failed: " + e.message, true);
754
  }
755
  }
756
 
757
+ function setStatus(on) {
758
+ document.getElementById("sdot").className = "status-dot" + (on ? " on" : "");
759
+ document.getElementById("stxt").textContent = on ? "online" : "offline";
760
  }
761
 
762
+ /* ── Toolbar commands ── */
763
+ window.cmd = function(c) {
764
+ if (c === "/clear") {
765
+ const ca = document.getElementById("chat-area");
766
+ ca.innerHTML = "";
767
+ chatHistory = [];
768
+ appendSys("terminal cleared");
769
+ return;
770
+ }
771
+ if (c === "/history") {
772
+ appendSys(`history buffer: ${chatHistory.length} messages`);
773
+ return;
774
+ }
775
+ if (c === "/model") {
776
+ appendSys("model: moonshotai/Kimi-K2.7-Code:fastest");
777
+ appendSys("host: https://router.huggingface.co/v1");
778
+ appendSys("context: 128k tokens Β· multimodal: yes");
779
+ return;
780
+ }
781
+ };
782
+
783
+ /* ── Chip fill ── */
784
+ window.fillPrompt = function(t) {
785
+ const ta = document.getElementById("pta");
786
+ ta.value = t;
787
  ta.dispatchEvent(new Event("input"));
788
  ta.focus();
789
  };
790
 
791
+ /* ── File handling ── */
792
+ window.triggerFile = function() { document.getElementById("file-in").click(); };
793
+ window.onFileChange = function(e) { const f = e.target.files[0]; if(f) stageFile(f); };
 
 
 
 
794
 
795
  function stageFile(file) {
796
  if (!file.type.startsWith("image/")) return;
 
799
  reader.onload = ev => {
800
  stagedB64 = ev.target.result;
801
  document.getElementById("staged-thumb").src = stagedB64;
802
+ document.getElementById("staged-name").textContent =
803
+ file.name + " (" + (file.size/1024).toFixed(1) + " KB)";
804
+ document.getElementById("staged-bar").classList.add("show");
805
+ document.getElementById("input-box").classList.add("has-img");
806
  };
807
  reader.readAsDataURL(file);
808
  }
809
 
810
+ window.clearImg = function() {
811
  stagedFile = null; stagedB64 = "";
812
+ document.getElementById("file-in").value = "";
813
+ document.getElementById("staged-bar").classList.remove("show");
814
+ document.getElementById("input-box").classList.remove("has-img");
815
  };
816
 
817
  /* ── Drag & drop ── */
818
  const overlay = document.getElementById("drag-overlay");
819
+ window.addEventListener("dragenter", e => { e.preventDefault(); overlay.classList.add("on"); });
820
+ overlay.addEventListener("dragover", e => e.preventDefault());
821
+ overlay.addEventListener("dragleave", e => { if (!overlay.contains(e.relatedTarget)) overlay.classList.remove("on"); });
822
  overlay.addEventListener("drop", e => {
823
+ e.preventDefault(); overlay.classList.remove("on");
824
  const f = e.dataTransfer.files[0]; if (f) stageFile(f);
825
  });
826
 
827
+ /* ── Textarea ── */
828
+ const pta = document.getElementById("pta");
829
+ pta.addEventListener("input", function() {
830
  this.style.height = "auto";
831
+ this.style.height = Math.min(this.scrollHeight, 180) + "px";
832
  });
833
+ pta.addEventListener("keydown", e => {
834
  if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); }
835
  });
836
 
837
  /* ── DOM helpers ── */
838
+ const ca = () => document.getElementById("chat-area");
839
+
840
  function hideHero() {
841
+ const h = document.getElementById("hero");
842
+ if (h) h.remove();
843
  }
844
 
845
+ function scrollBot() {
846
+ const el = ca(); el.scrollTop = el.scrollHeight;
 
847
  }
848
 
849
+ function appendSys(text, isErr = false) {
850
+ const d = document.createElement("div");
851
+ d.className = "sys-line" + (isErr ? " err" : "");
852
+ d.textContent = "// " + text;
853
+ ca().appendChild(d);
854
+ scrollBot();
855
  }
856
 
857
+ function addUserRow(prompt, imgSrc, imgName) {
858
  hideHero();
 
859
  const row = document.createElement("div");
860
  row.className = "msg-row";
861
  let imgHtml = "";
862
  if (imgSrc) {
863
+ imgHtml = `<div class="img-pill"><img src="${imgSrc}" alt=""><span>${escHtml(imgName)}</span></div><br>`;
864
  }
865
  row.innerHTML = `
866
+ <div class="msg-prefix">
867
+ <span class="prefix-sigil">β€Ί</span>
868
+ <span class="prefix-label">you</span>
869
  </div>
870
+ <div class="msg-body user">${imgHtml}${escHtml(prompt)}</div>
871
  `;
872
+ ca().appendChild(row);
873
+ scrollBot();
874
  }
875
 
876
  function addKimiRow() {
 
877
  const row = document.createElement("div");
878
  row.className = "msg-row";
879
  row.innerHTML = `
880
+ <div class="msg-prefix">
881
+ <span class="prefix-sigil" style="color:#888;">β—Ž</span>
882
+ <span class="prefix-label">kimi</span>
 
 
 
 
 
 
 
 
 
 
 
 
883
  </div>
884
+ <div class="msg-body kimi" id="kimi-active"><span class="cursor"></span></div>
885
  `;
886
+ ca().appendChild(row);
887
+ scrollBot();
888
+ return document.getElementById("kimi-active");
889
  }
890
 
891
+ function escHtml(t) {
892
+ return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
893
  }
894
 
895
+ /* ── Stream renderer ── */
896
+ function makeRenderer(el) {
897
+ let buf = "";
898
+ let pending = false;
 
899
 
900
  function render() {
901
+ pending = false;
902
+ el.innerHTML = marked.parse(buf) + '<span class="cursor"></span>';
903
+ scrollBot();
 
904
  }
905
 
906
  return {
907
  push(token) {
908
+ buf += token;
909
+ if (!pending) { pending = true; requestAnimationFrame(render); }
 
 
 
 
910
  },
911
  finish() {
912
+ el.id = "";
913
+ el.innerHTML = marked.parse(buf);
914
+ scrollBot();
915
+ return buf;
 
916
  }
917
  };
918
  }
919
 
920
+ /* ── Send ── */
921
  window.handleSend = async function() {
922
  if (isGenerating) return;
923
+ const prompt = pta.value.trim();
924
+ if (!prompt && !stagedFile) return;
925
 
926
+ const imgSrc = stagedB64;
927
+ const imgName = stagedFile ? stagedFile.name : "";
928
+ const imgB64 = stagedB64;
 
 
929
 
930
+ pta.value = ""; pta.style.height = "auto";
931
+ clearImg();
 
932
 
 
933
  addUserRow(prompt, imgSrc, imgName);
934
 
935
+ if (!app) {
936
+ appendSys("not connected β€” retrying…", true);
937
  await connect();
938
+ if (!app) return;
939
  }
940
 
941
  isGenerating = true;
942
  document.getElementById("send-btn").disabled = true;
943
 
944
+ const kimiEl = addKimiRow();
945
+ const renderer = makeRenderer(kimiEl);
946
+ let fullReply = "";
947
 
948
  try {
949
+ // Use app.submit for streaming β€” matches Gradio Server @app.api generator
950
+ const job = app.submit("/chat", {
951
+ prompt,
952
  history_json: JSON.stringify(chatHistory),
953
+ image_b64: imgB64
954
  });
955
 
956
  for await (const msg of job) {
957
  if (msg.type === "data") {
 
958
  const token = msg.data[0];
959
+ if (token) { renderer.push(token); fullReply += token; }
 
 
 
960
  } else if (msg.type === "status" && msg.stage === "error") {
961
+ renderer.push("\n\n[endpoint error β€” check logs]");
962
  }
963
  }
964
 
965
  fullReply = renderer.finish();
966
 
 
967
  if (fullReply) {
968
  chatHistory.push({ role: "user", content: prompt });
969
  chatHistory.push({ role: "assistant", content: fullReply });
970
  }
971
 
972
+ } catch(err) {
973
  renderer.finish();
974
+ appendSys("error: " + err.message, true);
975
  } finally {
976
  isGenerating = false;
977
  document.getElementById("send-btn").disabled = false;
978
+ pta.focus();
979
  }
980
  };
981
 
 
982
  connect();
983
  </script>
984
  </body>