Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cypher Coder CLI</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'JetBrains Mono', monospace; | |
| border-radius: 0 ; | |
| } | |
| body { | |
| background-color: #0D0D0D; | |
| color: #E0E0E0; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| /* Scrollbars custom */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #0D0D0D; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #2A2A2A; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #444444; | |
| } | |
| /* HEADER */ | |
| header { | |
| height: 48px; | |
| background-color: #111111; | |
| border-bottom: 1px solid #2A2A2A; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 20px; | |
| justify-content: space-between; | |
| flex-shrink: 0; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .pulsing-dot { | |
| width: 8px; | |
| height: 8px; | |
| background-color: #00FFAA; | |
| border-radius: 50% ; | |
| margin-right: 12px; | |
| display: inline-block; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| transform: scale(0.95); | |
| box-shadow: 0 0 0 0 rgba(0, 255, 170, 0.7); | |
| } | |
| 70% { | |
| transform: scale(1); | |
| box-shadow: 0 0 0 6px rgba(0, 255, 170, 0); | |
| } | |
| 100% { | |
| transform: scale(0.95); | |
| box-shadow: 0 0 0 0 rgba(0, 255, 170, 0); | |
| } | |
| } | |
| .header-title { | |
| font-weight: bold; | |
| letter-spacing: 1px; | |
| margin-right: 15px; | |
| } | |
| .header-separator { | |
| color: #2A2A2A; | |
| margin: 0 15px; | |
| } | |
| .header-meta { | |
| color: #888888; | |
| } | |
| /* ZONE DE CHAT */ | |
| main { | |
| flex-grow: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| background-color: #0D0D0D; | |
| } | |
| .chat-line { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .user-msg { | |
| color: #E0E0E0; | |
| } | |
| .user-prefix { | |
| color: #C792EA; | |
| font-weight: normal; | |
| } | |
| .msg-timestamp { | |
| color: #444444; | |
| font-size: 12px; | |
| margin-top: 4px; | |
| margin-left: 16px; | |
| } | |
| .agent-msg { | |
| color: #00FFAA; | |
| } | |
| .agent-prefix { | |
| color: #00FFAA; | |
| font-weight: normal; | |
| } | |
| .agent-content { | |
| white-space: pre-wrap; | |
| margin-left: 20px; | |
| } | |
| .agent-content * { | |
| font-weight: normal ; | |
| font-style: normal ; | |
| } | |
| /* Style des blocs de code */ | |
| pre { | |
| background-color: #111111; | |
| border: 1px solid #2A2A2A; | |
| padding: 12px; | |
| margin: 10px 0; | |
| width: 100%; | |
| overflow-x: auto; | |
| } | |
| code { | |
| color: #FFD700; | |
| } | |
| /* Outils et spinner */ | |
| .tool-block { | |
| margin-left: 20px; | |
| color: #569CD6; | |
| margin-top: 6px; | |
| margin-bottom: 6px; | |
| } | |
| .tool-header { | |
| color: #2A2A2A; | |
| } | |
| .tool-name { | |
| color: #FFD700; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| color: #FFD700; | |
| margin-right: 8px; | |
| } | |
| /* AUTOCOMPLETE POPUP */ | |
| #autocomplete-popup { | |
| position: absolute; | |
| bottom: 54px; | |
| left: 20px; | |
| width: 450px; | |
| background-color: #161616; | |
| border: 1px solid #2A2A2A; | |
| z-index: 1000; | |
| display: none; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .autocomplete-item { | |
| padding: 8px 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| cursor: pointer; | |
| } | |
| .autocomplete-item:hover, .autocomplete-item.selected { | |
| background-color: #1E1E1E; | |
| } | |
| .autocomplete-item.selected .cmd-name { | |
| color: #FFD700; | |
| } | |
| .cmd-name { | |
| color: #E0E0E0; | |
| } | |
| .cmd-desc { | |
| color: #666666; | |
| font-size: 12px; | |
| } | |
| /* BARRE D'INPUT */ | |
| .input-bar { | |
| height: 52px; | |
| background-color: #111111; | |
| border-top: 1px solid #2A2A2A; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 20px; | |
| position: relative; | |
| flex-shrink: 0; | |
| } | |
| .input-prefix { | |
| color: #00FFAA; | |
| margin-right: 12px; | |
| font-weight: bold; | |
| } | |
| #cli-input { | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: #E0E0E0; | |
| flex-grow: 1; | |
| font-size: 14px; | |
| } | |
| #cli-input::placeholder { | |
| color: #444444; | |
| } | |
| /* FOOTER */ | |
| footer { | |
| height: 28px; | |
| background-color: #0A0A0A; | |
| border-top: 1px solid #1A1A1A; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 20px; | |
| color: #444444; | |
| font-size: 11px; | |
| flex-shrink: 0; | |
| } | |
| /* ECRAN DE LOGIN */ | |
| .login-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100vh; | |
| background-color: #0D0D0D; | |
| padding: 20px; | |
| } | |
| .login-box { | |
| border: 1px solid #2A2A2A; | |
| background-color: #111111; | |
| padding: 30px; | |
| max-width: 500px; | |
| width: 100%; | |
| text-align: center; | |
| } | |
| .login-title { | |
| color: #00FFAA; | |
| font-size: 20px; | |
| margin-bottom: 15px; | |
| font-weight: bold; | |
| } | |
| .login-desc { | |
| color: #888888; | |
| margin-bottom: 25px; | |
| font-size: 13px; | |
| } | |
| .login-btn { | |
| background-color: transparent; | |
| border: 1px solid #00FFAA; | |
| color: #00FFAA; | |
| padding: 12px 24px; | |
| font-size: 14px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: all 0.2s; | |
| } | |
| .login-btn:hover { | |
| background-color: #00FFAA; | |
| color: #0D0D0D; | |
| } | |
| .ansi-banner { | |
| color: #569CD6; | |
| margin-bottom: 20px; | |
| white-space: pre; | |
| font-size: 12px; | |
| line-height: 1.2; | |
| text-align: left; | |
| display: inline-block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- MAIN APP LAYER --> | |
| <div id="app-layer" style="display: flex; flex-direction: column; height: 100vh; width: 100%;"> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="header-left"> | |
| <span class="pulsing-dot"></span> | |
| <span class="header-title" style="color: #E0E0E0;">CYPHER CODER v1.0</span> | |
| <span class="header-separator">|</span> | |
| <span id="header-username" class="header-meta">@loading</span> | |
| <span class="header-separator">|</span> | |
| <span id="header-model" class="header-meta">modèle : Qwen2.5-Coder-32B</span> | |
| <span class="header-separator">|</span> | |
| <span id="header-tokens" class="header-meta">tokens : 0</span> | |
| </div> | |
| </header> | |
| <!-- ZONE DE CHAT --> | |
| <main id="chat-area"> | |
| <!-- Startup Message (Section 10) --> | |
| <div class="chat-line" style="white-space: pre-wrap; color: #569CD6; margin-bottom: 20px;"> | |
| _____ _---------------+ | |
| / ____| | | ____ _ | | |
| | | _ _ _ __ | |__| _ \| | | CYPHER CODER CLI | |
| | | | | | | '_ \ | __ |_) | | | L'IA experte en développement local | |
| | |___| |_| | |_) || | | __/| |___ | Créé par DJAKOUA KWANKAM | |
| \_____\__, | .__/ |_| |_| |_____|| Institut Universitaire de Technologie de Douala (IUT) | |
| __/ | | | | |
| |___/|_| +-----------------------+ | |
| CYPHER CODER CLI — L'IA experte en developpement local | |
| Cree par DJAKOUA KWANKAM — Institut Universitaire de Technologie de Douala (IUT) | |
| ───────────────────────────────────────────────────────────────── | |
| Connecte en tant que : <span id="banner-username" style="color: #E0E0E0;">@loading</span> | |
| Modele actif : <span id="banner-model" style="color: #E0E0E0;">Qwen/Qwen2.5-Coder-32B-Instruct</span> | |
| Session : <span id="banner-session" style="color: #E0E0E0;">[loading]</span> | |
| Jeu de donnees : <span style="color: #E0E0E0;">Collecte active pour entrainement (dataset prive)</span> | |
| ───────────────────────────────────────────────────────────────── | |
| Tape /help pour voir les commandes disponibles.</div> | |
| </main> | |
| <!-- AUTOCOMPLETE POPUP --> | |
| <div id="autocomplete-popup"></div> | |
| <!-- BARRE D'INPUT --> | |
| <div class="input-bar"> | |
| <span class="input-prefix">❯</span> | |
| <input type="text" id="cli-input" placeholder="Tape une commande ou / pour les raccourcis..." autocomplete="off"> | |
| </div> | |
| <!-- FOOTER --> | |
| <footer> | |
| Cypher Coder CLI • DJAKOUA KWANKAM • IUT Douala • Enter:envoyer • /:commandes • ↑↓:historique | |
| </footer> | |
| </div> | |
| <script> | |
| const chatArea = document.getElementById("chat-area"); | |
| const cliInput = document.getElementById("cli-input"); | |
| const popup = document.getElementById("autocomplete-popup"); | |
| const headerUsername = document.getElementById("header-username"); | |
| const bannerUsername = document.getElementById("banner-username"); | |
| const bannerSession = document.getElementById("banner-session"); | |
| let currentUser = "invité"; | |
| let sessionId = "session_" + Math.random().toString(36).substring(2, 10); | |
| let chatHistory = []; | |
| let inputHistory = []; | |
| let historyIndex = -1; | |
| const commands = [ | |
| { name: '/help', desc: "Afficher l'aide" }, | |
| { name: '/clear', desc: "Vider le terminal" }, | |
| { name: '/reset', desc: "Réinitialiser session" }, | |
| { name: '/model', desc: "Changer de modèle" }, | |
| { name: '/history', desc: "Voir l'historique" }, | |
| { name: '/file load', desc: "Charger un fichier" }, | |
| { name: '/exec', desc: "Exécuter une commande" }, | |
| { name: '/status', desc: "Statut de l'agent" }, | |
| { name: '/context', desc: "Voir le contexte actuel" }, | |
| { name: '/tokens', desc: "Tokens utilisés" } | |
| ]; | |
| let filteredCommands = []; | |
| let selectedIndex = 0; | |
| // Fetch User Info on Load | |
| async function fetchUserInfo() { | |
| try { | |
| const res = await fetch("/api/user-profile"); | |
| const data = await res.json(); | |
| if (data.username) { | |
| currentUser = data.username; | |
| } | |
| headerUsername.innerText = "@" + currentUser; | |
| bannerUsername.innerText = "@" + currentUser; | |
| bannerSession.innerText = sessionId; | |
| } catch (e) { | |
| console.error("Auth fetch error:", e); | |
| headerUsername.innerText = "@" + currentUser; | |
| bannerUsername.innerText = "@" + currentUser; | |
| bannerSession.innerText = sessionId; | |
| } | |
| } | |
| fetchUserInfo(); | |
| // Autocomplete rendering | |
| function showPopup(query) { | |
| filteredCommands = commands.filter(c => c.name.startsWith(query)); | |
| if (filteredCommands.length === 0) { | |
| popup.style.display = "none"; | |
| return; | |
| } | |
| selectedIndex = 0; | |
| renderPopup(); | |
| popup.style.display = "block"; | |
| } | |
| function renderPopup() { | |
| popup.innerHTML = ""; | |
| filteredCommands.forEach((cmd, idx) => { | |
| const item = document.createElement("div"); | |
| item.className = "autocomplete-item" + (idx === selectedIndex ? " selected" : ""); | |
| item.innerHTML = `<span class="cmd-name">${cmd.name}</span><span class="cmd-desc">${cmd.desc}</span>`; | |
| item.onclick = () => { | |
| cliInput.value = cmd.name; | |
| popup.style.display = "none"; | |
| cliInput.focus(); | |
| }; | |
| popup.appendChild(item); | |
| }); | |
| } | |
| // Output formatting | |
| function appendUserMessage(msg) { | |
| const time = new Date().toTimeString().split(' ')[0]; | |
| const div = document.createElement("div"); | |
| div.className = "chat-line"; | |
| div.innerHTML = ` | |
| <div class="user-msg"><span class="user-prefix">❯ ${currentUser} :</span> ${msg}</div> | |
| <div class="msg-timestamp">└ [timestamp ${time}]</div> | |
| `; | |
| chatArea.appendChild(div); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| function appendAgentMessage(content) { | |
| const div = document.createElement("div"); | |
| div.className = "chat-line"; | |
| div.innerHTML = ` | |
| <div class="agent-msg"><span class="agent-prefix">▸ Cypher :</span></div> | |
| <div class="agent-content">${content}</div> | |
| `; | |
| chatArea.appendChild(div); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| function appendSystemMessage(msg, isError = false) { | |
| const div = document.createElement("div"); | |
| div.className = "chat-line"; | |
| const color = isError ? "#FF5555" : "#569CD6"; | |
| div.innerHTML = `<div style="color: ${color};">${msg}</div>`; | |
| chatArea.appendChild(div); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| // Active tools visual feedback (Section 5) | |
| function appendToolTracking(toolName) { | |
| const div = document.createElement("div"); | |
| div.className = "chat-line tool-block"; | |
| div.id = "active-tool-block"; | |
| div.innerHTML = ` | |
| <div class="tool-header">┌─ [TOOL] <span class="tool-name">${toolName}</span></div> | |
| <div class="tool-status">│ <span class="spinner">⠋</span>running...</div> | |
| <div class="tool-footer">└─</div> | |
| `; | |
| chatArea.appendChild(div); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| // Simple JS Spinner animation | |
| const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; | |
| let fIdx = 0; | |
| const spinnerInterval = setInterval(() => { | |
| const spinElem = div.querySelector(".spinner"); | |
| if (spinElem) { | |
| spinElem.innerText = frames[fIdx]; | |
| fIdx = (fIdx + 1) % frames.length; | |
| } else { | |
| clearInterval(spinnerInterval); | |
| } | |
| }, 80); | |
| return { | |
| done: (msg = "done") => { | |
| clearInterval(spinnerInterval); | |
| div.querySelector(".tool-status").innerHTML = `│ ${msg}`; | |
| div.querySelector(".tool-footer").innerHTML = `└─ done`; | |
| div.id = ""; // remove active id | |
| } | |
| }; | |
| } | |
| // Keypress handler | |
| cliInput.addEventListener("keydown", async (e) => { | |
| const val = cliInput.value; | |
| // Escape closes popup | |
| if (e.key === "Escape") { | |
| popup.style.display = "none"; | |
| return; | |
| } | |
| // Tab / Enter autocomplete navigation | |
| if (popup.style.display === "block") { | |
| if (e.key === "ArrowDown") { | |
| e.preventDefault(); | |
| selectedIndex = (selectedIndex + 1) % filteredCommands.length; | |
| renderPopup(); | |
| return; | |
| } | |
| if (e.key === "ArrowUp") { | |
| e.preventDefault(); | |
| selectedIndex = (selectedIndex - 1 + filteredCommands.length) % filteredCommands.length; | |
| renderPopup(); | |
| return; | |
| } | |
| if (e.key === "Tab" || e.key === "Enter") { | |
| e.preventDefault(); | |
| cliInput.value = filteredCommands[selectedIndex].name; | |
| popup.style.display = "none"; | |
| return; | |
| } | |
| } | |
| // Command history navigation | |
| if (e.key === "ArrowUp" && popup.style.display !== "block") { | |
| e.preventDefault(); | |
| if (inputHistory.length > 0) { | |
| if (historyIndex === -1) historyIndex = inputHistory.length; | |
| historyIndex = Math.max(0, historyIndex - 1); | |
| cliInput.value = inputHistory[historyIndex]; | |
| } | |
| return; | |
| } | |
| if (e.key === "ArrowDown" && popup.style.display !== "block") { | |
| e.preventDefault(); | |
| if (inputHistory.length > 0 && historyIndex !== -1) { | |
| historyIndex = Math.min(inputHistory.length - 1, historyIndex + 1); | |
| cliInput.value = inputHistory[historyIndex]; | |
| } | |
| return; | |
| } | |
| // Handle Input Submission | |
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| const text = val.trim(); | |
| if (!text) return; | |
| appendUserMessage(text); | |
| inputHistory.push(text); | |
| historyIndex = -1; | |
| cliInput.value = ""; | |
| popup.style.display = "none"; | |
| // Local execution commands handler | |
| if (text.startsWith("/")) { | |
| await handleLocalSlash(text); | |
| return; | |
| } | |
| // Send to backend | |
| await sendToBackend(text); | |
| } | |
| }); | |
| // Trigger autocomplete on typing '/' | |
| cliInput.addEventListener("input", (e) => { | |
| const val = cliInput.value; | |
| if (val.startsWith("/")) { | |
| showPopup(val); | |
| } else { | |
| popup.style.display = "none"; | |
| } | |
| }); | |
| // Local slash commands logic | |
| async function handleLocalSlash(text) { | |
| const cmd = text.split(" ")[0].toLowerCase(); | |
| if (cmd === "/clear") { | |
| chatArea.innerHTML = ""; | |
| appendSystemMessage("Terminal vide."); | |
| } else if (cmd === "/help") { | |
| appendSystemMessage(` | |
| 📚 COMMANDES DISPONIBLES : | |
| /help - Affiche ce menu d'aide | |
| /clear - Vide le terminal | |
| /reset - Réinitialise le contexte | |
| /model - Affiche ou modifie le modèle actif | |
| /history - Affiche l'historique des requêtes | |
| /status - Affiche le statut complet de la session | |
| /tokens - Affiche l'estimation des tokens | |
| `); | |
| } else if (cmd === "/reset") { | |
| chatHistory = []; | |
| appendSystemMessage("Discussion et contexte réinitialisés."); | |
| } else if (cmd === "/status") { | |
| appendSystemMessage(` | |
| === STATUT DE L'AGENT === | |
| Utilisateur : @${currentUser} | |
| Modèle : Qwen/Qwen2.5-Coder-32B-Instruct | |
| Session ID : ${sessionId} | |
| Dossier : [Mode Web distant - Outils système désactivés] | |
| `); | |
| } else if (cmd === "/tokens") { | |
| appendSystemMessage("Statistiques : ~0 tokens utilisés."); | |
| } else { | |
| appendSystemMessage(`Commande ${cmd} non disponible ou simulée en ligne. Utilisez le CLI local pour toutes les fonctionnalités système.`, true); | |
| } | |
| } | |
| // Send chat query to FastAPI backend | |
| async function sendToBackend(message) { | |
| chatHistory.push({ role: "user", content: message }); | |
| // Show typing indicator | |
| const typingDiv = document.createElement("div"); | |
| typingDiv.className = "chat-line"; | |
| typingDiv.id = "typing-indicator"; | |
| typingDiv.innerHTML = `<span class="agent-prefix">▸ Cypher ▋</span>`; | |
| chatArea.appendChild(typingDiv); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| try { | |
| // We POST to our backend `/api/chat` | |
| const res = await fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| messages: [ | |
| { role: "system", content: "Tu es Cypher Coder, assistant IA en monospace. Pas d'emoji, pas de gras, pas d'italique dans tes réponses." }, | |
| ...chatHistory | |
| ], | |
| username: currentUser | |
| }) | |
| }); | |
| const data = await res.json(); | |
| // Remove typing indicator | |
| const indicator = document.getElementById("typing-indicator"); | |
| if (indicator) indicator.remove(); | |
| if (data.error) { | |
| appendSystemMessage("Erreur serveur : " + data.error, true); | |
| return; | |
| } | |
| const reply = data.message.content || "[Aucun contenu]"; | |
| chatHistory.push({ role: "assistant", content: reply }); | |
| // Print response | |
| appendAgentMessage(reply); | |
| // Check if tokens headers or meta are sent | |
| if (data.tokens) { | |
| document.getElementById("header-tokens").innerText = "tokens : " + data.tokens; | |
| } | |
| } catch (e) { | |
| const indicator = document.getElementById("typing-indicator"); | |
| if (indicator) indicator.remove(); | |
| appendSystemMessage("Erreur réseau : " + e.message, true); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |