TheShellMaster commited on
Commit
1213128
·
verified ·
1 Parent(s): c610abe

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +23 -3
  2. index.html +738 -0
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import os
2
  import gradio as gr
3
  from fastapi import FastAPI, Request
4
- from fastapi.responses import JSONResponse, RedirectResponse
5
  from huggingface_hub import InferenceClient, HfApi, create_repo
6
  from duckduckgo_search import DDGS
7
  import json
@@ -63,8 +63,28 @@ def save_log(username: str, message: str, response: str):
63
  print(f"Erreur d'enregistrement du log de discussion: {e}")
64
 
65
  @app.get("/")
66
- async def root():
67
- return RedirectResponse(url="/gradio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  @app.post("/api/chat")
70
  async def chat(request: Request):
 
1
  import os
2
  import gradio as gr
3
  from fastapi import FastAPI, Request
4
+ from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse
5
  from huggingface_hub import InferenceClient, HfApi, create_repo
6
  from duckduckgo_search import DDGS
7
  import json
 
63
  print(f"Erreur d'enregistrement du log de discussion: {e}")
64
 
65
  @app.get("/")
66
+ async def root(request: Request):
67
+ try:
68
+ with open("index.html", "r", encoding="utf-8") as f:
69
+ html_content = f.read()
70
+ return HTMLResponse(content=html_content, status_code=200)
71
+ except Exception as e:
72
+ return HTMLResponse(content=f"Error loading interface: {str(e)}", status_code=500)
73
+
74
+ @app.get("/api/user-profile")
75
+ async def user_profile(request: Request):
76
+ username = request.headers.get("x-hf-user-name") or "invité"
77
+ avatar = request.headers.get("x-hf-user-avatar") or ""
78
+ email = request.headers.get("x-hf-user-email") or ""
79
+ return {"username": username, "avatar": avatar, "email": email}
80
+
81
+ @app.get("/login/huggingface")
82
+ async def login_hf():
83
+ return RedirectResponse(url="/gradio/login/huggingface")
84
+
85
+ @app.get("/logout")
86
+ async def logout_hf():
87
+ return RedirectResponse(url="/gradio/logout")
88
 
89
  @app.post("/api/chat")
90
  async def chat(request: Request):
index.html ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cypher Coder CLI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ * {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ font-family: 'JetBrains Mono', monospace;
16
+ border-radius: 0 !important;
17
+ }
18
+
19
+ body {
20
+ background-color: #0D0D0D;
21
+ color: #E0E0E0;
22
+ height: 100vh;
23
+ display: flex;
24
+ flex-direction: column;
25
+ overflow: hidden;
26
+ font-size: 14px;
27
+ line-height: 1.5;
28
+ }
29
+
30
+ /* Scrollbars custom */
31
+ ::-webkit-scrollbar {
32
+ width: 6px;
33
+ height: 6px;
34
+ }
35
+ ::-webkit-scrollbar-track {
36
+ background: #0D0D0D;
37
+ }
38
+ ::-webkit-scrollbar-thumb {
39
+ background: #2A2A2A;
40
+ }
41
+ ::-webkit-scrollbar-thumb:hover {
42
+ background: #444444;
43
+ }
44
+
45
+ /* HEADER */
46
+ header {
47
+ height: 48px;
48
+ background-color: #111111;
49
+ border-bottom: 1px solid #2A2A2A;
50
+ display: flex;
51
+ align-items: center;
52
+ padding: 0 20px;
53
+ justify-content: space-between;
54
+ flex-shrink: 0;
55
+ }
56
+
57
+ .header-left {
58
+ display: flex;
59
+ align-items: center;
60
+ }
61
+
62
+ .pulsing-dot {
63
+ width: 8px;
64
+ height: 8px;
65
+ background-color: #00FFAA;
66
+ border-radius: 50% !important;
67
+ margin-right: 12px;
68
+ display: inline-block;
69
+ animation: pulse 1.5s infinite;
70
+ }
71
+
72
+ @keyframes pulse {
73
+ 0% {
74
+ transform: scale(0.95);
75
+ box-shadow: 0 0 0 0 rgba(0, 255, 170, 0.7);
76
+ }
77
+ 70% {
78
+ transform: scale(1);
79
+ box-shadow: 0 0 0 6px rgba(0, 255, 170, 0);
80
+ }
81
+ 100% {
82
+ transform: scale(0.95);
83
+ box-shadow: 0 0 0 0 rgba(0, 255, 170, 0);
84
+ }
85
+ }
86
+
87
+ .header-title {
88
+ font-weight: bold;
89
+ letter-spacing: 1px;
90
+ margin-right: 15px;
91
+ }
92
+
93
+ .header-separator {
94
+ color: #2A2A2A;
95
+ margin: 0 15px;
96
+ }
97
+
98
+ .header-meta {
99
+ color: #888888;
100
+ }
101
+
102
+ /* ZONE DE CHAT */
103
+ main {
104
+ flex-grow: 1;
105
+ overflow-y: auto;
106
+ padding: 20px;
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 20px;
110
+ background-color: #0D0D0D;
111
+ }
112
+
113
+ .chat-line {
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: flex-start;
117
+ }
118
+
119
+ .user-msg {
120
+ color: #E0E0E0;
121
+ }
122
+
123
+ .user-prefix {
124
+ color: #C792EA;
125
+ font-weight: normal;
126
+ }
127
+
128
+ .msg-timestamp {
129
+ color: #444444;
130
+ font-size: 12px;
131
+ margin-top: 4px;
132
+ margin-left: 16px;
133
+ }
134
+
135
+ .agent-msg {
136
+ color: #00FFAA;
137
+ }
138
+
139
+ .agent-prefix {
140
+ color: #00FFAA;
141
+ font-weight: normal;
142
+ }
143
+
144
+ .agent-content {
145
+ white-space: pre-wrap;
146
+ margin-left: 20px;
147
+ }
148
+
149
+ .agent-content * {
150
+ font-weight: normal !important;
151
+ font-style: normal !important;
152
+ }
153
+
154
+ /* Style des blocs de code */
155
+ pre {
156
+ background-color: #111111;
157
+ border: 1px solid #2A2A2A;
158
+ padding: 12px;
159
+ margin: 10px 0;
160
+ width: 100%;
161
+ overflow-x: auto;
162
+ }
163
+
164
+ code {
165
+ color: #FFD700;
166
+ }
167
+
168
+ /* Outils et spinner */
169
+ .tool-block {
170
+ margin-left: 20px;
171
+ color: #569CD6;
172
+ margin-top: 6px;
173
+ margin-bottom: 6px;
174
+ }
175
+
176
+ .tool-header {
177
+ color: #2A2A2A;
178
+ }
179
+
180
+ .tool-name {
181
+ color: #FFD700;
182
+ }
183
+
184
+ .spinner {
185
+ display: inline-block;
186
+ color: #FFD700;
187
+ margin-right: 8px;
188
+ }
189
+
190
+ /* AUTOCOMPLETE POPUP */
191
+ #autocomplete-popup {
192
+ position: absolute;
193
+ bottom: 54px;
194
+ left: 20px;
195
+ width: 450px;
196
+ background-color: #161616;
197
+ border: 1px solid #2A2A2A;
198
+ z-index: 1000;
199
+ display: none;
200
+ max-height: 300px;
201
+ overflow-y: auto;
202
+ }
203
+
204
+ .autocomplete-item {
205
+ padding: 8px 12px;
206
+ display: flex;
207
+ justify-content: space-between;
208
+ cursor: pointer;
209
+ }
210
+
211
+ .autocomplete-item:hover, .autocomplete-item.selected {
212
+ background-color: #1E1E1E;
213
+ }
214
+
215
+ .autocomplete-item.selected .cmd-name {
216
+ color: #FFD700;
217
+ }
218
+
219
+ .cmd-name {
220
+ color: #E0E0E0;
221
+ }
222
+
223
+ .cmd-desc {
224
+ color: #666666;
225
+ font-size: 12px;
226
+ }
227
+
228
+ /* BARRE D'INPUT */
229
+ .input-bar {
230
+ height: 52px;
231
+ background-color: #111111;
232
+ border-top: 1px solid #2A2A2A;
233
+ display: flex;
234
+ align-items: center;
235
+ padding: 0 20px;
236
+ position: relative;
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .input-prefix {
241
+ color: #00FFAA;
242
+ margin-right: 12px;
243
+ font-weight: bold;
244
+ }
245
+
246
+ #cli-input {
247
+ background: transparent;
248
+ border: none;
249
+ outline: none;
250
+ color: #E0E0E0;
251
+ flex-grow: 1;
252
+ font-size: 14px;
253
+ }
254
+
255
+ #cli-input::placeholder {
256
+ color: #444444;
257
+ }
258
+
259
+ /* FOOTER */
260
+ footer {
261
+ height: 28px;
262
+ background-color: #0A0A0A;
263
+ border-top: 1px solid #1A1A1A;
264
+ display: flex;
265
+ align-items: center;
266
+ padding: 0 20px;
267
+ color: #444444;
268
+ font-size: 11px;
269
+ flex-shrink: 0;
270
+ }
271
+
272
+ /* ECRAN DE LOGIN */
273
+ .login-container {
274
+ display: flex;
275
+ flex-direction: column;
276
+ align-items: center;
277
+ justify-content: center;
278
+ height: 100vh;
279
+ background-color: #0D0D0D;
280
+ padding: 20px;
281
+ }
282
+
283
+ .login-box {
284
+ border: 1px solid #2A2A2A;
285
+ background-color: #111111;
286
+ padding: 30px;
287
+ max-width: 500px;
288
+ width: 100%;
289
+ text-align: center;
290
+ }
291
+
292
+ .login-title {
293
+ color: #00FFAA;
294
+ font-size: 20px;
295
+ margin-bottom: 15px;
296
+ font-weight: bold;
297
+ }
298
+
299
+ .login-desc {
300
+ color: #888888;
301
+ margin-bottom: 25px;
302
+ font-size: 13px;
303
+ }
304
+
305
+ .login-btn {
306
+ background-color: transparent;
307
+ border: 1px solid #00FFAA;
308
+ color: #00FFAA;
309
+ padding: 12px 24px;
310
+ font-size: 14px;
311
+ cursor: pointer;
312
+ font-weight: bold;
313
+ transition: all 0.2s;
314
+ }
315
+
316
+ .login-btn:hover {
317
+ background-color: #00FFAA;
318
+ color: #0D0D0D;
319
+ }
320
+
321
+ .ansi-banner {
322
+ color: #569CD6;
323
+ margin-bottom: 20px;
324
+ white-space: pre;
325
+ font-size: 12px;
326
+ line-height: 1.2;
327
+ text-align: left;
328
+ display: inline-block;
329
+ }
330
+ </style>
331
+ </head>
332
+ <body>
333
+
334
+ <!-- AUTHENTICATION LAYER -->
335
+ <div id="auth-layer" class="login-container" style="display: none;">
336
+ <div class="ansi-banner">
337
+ _____ _---------------+
338
+ / ____| | | ____ _ |
339
+ | | _ _ _ __ | |__| _ \| | | CYPHER CODER CLI
340
+ | | | | | | '_ \ | __ |_) | | | L'IA experte en développement local
341
+ | |___| |_| | |_) || | | __/| |___ | Créé par DJAKOUA KWANKAM
342
+ \_____\__, | .__/ |_| |_| |_____|| Institut Universitaire de Douala (IUD)
343
+ __/ | | |
344
+ |___/|_| +-----------------------+
345
+ </div>
346
+ <div class="login-box">
347
+ <div class="login-title">🔐 AUTHENTIFICATION REQUISE</div>
348
+ <div class="login-desc">Veuillez vous connecter avec votre compte Hugging Face pour accéder au terminal de Cypher Coder.</div>
349
+ <button class="login-btn" onclick="redirectToLogin()">SE CONNECTER AVEC HUGGING FACE</button>
350
+ </div>
351
+ </div>
352
+
353
+ <!-- MAIN APP LAYER -->
354
+ <div id="app-layer" style="display: flex; flex-direction: column; height: 100vh; width: 100%;">
355
+ <!-- HEADER -->
356
+ <header>
357
+ <div class="header-left">
358
+ <span class="pulsing-dot"></span>
359
+ <span class="header-title" style="color: #E0E0E0;">CYPHER CODER v1.0</span>
360
+ <span class="header-separator">|</span>
361
+ <span id="header-username" class="header-meta">@loading</span>
362
+ <span class="header-separator">|</span>
363
+ <span id="header-model" class="header-meta">modèle : Qwen2.5-Coder-32B</span>
364
+ <span class="header-separator">|</span>
365
+ <span id="header-tokens" class="header-meta">tokens : 0</span>
366
+ </div>
367
+ <div>
368
+ <a href="/logout" style="color: #FF5555; text-decoration: none; font-size: 12px;">[Déconnexion]</a>
369
+ </div>
370
+ </header>
371
+
372
+ <!-- ZONE DE CHAT -->
373
+ <main id="chat-area">
374
+ <!-- Startup Message (Section 10) -->
375
+ <div class="chat-line" style="white-space: pre-wrap; color: #569CD6; margin-bottom: 20px;">
376
+ _____ _---------------+
377
+ / ____| | | ____ _ |
378
+ | | _ _ _ __ | |__| _ \| | | CYPHER CODER CLI
379
+ | | | | | | '_ \ | __ |_) | | | L'IA experte en développement local
380
+ | |___| |_| | |_) || | | __/| |___ | Créé par DJAKOUA KWANKAM
381
+ \_____\__, | .__/ |_| |_| |_____|| Institut Universitaire de Douala (IUD)
382
+ __/ | | |
383
+ |___/|_| +-----------------------+
384
+
385
+ CYPHER CODER CLI — L'IA experte en developpement local
386
+ Cree par DJAKOUA KWANKAM — Institut Universitaire de Douala (IUD)
387
+ ─────────────────────────────────────────────────────────────────
388
+ Connecte en tant que : <span id="banner-username" style="color: #E0E0E0;">@loading</span>
389
+ Modele actif : <span id="banner-model" style="color: #E0E0E0;">Qwen/Qwen2.5-Coder-32B-Instruct</span>
390
+ Session : <span id="banner-session" style="color: #E0E0E0;">[loading]</span>
391
+ ─────────────────────────────────────────────────────────────────
392
+ Tape /help pour voir les commandes disponibles.</div>
393
+ </main>
394
+
395
+ <!-- AUTOCOMPLETE POPUP -->
396
+ <div id="autocomplete-popup"></div>
397
+
398
+ <!-- BARRE D'INPUT -->
399
+ <div class="input-bar">
400
+ <span class="input-prefix">❯</span>
401
+ <input type="text" id="cli-input" placeholder="Tape une commande ou / pour les raccourcis..." autocomplete="off">
402
+ </div>
403
+
404
+ <!-- FOOTER -->
405
+ <footer>
406
+ Cypher Coder CLI • DJAKOUA KWANKAM • IUT Douala • Enter:envoyer • /:commandes • ↑↓:historique
407
+ </footer>
408
+ </div>
409
+
410
+ <script>
411
+ const chatArea = document.getElementById("chat-area");
412
+ const cliInput = document.getElementById("cli-input");
413
+ const popup = document.getElementById("autocomplete-popup");
414
+ const headerUsername = document.getElementById("header-username");
415
+ const bannerUsername = document.getElementById("banner-username");
416
+ const bannerSession = document.getElementById("banner-session");
417
+
418
+ let currentUser = "invité";
419
+ let sessionId = "session_" + Math.random().toString(36).substring(2, 10);
420
+ let chatHistory = [];
421
+ let inputHistory = [];
422
+ let historyIndex = -1;
423
+
424
+ const commands = [
425
+ { name: '/help', desc: "Afficher l'aide" },
426
+ { name: '/clear', desc: "Vider le terminal" },
427
+ { name: '/reset', desc: "Réinitialiser session" },
428
+ { name: '/model', desc: "Changer de modèle" },
429
+ { name: '/history', desc: "Voir l'historique" },
430
+ { name: '/file load', desc: "Charger un fichier" },
431
+ { name: '/exec', desc: "Exécuter une commande" },
432
+ { name: '/status', desc: "Statut de l'agent" },
433
+ { name: '/context', desc: "Voir le contexte actuel" },
434
+ { name: '/tokens', desc: "Tokens utilisés" }
435
+ ];
436
+
437
+ let filteredCommands = [];
438
+ let selectedIndex = 0;
439
+
440
+ // Fetch User Info on Load
441
+ async function fetchUserInfo() {
442
+ try {
443
+ const res = await fetch("/api/user-profile");
444
+ const data = await res.json();
445
+ if (data.username && data.username !== "invité") {
446
+ currentUser = data.username;
447
+ document.getElementById("auth-layer").style.display = "none";
448
+ document.getElementById("app-layer").style.display = "flex";
449
+ } else {
450
+ document.getElementById("auth-layer").style.display = "flex";
451
+ document.getElementById("app-layer").style.display = "none";
452
+ }
453
+ headerUsername.innerText = "@" + currentUser;
454
+ bannerUsername.innerText = "@" + currentUser;
455
+ bannerSession.innerText = sessionId;
456
+ } catch (e) {
457
+ console.error("Auth fetch error:", e);
458
+ }
459
+ }
460
+
461
+ function redirectToLogin() {
462
+ window.location.href = "/login/huggingface";
463
+ }
464
+
465
+ fetchUserInfo();
466
+
467
+ // Autocomplete rendering
468
+ function showPopup(query) {
469
+ filteredCommands = commands.filter(c => c.name.startsWith(query));
470
+ if (filteredCommands.length === 0) {
471
+ popup.style.display = "none";
472
+ return;
473
+ }
474
+ selectedIndex = 0;
475
+ renderPopup();
476
+ popup.style.display = "block";
477
+ }
478
+
479
+ function renderPopup() {
480
+ popup.innerHTML = "";
481
+ filteredCommands.forEach((cmd, idx) => {
482
+ const item = document.createElement("div");
483
+ item.className = "autocomplete-item" + (idx === selectedIndex ? " selected" : "");
484
+ item.innerHTML = `<span class="cmd-name">${cmd.name}</span><span class="cmd-desc">${cmd.desc}</span>`;
485
+ item.onclick = () => {
486
+ cliInput.value = cmd.name;
487
+ popup.style.display = "none";
488
+ cliInput.focus();
489
+ };
490
+ popup.appendChild(item);
491
+ });
492
+ }
493
+
494
+ // Output formatting
495
+ function appendUserMessage(msg) {
496
+ const time = new Date().toTimeString().split(' ')[0];
497
+ const div = document.createElement("div");
498
+ div.className = "chat-line";
499
+ div.innerHTML = `
500
+ <div class="user-msg"><span class="user-prefix">❯ ${currentUser} :</span> ${msg}</div>
501
+ <div class="msg-timestamp">└ [timestamp ${time}]</div>
502
+ `;
503
+ chatArea.appendChild(div);
504
+ chatArea.scrollTop = chatArea.scrollHeight;
505
+ }
506
+
507
+ function appendAgentMessage(content) {
508
+ const div = document.createElement("div");
509
+ div.className = "chat-line";
510
+ div.innerHTML = `
511
+ <div class="agent-msg"><span class="agent-prefix">▸ Cypher :</span></div>
512
+ <div class="agent-content">${content}</div>
513
+ `;
514
+ chatArea.appendChild(div);
515
+ chatArea.scrollTop = chatArea.scrollHeight;
516
+ }
517
+
518
+ function appendSystemMessage(msg, isError = false) {
519
+ const div = document.createElement("div");
520
+ div.className = "chat-line";
521
+ const color = isError ? "#FF5555" : "#569CD6";
522
+ div.innerHTML = `<div style="color: ${color};">${msg}</div>`;
523
+ chatArea.appendChild(div);
524
+ chatArea.scrollTop = chatArea.scrollHeight;
525
+ }
526
+
527
+ // Active tools visual feedback (Section 5)
528
+ function appendToolTracking(toolName) {
529
+ const div = document.createElement("div");
530
+ div.className = "chat-line tool-block";
531
+ div.id = "active-tool-block";
532
+ div.innerHTML = `
533
+ <div class="tool-header">┌─ [TOOL] <span class="tool-name">${toolName}</span></div>
534
+ <div class="tool-status">│ <span class="spinner">⠋</span>running...</div>
535
+ <div class="tool-footer">└─</div>
536
+ `;
537
+ chatArea.appendChild(div);
538
+ chatArea.scrollTop = chatArea.scrollHeight;
539
+
540
+ // Simple JS Spinner animation
541
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
542
+ let fIdx = 0;
543
+ const spinnerInterval = setInterval(() => {
544
+ const spinElem = div.querySelector(".spinner");
545
+ if (spinElem) {
546
+ spinElem.innerText = frames[fIdx];
547
+ fIdx = (fIdx + 1) % frames.length;
548
+ } else {
549
+ clearInterval(spinnerInterval);
550
+ }
551
+ }, 80);
552
+
553
+ return {
554
+ done: (msg = "done") => {
555
+ clearInterval(spinnerInterval);
556
+ div.querySelector(".tool-status").innerHTML = `│ ${msg}`;
557
+ div.querySelector(".tool-footer").innerHTML = `└─ done`;
558
+ div.id = ""; // remove active id
559
+ }
560
+ };
561
+ }
562
+
563
+ // Keypress handler
564
+ cliInput.addEventListener("keydown", async (e) => {
565
+ const val = cliInput.value;
566
+
567
+ // Escape closes popup
568
+ if (e.key === "Escape") {
569
+ popup.style.display = "none";
570
+ return;
571
+ }
572
+
573
+ // Tab / Enter autocomplete navigation
574
+ if (popup.style.display === "block") {
575
+ if (e.key === "ArrowDown") {
576
+ e.preventDefault();
577
+ selectedIndex = (selectedIndex + 1) % filteredCommands.length;
578
+ renderPopup();
579
+ return;
580
+ }
581
+ if (e.key === "ArrowUp") {
582
+ e.preventDefault();
583
+ selectedIndex = (selectedIndex - 1 + filteredCommands.length) % filteredCommands.length;
584
+ renderPopup();
585
+ return;
586
+ }
587
+ if (e.key === "Tab" || e.key === "Enter") {
588
+ e.preventDefault();
589
+ cliInput.value = filteredCommands[selectedIndex].name;
590
+ popup.style.display = "none";
591
+ return;
592
+ }
593
+ }
594
+
595
+ // Command history navigation
596
+ if (e.key === "ArrowUp" && popup.style.display !== "block") {
597
+ e.preventDefault();
598
+ if (inputHistory.length > 0) {
599
+ if (historyIndex === -1) historyIndex = inputHistory.length;
600
+ historyIndex = Math.max(0, historyIndex - 1);
601
+ cliInput.value = inputHistory[historyIndex];
602
+ }
603
+ return;
604
+ }
605
+ if (e.key === "ArrowDown" && popup.style.display !== "block") {
606
+ e.preventDefault();
607
+ if (inputHistory.length > 0 && historyIndex !== -1) {
608
+ historyIndex = Math.min(inputHistory.length - 1, historyIndex + 1);
609
+ cliInput.value = inputHistory[historyIndex];
610
+ }
611
+ return;
612
+ }
613
+
614
+ // Handle Input Submission
615
+ if (e.key === "Enter") {
616
+ e.preventDefault();
617
+ const text = val.trim();
618
+ if (!text) return;
619
+
620
+ appendUserMessage(text);
621
+ inputHistory.push(text);
622
+ historyIndex = -1;
623
+ cliInput.value = "";
624
+ popup.style.display = "none";
625
+
626
+ // Local execution commands handler
627
+ if (text.startsWith("/")) {
628
+ await handleLocalSlash(text);
629
+ return;
630
+ }
631
+
632
+ // Send to backend
633
+ await sendToBackend(text);
634
+ }
635
+ });
636
+
637
+ // Trigger autocomplete on typing '/'
638
+ cliInput.addEventListener("input", (e) => {
639
+ const val = cliInput.value;
640
+ if (val.startsWith("/")) {
641
+ showPopup(val);
642
+ } else {
643
+ popup.style.display = "none";
644
+ }
645
+ });
646
+
647
+ // Local slash commands logic
648
+ async function handleLocalSlash(text) {
649
+ const cmd = text.split(" ")[0].toLowerCase();
650
+ if (cmd === "/clear") {
651
+ chatArea.innerHTML = "";
652
+ appendSystemMessage("Terminal vide.");
653
+ } else if (cmd === "/help") {
654
+ appendSystemMessage(`
655
+ 📚 COMMANDES DISPONIBLES :
656
+ /help - Affiche ce menu d'aide
657
+ /clear - Vide le terminal
658
+ /reset - Réinitialise le contexte
659
+ /model - Affiche ou modifie le modèle actif
660
+ /history - Affiche l'historique des requêtes
661
+ /status - Affiche le statut complet de la session
662
+ /tokens - Affiche l'estimation des tokens
663
+ `);
664
+ } else if (cmd === "/reset") {
665
+ chatHistory = [];
666
+ appendSystemMessage("Discussion et contexte réinitialisés.");
667
+ } else if (cmd === "/status") {
668
+ appendSystemMessage(`
669
+ === STATUT DE L'AGENT ===
670
+ Utilisateur : @${currentUser}
671
+ Modèle : Qwen/Qwen2.5-Coder-32B-Instruct
672
+ Session ID : ${sessionId}
673
+ Dossier : [Mode Web distant - Outils système désactivés]
674
+ `);
675
+ } else if (cmd === "/tokens") {
676
+ appendSystemMessage("Statistiques : ~0 tokens utilisés.");
677
+ } else {
678
+ appendSystemMessage(`Commande ${cmd} non disponible ou simulée en ligne. Utilisez le CLI local pour toutes les fonctionnalités système.`, true);
679
+ }
680
+ }
681
+
682
+ // Send chat query to FastAPI backend
683
+ async function sendToBackend(message) {
684
+ chatHistory.push({ role: "user", content: message });
685
+
686
+ // Show typing indicator
687
+ const typingDiv = document.createElement("div");
688
+ typingDiv.className = "chat-line";
689
+ typingDiv.id = "typing-indicator";
690
+ typingDiv.innerHTML = `<span class="agent-prefix">▸ Cypher ▋</span>`;
691
+ chatArea.appendChild(typingDiv);
692
+ chatArea.scrollTop = chatArea.scrollHeight;
693
+
694
+ try {
695
+ // We POST to our backend `/api/chat`
696
+ const res = await fetch("/api/chat", {
697
+ method: "POST",
698
+ headers: { "Content-Type": "application/json" },
699
+ body: JSON.stringify({
700
+ messages: [
701
+ { role: "system", content: "Tu es Cypher Coder, assistant IA en monospace. Pas d'emoji, pas de gras, pas d'italique dans tes réponses." },
702
+ ...chatHistory
703
+ ],
704
+ username: currentUser
705
+ })
706
+ });
707
+
708
+ const data = await res.json();
709
+
710
+ // Remove typing indicator
711
+ const indicator = document.getElementById("typing-indicator");
712
+ if (indicator) indicator.remove();
713
+
714
+ if (data.error) {
715
+ appendSystemMessage("Erreur serveur : " + data.error, true);
716
+ return;
717
+ }
718
+
719
+ const reply = data.message.content || "[Aucun contenu]";
720
+ chatHistory.push({ role: "assistant", content: reply });
721
+
722
+ // Print response
723
+ appendAgentMessage(reply);
724
+
725
+ // Check if tokens headers or meta are sent
726
+ if (data.tokens) {
727
+ document.getElementById("header-tokens").innerText = "tokens : " + data.tokens;
728
+ }
729
+
730
+ } catch (e) {
731
+ const indicator = document.getElementById("typing-indicator");
732
+ if (indicator) indicator.remove();
733
+ appendSystemMessage("Erreur réseau : " + e.message, true);
734
+ }
735
+ }
736
+ </script>
737
+ </body>
738
+ </html>