akhaliq HF Staff commited on
Commit
86adb5f
·
1 Parent(s): 8851c59

Implement Kimi K2.7-Code terminal chat app

Browse files
Files changed (2) hide show
  1. app.py +116 -0
  2. index.html +933 -0
app.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import base64
4
+ import mimetypes
5
+ from typing import Optional, List
6
+ from fastapi.responses import HTMLResponse
7
+ from gradio import Server
8
+ from gradio.data_classes import FileData
9
+ from openai import OpenAI
10
+
11
+ # Initialize the Gradio Server app
12
+ app = Server()
13
+
14
+ @app.get("/")
15
+ async def homepage():
16
+ """Serves the custom terminal frontend page."""
17
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
18
+ if not os.path.exists(html_path):
19
+ return HTMLResponse(
20
+ content="<html><body><h1>[FATAL ERROR]: index.html not found in workspace.</h1></body></html>",
21
+ status_code=404
22
+ )
23
+ with open(html_path, "r", encoding="utf-8") as f:
24
+ return HTMLResponse(content=f.read(), status_code=200)
25
+
26
+ @app.api(name="chat")
27
+ def chat(prompt: str, history_json: str, image_file: Optional[FileData] = None):
28
+ """
29
+ Handles streaming chat completions with option for an uploaded image.
30
+ Uses the Hugging Face router to query moonshotai/Kimi-K2.7-Code:fastest.
31
+ """
32
+ # Verify HF Token
33
+ api_key = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_TOKEN") or ""
34
+ if not api_key:
35
+ yield (
36
+ "[SYSTEM ERROR]: HF_TOKEN is not set in the environment variables.\n\n"
37
+ "To use this space, please configure HF_TOKEN in your Hugging Face Space settings or local environment."
38
+ )
39
+ return
40
+
41
+ # Parse conversation history
42
+ try:
43
+ messages = json.loads(history_json) if history_json else []
44
+ except Exception as e:
45
+ messages = []
46
+
47
+ # Process image if present
48
+ user_content = []
49
+ if image_file:
50
+ # Resolve file path safely
51
+ if isinstance(image_file, dict):
52
+ path = image_file.get("path")
53
+ else:
54
+ path = getattr(image_file, "path", None)
55
+
56
+ if path and os.path.exists(path):
57
+ try:
58
+ with open(path, "rb") as f:
59
+ encoded_image = base64.b64encode(f.read()).decode("utf-8")
60
+
61
+ mime_type, _ = mimetypes.guess_type(path)
62
+ if not mime_type:
63
+ mime_type = "image/jpeg"
64
+
65
+ user_content.append({
66
+ "type": "image_url",
67
+ "image_url": {
68
+ "url": f"data:{mime_type};base64,{encoded_image}"
69
+ }
70
+ })
71
+ except Exception as e:
72
+ yield f"[SYSTEM ERROR]: Failed to encode image: {str(e)}"
73
+ return
74
+
75
+ # Construct the user message content
76
+ if user_content:
77
+ user_content.append({
78
+ "type": "text",
79
+ "text": prompt
80
+ })
81
+ messages.append({
82
+ "role": "user",
83
+ "content": user_content
84
+ })
85
+ else:
86
+ messages.append({
87
+ "role": "user",
88
+ "content": prompt
89
+ })
90
+
91
+ # Call Hugging Face Router
92
+ try:
93
+ client = OpenAI(
94
+ base_url="https://router.huggingface.co/v1",
95
+ api_key=api_key,
96
+ default_headers={
97
+ "X-HF-Bill-To": "huggingface"
98
+ }
99
+ )
100
+
101
+ stream = client.chat.completions.create(
102
+ model="moonshotai/Kimi-K2.7-Code:fastest",
103
+ messages=messages,
104
+ stream=True,
105
+ )
106
+
107
+ accumulated_text = ""
108
+ for chunk in stream:
109
+ if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content:
110
+ accumulated_text += chunk.choices[0].delta.content
111
+ yield accumulated_text
112
+ except Exception as e:
113
+ yield f"[SYSTEM ERROR]: API request failed.\nDetails: {str(e)}"
114
+
115
+ if __name__ == "__main__":
116
+ app.launch()
index.html ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
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 Interface</title>
7
+ <!-- Fonts -->
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=Share+Tech+Mono&family=JetBrains+Mono:wght@300;400;700&display=swap" rel="stylesheet">
11
+ <!-- PrismJS for Terminal Code Highlighting -->
12
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
13
+ <!-- Markdown Parser -->
14
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
+ <style>
16
+ /* Modern CSS Reset & Variable Themes */
17
+ :root {
18
+ --bg-color: #030712;
19
+ --text-color: #00ff66;
20
+ --glow-color: rgba(0, 255, 102, 0.35);
21
+ --border-color: #00b344;
22
+ --dim-color: #006622;
23
+ --terminal-header-bg: #0b132b;
24
+ --terminal-error: #ff3333;
25
+ --error-glow: rgba(255, 51, 51, 0.4);
26
+ --font-family: 'Share Tech Mono', monospace;
27
+ --code-font: 'JetBrains Mono', monospace;
28
+ }
29
+
30
+ /* Amber Theme */
31
+ body.theme-amber {
32
+ --text-color: #ffb000;
33
+ --glow-color: rgba(255, 176, 0, 0.35);
34
+ --border-color: #cc8d00;
35
+ --dim-color: #805800;
36
+ --terminal-header-bg: #1a1000;
37
+ }
38
+
39
+ /* Dracula/Purple Theme */
40
+ body.theme-dracula {
41
+ --text-color: #ff79c6;
42
+ --glow-color: rgba(255, 121, 198, 0.35);
43
+ --border-color: #bd93f9;
44
+ --dim-color: #6272a4;
45
+ --terminal-header-bg: #1e1f29;
46
+ }
47
+
48
+ /* Cyber/Cyan Theme */
49
+ body.theme-cyber {
50
+ --text-color: #00f0ff;
51
+ --glow-color: rgba(0, 240, 255, 0.35);
52
+ --border-color: #00a8b3;
53
+ --dim-color: #005c66;
54
+ --terminal-header-bg: #00121a;
55
+ }
56
+
57
+ /* Classic White Theme */
58
+ body.theme-white {
59
+ --text-color: #f8f8f2;
60
+ --glow-color: rgba(248, 248, 242, 0.2);
61
+ --border-color: #888888;
62
+ --dim-color: #444444;
63
+ --terminal-header-bg: #121212;
64
+ }
65
+
66
+ * {
67
+ box-sizing: border-box;
68
+ margin: 0;
69
+ padding: 0;
70
+ }
71
+
72
+ body {
73
+ background-color: #000;
74
+ color: var(--text-color);
75
+ font-family: var(--font-family);
76
+ font-size: 16px;
77
+ line-height: 1.5;
78
+ height: 100vh;
79
+ overflow: hidden;
80
+ display: flex;
81
+ justify-content: center;
82
+ align-items: center;
83
+ text-shadow: 0 0 5px var(--glow-color);
84
+ }
85
+
86
+ /* CRT Filter Effects */
87
+ .crt {
88
+ width: 100%;
89
+ height: 100%;
90
+ display: flex;
91
+ flex-direction: column;
92
+ position: relative;
93
+ background: var(--bg-color);
94
+ overflow: hidden;
95
+ animation: textShadow 1.6s infinite alternate;
96
+ }
97
+
98
+ /* Vignette overlay for retro monitor curve shadowing */
99
+ .crt::after {
100
+ content: " ";
101
+ display: block;
102
+ position: absolute;
103
+ top: 0; left: 0; bottom: 0; right: 0;
104
+ background: radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,0.85) 100%);
105
+ pointer-events: none;
106
+ z-index: 100;
107
+ }
108
+
109
+ /* Horizontal scanlines */
110
+ .scanlines {
111
+ position: absolute;
112
+ top: 0; left: 0; width: 100%; height: 100%;
113
+ background: linear-gradient(
114
+ rgba(18, 16, 16, 0) 50%,
115
+ rgba(0, 0, 0, 0.15) 50%
116
+ );
117
+ background-size: 100% 4px;
118
+ z-index: 99;
119
+ pointer-events: none;
120
+ }
121
+
122
+ /* CRT flicker keyframes */
123
+ @keyframes crt-flicker {
124
+ 0% { opacity: 0.975; }
125
+ 50% { opacity: 1; }
126
+ 100% { opacity: 0.985; }
127
+ }
128
+
129
+ .crt-flicker-overlay {
130
+ position: absolute;
131
+ top: 0; left: 0; width: 100%; height: 100%;
132
+ background: rgba(0, 255, 102, 0.005);
133
+ pointer-events: none;
134
+ z-index: 98;
135
+ animation: crt-flicker 0.15s infinite;
136
+ }
137
+
138
+ /* Main Window Layout */
139
+ .terminal-container {
140
+ width: 96%;
141
+ height: 94%;
142
+ max-width: 1200px;
143
+ border: 2px solid var(--border-color);
144
+ background: rgba(3, 7, 18, 0.9);
145
+ box-shadow: 0 0 20px var(--glow-color), inset 0 0 10px var(--glow-color);
146
+ display: flex;
147
+ flex-direction: column;
148
+ position: relative;
149
+ z-index: 10;
150
+ border-radius: 4px;
151
+ }
152
+
153
+ /* Title Bar */
154
+ .terminal-header {
155
+ background-color: var(--terminal-header-bg);
156
+ border-bottom: 2px solid var(--border-color);
157
+ padding: 8px 16px;
158
+ display: flex;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ font-weight: bold;
162
+ font-size: 15px;
163
+ letter-spacing: 1px;
164
+ user-select: none;
165
+ }
166
+
167
+ .terminal-header-title {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 8px;
171
+ }
172
+
173
+ .terminal-status-light {
174
+ width: 8px;
175
+ height: 8px;
176
+ border-radius: 50%;
177
+ background-color: var(--text-color);
178
+ box-shadow: 0 0 8px var(--text-color);
179
+ display: inline-block;
180
+ }
181
+
182
+ .terminal-header-controls {
183
+ display: flex;
184
+ gap: 8px;
185
+ }
186
+
187
+ .header-btn {
188
+ width: 12px;
189
+ height: 12px;
190
+ border: 1px solid var(--border-color);
191
+ border-radius: 2px;
192
+ cursor: pointer;
193
+ }
194
+
195
+ /* Info HUD Ribbon */
196
+ .hud-ribbon {
197
+ display: flex;
198
+ justify-content: space-between;
199
+ padding: 4px 16px;
200
+ font-size: 12px;
201
+ border-bottom: 1px dashed var(--dim-color);
202
+ background: rgba(0, 0, 0, 0.4);
203
+ color: var(--dim-color);
204
+ }
205
+
206
+ .hud-ribbon span {
207
+ color: var(--text-color);
208
+ opacity: 0.8;
209
+ }
210
+
211
+ /* Content Log Area */
212
+ .terminal-body {
213
+ flex: 1;
214
+ padding: 20px;
215
+ overflow-y: auto;
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 16px;
219
+ font-family: var(--font-family);
220
+ scrollbar-width: thin;
221
+ scrollbar-color: var(--border-color) transparent;
222
+ }
223
+
224
+ .terminal-body::-webkit-scrollbar {
225
+ width: 6px;
226
+ }
227
+
228
+ .terminal-body::-webkit-scrollbar-track {
229
+ background: transparent;
230
+ }
231
+
232
+ .terminal-body::-webkit-scrollbar-thumb {
233
+ background: var(--border-color);
234
+ border-radius: 3px;
235
+ }
236
+
237
+ /* Log items */
238
+ .log-entry {
239
+ word-wrap: break-word;
240
+ white-space: pre-wrap;
241
+ }
242
+
243
+ .log-entry.system {
244
+ color: var(--dim-color);
245
+ }
246
+
247
+ .log-entry.error {
248
+ color: var(--terminal-error);
249
+ text-shadow: 0 0 5px var(--error-glow);
250
+ }
251
+
252
+ .log-entry.user-prompt {
253
+ color: var(--text-color);
254
+ font-weight: bold;
255
+ }
256
+
257
+ .log-entry.bot-response {
258
+ background: rgba(255, 255, 255, 0.02);
259
+ border-left: 2px solid var(--border-color);
260
+ padding-left: 14px;
261
+ margin-top: 4px;
262
+ }
263
+
264
+ /* Drag & Drop Visuals */
265
+ .drag-overlay {
266
+ display: none;
267
+ position: absolute;
268
+ top: 0; left: 0; right: 0; bottom: 0;
269
+ background: rgba(0, 0, 0, 0.85);
270
+ border: 3px dashed var(--text-color);
271
+ z-index: 1000;
272
+ justify-content: center;
273
+ align-items: center;
274
+ flex-direction: column;
275
+ gap: 15px;
276
+ font-size: 24px;
277
+ letter-spacing: 2px;
278
+ color: var(--text-color);
279
+ text-shadow: 0 0 10px var(--glow-color);
280
+ }
281
+
282
+ .drag-overlay.active {
283
+ display: flex;
284
+ }
285
+
286
+ .drag-overlay svg {
287
+ width: 64px;
288
+ height: 64px;
289
+ fill: var(--text-color);
290
+ filter: drop-shadow(0 0 5px var(--glow-color));
291
+ }
292
+
293
+ /* Image Preview Box */
294
+ .image-preview-container {
295
+ display: none;
296
+ border: 1px dashed var(--border-color);
297
+ background: rgba(0, 0, 0, 0.6);
298
+ padding: 8px;
299
+ align-self: flex-start;
300
+ position: relative;
301
+ margin-top: 8px;
302
+ margin-bottom: 8px;
303
+ }
304
+
305
+ .image-preview-container.active {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 6px;
309
+ }
310
+
311
+ .staged-img-wrapper {
312
+ position: relative;
313
+ max-width: 150px;
314
+ max-height: 150px;
315
+ border: 1px solid var(--border-color);
316
+ }
317
+
318
+ .image-preview-container img {
319
+ width: 100%;
320
+ height: auto;
321
+ max-height: 140px;
322
+ display: block;
323
+ filter: grayscale(40%) contrast(120%) brightness(90%) sepia(20%);
324
+ }
325
+
326
+ .close-preview-btn {
327
+ position: absolute;
328
+ top: 2px;
329
+ right: 2px;
330
+ background: var(--bg-color);
331
+ color: var(--text-color);
332
+ border: 1px solid var(--border-color);
333
+ cursor: pointer;
334
+ width: 20px;
335
+ height: 20px;
336
+ display: flex;
337
+ justify-content: center;
338
+ align-items: center;
339
+ font-weight: bold;
340
+ font-size: 12px;
341
+ }
342
+
343
+ .close-preview-btn:hover {
344
+ background: var(--text-color);
345
+ color: var(--bg-color);
346
+ }
347
+
348
+ .preview-details {
349
+ font-size: 11px;
350
+ color: var(--dim-color);
351
+ }
352
+
353
+ /* Input Area */
354
+ .terminal-input-bar {
355
+ border-top: 2px solid var(--border-color);
356
+ padding: 12px 16px;
357
+ background: rgba(0, 0, 0, 0.6);
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 12px;
361
+ }
362
+
363
+ .terminal-prompt-prefix {
364
+ font-weight: bold;
365
+ white-space: nowrap;
366
+ user-select: none;
367
+ }
368
+
369
+ .terminal-textarea-wrapper {
370
+ flex: 1;
371
+ position: relative;
372
+ display: flex;
373
+ align-items: center;
374
+ }
375
+
376
+ .terminal-textarea {
377
+ width: 100%;
378
+ background: transparent;
379
+ border: none;
380
+ color: var(--text-color);
381
+ font-family: var(--font-family);
382
+ font-size: 16px;
383
+ outline: none;
384
+ resize: none;
385
+ height: 24px;
386
+ line-height: 24px;
387
+ overflow-y: hidden;
388
+ text-shadow: 0 0 3px var(--glow-color);
389
+ }
390
+
391
+ .terminal-textarea::placeholder {
392
+ color: var(--dim-color);
393
+ opacity: 0.6;
394
+ }
395
+
396
+ /* Action Buttons */
397
+ .terminal-actions {
398
+ display: flex;
399
+ gap: 8px;
400
+ align-items: center;
401
+ }
402
+
403
+ .action-btn {
404
+ background: transparent;
405
+ border: 1px solid var(--border-color);
406
+ color: var(--text-color);
407
+ padding: 4px 10px;
408
+ font-family: var(--font-family);
409
+ font-size: 14px;
410
+ cursor: pointer;
411
+ transition: all 0.2s ease;
412
+ text-shadow: 0 0 3px var(--glow-color);
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 6px;
416
+ }
417
+
418
+ .action-btn:hover {
419
+ background: var(--text-color);
420
+ color: var(--bg-color);
421
+ box-shadow: 0 0 10px var(--text-color);
422
+ }
423
+
424
+ .action-btn svg {
425
+ width: 14px;
426
+ height: 14px;
427
+ fill: currentColor;
428
+ }
429
+
430
+ /* Hidden File Input */
431
+ #hidden-file-input {
432
+ display: none;
433
+ }
434
+
435
+ /* Code block overrides for PrismJS inside Terminal */
436
+ pre {
437
+ background: rgba(0, 0, 0, 0.55) !important;
438
+ border: 1px dashed var(--border-color) !important;
439
+ padding: 12px !important;
440
+ border-radius: 4px !important;
441
+ overflow-x: auto;
442
+ margin: 10px 0 !important;
443
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);
444
+ }
445
+
446
+ code {
447
+ font-family: var(--code-font) !important;
448
+ text-shadow: none !important;
449
+ font-size: 14px !important;
450
+ }
451
+
452
+ .token.comment, .token.prolog, .token.doctype, .token.cdata {
453
+ color: var(--dim-color) !important;
454
+ opacity: 0.7;
455
+ }
456
+
457
+ /* Adjust colors based on active theme to maintain perfect readability */
458
+ .token.string {
459
+ color: #f1fa8c !important; /* light yellow */
460
+ }
461
+ .token.keyword, .token.boolean, .token.number {
462
+ color: var(--text-color) !important;
463
+ font-weight: bold;
464
+ }
465
+ .token.function {
466
+ color: #50fa7b !important; /* cyan/greenish */
467
+ }
468
+
469
+ /* Blinking terminal cursor simulation */
470
+ .blinking-cursor {
471
+ display: inline-block;
472
+ width: 8px;
473
+ height: 16px;
474
+ background-color: var(--text-color);
475
+ animation: cursor-blink 1s step-end infinite;
476
+ vertical-align: middle;
477
+ margin-left: 2px;
478
+ }
479
+
480
+ @keyframes cursor-blink {
481
+ from, to { background-color: transparent }
482
+ 50% { background-color: var(--text-color); }
483
+ }
484
+
485
+ /* ASCII Progress Bar Helper */
486
+ .ascii-progress-bar {
487
+ letter-spacing: 1px;
488
+ font-family: var(--font-family);
489
+ }
490
+
491
+ /* Simple responsive overrides */
492
+ @media (max-width: 600px) {
493
+ .hud-ribbon {
494
+ display: none;
495
+ }
496
+ .terminal-container {
497
+ width: 98%;
498
+ height: 98%;
499
+ }
500
+ .terminal-input-bar {
501
+ flex-direction: column;
502
+ align-items: stretch;
503
+ gap: 8px;
504
+ }
505
+ .terminal-actions {
506
+ justify-content: flex-end;
507
+ }
508
+ }
509
+ </style>
510
+ </head>
511
+ <body class="theme-green">
512
+
513
+ <div class="crt">
514
+ <div class="scanlines"></div>
515
+ <div class="crt-flicker-overlay"></div>
516
+
517
+ <!-- Drag and Drop Overlay -->
518
+ <div class="drag-overlay" id="drag-overlay">
519
+ <svg viewBox="0 0 24 24">
520
+ <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
521
+ </svg>
522
+ <div>[ STAGE IMAGE FOR UPLOAD ]</div>
523
+ </div>
524
+
525
+ <div class="terminal-container">
526
+ <!-- Terminal Header -->
527
+ <div class="terminal-header">
528
+ <div class="terminal-header-title">
529
+ <span class="terminal-status-light"></span>
530
+ <span>KIMI-K2.7-CODE // TERMINAL SYSTEM v2.7.0</span>
531
+ </div>
532
+ <div class="terminal-header-controls">
533
+ <span class="header-btn" title="Min"></span>
534
+ <span class="header-btn" title="Max"></span>
535
+ <span class="header-btn" title="Close" onclick="location.reload();"></span>
536
+ </div>
537
+ </div>
538
+
539
+ <!-- HUD ribbon -->
540
+ <div class="hud-ribbon">
541
+ <div>SYS_STATUS: <span>ONLINE</span></div>
542
+ <div>MODEL: <span>Kimi-K2.7-Code:fastest</span></div>
543
+ <div>HOST: <span>HF-ROUTER</span></div>
544
+ <div>LOC_TIME: <span id="hud-time">12:00:00</span></div>
545
+ </div>
546
+
547
+ <!-- Main terminal output log -->
548
+ <div class="terminal-body" id="terminal-body">
549
+ <!-- System Boot messages printed here -->
550
+ </div>
551
+
552
+ <!-- Image Staged Area -->
553
+ <div class="image-preview-container" id="image-preview-container">
554
+ <button class="close-preview-btn" onclick="clearStagedImage()">X</button>
555
+ <div class="staged-img-wrapper">
556
+ <img id="staged-image-element" src="" alt="Staged Upload">
557
+ </div>
558
+ <div class="preview-details" id="preview-details">
559
+ Buffer load: file_staged.jpg (0 KB)
560
+ </div>
561
+ <div class="ascii-progress-bar" id="ascii-upload-bar">[========================================>] 100%</div>
562
+ </div>
563
+
564
+ <!-- Command Input Bar -->
565
+ <div class="terminal-input-bar">
566
+ <span class="terminal-prompt-prefix">visitor@kimi-k2.7:~$</span>
567
+ <div class="terminal-textarea-wrapper">
568
+ <textarea
569
+ class="terminal-textarea"
570
+ id="user-input"
571
+ placeholder="Type prompt or command (e.g. /help, /theme)..."
572
+ rows="1"
573
+ autofocus
574
+ ></textarea>
575
+ </div>
576
+
577
+ <div class="terminal-actions">
578
+ <button class="action-btn" id="attach-btn" onclick="triggerFileInput()" title="Attach Image">
579
+ <svg viewBox="0 0 24 24">
580
+ <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-3.31 2.69-6 6-6s6 2.69 6 6v10c0 4.42-3.58 8-8 8s-8-3.58-8-8V4h2v11c0 3.31 2.69 6 6 6s6-2.69 6-6V5c0-2.21-1.79-4-4-4s-4 1.79-4 4v12.5c0 1.1.9 2 2 2s2-.9 2-2V6h2z"/>
581
+ </svg>
582
+ <span>ATTACH</span>
583
+ </button>
584
+ <button class="action-btn" id="send-btn" onclick="handleSend()" title="Execute Command">
585
+ <span>EXECUTE</span>
586
+ </button>
587
+ </div>
588
+ <input type="file" id="hidden-file-input" accept="image/*" onchange="handleFileSelect(event)">
589
+ </div>
590
+ </div>
591
+ </div>
592
+
593
+ <!-- Gradio Client Integration -->
594
+ <script type="module">
595
+ import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
596
+
597
+ // Global variables
598
+ let client = null;
599
+ let chatHistory = [];
600
+ let stagedFile = null;
601
+ let isSystemBooted = false;
602
+ let isGenerating = false;
603
+
604
+ // Initialize HUD Time
605
+ function updateHUDTime() {
606
+ const timeSpan = document.getElementById("hud-time");
607
+ if (timeSpan) {
608
+ const now = new Date();
609
+ timeSpan.textContent = now.toTimeString().split(' ')[0];
610
+ }
611
+ }
612
+ setInterval(updateHUDTime, 1000);
613
+ updateHUDTime();
614
+
615
+ // Connect to Gradio.Server
616
+ async function connectGradio() {
617
+ try {
618
+ // Initialize Gradio client targeting the local origin
619
+ client = await Client.connect(window.location.origin);
620
+ appendLog("System handshake: Gradio backend CONNECTED.", "system");
621
+ } catch (err) {
622
+ appendLog(`[FATAL ERROR]: Failed to bind Gradio client. Ensure server is online. Details: ${err.message}`, "error");
623
+ }
624
+ }
625
+
626
+ // Boot Sequence Simulation
627
+ const bootLines = [
628
+ "KIMI-K2.7-CODE TERMINAL OS [Version 2.7.19-LTS]",
629
+ "(C) Moonshot AI & Hugging Face Corporation.",
630
+ "",
631
+ "Initializing standard interfaces...",
632
+ "Loading local configurations...",
633
+ "Validating network links with HF Serverless API...",
634
+ "GPU Allocation: OK (Shared Serverless Hub Mode)",
635
+ "System boot code: 0x93FA2B49",
636
+ "Establishing connection to backend router...",
637
+ "SYSTEM STATUS: ONLINE AND READY.",
638
+ "",
639
+ "Type /help for instructions on how to use terminal variables.",
640
+ "Drag and drop any image to run vision analysis (Kimi-K2.7-Code multimodal context).",
641
+ "--------------------------------------------------------------------------------",
642
+ ""
643
+ ];
644
+
645
+ async function playBootSequence() {
646
+ const body = document.getElementById("terminal-body");
647
+ for (let i = 0; i < bootLines.length; i++) {
648
+ await new Promise(r => setTimeout(r, 60));
649
+ appendLog(bootLines[i], "system");
650
+ }
651
+ isSystemBooted = true;
652
+ await connectGradio();
653
+ }
654
+
655
+ // Custom Log Appender
656
+ window.appendLog = function(text, type = "") {
657
+ const body = document.getElementById("terminal-body");
658
+ const entry = document.createElement("div");
659
+ entry.className = `log-entry ${type}`;
660
+
661
+ if (type === "bot-response") {
662
+ // Bots render markdown
663
+ entry.innerHTML = marked.parse(text);
664
+ // Trigger Prism Highlight
665
+ if (window.Prism) {
666
+ Prism.highlightAllUnder(entry);
667
+ }
668
+ } else {
669
+ entry.textContent = text;
670
+ }
671
+
672
+ body.appendChild(entry);
673
+ body.scrollTop = body.scrollHeight;
674
+ return entry;
675
+ };
676
+
677
+ // File/Image Uploader Utilities
678
+ window.triggerFileInput = function() {
679
+ document.getElementById("hidden-file-input").click();
680
+ };
681
+
682
+ window.handleFileSelect = function(e) {
683
+ const file = e.target.files[0];
684
+ if (file) {
685
+ stageImage(file);
686
+ }
687
+ };
688
+
689
+ function stageImage(file) {
690
+ if (!file.type.startsWith("image/")) {
691
+ appendLog("[ERR]: File type not supported. Staged file must be an image.", "error");
692
+ return;
693
+ }
694
+ stagedFile = file;
695
+
696
+ const reader = new FileReader();
697
+ reader.onload = function(e) {
698
+ document.getElementById("staged-image-element").src = e.target.result;
699
+ document.getElementById("image-preview-container").classList.add("active");
700
+
701
+ const sizeKb = (file.size / 1024).toFixed(1);
702
+ document.getElementById("preview-details").textContent = `Buffer load: ${file.name} (${sizeKb} KB)`;
703
+
704
+ appendLog(`[SYS]: File loaded into buffer: ${file.name} (${sizeKb} KB) - Ready to transmit.`, "system");
705
+ };
706
+ reader.readAsDataURL(file);
707
+ }
708
+
709
+ window.clearStagedImage = function() {
710
+ stagedFile = null;
711
+ document.getElementById("hidden-file-input").value = "";
712
+ document.getElementById("staged-image-element").src = "";
713
+ document.getElementById("image-preview-container").classList.remove("active");
714
+ appendLog("[SYS]: Staged image buffer cleared.", "system");
715
+ };
716
+
717
+ // Command Processor
718
+ function processLocalCommand(cmdString) {
719
+ const args = cmdString.trim().split(/\s+/);
720
+ const primary = args[0].toLowerCase();
721
+
722
+ if (primary === "/help") {
723
+ appendLog("================================================================================", "system");
724
+ appendLog("AVAILABLE TERMINAL COMMANDS:", "system");
725
+ appendLog(" /help Display this command list", "system");
726
+ appendLog(" /clear Clear scrollback history from terminal log", "system");
727
+ appendLog(" /theme <name> Change terminal visual style (green, amber, dracula, cyber, white)", "system");
728
+ appendLog(" /history Display conversation buffer length", "system");
729
+ appendLog(" /sys Display fake system diagnostics", "system");
730
+ appendLog(" /model Retrieve details about current active model", "system");
731
+ appendLog(" /clearimage Clear currently staged image from buffer", "system");
732
+ appendLog("================================================================================", "system");
733
+ return true;
734
+ }
735
+
736
+ if (primary === "/clear") {
737
+ document.getElementById("terminal-body").innerHTML = "";
738
+ appendLog("Console logs cleared. Ready for next prompt.", "system");
739
+ return true;
740
+ }
741
+
742
+ if (primary === "/clearimage") {
743
+ clearStagedImage();
744
+ return true;
745
+ }
746
+
747
+ if (primary === "/theme") {
748
+ const targetTheme = args[1] ? args[1].toLowerCase() : "";
749
+ const validThemes = ["green", "amber", "dracula", "cyber", "white"];
750
+ if (validThemes.includes(targetTheme)) {
751
+ document.body.className = `theme-${targetTheme}`;
752
+ appendLog(`[SYS]: Dynamic palette updated to theme '${targetTheme}'.`, "system");
753
+ } else {
754
+ appendLog(`[ERR]: Theme not recognized. Available themes: ${validThemes.join(", ")}`, "error");
755
+ }
756
+ return true;
757
+ }
758
+
759
+ if (primary === "/history") {
760
+ appendLog(`[SYS]: Buffer active. Messages in history array: ${chatHistory.length}`, "system");
761
+ return true;
762
+ }
763
+
764
+ if (primary === "/sys") {
765
+ appendLog("--- SYSTEM DIAGNOSTICS ---", "system");
766
+ appendLog("PROCESSOR: Moonshot Kimi-K2.7-Code Acceleration Core", "system");
767
+ appendLog(`MEM_USAGE: ${(Math.random() * 20 + 35).toFixed(2)}% (Sustained)`, "system");
768
+ appendLog("API_ENDPOINT: https://router.huggingface.co/v1", "system");
769
+ appendLog(`DRIVE_TEMP: ${(Math.random() * 5 + 37).toFixed(1)}C`, "system");
770
+ appendLog("STABILITY: NOMINAL", "system");
771
+ appendLog("--------------------------", "system");
772
+ return true;
773
+ }
774
+
775
+ if (primary === "/model") {
776
+ appendLog("--- MODEL SPECIFICATIONS ---", "system");
777
+ appendLog("Name: moonshotai/Kimi-K2.7-Code:fastest", "system");
778
+ appendLog("Context Length: 128k Tokens (Standard)", "system");
779
+ appendLog("Multimodal: Yes (Supports text instruction + image inputs)", "system");
780
+ appendLog("Developer: Moonshot AI, hosted on Hugging Face Serverless Router", "system");
781
+ appendLog("----------------------------", "system");
782
+ return true;
783
+ }
784
+
785
+ return false;
786
+ }
787
+
788
+ // Send Handler
789
+ window.handleSend = async function() {
790
+ if (isGenerating) return;
791
+
792
+ const textarea = document.getElementById("user-input");
793
+ const rawPrompt = textarea.value;
794
+ if (!rawPrompt.trim() && !stagedFile) return;
795
+
796
+ textarea.value = "";
797
+ textarea.style.height = "24px";
798
+
799
+ // If it is a system command
800
+ if (rawPrompt.startsWith("/")) {
801
+ const wasCommand = processLocalCommand(rawPrompt);
802
+ if (wasCommand) return;
803
+ }
804
+
805
+ // Print user prompt
806
+ let userDisplayMessage = `visitor@kimi-k2.7:~$ ${rawPrompt}`;
807
+ if (stagedFile) {
808
+ userDisplayMessage += `\n[TRANSMITTING STAGED IMAGE: ${stagedFile.name}]`;
809
+ }
810
+ appendLog(userDisplayMessage, "user-prompt");
811
+
812
+ if (!client) {
813
+ appendLog("[ERR]: Gradio server client is offline. Action aborted.", "error");
814
+ return;
815
+ }
816
+
817
+ isGenerating = true;
818
+ document.getElementById("send-btn").textContent = "WAITING...";
819
+ document.getElementById("send-btn").disabled = true;
820
+
821
+ // Prepare inputs
822
+ const imageInput = stagedFile ? handle_file(stagedFile) : null;
823
+
824
+ // Append a placeholder element to stream the chatbot response into
825
+ const botLogElement = appendLog("Processing...", "bot-response");
826
+
827
+ try {
828
+ // Submit job to /chat endpoint
829
+ const job = client.submit("/chat", {
830
+ prompt: rawPrompt,
831
+ history_json: JSON.stringify(chatHistory),
832
+ image_file: imageInput
833
+ });
834
+
835
+ // Clear staged image now that it has been sent
836
+ if (stagedFile) {
837
+ clearStagedImage();
838
+ }
839
+
840
+ let lastTextResponse = "";
841
+
842
+ // Stream response chunks
843
+ for await (const msg of job) {
844
+ if (msg.type === "data") {
845
+ const newResponseText = msg.data[0];
846
+ if (newResponseText) {
847
+ lastTextResponse = newResponseText;
848
+ botLogElement.innerHTML = marked.parse(lastTextResponse);
849
+
850
+ // Highlight any code blocks that were parsed
851
+ if (window.Prism) {
852
+ Prism.highlightAllUnder(botLogElement);
853
+ }
854
+
855
+ // Keep scroll at bottom
856
+ const body = document.getElementById("terminal-body");
857
+ body.scrollTop = body.scrollHeight;
858
+ }
859
+ } else if (msg.type === "status") {
860
+ if (msg.stage === "error") {
861
+ botLogElement.innerHTML = `<span class="log-entry error">[API ERROR] Request rejected by endpoint.</span>`;
862
+ }
863
+ }
864
+ }
865
+
866
+ // If response completed successfully, append it to history
867
+ if (lastTextResponse) {
868
+ // Save history as simple text strings to prevent sending huge base64 on every request
869
+ chatHistory.push({ role: "user", content: rawPrompt });
870
+ chatHistory.push({ role: "assistant", content: lastTextResponse });
871
+ }
872
+
873
+ } catch (err) {
874
+ botLogElement.innerHTML = `<span class="log-entry error">[SYS RUNTIME ERROR]: ${err.message}</span>`;
875
+ } finally {
876
+ isGenerating = false;
877
+ document.getElementById("send-btn").textContent = "EXECUTE";
878
+ document.getElementById("send-btn").disabled = false;
879
+ }
880
+ };
881
+
882
+ // Textarea auto-growing and keybindings
883
+ const textarea = document.getElementById("user-input");
884
+ textarea.addEventListener("input", function() {
885
+ this.style.height = "24px";
886
+ this.style.height = (this.scrollHeight - 4) + "px";
887
+ });
888
+
889
+ textarea.addEventListener("keydown", function(e) {
890
+ if (e.key === "Enter" && !e.shiftKey) {
891
+ e.preventDefault();
892
+ handleSend();
893
+ }
894
+ });
895
+
896
+ // Drag & Drop event bindings
897
+ const dropArea = document.getElementById("drag-overlay");
898
+
899
+ window.addEventListener("dragenter", (e) => {
900
+ e.preventDefault();
901
+ dropArea.classList.add("active");
902
+ });
903
+
904
+ dropArea.addEventListener("dragover", (e) => {
905
+ e.preventDefault();
906
+ });
907
+
908
+ dropArea.addEventListener("dragleave", (e) => {
909
+ e.preventDefault();
910
+ // Check if leaving body/overlay boundary
911
+ if (e.relatedTarget === null || !dropArea.contains(e.relatedTarget)) {
912
+ dropArea.classList.remove("active");
913
+ }
914
+ });
915
+
916
+ dropArea.addEventListener("drop", (e) => {
917
+ e.preventDefault();
918
+ dropArea.classList.remove("active");
919
+ const file = e.dataTransfer.files[0];
920
+ if (file) {
921
+ stageImage(file);
922
+ }
923
+ });
924
+
925
+ // Run boot sequence on startup
926
+ playBootSequence();
927
+
928
+ </script>
929
+ <!-- PrismJS script files -->
930
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
931
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
932
+ </body>
933
+ </html>