From da548f7bc05bc633b08a44b94e999c48db34901b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 16:17:33 +0000 Subject: [PATCH] =?UTF-8?q?Feature:=20Chatbox=20IA=20avec=20OpenRouter=20e?= =?UTF-8?q?t=20s=C3=A9lecteur=20de=20mod=C3=A8les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout d'un chatbot interactif alimenté par OpenRouter API avec les fonctionnalités suivantes: - Bouton flottant en bas à droite pour ouvrir le chat - Sélecteur de modèles IA avec tous les modèles disponibles via l'API OpenRouter - Interface de chat avec historique des messages (utilisateur/assistant) - Indicateur de chargement avec animation - Boutons pour effacer l'historique et fermer le chat - Auto-scroll vers le dernier message - Backend: route POST /api/openrouter/chat pour la communication avec l'API - Service frontend: méthode sendMessage() dans OpenRouterService Retrait du composant MadeWithDyad comme demandé. --- routes/openrouter.routes.js | 51 +++++++++ src/pages/Index.tsx | 168 +++++++++++++++++++++++++++++- src/services/OpenRouterService.ts | 27 +++++ 3 files changed, 241 insertions(+), 5 deletions(-) diff --git a/routes/openrouter.routes.js b/routes/openrouter.routes.js index c3bc7e3..f890818 100644 --- a/routes/openrouter.routes.js +++ b/routes/openrouter.routes.js @@ -137,4 +137,55 @@ router.post('/refresh-models', authenticateToken, requireAdmin, async (req, res) } }); +/** + * POST /api/openrouter/chat + * Envoyer un message au modèle IA + */ +router.post('/chat', authenticateToken, async (req, res) => { + try { + const { model, messages } = req.body; + + if (!model || !messages || !Array.isArray(messages)) { + return res.status(400).json({ error: 'Modèle et messages requis' }); + } + + // Récupérer la clé API + const setting = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_api_key']); + + if (!setting || !setting.value) { + return res.status(400).json({ error: 'Clé API OpenRouter non configurée' }); + } + + // Appeler l'API OpenRouter + const fetch = (await import('node-fetch')).default; + const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${setting.value}`, + 'HTTP-Referer': process.env.APP_URL || 'http://localhost:5000', + 'X-Title': 'NoteFlow', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: model, + messages: messages + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + logger.error(`Erreur API OpenRouter chat: ${response.status}`, errorData); + return res.status(response.status).json({ + error: errorData.error?.message || 'Erreur lors de la communication avec l\'IA' + }); + } + + const data = await response.json(); + res.json(data); + } catch (error) { + logger.error('Erreur lors du chat OpenRouter:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + module.exports = router; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index acafeb5..844b815 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -11,10 +11,9 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { RichTextEditor } from "@/components/RichTextEditor"; -import { MadeWithDyad } from "@/components/made-with-dyad"; import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, - Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon + Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send } from "lucide-react"; import AuthService from "@/services/AuthService"; import AdminService from "@/services/AdminService"; @@ -91,8 +90,16 @@ const Index = () => { const RSS_MAX_ARTICLES = 14; const RSS_PER_PAGE = 7; + // Chatbox states + const [chatOpen, setChatOpen] = useState(false); + const [chatMessages, setChatMessages] = useState>([]); + const [chatInput, setChatInput] = useState(""); + const [chatLoading, setChatLoading] = useState(false); + const [selectedModel, setSelectedModel] = useState(""); + // Debounce timer for auto-save const saveTimerRef = useRef(null); + const chatEndRef = useRef(null); // Update current date/time every second useEffect(() => { @@ -491,6 +498,43 @@ const Index = () => { } }; + const handleSendMessage = async () => { + if (!chatInput.trim() || !selectedModel || chatLoading) return; + + const userMessage = { role: 'user', content: chatInput }; + const newMessages = [...chatMessages, userMessage]; + + setChatMessages(newMessages); + setChatInput(""); + setChatLoading(true); + + try { + const response = await OpenRouterService.sendMessage(selectedModel, newMessages); + setChatMessages([...newMessages, { role: 'assistant', content: response }]); + } catch (error: any) { + showError(error.message || 'Erreur lors de l\'envoi du message'); + setChatMessages([...newMessages, { role: 'assistant', content: 'Erreur: ' + error.message }]); + } finally { + setChatLoading(false); + } + }; + + const handleClearChat = () => { + setChatMessages([]); + }; + + // Scroll to bottom when new message arrives + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [chatMessages]); + + // Set default model when models are loaded + useEffect(() => { + if (openRouterModels.length > 0 && !selectedModel) { + setSelectedModel(openRouterModels[0].id); + } + }, [openRouterModels, selectedModel]); + const confirmAddUser = async (username: string, password: string, isAdmin: boolean) => { try { const newUser = await AdminService.createUser(username, password, isAdmin); @@ -2235,9 +2279,123 @@ const Index = () => { -
- -
+ {/* AI Chatbox */} + {!chatOpen ? ( + + ) : ( + + +
+ + + Assistant IA + +
+ + +
+
+
+ +
+
+ + + {chatMessages.length === 0 ? ( +
+ Commencez une conversation avec l'IA +
+ ) : ( + chatMessages.map((msg, idx) => ( +
+
+ {msg.content} +
+
+ )) + )} + {chatLoading && ( +
+
+
+ ● + ● + ● +
+
+
+ )} +
+ + +
+
{ + e.preventDefault(); + handleSendMessage(); + }} + className="flex gap-2" + > + setChatInput(e.target.value)} + placeholder="Posez une question..." + disabled={chatLoading || !selectedModel} + className="flex-1" + /> + +
+
+ + )} {/* Image modal for fullscreen view */} {selectedImage && ( diff --git a/src/services/OpenRouterService.ts b/src/services/OpenRouterService.ts index 32ace61..a402429 100644 --- a/src/services/OpenRouterService.ts +++ b/src/services/OpenRouterService.ts @@ -54,6 +54,33 @@ class OpenRouterService { throw error; } } + + /** + * Send chat message to AI model + */ + async sendMessage(model: string, messages: Array<{ role: string; content: string }>): Promise { + try { + const response = await fetch('/api/openrouter/chat', { + method: 'POST', + headers: { + ...AuthService.getHeaders(), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ model, messages }) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Erreur inconnue' })); + throw new Error(error.error || 'Erreur lors de la communication avec l\'IA'); + } + + const data = await response.json(); + return data.choices?.[0]?.message?.content || 'Pas de réponse'; + } catch (error: any) { + console.error('Erreur lors de l\'envoi du message:', error); + throw error; + } + } } export default new OpenRouterService();