// js/chat.js // Gestion de la logique de chat et appel à l'API DeepSeek via backend import storage from './storage.js'; import CONFIG from './config.js'; class ChatManager { constructor() { this.currentConversationId = null; this.currentMessages = []; this.isProcessing = false; this.currentStreamMessage = null; this.lastAIMessageIndex = -1; // Index du dernier message IA pour régénération this.lastUserMessageIndex = -1; // Index du dernier message utilisateur pour édition } // Initialiser avec la conversation active ou en créer une nouvelle init() { const activeId = storage.getActiveConversationId(); if (activeId) { const conv = storage.getConversation(activeId); if (conv) { this.loadConversation(conv); return; } } // Créer une nouvelle conversation par défaut this.createNewConversation(); } // Charger une conversation existante loadConversation(conversation) { this.currentConversationId = conversation.id; this.currentMessages = conversation.messages || []; storage.setActiveConversationId(conversation.id); this._updateLastIndexes(); } // Créer une nouvelle conversation createNewConversation() { const newConv = storage.createNewConversation(); this.currentConversationId = newConv.id; this.currentMessages = []; storage.setActiveConversationId(newConv.id); this._updateLastIndexes(); return newConv; } // Mettre à jour les index des derniers messages _updateLastIndexes() { this.lastAIMessageIndex = -1; this.lastUserMessageIndex = -1; for (let i = this.currentMessages.length - 1; i >= 0; i--) { const msg = this.currentMessages[i]; if (msg.role === 'assistant' && this.lastAIMessageIndex === -1) { this.lastAIMessageIndex = i; } if (msg.role === 'user' && this.lastUserMessageIndex === -1) { this.lastUserMessageIndex = i; } if (this.lastAIMessageIndex !== -1 && this.lastUserMessageIndex !== -1) break; } } // Régénérer la dernière réponse IA async regenerateLastResponse(onToken = null, onComplete = null, onError = null) { if (this.isProcessing) return; if (this.lastAIMessageIndex === -1) { if (onError) onError('Aucune réponse à régénérer'); return; } // Récupérer le dernier message utilisateur const lastUserMsg = this.currentMessages.find(m => m.role === 'user'); if (!lastUserMsg) { if (onError) onError('Aucun message utilisateur trouvé'); return; } // Supprimer le dernier message IA et tous les messages suivants const messagesToRemove = this.currentMessages.length - this.lastAIMessageIndex; this.currentMessages.splice(this.lastAIMessageIndex, messagesToRemove); this._updateLastIndexes(); this._saveCurrentConversation(); // Réenvoyer le message utilisateur await this.sendUserMessage(lastUserMsg.content, onToken, onComplete, onError); } // Éditer un message utilisateur async editUserMessage(index, newContent, onToken = null, onComplete = null, onError = null) { if (this.isProcessing) return; if (index < 0 || index >= this.currentMessages.length) { if (onError) onError('Index de message invalide'); return; } if (this.currentMessages[index].role !== 'user') { if (onError) onError('Ce n\'est pas un message utilisateur'); return; } // Supprimer tous les messages après celui-ci const messagesToRemove = this.currentMessages.length - index - 1; this.currentMessages.splice(index + 1, messagesToRemove); // Mettre à jour le contenu du message this.currentMessages[index].content = newContent.trim(); this._updateLastIndexes(); this._saveCurrentConversation(); // Réenvoyer le message modifié await this.sendUserMessage(newContent.trim(), onToken, onComplete, onError); } // Envoyer un message utilisateur (avec streaming) async sendUserMessage(content, onToken = null, onComplete = null, onError = null) { if (!content || content.trim() === '') return null; if (this.isProcessing) return null; // Ajouter le message utilisateur const userMsg = { role: 'user', content: content.trim(), timestamp: new Date().toISOString(), usage: null, // sera rempli plus tard }; this.currentMessages.push(userMsg); this._saveCurrentConversation(); // Initialiser le message IA (vide pour l'instant) const aiMsg = { role: 'assistant', content: '', timestamp: new Date().toISOString(), usage: null, }; this.currentMessages.push(aiMsg); this.currentStreamMessage = aiMsg; this.isProcessing = true; try { await this.callAIStream( content.trim(), (token, fullContent) => { // Callback pour chaque token reçu aiMsg.content = fullContent; if (onToken) onToken(token, fullContent); this._saveCurrentConversation(); }, (fullContent, usage) => { // Callback quand le streaming est terminé aiMsg.content = fullContent; aiMsg.usage = usage; if (onComplete) onComplete(fullContent, usage); this._updateLastIndexes(); this._saveCurrentConversation(); }, (error) => { // Callback en cas d'erreur if (onError) onError(error); // Supprimer le message vide et ajouter un message d'erreur this.currentMessages.pop(); const errorMsg = { role: 'assistant', content: "Désolé, une erreur s'est produite lors du streaming. Veuillez réessayer.", timestamp: new Date().toISOString(), usage: null, }; this.currentMessages.push(errorMsg); this._updateLastIndexes(); this._saveCurrentConversation(); } ); } catch (error) { console.error('Erreur générale:', error); if (onError) onError(error); } finally { this.isProcessing = false; this.currentStreamMessage = null; } } // Appel à l'API DeepSeek en streaming async callAIStream(userMessage, onToken, onComplete, onError) { // Construire l'historique des messages (sans le dernier message IA vide) const messages = this.currentMessages .filter(msg => msg.role !== 'assistant' || msg.content !== '') .map(msg => ({ role: msg.role, content: msg.content, })); try { const response = await fetch(CONFIG.BACKEND_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ messages: messages, temperature: CONFIG.DEFAULT_TEMPERATURE, max_tokens: CONFIG.DEFAULT_MAX_TOKENS, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Erreur HTTP ${response.status}`); } // Lire le flux SSE (Server-Sent Events) const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullContent = ''; let usage = null; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Traiter les lignes complètes const lines = buffer.split('\n'); buffer = lines.pop(); // Garder le dernier morceau for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); try { const parsed = JSON.parse(data); if (parsed.type === 'token') { // Token reçu fullContent += parsed.content; if (onToken) onToken(parsed.content, fullContent); } else if (parsed.type === 'complete') { // Streaming terminé fullContent = parsed.full; usage = parsed.usage || null; if (onComplete) onComplete(fullContent, usage); return; } else if (parsed.type === 'error') { // Erreur du backend throw new Error(parsed.error || 'Erreur de streaming'); } } catch (e) { console.warn('Erreur parsing SSE:', e.message); } } } } // Fin normale (si on n'a pas reçu 'complete') if (onComplete) onComplete(fullContent, usage); } catch (error) { console.error('Erreur streaming:', error); // En mode fallback, on utilise la simulation if (CONFIG.FALLBACK_MODE) { console.warn('⚠️ Utilisation du mode fallback (simulation)'); const fallbackResponse = this._simulateFallback(userMessage); // Simuler un streaming token par token const tokens = fallbackResponse.split(' '); for (let i = 0; i < tokens.length; i++) { const token = tokens[i] + (i < tokens.length - 1 ? ' ' : ''); if (onToken) onToken(token, fallbackResponse.substring(0, tokens.slice(0, i+1).join(' ').length)); // Petit délai pour simuler le streaming await new Promise(r => setTimeout(r, 50 + Math.random() * 100)); } if (onComplete) onComplete(fallbackResponse, { total_tokens: 100, prompt_tokens: 50, completion_tokens: 50 }); return; } if (onError) onError(error); } } // Fallback pour le développement (simulation) _simulateFallback(userMessage) { const responses = [ "Je suis en mode fallback (backend non connecté). Voici une réponse simulée avec streaming.", "DeepSeek n'est pas disponible actuellement. Je vous réponds en mode local avec animation.", "Mode hors ligne activé. Nixaura vous assiste avec des réponses prédéfinies en streaming.", "Le proxy DeepSeek n'est pas joignable. Vérifiez que le backend est lancé.", "Je fonctionne en mode démonstration. Pour utiliser l'IA réelle, démarrez le backend.", ]; // Essayer de donner une réponse un peu contextuelle const words = userMessage.toLowerCase().split(' '); if (words.some(w => ['bonjour', 'salut', 'hey'].includes(w))) { return "Bonjour ! Je suis Nixaura en mode fallback. Comment puis-je vous aider aujourd'hui ?"; } if (words.some(w => ['aide', 'assistance', 'support'].includes(w))) { return "Je vous aide volontiers, même en mode fallback ! Posez-moi vos questions, je vous répondrai avec le plus grand soin."; } return responses[Math.floor(Math.random() * responses.length)]; } // Sauvegarder la conversation courante _saveCurrentConversation() { if (!this.currentConversationId) return; const conv = storage.getConversation(this.currentConversationId); if (conv) { conv.messages = this.currentMessages; conv.title = storage.generateTitle(this.currentMessages); conv.updatedAt = new Date().toISOString(); storage.saveConversation(conv); } else { // Si la conversation n'existe plus, en créer une nouvelle const newConv = storage.createNewConversation(); newConv.messages = this.currentMessages; newConv.title = storage.generateTitle(this.currentMessages); storage.saveConversation(newConv); this.currentConversationId = newConv.id; storage.setActiveConversationId(newConv.id); } } // Supprimer la conversation courante deleteCurrentConversation() { if (this.currentConversationId) { storage.deleteConversation(this.currentConversationId); this.currentConversationId = null; this.currentMessages = []; // Créer une nouvelle conversation this.createNewConversation(); } } // Obtenir les messages de la conversation courante getCurrentMessages() { return this.currentMessages; } // Obtenir l'index du dernier message IA getLastAIMessageIndex() { return this.lastAIMessageIndex; } // Obtenir l'index du dernier message utilisateur getLastUserMessageIndex() { return this.lastUserMessageIndex; } } // Instance unique const chat = new ChatManager(); export default chat;