Feature: Chatbox IA avec OpenRouter et sélecteur de modèles

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é.
This commit is contained in:
Claude committed 2025-11-12 16:17:33 +00:00
1 parent 98a11b9ddd
commit da548f7bc0
3 files changed
+241 -5

No files matched your search

+51
View File
@@ -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;
+163 -5
View File
@@ -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<Array<{ role: string; content: string }>>([]);
const [chatInput, setChatInput] = useState("");
const [chatLoading, setChatLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>("");
// Debounce timer for auto-save
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
const chatEndRef = useRef<HTMLDivElement>(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 = () => {
</DialogContent>
</Dialog>
<div className="fixed bottom-4 right-4">
<MadeWithDyad />
</div>
{/* AI Chatbox */}
{!chatOpen ? (
<Button
className="fixed bottom-4 right-4 h-14 w-14 rounded-full shadow-lg"
onClick={() => setChatOpen(true)}
>
<MessageSquare className="h-6 w-6" />
</Button>
) : (
<Card className="fixed bottom-4 right-4 w-96 h-[600px] shadow-2xl flex flex-col">
<CardHeader className="pb-3 border-b">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Assistant IA
</CardTitle>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleClearChat}
disabled={chatMessages.length === 0}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setChatOpen(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<div className="mt-2">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full text-xs px-2 py-1.5 border border-input rounded-md bg-background"
disabled={chatLoading}
>
{openRouterModels.length === 0 ? (
<option value="">Aucun modèle disponible</option>
) : (
openRouterModels.map(model => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))
)}
</select>
</div>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4 space-y-3">
{chatMessages.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8">
Commencez une conversation avec l'IA
</div>
) : (
chatMessages.map((msg, idx) => (
<div
key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg px-3 py-2 text-sm ${
msg.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted'
}`}
>
{msg.content}
</div>
</div>
))
)}
{chatLoading && (
<div className="flex justify-start">
<div className="bg-muted rounded-lg px-3 py-2 text-sm">
<div className="flex gap-1">
<span className="animate-bounce">●</span>
<span className="animate-bounce delay-100">●</span>
<span className="animate-bounce delay-200">●</span>
</div>
</div>
</div>
)}
<div ref={chatEndRef} />
</CardContent>
<div className="border-t p-3">
<form
onSubmit={(e) => {
e.preventDefault();
handleSendMessage();
}}
className="flex gap-2"
>
<Input
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
placeholder="Posez une question..."
disabled={chatLoading || !selectedModel}
className="flex-1"
/>
<Button
type="submit"
size="icon"
disabled={chatLoading || !chatInput.trim() || !selectedModel}
>
<Send className="h-4 w-4" />
</Button>
</form>
</div>
</Card>
)}
{/* Image modal for fullscreen view */}
{selectedImage && (
+27
View File
@@ -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<string> {
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();