kibali-api / src /App.jsx
BelikanM's picture
Mise à jour complète avec l'application Streamlit Kibali IA
12818b2
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import {
Send, FileText, Upload, Globe, MapPin,
Loader2, Trash2, Image as ImageIcon, Search, Brain, ChevronDown, ChevronUp, User,
Database, Clock, TrendingUp, AlertCircle, CheckCircle, Zap,
LogOut
} from 'lucide-react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
GoogleAuthProvider,
signInWithRedirect,
signOut,
onAuthStateChanged,
getRedirectResult
} from 'firebase/auth';
// Configuration Firebase
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || "AIzaSyCel4qvAtZomh4aQOCArCLIYSYmeZ4Qbig",
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || "sevenstatut.firebaseapp.com",
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL || "https://sevenstatut-default-rtdb.firebaseio.com",
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID || "sevenstatut",
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET || "sevenstatut.firebasestorage.app",
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID || "838268990346",
appId: import.meta.env.VITE_FIREBASE_APP_ID || "1:838268990346:web:dba8702c39e82981704e73"
};
// Initialiser Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const googleProvider = new GoogleAuthProvider();
const API_BASE = "https://belikanm-kibali-api.hf.space";
const LOGO_PATH = "/kibali_logo.svg";
function App() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [status, setStatus] = useState({
doc_chunks: 0,
memory_entries: 0,
current_subject: null,
subject_message_count: 0,
torch_cuda_available: false,
status: 'unknown'
});
const [showThinking, setShowThinking] = useState({});
const [uploadProgress, setUploadProgress] = useState(null);
const [user, setUser] = useState(null);
const [authLoading, setAuthLoading] = useState(true);
const scrollRef = useRef(null);
const pollingInterval = useRef(null);
// Gestion de l'authentification
useEffect(() => {
// Vérifier le résultat de la redirection
getRedirectResult(auth)
.then((result) => {
if (result) {
setUser(result.user);
}
setAuthLoading(false);
})
.catch((error) => {
console.error("Erreur lors de la redirection:", error);
setAuthLoading(false);
});
// Écouter les changements d'état d'authentification
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
setUser(firebaseUser);
if (!firebaseUser) {
setAuthLoading(false);
}
});
return () => unsubscribe();
}, []);
// Récupération du statut du backend au démarrage et toutes les 10 secondes
useEffect(() => {
fetchStatus();
pollingInterval.current = setInterval(fetchStatus, 10000);
return () => {
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
}
};
}, []);
const fetchStatus = async () => {
try {
const response = await axios.get(`${API_BASE}/status`, { timeout: 5000 });
setStatus(response.data);
} catch (error) {
console.error("Erreur récupération status:", error);
}
};
// Connexion Google avec redirection
const handleGoogleLogin = async () => {
try {
await signInWithRedirect(auth, googleProvider);
} catch (error) {
console.error("Erreur de connexion Google:", error);
alert(`Erreur de connexion: ${error.message}`);
}
};
// Déconnexion
const handleLogout = async () => {
try {
await signOut(auth);
setUser(null);
} catch (error) {
console.error("Erreur de déconnexion:", error);
}
};
// Auto-scroll vers le bas
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loading]);
// Envoi du message
const handleSend = async () => {
if (!input.trim() || loading) return;
const userMsg = {
role: "user",
content: input.trim(),
userPhoto: user?.photoURL || null,
userName: user?.displayName || "Utilisateur"
};
setMessages(prev => [...prev, userMsg]);
setInput("");
setLoading(true);
try {
const response = await axios.post(`${API_BASE}/chat`, {
messages: [...messages, userMsg],
latitude: 0.4061,
longitude: 9.4673,
city: "Libreville",
thinking_mode: true
}, {
timeout: 120000
});
const aiResponse = response.data.response || "Réponse reçue du serveur.";
const aiImages = response.data.images || [];
const contextInfo = response.data.context_info || {};
setMessages(prev => [...prev, {
role: "assistant",
content: aiResponse,
images: aiImages,
context_info: contextInfo,
timestamp: new Date().toISOString()
}]);
fetchStatus();
} catch (error) {
console.error("Erreur lors de l'appel au backend:", error);
let errorMsg = "Erreur : impossible de contacter le serveur IA.";
if (error.code === 'ERR_NETWORK') {
errorMsg = "⚠️ Serveur injoignable. Vérifiez que votre backend est lancé.";
} else if (error.code === 'ECONNABORTED') {
errorMsg = "⏱️ Timeout : la requête a pris trop de temps.";
} else if (error.response?.status === 400) {
errorMsg = `❌ Erreur de requête : ${error.response.data.detail || 'Format invalide'}`;
} else if (error.response?.status === 500) {
errorMsg = "🔥 Erreur serveur interne. Vérifiez les logs du backend.";
} else if (error.response?.data?.detail) {
errorMsg = `Erreur serveur : ${error.response.data.detail}`;
}
setMessages(prev => [...prev, {
role: "assistant",
content: errorMsg,
error: true,
timestamp: new Date().toISOString()
}]);
} finally {
setLoading(false);
}
};
// Upload de fichiers PDF
const handleFileUpload = async (e) => {
const files = Array.from(e.target.files);
if (!files.length) return;
setUploading(true);
setUploadProgress({ current: 0, total: files.length });
const formData = new FormData();
files.forEach(file => formData.append('files', file));
try {
const res = await axios.post(`${API_BASE}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 180000,
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setUploadProgress(prev => ({ ...prev, percent: percentCompleted }));
}
});
const chunksAdded = res.data.chunks_added || 0;
const filesProcessed = res.data.files_processed || 0;
const totalChunks = res.data.total_doc_chunks || 0;
setStatus(prev => ({ ...prev, doc_chunks: totalChunks }));
setMessages(prev => [...prev, {
role: "assistant",
content: `✅ **Import réussi !**\n\n📄 ${filesProcessed} document(s) traité(s)\n📊 ${chunksAdded} nouveaux segments ajoutés\n💾 Total en base : **${totalChunks} chunks**\n\nVous pouvez maintenant poser des questions sur ces documents !`,
success: true,
timestamp: new Date().toISOString()
}]);
fetchStatus();
} catch (err) {
console.error("Erreur upload:", err);
let errorMsg = "❌ Échec de l'import des documents.";
if (err.code === 'ECONNABORTED') {
errorMsg += " Timeout : les fichiers sont peut-être trop volumineux.";
} else if (err.response?.data?.detail) {
errorMsg += ` Détails : ${err.response.data.detail}`;
}
setMessages(prev => [...prev, {
role: "assistant",
content: errorMsg,
error: true,
timestamp: new Date().toISOString()
}]);
} finally {
setUploading(false);
setUploadProgress(null);
e.target.value = '';
}
};
// Réinitialisation complète (chat + mémoire)
const handleReset = async () => {
if (!window.confirm("Voulez-vous réinitialiser la conversation ET effacer la mémoire du modèle ?")) return;
try {
await axios.post(`${API_BASE}/clear-memory`, {}, { timeout: 5000 });
setMessages([]);
fetchStatus();
setMessages([{
role: "assistant",
content: "✅ Conversation et mémoire réinitialisées avec succès.",
success: true,
timestamp: new Date().toISOString()
}]);
} catch (error) {
console.error("Erreur lors de la réinitialisation:", error);
setMessages([{
role: "assistant",
content: "⚠️ Chat réinitialisé, mais impossible de contacter le serveur pour effacer la mémoire.",
error: true,
timestamp: new Date().toISOString()
}]);
}
};
const toggleThinking = (msgIndex) => {
setShowThinking(prev => ({ ...prev, [msgIndex]: !prev[msgIndex] }));
};
const formatTimeAgo = (timestamp) => {
if (!timestamp) return "";
const seconds = Math.floor((new Date() - new Date(timestamp)) / 1000);
if (seconds < 60) return "à l'instant";
if (seconds < 3600) return `il y a ${Math.floor(seconds / 60)}m`;
if (seconds < 86400) return `il y a ${Math.floor(seconds / 3600)}h`;
return `il y a ${Math.floor(seconds / 86400)}j`;
};
// Styles centralisés
const styles = {
container: { display: 'flex', height: '100vh', backgroundColor: '#020617', color: '#f8fafc', fontFamily: 'system-ui, -apple-system, sans-serif' },
sidebar: { width: '340px', backgroundColor: '#0f172a', borderRight: '1px solid #1e293b', display: 'flex', flexDirection: 'column', overflowY: 'auto' },
main: { flex: 1, display: 'flex', flexDirection: 'column', position: 'relative', background: 'radial-gradient(circle at 50% 0%, #1e293b 0%, #020617 100%)' },
messageArea: { flex: 1, overflowY: 'auto', padding: '2rem 10% 2rem 10%' },
userBubble: { backgroundColor: '#059669', color: 'white', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 0.25rem 1.25rem', boxShadow: '0 4px 15px rgba(5, 150, 105, 0.2)' },
aiBubble: { backgroundColor: '#1e293b', border: '1px solid #334155', color: '#f1f5f9', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 1.25rem 0.25rem', boxShadow: '0 10px 30px rgba(0,0,0,0.5)' },
successBubble: { backgroundColor: '#064e3b', border: '1px solid #059669', color: '#d1fae5' },
errorBubble: { backgroundColor: '#7f1d1d', border: '1px solid #991b1b', color: '#fca5a5' },
inputContainer: { padding: '2rem', background: 'linear-gradient(to top, #020617 70%, transparent)' },
inputWrapper: { display: 'flex', alignItems: 'center', backgroundColor: 'rgba(30, 41, 59, 0.7)', backdropFilter: 'blur(20px)', border: '1px solid #334155', borderRadius: '1.5rem', padding: '0.6rem 1.2rem', maxWidth: '900px', margin: '0 auto' },
inputField: { flex: 1, background: 'transparent', border: 'none', color: 'white', padding: '0.8rem', outline: 'none', fontSize: '1rem' },
sendBtn: { backgroundColor: '#10b981', color: 'white', border: 'none', borderRadius: '1rem', width: '45px', height: '45px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' },
toolBadge: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', backgroundColor: '#020617', padding: '6px 12px', borderRadius: '10px', border: '1px solid #1e293b', color: '#94a3b8' },
statCard: { backgroundColor: 'rgba(2, 6, 23, 0.5)', borderRadius: '1rem', padding: '1rem', border: '1px solid #1e293b', marginBottom: '0.8rem' },
googleBtn: {
width: '100%',
padding: '12px 16px',
backgroundColor: '#0f172a',
border: '1px solid #334155',
borderRadius: '12px',
color: '#f1f5f9',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
cursor: 'pointer',
fontWeight: '600',
fontSize: '14px',
transition: 'all 0.2s',
marginBottom: '1rem'
},
userProfile: {
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px 16px',
backgroundColor: 'rgba(2, 6, 23, 0.5)',
borderRadius: '12px',
border: '1px solid #334155',
marginBottom: '1rem'
}
};
return (
<div style={styles.container}>
{/* SIDEBAR */}
<aside style={styles.sidebar}>
<div style={{ padding: '2rem', borderBottom: '1px solid #1e293b', display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ width: '45px', height: '45px', backgroundColor: '#1e293b', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #334155' }}>
<img src={LOGO_PATH} alt="K" style={{ width: '30px', height: '30px', objectFit: 'contain' }} onError={(e) => e.target.style.display = 'none'} />
</div>
<div>
<h1 style={{ fontSize: '1.3rem', fontWeight: '900', margin: 0, letterSpacing: '-0.5px' }}>
Kibali <span style={{ color: '#10b981' }}>AI</span>
</h1>
<p style={{ fontSize: '10px', color: '#475569', margin: 0, marginTop: '4px', fontWeight: '700' }}>
v1.0 • {status.status === 'ready' ? '🟢 ONLINE' : '🔴 OFFLINE'}
</p>
</div>
</div>
<div style={{ padding: '2rem', flex: 1, display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{/* Section Connexion */}
{authLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80px' }}>
<Loader2 className="animate-spin" color="#10b981" size={24} />
</div>
) : user ? (
<div style={styles.userProfile}>
<div style={{ position: 'relative' }}>
<img
src={user.photoURL}
alt={user.displayName || "Utilisateur"}
style={{
width: '50px',
height: '50px',
borderRadius: '50%',
border: '2px solid #10b981'
}}
onError={(e) => {
e.target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.displayName || "User")}&background=059669&color=fff`;
}}
/>
<div style={{
position: 'absolute',
bottom: 0,
right: 0,
width: '12px',
height: '12px',
backgroundColor: '#10b981',
borderRadius: '50%',
border: '2px solid #0f172a'
}} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '600', fontSize: '15px', color: '#f1f5f9' }}>
{user.displayName || "Utilisateur"}
</div>
<div style={{ fontSize: '12px', color: '#64748b', marginTop: '4px' }}>
{user.email}
</div>
</div>
<button
onClick={handleLogout}
style={{
backgroundColor: 'transparent',
border: '1px solid #334155',
borderRadius: '8px',
width: '36px',
height: '36px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
color: '#94a3b8',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.target.style.borderColor = '#ef4444';
e.target.style.color = '#ef4444';
}}
onMouseLeave={(e) => {
e.target.style.borderColor = '#334155';
e.target.style.color = '#94a3b8';
}}
>
<LogOut size={18} />
</button>
</div>
) : (
<button
onClick={handleGoogleLogin}
style={styles.googleBtn}
onMouseEnter={(e) => {
e.target.style.backgroundColor = '#1e293b';
e.target.style.borderColor = '#475569';
}}
onMouseLeave={(e) => {
e.target.style.backgroundColor = '#0f172a';
e.target.style.borderColor = '#334155';
}}
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Connexion Google
</button>
)}
{/* Upload */}
<div>
<h2 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', letterSpacing: '1.5px', marginBottom: '1rem', fontWeight: '800' }}>
📚 Documents
</h2>
<label style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '130px',
border: uploading ? '2px solid #10b981' : '2px dashed #1e293b',
borderRadius: '1.5rem',
cursor: uploading ? 'not-allowed' : 'pointer',
transition: 'all 0.3s',
backgroundColor: uploading ? 'rgba(16, 185, 129, 0.05)' : '#020617',
position: 'relative',
overflow: 'hidden'
}}>
{uploading ? (
<>
<Loader2 className="animate-spin" color="#10b981" size={32} />
<span style={{ fontSize: '12px', marginTop: '12px', color: '#10b981', fontWeight: '600' }}>
{uploadProgress?.percent ? `${uploadProgress.percent}%` : 'Traitement...'}
</span>
{uploadProgress?.total && (
<span style={{ fontSize: '10px', color: '#64748b', marginTop: '4px' }}>
{uploadProgress.current}/{uploadProgress.total} fichiers
</span>
)}
</>
) : (
<>
<Upload color="#10b981" size={32} />
<span style={{ fontSize: '12px', marginTop: '12px', color: '#64748b', fontWeight: '500' }}>
Importer rapports PDF
</span>
<span style={{ fontSize: '10px', color: '#475569', marginTop: '4px' }}>
Plusieurs fichiers acceptés
</span>
</>
)}
<input type="file" hidden multiple accept=".pdf" onChange={handleFileUpload} disabled={uploading} />
</label>
</div>
{/* Statistiques */}
<div>
<h3 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', marginBottom: '1rem', fontWeight: '800', letterSpacing: '1.5px' }}>
📊 Statistiques
</h3>
<div style={styles.statCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<Database size={14} color="#10b981" /> Base documentaire
</span>
<span style={{ color: '#10b981', fontWeight: 'bold', fontSize: '15px' }}>
{status.doc_chunks || 0}
</span>
</div>
<div style={{ fontSize: '10px', color: '#475569', marginTop: '6px' }}>
segments vectorisés
</div>
</div>
<div style={styles.statCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<Brain size={14} color="#3b82f6" /> Mémoire active
</span>
<span style={{ color: '#3b82f6', fontWeight: 'bold', fontSize: '15px' }}>
{status.memory_entries || 0}
</span>
</div>
<div style={{ fontSize: '10px', color: '#475569', marginTop: '6px' }}>
échanges mémorisés
</div>
</div>
{status.current_subject && (
<div style={styles.statCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<TrendingUp size={14} color="#f59e0b" /> Sujet actuel
</span>
<span style={{ color: '#f59e0b', fontWeight: 'bold', fontSize: '15px' }}>
{status.subject_message_count}
</span>
</div>
<div style={{ fontSize: '10px', color: '#475569', marginTop: '6px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{status.current_subject}
</div>
</div>
)}
<div style={styles.statCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '12px' }}>Position</span>
<span style={{ color: '#f1f5f9', display: 'flex', alignItems: 'center', gap: '6px', fontWeight: '600', fontSize: '13px' }}>
<MapPin size={14} color="#ef4444" /> Libreville
</span>
</div>
</div>
<div style={styles.statCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<Zap size={14} color={status.torch_cuda_available ? '#10b981' : '#f59e0b'} /> GPU
</span>
<span style={{ color: status.torch_cuda_available ? '#10b981' : '#f59e0b', fontWeight: 'bold', fontSize: '11px' }}>
{status.torch_cuda_available ? 'ACTIVÉ' : 'CPU ONLY'}
</span>
</div>
</div>
</div>
</div>
<button
onClick={handleReset}
style={{
margin: '2rem',
background: '#020617',
border: '1px solid #1e293b',
color: '#475569',
padding: '14px',
borderRadius: '14px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
fontWeight: '700',
fontSize: '12px',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.target.style.borderColor = '#ef4444';
e.target.style.color = '#ef4444';
}}
onMouseLeave={(e) => {
e.target.style.borderColor = '#1e293b';
e.target.style.color = '#475569';
}}
>
<Trash2 size={16} /> RÉINITIALISER TOUT
</button>
</aside>
{/* MAIN CONTENT */}
<main style={styles.main}>
<div style={styles.messageArea}>
{messages.length === 0 && (
<div style={{ height: '80%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>
<div style={{
width: '100px',
height: '100px',
backgroundColor: '#0f172a',
borderRadius: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '2rem',
border: '1px solid #334155',
boxShadow: '0 20px 50px rgba(0,0,0,0.5)'
}}>
<img src={LOGO_PATH} alt="Kibali" style={{ width: '60px' }} onError={(e) => e.target.style.display = 'none'} />
</div>
<h2 style={{ fontSize: '2.5rem', fontWeight: '900', marginBottom: '0.8rem', letterSpacing: '-1px' }}>
Bienvenue sur Kibali AI
</h2>
<p style={{ color: '#64748b', maxWidth: '450px', lineHeight: '1.7', fontSize: '1.1rem' }}>
Assistant IA expert du Gabon avec mémoire contextuelle et analyse documentaire avancée.
</p>
{!user && (
<div style={{ marginTop: '2rem' }}>
<button
onClick={handleGoogleLogin}
style={{
padding: '12px 24px',
backgroundColor: '#0f172a',
border: '1px solid #334155',
borderRadius: '12px',
color: '#f1f5f9',
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
fontWeight: '600',
fontSize: '14px',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.target.style.backgroundColor = '#1e293b';
e.target.style.borderColor = '#475569';
}}
onMouseLeave={(e) => {
e.target.style.backgroundColor = '#0f172a';
e.target.style.borderColor = '#334155';
}}
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Connectez-vous pour commencer
</button>
</div>
)}
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#10b981' }}>{status.doc_chunks}</div>
<div style={{ fontSize: '12px', color: '#64748b' }}>Chunks PDF</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#3b82f6' }}>{status.memory_entries}</div>
<div style={{ fontSize: '12px', color: '#64748b' }}>Mémoires</div>
</div>
</div>
</div>
)}
{messages.map((m, i) => (
<div key={i} style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start', marginBottom: '3rem' }}>
<div style={{ display: 'flex', gap: '1.2rem', maxWidth: '85%', flexDirection: m.role === 'user' ? 'row-reverse' : 'row' }}>
<div style={{
width: '45px',
height: '45px',
borderRadius: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: m.role === 'user' ? '#059669' : m.error ? '#7f1d1d' : m.success ? '#064e3b' : '#1e293b',
flexShrink: 0,
border: '1px solid #334155',
overflow: 'hidden'
}}>
{m.role === 'user' ? (
m.userPhoto ? (
<img
src={m.userPhoto}
alt={m.userName}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
onError={(e) => {
e.target.style.display = 'none';
e.target.parentElement.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:#059669;"><User size={24} color="white" /></div>`;
}}
/>
) : (
<User size={24} color="white" />
)
) : m.error ? (
<AlertCircle size={24} color="#ef4444" />
) : m.success ? (
<CheckCircle size={24} color="#10b981" />
) : (
<img src={LOGO_PATH} style={{ width: '28px' }} alt="K" onError={(e) => e.target.style.display = 'none'} />
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem', width: '100%' }}>
{m.role === 'assistant' && !m.error && !m.success && m.context_info && (
<div style={{ backgroundColor: 'rgba(30, 41, 59, 0.3)', borderRadius: '12px', padding: '10px 14px', border: '1px solid #1e293b' }}>
<button
onClick={() => toggleThinking(i)}
style={{ background: 'none', border: 'none', color: '#64748b', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', fontWeight: '700', textTransform: 'uppercase', width: '100%', justifyContent: 'space-between' }}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Brain size={14} color="#10b981" /> Intelligence contextuelle
</span>
{showThinking[i] ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
{showThinking[i] && (
<>
<div style={{ display: 'flex', gap: '8px', marginTop: '12px', flexWrap: 'wrap' }}>
<div style={styles.toolBadge}>
<Globe size={13} color="#3b82f6" /> Web Search
{m.context_info.web_results > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.web_results}</span>}
</div>
<div style={styles.toolBadge}>
<FileText size={13} color="#10b981" /> PDF Vault
{m.context_info.rag_sources?.length > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.rag_sources.length}</span>}
</div>
<div style={styles.toolBadge}>
<Brain size={13} color="#f59e0b" /> Mémoire
{m.context_info.memory_used > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.memory_used}</span>}
</div>
<div style={styles.toolBadge}>
<MapPin size={13} color="#ef4444" /> Geo-Context
</div>
</div>
<div style={{ marginTop: '12px', fontSize: '11px', color: '#64748b', padding: '10px', backgroundColor: '#020617', borderRadius: '8px' }}>
{m.context_info.subject_keywords?.length > 0 && (
<div style={{ marginBottom: '8px' }}>
<strong style={{ color: '#94a3b8' }}>Mots-clés du sujet :</strong> {m.context_info.subject_keywords.join(', ')}
</div>
)}
{m.context_info.message_count > 0 && (
<div style={{ marginBottom: '8px' }}>
<strong style={{ color: '#94a3b8' }}>Messages sur ce sujet :</strong> {m.context_info.message_count}
</div>
)}
{m.context_info.rag_sources?.length > 0 && (
<div>
<strong style={{ color: '#94a3b8' }}>Sources documentaires :</strong> {m.context_info.rag_sources.join(', ')}
</div>
)}
</div>
</>
)}
</div>
)}
<div style={{
...(m.role === 'user' ? styles.userBubble : m.error ? styles.errorBubble : m.success ? styles.successBubble : styles.aiBubble),
position: 'relative'
}}>
<p style={{ margin: 0, fontSize: '1.05rem', lineHeight: '1.6', fontWeight: '400', whiteSpace: 'pre-wrap' }}>
{m.content}
</p>
{m.timestamp && (
<div style={{ fontSize: '10px', color: 'rgba(255,255,255,0.4)', marginTop: '8px', display: 'flex', alignItems: 'center', gap: '4px' }}>
<Clock size={10} /> {formatTimeAgo(m.timestamp)}
</div>
)}
</div>
{m.images && m.images.length > 0 && (
<div style={{ display: 'flex', gap: '12px', marginTop: '8px', overflowX: 'auto', paddingBottom: '10px' }}>
{m.images.map((img, idx) => (
<div key={idx} style={{ flexShrink: 0, position: 'relative', borderRadius: '1.2rem', overflow: 'hidden', border: '2px solid #334155', backgroundColor: '#0f172a' }}>
<img
src={img}
alt={`Image ${idx + 1}`}
style={{ height: '180px', width: '280px', objectFit: 'cover' }}
onError={(e) => {
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<div style="height:180px;width:280px;display:flex;align-items:center;justify-content:center;color:#64748b;font-size:12px;"><ImageIcon size={24} /> Image indisponible</div>';
}}
/>
</div>
))}
</div>
)}
</div>
</div>
</div>
))}
{loading && (
<div style={{ display: 'flex', gap: '1.2rem', alignItems: 'center' }}>
<div style={{
width: '45px',
height: '45px',
borderRadius: '14px',
backgroundColor: '#1e293b',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #334155'
}}>
<Loader2 className="animate-spin" color="#10b981" size={24} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div style={{ color: '#f1f5f9', fontSize: '15px', fontWeight: '600' }}>
Kibali analyse votre demande...
</div>
<div style={{ color: '#64748b', fontSize: '12px' }}>
Recherche multi-sources • Traitement vectoriel • Génération contextuelle
</div>
</div>
</div>
)}
<div ref={scrollRef} />
</div>
<div style={styles.inputContainer}>
<div style={styles.inputWrapper}>
<Search size={22} color="#475569" style={{ marginRight: '10px' }} />
<input
style={styles.inputField}
placeholder={user ? "Posez votre question sur le Gabon, vos documents ou l'actualité..." : "Connectez-vous pour poser des questions..."}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && user) {
e.preventDefault();
handleSend();
}
}}
disabled={loading || !user}
/>
<button
style={{
...styles.sendBtn,
opacity: (input.trim() && !loading && user) ? 1 : 0.4,
cursor: (input.trim() && !loading && user) ? 'pointer' : 'not-allowed',
transform: (input.trim() && !loading && user) ? 'scale(1)' : 'scale(0.95)'
}}
onClick={handleSend}
disabled={loading || !input.trim() || !user}
onMouseEnter={(e) => {
if (input.trim() && !loading && user) {
e.target.style.backgroundColor = '#059669';
e.target.style.transform = 'scale(1.05)';
}
}}
onMouseLeave={(e) => {
e.target.style.backgroundColor = '#10b981';
e.target.style.transform = 'scale(1)';
}}
>
{loading ? <Loader2 className="animate-spin" size={20} /> : <Send size={20} />}
</button>
</div>
<div style={{ textAlign: 'center', marginTop: '1.5rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
<p style={{
color: '#334155',
fontSize: '10px',
fontWeight: '800',
letterSpacing: '2px',
margin: 0
}}>
GABONESE SOVEREIGN ARTIFICIAL INTELLIGENCE
</p>
<div style={{ display: 'flex', gap: '1rem', fontSize: '9px', color: '#1e293b', fontWeight: '700' }}>
<span>SYSTEM KIBALI-1</span>
<span></span>
<span>SETRAF-GABON</span>
<span></span>
<span style={{ color: status.torch_cuda_available ? '#10b981' : '#f59e0b' }}>
{status.torch_cuda_available ? '⚡ GPU ACCELERATED' : '⚙️ CPU MODE'}
</span>
</div>
</div>
</div>
</main>
</div>
);
}
export default App;