import { useState, useEffect, useRef } from "react";
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:8000/api";
const api = async (endpoint, method = "GET", body = null, token = null) => {
const headers = { "Content-Type": "application/json" };
if (token) headers["Authorization"] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}${endpoint}`, {
method, headers, body: body ? JSON.stringify(body) : null,
});
if (!res.ok) { const err = await res.json(); throw new Error(err.detail || "API Error"); }
return res.json();
};
// ── TRANSLATIONS ──────────────────────────────────────────────────────────────
const TRANSLATIONS = {
en: {
tagline: "Your intelligent study assistant",
signIn: "Sign in",
createAccount: "Create account",
username: "Username",
email: "Email",
password: "Password",
processing: "Processing...",
chatSubtitle: "Ask anything or explore your documents",
quizSubtitle: "Test your knowledge",
flashcardsSubtitle: "Study with smart cards",
explainSubtitle: "Get clear explanations",
uploadSubtitle: "Manage your course materials",
profileSubtitle: "Your learning progress",
generalChat: "General Chat",
myDocs: "My Documents (RAG)",
shiftEnter: "Shift+Enter for new line",
chatPlaceholderRag: "Ask a question...",
chatPlaceholder: "Type your message...",
quizGenerator: "Quiz Generator",
topic: "Topic",
topicPlaceholderQuiz: "Enter a topic...",
questions: "Questions",
difficulty: "Difficulty",
easy: "Easy",
medium: "Medium",
hard: "Hard",
startQuiz: "Start Quiz",
generating: "Generating...",
result: "Result",
correct: "Correct",
wrong: "Wrong",
time: "Time",
newQuiz: "New Quiz",
excellentWork: "Excellent work",
goodEffort: "Good effort",
keepStudying: "Keep studying",
explanation: "Explanation",
previous: "Previous",
next: "Next",
submit: "Submit",
flashcardGenerator: "Flashcard Generator",
topicPlaceholderFC: "Enter a topic...",
generateFlashcards: "Generate Flashcards",
noFlashcards: "No flashcards generated.",
newTopic: "New Topic",
question: "Question",
answer: "Answer",
clickReveal: "Click to reveal answer",
clickQuestion: "Click to see question",
conceptExplainer: "Concept Explainer",
concept: "Concept",
topicPlaceholderExplain: "Enter a concept...",
level: "Level",
beginner: "Beginner",
intermediate: "Intermediate",
advanced: "Advanced",
explain: "Explain",
output: "Output",
documentManager: "Document Manager",
subject: "Subject",
indexingDoc: "Indexing document...",
chunkingNote: "Chunking + embedding into ChromaDB",
dragOrClick: "Drag a file or click to upload",
dropping: "Drop your file here",
indexedDocs: "Indexed Documents",
noDocsYet: "No documents yet",
uploadNote: "Upload your course materials to use RAG in Chat",
files: "files",
chunks: "chunks",
delete: "Delete",
unsupportedFormat: "Unsupported format. Accepted: PDF, TXT, DOCX",
sessions: "Sessions",
quizzes: "Quizzes",
average: "Average",
best: "Best",
quizHistory: "Quiz History",
memberSince: "Member since",
dayStreak: "Day streak",
failedProfile: "Failed to load profile.",
out: "Out",
learning: "Learning",
resources: "Resources",
noQuestionsGenerated: "No questions generated.",
nav: { chat: "Chat", quiz: "Quiz", flashcards: "Flashcards", explain: "Explain", upload: "Documents", profile: "Profile" },
},
fr: {
tagline: "Votre assistant d'étude intelligent",
signIn: "Se connecter",
createAccount: "Créer un compte",
username: "Nom d'utilisateur",
email: "E-mail",
password: "Mot de passe",
processing: "Traitement...",
chatSubtitle: "Posez vos questions ou explorez vos documents",
quizSubtitle: "Testez vos connaissances",
flashcardsSubtitle: "Étudiez avec des fiches intelligentes",
explainSubtitle: "Obtenez des explications claires",
uploadSubtitle: "Gérez vos supports de cours",
profileSubtitle: "Votre progression d'apprentissage",
generalChat: "Chat Général",
myDocs: "Mes Documents (RAG)",
shiftEnter: "Maj+Entrée pour nouvelle ligne",
chatPlaceholderRag: "Posez une question...",
chatPlaceholder: "Tapez votre message...",
quizGenerator: "Générateur de Quiz",
topic: "Sujet",
topicPlaceholderQuiz: "Entrez un sujet...",
questions: "Questions",
difficulty: "Difficulté",
easy: "Facile",
medium: "Moyen",
hard: "Difficile",
startQuiz: "Démarrer le Quiz",
generating: "Génération...",
result: "Résultat",
correct: "Correct",
wrong: "Faux",
time: "Temps",
newQuiz: "Nouveau Quiz",
excellentWork: "Excellent travail",
goodEffort: "Bon effort",
keepStudying: "Continuez à étudier",
explanation: "Explication",
previous: "Précédent",
next: "Suivant",
submit: "Soumettre",
flashcardGenerator: "Générateur de Fiches",
topicPlaceholderFC: "Entrez un sujet...",
generateFlashcards: "Générer les Fiches",
noFlashcards: "Aucune fiche générée.",
newTopic: "Nouveau Sujet",
question: "Question",
answer: "Réponse",
clickReveal: "Cliquer pour révéler la réponse",
clickQuestion: "Cliquer pour voir la question",
conceptExplainer: "Explication de Concept",
concept: "Concept",
topicPlaceholderExplain: "Entrez un concept...",
level: "Niveau",
beginner: "Débutant",
intermediate: "Intermédiaire",
advanced: "Avancé",
explain: "Expliquer",
output: "Résultat",
documentManager: "Gestionnaire de Documents",
subject: "Matière",
indexingDoc: "Indexation du document...",
chunkingNote: "Découpage + intégration dans ChromaDB",
dragOrClick: "Glissez un fichier ou cliquez pour télécharger",
dropping: "Déposez votre fichier ici",
indexedDocs: "Documents Indexés",
noDocsYet: "Aucun document pour l'instant",
uploadNote: "Téléchargez vos supports de cours pour utiliser le RAG dans le Chat",
files: "fichiers",
chunks: "segments",
delete: "Supprimer",
unsupportedFormat: "Format non pris en charge. Acceptés : PDF, TXT, DOCX",
sessions: "Sessions",
quizzes: "Quiz",
average: "Moyenne",
best: "Meilleur",
quizHistory: "Historique des Quiz",
memberSince: "Membre depuis",
dayStreak: "Jours consécutifs",
failedProfile: "Échec du chargement du profil.",
out: "Déco",
learning: "Apprentissage",
resources: "Ressources",
noQuestionsGenerated: "Aucune question générée.",
nav: { chat: "Chat", quiz: "Quiz", flashcards: "Fiches", explain: "Expliquer", upload: "Documents", profile: "Profil" },
},
};
const GlobalStyle = () => (
);
const C = {
bg: "#F7F5F0",
surface: "#FFFFFF",
card: "#FFFFFF",
sidebar: "#1C1917",
sidebarHover: "#292524",
sidebarActive: "#292524",
sidebarText: "#A8A29E",
sidebarMuted: "#57534E",
border: "#E7E5E0",
borderStrong: "#D6D3CD",
accent: "#C2410C",
accentMid: "#EA580C",
accentLight: "#FEF3EC",
accentBorder: "#FDDCCA",
text: "#1C1917",
secondary: "#78716C",
muted: "#A8A29E",
green: "#16A34A",
yellow: "#D97706",
red: "#DC2626",
white: "#FFFFFF",
};
// ── LANGUAGE SWITCHER ─────────────────────────────────────────────────────────
const LangSwitcher = ({ lang, setLang }) => (
{["en", "fr"].map(l => (
))}
);
// ── SHARED COMPONENTS ─────────────────────────────────────────────────────────
const SectionBar = ({ label }) => (
);
const Tag = ({ children, color = C.accent }) => (
{children}
);
const Spinner = () => (
);
// ── AUTH ──────────────────────────────────────────────────────────────────────
const AuthPage = ({ onLogin, t, lang, setLang }) => {
const [mode, setMode] = useState("login");
const [form, setForm] = useState({ username: "", email: "", password: "" });
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handle = async () => {
setError(""); setLoading(true);
try {
const endpoint = mode === "login" ? "/auth/login" : "/auth/register";
const body = mode === "login"
? { email: form.email, password: form.password }
: { username: form.username, email: form.email, password: form.password };
const data = await api(endpoint, "POST", body);
onLogin(data);
} catch (e) { setError(e.message); }
setLoading(false);
};
return (
<>
{/* Overlay */}
{/* Lang switcher top-right */}
{["en", "fr"].map(l => (
))}
{/* Card */}
{["login", "register"].map(tab => (
))}
{mode === "register" && (
)}
{error && (
{error}
)}
>
);
};
// ── CHAT ──────────────────────────────────────────────────────────────────────
const ChatPage = ({ token, username, t }) => {
const [messages, setMessages] = useState([
{ role: "ai", text: `Hello ${username}.\n\n${t.generalChat}: open questions\nRAG Mode: answers from your uploaded documents` }
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState("chat");
const endRef = useRef(null);
const textareaRef = useRef(null);
useEffect(() => { endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]);
const handleInput = (e) => {
setInput(e.target.value);
e.target.style.height = "auto";
e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
};
const send = async () => {
if (!input.trim() || loading) return;
const q = input.trim(); setInput("");
if (textareaRef.current) textareaRef.current.style.height = "auto";
setLoading(true);
setMessages(m => [...m, { role: "user", text: q }]);
try {
const endpoint = mode === "rag" ? "/rag-qa" : "/chat";
const data = await api(endpoint, "POST", { query: q, user_id: username }, token);
const sources = data.sources?.length > 0 ? `\n\nSources: ${data.sources.join(", ")}` : "";
setMessages(m => [...m, { role: "ai", text: (data.answer || "No response.") + sources }]);
} catch (e) {
setMessages(m => [...m, { role: "ai", text: "Error: " + e.message }]);
}
setLoading(false);
};
const initials = username?.[0]?.toUpperCase() || "U";
return (
Mode:
{[{ id: "chat", label: t.generalChat }, { id: "rag", label: t.myDocs }].map(m => (
))}
{messages.map((m, i) => (
{m.role === "ai" && (
P
)}
{m.text}
{m.role === "user" && (
{initials}
)}
))}
{loading && (
)}
);
};
// ── QUIZ ──────────────────────────────────────────────────────────────────────
const QuizPage = ({ token, t }) => {
const [step, setStep] = useState("config");
const [config, setConfig] = useState({ topic: "", num_questions: 5, difficulty: "medium" });
const [questions, setQuestions] = useState([]);
const [current, setCurrent] = useState(0);
const [answers, setAnswers] = useState({});
const [loading, setLoading] = useState(false);
const [score, setScore] = useState(null);
const [startTime, setStartTime] = useState(null);
const startQuiz = async () => {
if (!config.topic.trim()) return;
setLoading(true);
try {
const data = await api("/quiz", "POST", config, token);
setQuestions(data.questions || []);
setStep("quiz"); setCurrent(0); setAnswers({}); setStartTime(Date.now());
} catch (e) { alert(e.message); }
setLoading(false);
};
const finish = async () => {
const correct = questions.filter((q, i) => answers[i] === q.correct_answer).length;
const scoreVal = Math.round((correct / questions.length) * 100);
const duration = Math.round((Date.now() - startTime) / 1000);
setScore({ correct, total: questions.length, value: scoreVal, duration });
setStep("result");
try { await api("/quiz/result", "POST", { topic: config.topic, score: scoreVal, total_questions: questions.length, correct_answers: correct, difficulty: config.difficulty, duration_sec: duration }, token); } catch (e) { }
};
if (step === "config") return (
{t.questions}
{t.difficulty}
);
if (step === "quiz") {
const q = questions[current];
if (!q) return {t.noQuestionsGenerated}
;
return (
{current + 1} / {questions.length}
{config.difficulty}
{q.question}
{(q.options || []).map((opt, i) => {
const letter = ["A", "B", "C", "D"][i];
const ua = answers[current];
let borderColor = C.border, bgColor = "transparent", textColor = C.text;
if (ua) {
if (letter === q.correct_answer) { borderColor = C.green; bgColor = "#F0FDF4"; textColor = C.green; }
else if (letter === ua) { borderColor = C.red; bgColor = "#FEF2F2"; textColor = C.red; }
}
return (
);
})}
{answers[current] && q.explanation && (
{t.explanation}
{q.explanation}
)}
{current > 0 &&
}
{current < questions.length - 1
?
:
}
);
}
return (
{t.result}
{score.value}%
= 80 ? C.green : score.value >= 60 ? C.yellow : C.red, marginBottom: 32 }}>
{score.value >= 80 ? t.excellentWork : score.value >= 60 ? t.goodEffort : t.keepStudying}
{[{ l: t.correct, v: score.correct }, { l: t.wrong, v: score.total - score.correct }, { l: t.time, v: `${score.duration}s` }].map((s, i) => (
))}
);
};
// ── FLASHCARDS ────────────────────────────────────────────────────────────────
const FlashcardsPage = ({ token, t }) => {
const [step, setStep] = useState("config");
const [topic, setTopic] = useState("");
const [cards, setCards] = useState([]);
const [current, setCurrent] = useState(0);
const [flipped, setFlipped] = useState(false);
const [loading, setLoading] = useState(false);
const generate = async () => {
if (!topic.trim()) return;
setLoading(true);
try {
const data = await api("/flashcards", "POST", { topic, num_cards: 8 }, token);
setCards(data.flashcards || []);
setCurrent(0); setFlipped(false); setStep("study");
} catch (e) { alert(e.message); }
setLoading(false);
};
if (step === "config") return (
);
if (!cards.length) return {t.noFlashcards}
;
const card = cards[current];
return (
{current + 1} / {cards.length}
setFlipped(f => !f)} style={{
width: "100%", minHeight: 280, cursor: "pointer", transition: "all 0.25s",
background: flipped ? C.accentLight : C.surface,
border: `1px solid ${flipped ? C.accentBorder : C.border}`,
borderRadius: 16, position: "relative", display: "flex", alignItems: "center", justifyContent: "center",
}}>
{flipped ? t.answer : t.question}
{flipped ? card.back : card.front}
{flipped ? t.clickQuestion : t.clickReveal}
{cards.map((_, i) => (
{ setCurrent(i); setFlipped(false); }} style={{
width: i === current ? 20 : 6, height: 6, borderRadius: 3,
background: i === current ? C.accent : C.border, cursor: "pointer", transition: "all 0.3s",
}} />
))}
);
};
// ── EXPLAIN ───────────────────────────────────────────────────────────────────
const ExplainPage = ({ token, t }) => {
const [concept, setConcept] = useState("");
const [level, setLevel] = useState("intermediate");
const [result, setResult] = useState("");
const [loading, setLoading] = useState(false);
const explain = async () => {
if (!concept.trim()) return;
setLoading(true); setResult("");
try {
const data = await api("/explain", "POST", { concept, level }, token);
setResult(data.explanation || "No result.");
} catch (e) { setResult("Error: " + e.message); }
setLoading(false);
};
return (
{t.level}
{[{ id: "beginner", l: t.beginner }, { id: "intermediate", l: t.intermediate }, { id: "advanced", l: t.advanced }].map(x => (
))}
{result && (
)}
);
};
// ── UPLOAD ────────────────────────────────────────────────────────────────────
const UploadPage = ({ token, t }) => {
const [dragging, setDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const [documents, setDocuments] = useState([]);
const [subject, setSubject] = useState("general");
const [message, setMessage] = useState(null);
const fileRef = useRef(null);
const subjects = ["general", "biology", "chemistry", "physics", "mathematics", "history", "computer science", "languages"];
useEffect(() => { loadDocs(); }, []);
const loadDocs = async () => {
try { const d = await api("/documents", "GET", null, token); setDocuments(d.documents || []); } catch (e) { }
};
const uploadFile = async (file) => {
if (!file) return;
const ext = "." + file.name.split(".").pop().toLowerCase();
if (![".pdf", ".txt", ".docx"].includes(ext)) { setMessage({ type: "error", text: t.unsupportedFormat }); return; }
setUploading(true); setMessage(null);
const fd = new FormData();
fd.append("file", file); fd.append("subject", subject);
try {
const res = await fetch(`${API_BASE}/upload`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: fd });
const d = await res.json();
if (!res.ok) throw new Error(d.detail || "Upload error");
setMessage({ type: "success", text: `${d.message} — ${d.chunks} ${t.chunks} indexed` });
loadDocs();
} catch (e) { setMessage({ type: "error", text: e.message }); }
setUploading(false);
};
const deleteDoc = async (filename) => {
if (!window.confirm(`Delete "${filename}"?`)) return;
try {
await api(`/documents/${encodeURIComponent(filename)}`, "DELETE", null, token);
setMessage({ type: "success", text: `"${filename}" deleted` });
loadDocs();
} catch (e) { setMessage({ type: "error", text: e.message }); }
};
return (
{t.subject}
{subjects.map(s => (
))}
{ e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); uploadFile(e.dataTransfer.files[0]); }}
onClick={() => fileRef.current?.click()}>
uploadFile(e.target.files[0])} />
{uploading ? (
{t.indexingDoc}
{t.chunkingNote}
) : (
{dragging ? t.dropping : t.dragOrClick}
{["PDF", "TXT", "DOCX"].map(tag => {tag})}
)}
{message && (
{message.text}
)}
{t.indexedDocs}
{documents.length} {t.files}
{documents.length === 0 ? (
{t.noDocsYet}
{t.uploadNote}
) : documents.map((doc, i) => (
{doc.filename}
{doc.subject}
{doc.chunks} {t.chunks}
))}
);
};
// ── PROFILE ───────────────────────────────────────────────────────────────────
const ProfilePage = ({ token, t }) => {
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api("/profile", "GET", null, token).then(setProfile).catch(console.error).finally(() => setLoading(false));
}, [token]);
if (loading) return
;
if (!profile) return
{t.failedProfile}
;
const { user, stats, recent_quiz } = profile;
return (
{user.username?.[0]?.toUpperCase()}
{user.username}
{user.email}
{user.niveau}
{user.streak_days}
{t.dayStreak}
{[{ l: t.sessions, v: stats.total_sessions }, { l: t.quizzes, v: stats.total_quiz }, { l: t.average, v: `${stats.average_score}%` }, { l: t.best, v: `${stats.best_score}%` }].map((s, i) => (
))}
{recent_quiz?.length > 0 && (
{t.quizHistory}
{recent_quiz.map((r, i) => (
{r.topic}
{r.date} · {r.difficulty}
= 80 ? "#F0FDF4" : r.score >= 60 ? "#FFFBEB" : "#FEF2F2", color: r.score >= 80 ? C.green : r.score >= 60 ? C.yellow : C.red }}>
{r.score}%
))}
)}
{t.memberSince} {user.member_since}
);
};
// ── MAIN APP ──────────────────────────────────────────────────────────────────
const NAV_ITEMS = [
{ id: "chat", section: "learning", icon: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" },
{ id: "quiz", section: null, icon: "M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" },
{ id: "flashcards", section: null, icon: "M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zM3 9h18" },
{ id: "explain", section: null, icon: "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 8v4M12 16h.01" },
{ id: "upload", section: "resources",icon: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8" },
{ id: "profile", section: null, icon: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 3a4 4 0 100 8 4 4 0 000-8z" },
];
const SIDEBAR_W = 210;
export default function App() {
const [auth, setAuth] = useState(null);
const [page, setPage] = useState("chat");
const [lang, setLang] = useState("en");
const t = TRANSLATIONS[lang];
if (!auth) return
;
const sections = [];
let current = null;
for (const item of NAV_ITEMS) {
if (item.section) { current = { key: item.section, items: [item] }; sections.push(current); }
else if (current) current.items.push(item);
}
const currentPage = NAV_ITEMS.find(p => p.id === page);
const pageSubtitles = {
chat: t.chatSubtitle, quiz: t.quizSubtitle, flashcards: t.flashcardsSubtitle,
explain: t.explainSubtitle, upload: t.uploadSubtitle, profile: t.profileSubtitle,
};
return (
<>
{/* SIDEBAR */}
{/* MAIN */}
{/* Header avec lang switcher à droite */}
{t.nav[page]}
{pageSubtitles[page]}
{new Date().toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US", { weekday: "short", month: "short", day: "numeric" })}
{page === "chat" &&
}
{page === "quiz" &&
}
{page === "flashcards" &&
}
{page === "explain" &&
}
{page === "upload" &&
}
{page === "profile" &&
}
>
);
}
const S = {
input: {
padding: "11px 14px", background: C.bg, border: `1px solid ${C.border}`,
color: C.text, fontSize: 14, outline: "none", width: "100%", borderRadius: 10,
fontFamily: "'DM Sans', sans-serif", transition: "border-color 0.2s",
},
btnPrimary: {
padding: "12px 24px", background: C.text, border: `1px solid ${C.text}`,
color: C.white, fontSize: 13, fontWeight: 600, cursor: "pointer", width: "100%",
borderRadius: 10, display: "flex", alignItems: "center", justifyContent: "center", gap: 10,
transition: "all 0.15s", fontFamily: "'DM Sans', sans-serif",
},
btnSecondary: {
padding: "10px 20px", background: C.surface, border: `1px solid ${C.border}`,
color: C.secondary, fontSize: 13, cursor: "pointer", borderRadius: 10,
fontFamily: "'DM Sans', sans-serif", fontWeight: 500, transition: "all 0.15s",
},
label: { fontSize: 12, fontWeight: 600, color: C.muted, letterSpacing: 0.3, textTransform: "uppercase" },
};