// chat.js — Client Léa conversationnelle // Logique minimaliste : pas de framework, fetch + polling. const API_BASE = "/api/chat"; // Proxyfié par le dashboard Flask vers :5005 let sessionId = null; let pollTimer = null; let lastMessageCount = 0; let currentState = "idle"; const STATE_LABELS = { idle: "En attente", planning: "Léa réfléchit…", awaiting_confirmation: "En attente de confirmation", executing: "Léa exécute le workflow…", done: "Terminé", error: "Erreur", }; // ----------------------------------------------------------------------------- // Initialisation // ----------------------------------------------------------------------------- async function initChat() { try { const resp = await fetch(`${API_BASE}/session`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ machine_id: "default" }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); sessionId = data.session_id; currentState = data.state || "idle"; updateStatus(currentState); renderMessages(data.history || []); document.getElementById("sessionInfo").textContent = `Session ${sessionId}`; startPolling(); } catch (err) { console.error("Impossible de créer la session chat :", err); showSystemMessage(`Impossible de créer la session chat : ${err.message}. Vérifiez que le serveur streaming (5005) est démarré.`); } } // ----------------------------------------------------------------------------- // Envoi de messages // ----------------------------------------------------------------------------- async function sendMessage() { const input = document.getElementById("composerInput"); const text = (input.value || "").trim(); if (!text || !sessionId) return; const sendBtn = document.getElementById("sendBtn"); sendBtn.disabled = true; input.value = ""; autosizeTextarea(); // Affichage optimiste appendMessage({ role: "user", content: text, timestamp: Date.now() / 1000, }); try { updateStatus("planning"); const resp = await fetch(`${API_BASE}/${sessionId}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); currentState = data.state || "idle"; updateStatus(currentState); renderMessages(data.history || []); } catch (err) { console.error("Erreur envoi message :", err); showSystemMessage(`Erreur : ${err.message}`); updateStatus("error"); } finally { sendBtn.disabled = false; input.focus(); } } async function confirmPlan(confirmed) { if (!sessionId) return; const confirmBar = document.getElementById("confirmBar"); confirmBar.classList.remove("visible"); try { const resp = await fetch(`${API_BASE}/${sessionId}/confirm`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirmed }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); currentState = data.state || "idle"; updateStatus(currentState); renderMessages(data.history || []); } catch (err) { console.error("Erreur confirmation :", err); showSystemMessage(`Erreur confirmation : ${err.message}`); } } // ----------------------------------------------------------------------------- // Polling // ----------------------------------------------------------------------------- function startPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(pollHistory, 2000); } async function pollHistory() { if (!sessionId) return; try { const resp = await fetch(`${API_BASE}/${sessionId}/history`); if (!resp.ok) return; const data = await resp.json(); const snap = data.snapshot || {}; currentState = snap.state || "idle"; updateStatus(currentState, snap.progress || {}); const messages = snap.messages || []; if (messages.length !== lastMessageCount) { renderMessages(messages); } } catch (err) { // Silencieux — on réessayera au prochain tick } } // ----------------------------------------------------------------------------- // Rendu // ----------------------------------------------------------------------------- function renderMessages(messages) { const container = document.getElementById("messages"); container.innerHTML = ""; messages.forEach(msg => appendMessage(msg, false)); lastMessageCount = messages.length; container.scrollTop = container.scrollHeight; // Afficher/masquer la barre de confirmation const confirmBar = document.getElementById("confirmBar"); if (currentState === "awaiting_confirmation") { confirmBar.classList.add("visible"); } else { confirmBar.classList.remove("visible"); } } function appendMessage(msg, autoscroll = true) { const container = document.getElementById("messages"); const div = document.createElement("div"); div.className = `message ${msg.role}`; const avatar = document.createElement("div"); avatar.className = "avatar"; if (msg.role === "user") avatar.textContent = "Vous"; else if (msg.role === "lea") avatar.textContent = "L"; else avatar.textContent = "i"; const bubbleWrap = document.createElement("div"); const bubble = document.createElement("div"); bubble.className = "bubble"; bubble.textContent = msg.content || ""; bubbleWrap.appendChild(bubble); const ts = document.createElement("div"); ts.className = "timestamp"; try { const d = new Date((msg.timestamp || 0) * 1000); ts.textContent = d.toLocaleTimeString("fr-FR"); } catch (e) { ts.textContent = ""; } bubbleWrap.appendChild(ts); div.appendChild(avatar); div.appendChild(bubbleWrap); container.appendChild(div); if (autoscroll) container.scrollTop = container.scrollHeight; } function showSystemMessage(text) { appendMessage({ role: "system", content: text, timestamp: Date.now() / 1000, }); } function updateStatus(state, progress = {}) { const dot = document.getElementById("statusDot"); const txt = document.getElementById("statusText"); dot.className = `status-dot ${state}`; let label = STATE_LABELS[state] || state; if (state === "executing" && progress && progress.total_actions) { const done = progress.completed_actions || 0; const total = progress.total_actions || 0; label = `Léa exécute… ${done}/${total}`; } txt.textContent = label; // Bloquer la saisie pendant planning/executing const input = document.getElementById("composerInput"); const sendBtn = document.getElementById("sendBtn"); const blocked = (state === "planning" || state === "executing"); input.disabled = blocked; sendBtn.disabled = blocked; } // ----------------------------------------------------------------------------- // UX composer // ----------------------------------------------------------------------------- function handleKeydown(event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); sendMessage(); } } function autosizeTextarea() { const input = document.getElementById("composerInput"); input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 120) + "px"; } document.addEventListener("DOMContentLoaded", () => { const input = document.getElementById("composerInput"); input.addEventListener("input", autosizeTextarea); initChat(); });