diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..97e205d1d1cd12dbf1305abd359813c0c9553d83 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e49311786e0f764352e8c441a3e6d4d7825352e1 Binary files /dev/null and b/.gitignore differ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a313016e6ba9cb4c4e68c2c0084239e3f18f3305 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# ── Build Frontend ──────────────────────────────────────────────────────────── +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL +RUN npm run build + +# ── Backend + Frontend servi par FastAPI ────────────────────────────────────── +FROM python:3.10-slim +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY backend/ . + +# Copier le build frontend dans le dossier static du backend +COPY --from=frontend-builder /app/frontend/dist ./static + +RUN mkdir -p /app/chroma_db /app/documents + +EXPOSE 7860 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -f http://localhost:7860/health || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/README.md b/README.md index ff737ecf32d9d07c711b69f9f14731173de39782..0ed474556178c99f3f6b7b9600aa6a6de518255f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ --- -title: PaperBrainAI -emoji: 🐢 -colorFrom: blue -colorTo: yellow +title: PaperBrain +emoji: 💻 +colorFrom: green +colorTo: indigo sdk: docker pinned: false --- diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..9150b02cd32c9797b983ce9b3639c7f97e0cbbb3 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.venv/ +venv/ +env/ +chroma_db/ +smartstudydb +.git/ +.idea/ +.vscode/ \ No newline at end of file diff --git a/backend/.spyproject/config/backups/codestyle.ini.bak b/backend/.spyproject/config/backups/codestyle.ini.bak new file mode 100644 index 0000000000000000000000000000000000000000..0f54b4c43608018a7326818dec70b1bcc553bc66 --- /dev/null +++ b/backend/.spyproject/config/backups/codestyle.ini.bak @@ -0,0 +1,8 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/backups/encoding.ini.bak b/backend/.spyproject/config/backups/encoding.ini.bak new file mode 100644 index 0000000000000000000000000000000000000000..a17acedd726ca82a69c584c98d0bf649af3b20d6 --- /dev/null +++ b/backend/.spyproject/config/backups/encoding.ini.bak @@ -0,0 +1,6 @@ +[encoding] +text_encoding = utf-8 + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/backups/vcs.ini.bak b/backend/.spyproject/config/backups/vcs.ini.bak new file mode 100644 index 0000000000000000000000000000000000000000..fd66eae017a103d4e26f0ce25355ab7cc0534b8d --- /dev/null +++ b/backend/.spyproject/config/backups/vcs.ini.bak @@ -0,0 +1,7 @@ +[vcs] +use_version_control = False +version_control_system = + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/backups/workspace.ini.bak b/backend/.spyproject/config/backups/workspace.ini.bak new file mode 100644 index 0000000000000000000000000000000000000000..cd3735e03386b9eb9307b3bf3cadde9b06c4f7a8 --- /dev/null +++ b/backend/.spyproject/config/backups/workspace.ini.bak @@ -0,0 +1,12 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False +project_type = 'empty-project-type' +recent_files = ['app\\auth\\jwt_handler.py', '.dockerignore', 'app\\main.py', 'app\\tools\\tool_flashcards.py', 'app\\tools\\tool_quiz.py', 'app\\tools\\tool_rag_qa.py', 'app\\tools\\tool_resume.py', 'app\\tools\\tool_simple_explain.py', 'app\\agent.py', 'app\\ingest.py', 'app\\rag_evaluator.py'] + +[main] +version = 0.2.0 +recent_files = [] + diff --git a/backend/.spyproject/config/codestyle.ini b/backend/.spyproject/config/codestyle.ini new file mode 100644 index 0000000000000000000000000000000000000000..0f54b4c43608018a7326818dec70b1bcc553bc66 --- /dev/null +++ b/backend/.spyproject/config/codestyle.ini @@ -0,0 +1,8 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini b/backend/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..0b95e5cee73129de3449c878d8dda2853ed86ee4 --- /dev/null +++ b/backend/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini @@ -0,0 +1,5 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + diff --git a/backend/.spyproject/config/defaults/defaults-encoding-0.2.0.ini b/backend/.spyproject/config/defaults/defaults-encoding-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..0ce193c1e31fea3975f234a934bb07918b05d738 --- /dev/null +++ b/backend/.spyproject/config/defaults/defaults-encoding-0.2.0.ini @@ -0,0 +1,3 @@ +[encoding] +text_encoding = utf-8 + diff --git a/backend/.spyproject/config/defaults/defaults-vcs-0.2.0.ini b/backend/.spyproject/config/defaults/defaults-vcs-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..ee2548333213981054a3a42dbc3e0a033746b25f --- /dev/null +++ b/backend/.spyproject/config/defaults/defaults-vcs-0.2.0.ini @@ -0,0 +1,4 @@ +[vcs] +use_version_control = False +version_control_system = + diff --git a/backend/.spyproject/config/defaults/defaults-workspace-0.2.0.ini b/backend/.spyproject/config/defaults/defaults-workspace-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..2a73ab7ad0a908810de32981505f94e2b8724c38 --- /dev/null +++ b/backend/.spyproject/config/defaults/defaults-workspace-0.2.0.ini @@ -0,0 +1,6 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False + diff --git a/backend/.spyproject/config/encoding.ini b/backend/.spyproject/config/encoding.ini new file mode 100644 index 0000000000000000000000000000000000000000..a17acedd726ca82a69c584c98d0bf649af3b20d6 --- /dev/null +++ b/backend/.spyproject/config/encoding.ini @@ -0,0 +1,6 @@ +[encoding] +text_encoding = utf-8 + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/vcs.ini b/backend/.spyproject/config/vcs.ini new file mode 100644 index 0000000000000000000000000000000000000000..fd66eae017a103d4e26f0ce25355ab7cc0534b8d --- /dev/null +++ b/backend/.spyproject/config/vcs.ini @@ -0,0 +1,7 @@ +[vcs] +use_version_control = False +version_control_system = + +[main] +version = 0.2.0 + diff --git a/backend/.spyproject/config/workspace.ini b/backend/.spyproject/config/workspace.ini new file mode 100644 index 0000000000000000000000000000000000000000..f4adf55fe7e670bb79861b886774cd816758470a --- /dev/null +++ b/backend/.spyproject/config/workspace.ini @@ -0,0 +1,12 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False +project_type = 'empty-project-type' +recent_files = ['app\\auth\\jwt_handler.py', '.dockerignore', 'app\\main.py', 'app\\tools\\tool_flashcards.py', 'app\\tools\\tool_quiz.py', 'app\\tools\\tool_rag_qa.py', 'app\\tools\\tool_resume.py', 'app\\tools\\tool_simple_explain.py', 'app\\agent.py', 'app\\ingest.py', 'app\\rag_evaluator.py', 'app\\rag.py', 'app\\router_service.py', 'app\\schemas_new.py', 'app\\schemas.py', 'app\\auth\\middleware.py', 'app\\db\\crud.py', 'app\\db\\database.py', 'app\\db\\models.py'] + +[main] +version = 0.2.0 +recent_files = [] + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..09e78a565004128d683bdc51efa9cf9a10ef3313 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.10-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/chroma_db /app/documents + +# HuggingFace Spaces impose le port 7860 +EXPOSE 7860 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -f http://localhost:7860/health || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/backend/app/.spyproject/config/codestyle.ini b/backend/app/.spyproject/config/codestyle.ini new file mode 100644 index 0000000000000000000000000000000000000000..0f54b4c43608018a7326818dec70b1bcc553bc66 --- /dev/null +++ b/backend/app/.spyproject/config/codestyle.ini @@ -0,0 +1,8 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + +[main] +version = 0.2.0 + diff --git a/backend/app/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini b/backend/app/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..0b95e5cee73129de3449c878d8dda2853ed86ee4 --- /dev/null +++ b/backend/app/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini @@ -0,0 +1,5 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + diff --git a/backend/app/.spyproject/config/defaults/defaults-encoding-0.2.0.ini b/backend/app/.spyproject/config/defaults/defaults-encoding-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..0ce193c1e31fea3975f234a934bb07918b05d738 --- /dev/null +++ b/backend/app/.spyproject/config/defaults/defaults-encoding-0.2.0.ini @@ -0,0 +1,3 @@ +[encoding] +text_encoding = utf-8 + diff --git a/backend/app/.spyproject/config/defaults/defaults-vcs-0.2.0.ini b/backend/app/.spyproject/config/defaults/defaults-vcs-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..ee2548333213981054a3a42dbc3e0a033746b25f --- /dev/null +++ b/backend/app/.spyproject/config/defaults/defaults-vcs-0.2.0.ini @@ -0,0 +1,4 @@ +[vcs] +use_version_control = False +version_control_system = + diff --git a/backend/app/.spyproject/config/defaults/defaults-workspace-0.2.0.ini b/backend/app/.spyproject/config/defaults/defaults-workspace-0.2.0.ini new file mode 100644 index 0000000000000000000000000000000000000000..2a73ab7ad0a908810de32981505f94e2b8724c38 --- /dev/null +++ b/backend/app/.spyproject/config/defaults/defaults-workspace-0.2.0.ini @@ -0,0 +1,6 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False + diff --git a/backend/app/.spyproject/config/encoding.ini b/backend/app/.spyproject/config/encoding.ini new file mode 100644 index 0000000000000000000000000000000000000000..a17acedd726ca82a69c584c98d0bf649af3b20d6 --- /dev/null +++ b/backend/app/.spyproject/config/encoding.ini @@ -0,0 +1,6 @@ +[encoding] +text_encoding = utf-8 + +[main] +version = 0.2.0 + diff --git a/backend/app/.spyproject/config/vcs.ini b/backend/app/.spyproject/config/vcs.ini new file mode 100644 index 0000000000000000000000000000000000000000..fd66eae017a103d4e26f0ce25355ab7cc0534b8d --- /dev/null +++ b/backend/app/.spyproject/config/vcs.ini @@ -0,0 +1,7 @@ +[vcs] +use_version_control = False +version_control_system = + +[main] +version = 0.2.0 + diff --git a/backend/app/.spyproject/config/workspace.ini b/backend/app/.spyproject/config/workspace.ini new file mode 100644 index 0000000000000000000000000000000000000000..4d9540ac2f7eac94a753cd7733e32d35ef6cc738 --- /dev/null +++ b/backend/app/.spyproject/config/workspace.ini @@ -0,0 +1,12 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False +project_type = 'empty-project-type' +recent_files = [] + +[main] +version = 0.2.0 +recent_files = [] + diff --git a/backend/app/agent.py b/backend/app/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..8416a8c1ff8b04b975aac21f0f59283141377d8e --- /dev/null +++ b/backend/app/agent.py @@ -0,0 +1,313 @@ +import asyncio +import json +import re +import os +from huggingface_hub import InferenceClient + +# ── Config ──────────────────────────────────────────────────────────────────── +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +conversation_store: dict[str, list] = {} + +_client: InferenceClient | None = None + + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +# ── Core call — utilise chat_completion (compatible tous providers HF) ───────── +def _call_hf( + system: str, + user: str, + max_tokens: int = 1024, + temperature: float = 0.4, +) -> str: + try: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + except Exception as e: + raise Exception(f"HuggingFace InferenceClient error: {str(e)}") + + +# ── JSON helpers ────────────────────────────────────────────────────────────── +def _fix_json(s: str) -> str: + s = re.sub(r',\s*([}\]])', r'\1', s) + s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) + return s + + +def _extract_json_array(raw: str) -> list: + cleaned = re.sub(r'```(?:json)?\s*', '', raw) + cleaned = re.sub(r'```', '', cleaned).strip() + + try: + result = json.loads(cleaned) + if isinstance(result, list): + return result + except Exception: + pass + + start = cleaned.find('[') + if start != -1: + depth = 0 + for i, ch in enumerate(cleaned[start:], start): + if ch == '[': + depth += 1 + elif ch == ']': + depth -= 1 + if depth == 0: + candidate = cleaned[start:i + 1] + for attempt in (candidate, _fix_json(candidate)): + try: + result = json.loads(attempt) + if isinstance(result, list): + return result + except Exception: + pass + break + + match = re.search(r'\[[\s\S]*\]', cleaned) + if match: + for attempt in (match.group(), _fix_json(match.group())): + try: + return json.loads(attempt) + except Exception: + pass + + return [] + + +# ── Conversation history ────────────────────────────────────────────────────── +def _get_history(user_id: str) -> list: + return conversation_store.get(user_id, []) + + +def _save_history(user_id: str, user_msg: str, ai_msg: str) -> None: + if user_id not in conversation_store: + conversation_store[user_id] = [] + conversation_store[user_id].append({"user": user_msg, "assistant": ai_msg}) + conversation_store[user_id] = conversation_store[user_id][-5:] + + +# ── Async entry point ───────────────────────────────────────────────────────── +async def run_agent(action: str, data: dict) -> dict: + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _run_sync, action, data) + + +def _run_sync(action: str, data: dict) -> dict: + dispatch = { + "chat": _chat, + "quiz": _quiz, + "flashcards": _flashcards, + "explain": _explain, + "resume": _resume, + "rag-qa": _rag_qa, + } + handler = dispatch.get(action) + if handler: + return handler(data) + return {"answer": f"Unknown action: {action}", "action": action} + + +# ── Action handlers ─────────────────────────────────────────────────────────── + +def _chat(data: dict) -> dict: + query = data.get("query", "") + user_id = data.get("user_id", "anonymous") + history = _get_history(user_id) + + history_text = "" + if history: + history_text = "Conversation récente :\n" + "\n".join( + f"Utilisateur: {h['user']}\nAssistant: {h['assistant']}" + for h in history + ) + "\n\n" + + system = ( + "Tu es PaperBrain AI, un assistant pédagogique pour les étudiants. " + "Aide les étudiants à comprendre leurs cours, préparer leurs examens et apprendre efficacement. " + "Réponds toujours dans la même langue que la question. " + "Sois clair, structuré et pédagogique." + ) + user = f"{history_text}Utilisateur : {query}" + + answer = _call_hf(system, user, max_tokens=1024, temperature=0.5) + _save_history(user_id, query, answer) + return {"answer": answer, "user_id": user_id} + + +def _quiz(data: dict) -> dict: + topic = data.get("topic", "") + num_questions = data.get("num_questions", 5) + difficulty = data.get("difficulty", "medium") + + difficulty_map = { + "easy": "simples et directes, pour débutants", + "medium": "de difficulté intermédiaire", + "hard": "difficiles et approfondies, pour experts", + } + level_desc = difficulty_map.get(difficulty, "de difficulté intermédiaire") + + system = ( + "Tu es un générateur de quiz pédagogique. " + "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après, sans balises markdown." + ) + user = ( + f"Génère {num_questions} questions QCM ({level_desc}) sur : \"{topic}\".\n\n" + "Chaque objet JSON doit contenir : question, options (tableau de 4 chaînes " + "\"A) ...\", \"B) ...\", \"C) ...\", \"D) ...\"), correct_answer (A/B/C/D), explanation.\n\n" + "Réponds UNIQUEMENT avec le tableau JSON." + ) + + raw = _call_hf(system, user, max_tokens=1500, temperature=0.3) + questions = _extract_json_array(raw) + + if questions: + clean = [ + { + "question": str(q.get("question", "")), + "options": list(q.get("options", [])), + "correct_answer": str(q.get("correct_answer", "A")), + "explanation": str(q.get("explanation", "")), + } + for q in questions + if isinstance(q, dict) and q.get("question") and q.get("options") + ] + if clean: + return {"questions": clean, "topic": topic, "difficulty": difficulty} + + return {"questions": [], "topic": topic, "error": "JSON invalide.", "raw_preview": raw[:300]} + + +def _flashcards(data: dict) -> dict: + topic = data.get("topic", "") + num_cards = data.get("num_cards", 8) + + system = ( + "Tu es un générateur de flashcards pédagogiques. " + "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après, sans balises markdown." + ) + user = ( + f"Génère {num_cards} flashcards sur : \"{topic}\".\n\n" + "Chaque objet JSON doit contenir : front (question/terme) et back (réponse/définition).\n\n" + "Réponds UNIQUEMENT avec le tableau JSON." + ) + + raw = _call_hf(system, user, max_tokens=1024, temperature=0.3) + cards = _extract_json_array(raw) + + if cards: + clean = [ + {"front": str(c.get("front", "")), "back": str(c.get("back", ""))} + for c in cards + if isinstance(c, dict) and c.get("front") and c.get("back") + ] + if clean: + return {"flashcards": clean, "topic": topic} + + return {"flashcards": [], "topic": topic, "error": "Impossible de parser les flashcards."} + + +def _explain(data: dict) -> dict: + concept = data.get("concept", "") + level = data.get("level", "intermediate") + + level_map = { + "beginner": "de manière très simple, avec des analogies du quotidien, pour un lycéen", + "intermediate": "clairement avec les concepts essentiels, pour un étudiant universitaire", + "advanced": "de manière approfondie et technique, pour un expert du domaine", + } + level_desc = level_map.get(level, level_map["intermediate"]) + + system = ( + "Tu es un professeur pédagogue expert. " + "Réponds dans la même langue que le concept demandé." + ) + user = ( + f"Explique le concept suivant {level_desc}.\n\n" + "Structure ta réponse avec :\n" + "1. Définition courte et claire\n" + "2. Points clés à retenir\n" + "3. Exemple concret\n" + "4. Applications pratiques\n\n" + f"Concept : {concept}" + ) + + explanation = _call_hf(system, user, max_tokens=1024, temperature=0.5) + return {"explanation": explanation, "concept": concept, "level": level} + + +def _resume(data: dict) -> dict: + text = data.get("text", "") + if not text: + return {"summary": "Aucun texte fourni."} + + system = ( + "Tu es un assistant pédagogique expert en synthèse de documents. " + "Réponds dans la même langue que le texte fourni." + ) + user = ( + "Résume le texte suivant de façon claire et structurée.\n" + "Utilise des titres et des points clés.\n\n" + f"Texte :\n{text[:3000]}" + ) + + summary = _call_hf(system, user, max_tokens=1024, temperature=0.4) + return {"summary": summary} + + +def _rag_qa(data: dict) -> dict: + query = data.get("query", "") + + try: + from app.rag import query_documents + + results = query_documents(query, n_results=4) + documents = results.get("documents", [[]])[0] + metadatas = results.get("metadatas", [[]])[0] + distances = results.get("distances", [[]])[0] + + THRESHOLD = 0.8 + relevant = [ + (doc, meta) + for doc, meta, dist in zip(documents, metadatas, distances) + if dist < THRESHOLD + ] + + if not relevant: + return { + "answer": "Aucune information pertinente trouvée dans vos documents.", + "sources": [], + } + + context = "\n\n---\n\n".join([doc for doc, _ in relevant]) + sources = list(set([meta.get("source", "inconnu") for _, meta in relevant])) + + system = ( + "Tu es un assistant pédagogique RAG. " + "Réponds à la question en te basant UNIQUEMENT sur le contexte fourni. " + "Si la réponse n'est pas dans le contexte, dis-le clairement. " + "Réponds dans la même langue que la question." + ) + user = f"Contexte :\n{context[:3000]}\n\nQuestion : {query}" + + answer = _call_hf(system, user, max_tokens=1024, temperature=0.4) + return {"answer": answer, "sources": sources} + + except Exception as e: + return {"answer": f"Erreur RAG : {str(e)}", "sources": []} \ No newline at end of file diff --git a/backend/app/auth/jwt_handler.py b/backend/app/auth/jwt_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca701492cb915f92303038cec354ae965ad691d --- /dev/null +++ b/backend/app/auth/jwt_handler.py @@ -0,0 +1,19 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt + +SECRET_KEY = "smartstudy_secret_key_2024_change_in_production" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_HOURS = 24 + + +def create_token(data: dict) -> str: + payload = data.copy() + payload["exp"] = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) + return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) + + +def decode_token(token: str) -> dict: + try: + return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + except JWTError: + return None \ No newline at end of file diff --git a/backend/app/auth/middleware.py b/backend/app/auth/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..75abb6510b23e44fdd1d10c331d3300e0d97fb52 --- /dev/null +++ b/backend/app/auth/middleware.py @@ -0,0 +1,33 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from app.db.database import get_db +from app.auth.jwt_handler import decode_token +from app.db.crud import get_user_by_id + +security = HTTPBearer() + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) +): + token = credentials.credentials + payload = decode_token(token) + + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token invalide ou expiré" + ) + + user_id = int(payload.get("sub", 0)) + user = get_user_by_id(db, user_id) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Utilisateur non trouvé" + ) + + return user \ No newline at end of file diff --git a/backend/app/db/crud.py b/backend/app/db/crud.py new file mode 100644 index 0000000000000000000000000000000000000000..5ec20d24a0ba20b55ef97e9b57550c830558d219 --- /dev/null +++ b/backend/app/db/crud.py @@ -0,0 +1,160 @@ +import hashlib +import secrets +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from app.db.models import User, QuizResult, StudySession + + +# ── Password +def hash_password(password: str) -> str: + salt = secrets.token_hex(16) + hashed = hashlib.sha256((password + salt).encode()).hexdigest() + return f"{salt}:{hashed}" + + +def verify_password(plain: str, hashed: str) -> bool: + try: + salt, hash_val = hashed.split(":") + return hashlib.sha256((plain + salt).encode()).hexdigest() == hash_val + except: + return False + + +# ── User CRUD +def get_user_by_email(db: Session, email: str): + return db.query(User).filter(User.email == email).first() + + +def get_user_by_username(db: Session, username: str): + return db.query(User).filter(User.username == username).first() + + +def get_user_by_id(db: Session, user_id: int): + return db.query(User).filter(User.id == user_id).first() + + +def create_user(db: Session, username: str, email: str, password: str): + user = User( + username=username, + email=email, + password=hash_password(password), + created_at=datetime.utcnow() + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +def update_streak(db: Session, user: User): + now = datetime.utcnow() + if user.last_login: + diff = (now.date() - user.last_login.date()).days + if diff == 1: + user.streak_days += 1 + elif diff > 1: + user.streak_days = 1 + else: + user.streak_days = 1 + user.last_login = now + db.commit() + + +# ── Quiz Results +def save_quiz_result(db: Session, user_id: int, req): + result = QuizResult( + user_id=user_id, + topic=req.topic, + score=req.score, + total_questions=req.total_questions, + correct_answers=req.correct_answers, + difficulty=req.difficulty, + duration_sec=req.duration_sec + ) + db.add(result) + + # Mise à jour du niveau utilisateur + user = get_user_by_id(db, user_id) + if user: + results = db.query(QuizResult).filter(QuizResult.user_id == user_id).all() + if len(results) > 0: + avg = sum(r.score for r in results) / len(results) + if avg >= 80: + user.niveau = "expert" + elif avg >= 60: + user.niveau = "intermédiaire" + else: + user.niveau = "débutant" + + db.commit() + db.refresh(result) + return result + + +# ── Profile +def get_student_profile(db: Session, user_id: int) -> dict: + user = get_user_by_id(db, user_id) + if not user: + return {} + + quiz_results = db.query(QuizResult).filter( + QuizResult.user_id == user_id + ).order_by(QuizResult.created_at.desc()).all() + + sessions = db.query(StudySession).filter( + StudySession.user_id == user_id + ).all() + + scores = [r.score for r in quiz_results] + avg_score = round(sum(scores) / len(scores), 1) if scores else 0 + best_score = max(scores) if scores else 0 + + # Top matières + subjects = {} + for s in sessions: + subjects[s.subject] = subjects.get(s.subject, 0) + 1 + top_subjects = sorted( + [{"subject": k, "count": v} for k, v in subjects.items()], + key=lambda x: x["count"], reverse=True + )[:5] + + recent_quiz = [ + { + "topic": r.topic, + "score": r.score, + "date": r.created_at.strftime("%d/%m/%Y"), + "difficulty": r.difficulty + } + for r in quiz_results[:10] + ] + + return { + "user": { + "username": user.username, + "email": user.email, + "niveau": user.niveau, + "streak_days": user.streak_days, + "member_since": user.created_at.strftime("%d/%m/%Y") if user.created_at else "N/A" + }, + "stats": { + "total_sessions": len(sessions), + "total_quiz": len(quiz_results), + "average_score": avg_score, + "best_score": best_score, + "top_subjects": top_subjects + }, + "recent_quiz": recent_quiz + } + + +def get_progress(db: Session, user_id: int) -> dict: + results = db.query(QuizResult).filter( + QuizResult.user_id == user_id + ).order_by(QuizResult.created_at.asc()).all() + + return { + "progression": [ + {"date": r.created_at.strftime("%d/%m"), "score": r.score, "topic": r.topic} + for r in results + ] + } \ No newline at end of file diff --git a/backend/app/db/database.py b/backend/app/db/database.py new file mode 100644 index 0000000000000000000000000000000000000000..4290d4247b3a62bbe07b650a2fd1ca2c373bc1ea --- /dev/null +++ b/backend/app/db/database.py @@ -0,0 +1,26 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "sqlite:///./smartstudy.db" + +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def create_tables(): + from app.db.models import User, QuizResult, StudySession, FlashcardProgress + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/backend/app/db/models.py b/backend/app/db/models.py new file mode 100644 index 0000000000000000000000000000000000000000..241af5a0078c1978b2698cefa4aed2b37d7f5b65 --- /dev/null +++ b/backend/app/db/models.py @@ -0,0 +1,61 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +from app.db.database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + password = Column(String) + niveau = Column(String, default="débutant") + streak_days = Column(Integer, default=0) + last_login = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=datetime.utcnow) + is_active = Column(Boolean, default=True) + + quiz_results = relationship("QuizResult", back_populates="user") + study_sessions = relationship("StudySession", back_populates="user") + + +class QuizResult(Base): + __tablename__ = "quiz_results" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + topic = Column(String) + score = Column(Float) + total_questions = Column(Integer) + correct_answers = Column(Integer) + difficulty = Column(String, default="medium") + duration_sec = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User", back_populates="quiz_results") + + +class StudySession(Base): + __tablename__ = "study_sessions" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + action = Column(String) # chat, quiz, flashcards, explain, rag-qa + subject = Column(String, default="general") + duration = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User", back_populates="study_sessions") + + +class FlashcardProgress(Base): + __tablename__ = "flashcard_progress" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + topic = Column(String) + known = Column(Integer, default=0) + unknown = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/backend/app/ingest.py b/backend/app/ingest.py new file mode 100644 index 0000000000000000000000000000000000000000..5e17b40e670cc41d2b5eee50240c289539559ac0 --- /dev/null +++ b/backend/app/ingest.py @@ -0,0 +1,123 @@ +import os +import uuid +from app.rag import add_documents, get_collection + +CHUNK_SIZE = 600 +CHUNK_OVERLAP = 80 + + +def chunk_text(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list: + """Découpe le texte en chunks avec overlap.""" + paragraphs = text.split("\n\n") + chunks = [] + current = "" + + for para in paragraphs: + para = para.strip() + if not para: + continue + if len(current) + len(para) < chunk_size: + current += ("\n\n" + para) if current else para + else: + if current: + chunks.append(current.strip()) + current = para + + if current: + chunks.append(current.strip()) + + # Si les paragraphes sont trop grands, découper par caractères + final_chunks = [] + for chunk in chunks: + if len(chunk) > chunk_size * 2: + for i in range(0, len(chunk), chunk_size - overlap): + part = chunk[i:i + chunk_size] + if part.strip(): + final_chunks.append(part.strip()) + else: + final_chunks.append(chunk) + + return final_chunks + + +def read_file(file_path: str) -> str: + """Lit un fichier PDF, DOCX ou TXT et retourne le texte.""" + ext = os.path.splitext(file_path)[1].lower() + + if ext == ".txt": + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + return f.read() + + elif ext == ".pdf": + try: + import pdfplumber + with pdfplumber.open(file_path) as pdf: + pages = [] + for page in pdf.pages: + text = page.extract_text() + if text: + pages.append(text) + return "\n\n".join(pages) + except ImportError: + raise ImportError("pdfplumber requis: pip install pdfplumber") + + elif ext in [".docx", ".doc"]: + try: + import docx + doc = docx.Document(file_path) + return "\n\n".join(p.text for p in doc.paragraphs if p.text.strip()) + except ImportError: + raise ImportError("python-docx requis: pip install python-docx") + + else: + raise ValueError(f"Format non supporté: {ext}. Acceptés: .pdf, .txt, .docx") + + +def check_duplicate(file_name: str) -> bool: + """Vérifie si le document existe déjà dans ChromaDB.""" + try: + collection = get_collection() + results = collection.get(where={"source": file_name}) + return len(results.get("ids", [])) > 0 + except: + return False + + +def ingest_document(file_path: str, subject: str = "general") -> int: + """Ingère un document dans ChromaDB. Retourne le nombre de chunks.""" + file_name = os.path.basename(file_path) + + # Supprimer les anciens chunks si le fichier existe déjà + try: + collection = get_collection() + old = collection.get(where={"source": file_name}) + if old.get("ids"): + collection.delete(ids=old["ids"]) + print(f"🗑️ Anciens chunks supprimés pour '{file_name}'") + except Exception as e: + print(f"Warning suppression: {e}") + + # Lire et découper + text = read_file(file_path) + if not text.strip(): + raise ValueError("Le document est vide ou illisible") + + chunks = chunk_text(text) + if not chunks: + raise ValueError("Impossible de découper le document en chunks") + + # Préparer les métadonnées + ids = [str(uuid.uuid4()) for _ in chunks] + metadatas = [ + { + "source": file_name, + "subject": subject, + "chunk_index": i, + "total_chunks": len(chunks) + } + for i in range(len(chunks)) + ] + + add_documents(chunks, metadatas, ids) + print(f"✅ {len(chunks)} chunks ingérés depuis '{file_name}' (matière: {subject})") + return len(chunks) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..883a3dd869a586e34b9337c6275b63b488d966f2 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.router_service import router +from app.db.database import create_tables +import os + +app = FastAPI( + title="PaperBrain API BY HICHAM", + description="API d'assistance à l'apprentissage avec auth et profils", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +@app.on_event("startup") +def startup(): + create_tables() + print("Tables créées avec succès") + +app.include_router(router, prefix="/api") + +@app.get("/health") +def health(): + return {"status": "ok"} + +# Servir le frontend React — DOIT être en dernier +if os.path.exists("static"): + app.mount("/", StaticFiles(directory="static", html=True), name="static") +else: + @app.get("/") + def root(): + return {"message": "SmartStudyAI v2.0 running"} \ No newline at end of file diff --git a/backend/app/rag.py b/backend/app/rag.py new file mode 100644 index 0000000000000000000000000000000000000000..2df0d696dd79af73b41b44b3ce70bfd5c370d863 --- /dev/null +++ b/backend/app/rag.py @@ -0,0 +1,35 @@ +import chromadb + +CHROMA_PATH = "./chroma_db" +COLLECTION_NAME = "smartstudy_docs" + + +def get_chroma_client(): + return chromadb.PersistentClient(path=CHROMA_PATH) + + +def get_collection(): + client = get_chroma_client() + return client.get_or_create_collection( + name=COLLECTION_NAME, + metadata={"hnsw:space": "cosine"} + ) + + +def add_documents(chunks: list, metadatas: list, ids: list): + collection = get_collection() + collection.add(documents=chunks, metadatas=metadatas, ids=ids) + + +def query_documents(query: str, n_results: int = 4) -> dict: + collection = get_collection() + count = collection.count() + if count == 0: + return {"documents": [[]], "metadatas": [[]], "distances": [[]]} + actual_n = min(n_results, count) + return collection.query(query_texts=[query], n_results=actual_n) + + +def delete_collection(): + client = get_chroma_client() + client.delete_collection(COLLECTION_NAME) \ No newline at end of file diff --git a/backend/app/rag_evaluator.py b/backend/app/rag_evaluator.py new file mode 100644 index 0000000000000000000000000000000000000000..144152665fcaa1f2b685893a9c1a0d603ceb8e36 --- /dev/null +++ b/backend/app/rag_evaluator.py @@ -0,0 +1,210 @@ +import json +import re +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +_client: InferenceClient | None = None + + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(model=MODEL_NAME, token=HF_TOKEN or None) + return _client + + +def _call_hf(prompt: str, max_tokens: int = 256, temperature: float = 0.1) -> str: + client = _get_client() + response = client.text_generation( + prompt, + max_new_tokens=max_tokens, + temperature=temperature, + do_sample=False, # deterministic for evaluation + return_full_text=False, + ) + return response.strip() + + +def _extract_score(raw: str) -> float: + try: + cleaned = re.sub(r'```(?:json)?\s*|```', '', raw).strip() + data = json.loads(cleaned) + if isinstance(data, dict): + for key in ["score", "value", "result", "rating"]: + if key in data: + val = float(data[key]) + return max(0.0, min(1.0, val if val <= 1.0 else val / 10.0)) + except Exception: + pass + + matches = re.findall(r'\b(0\.\d+|1\.0|[0-9](?:\.[0-9]+)?)\b', raw) + for m in matches: + val = float(m) + if 0.0 <= val <= 1.0: + return val + if 1.0 < val <= 10.0: + return val / 10.0 + + raw_lower = raw.lower() + if any(w in raw_lower for w in ["excellent", "perfect", "fully", "completely"]): + return 0.9 + if any(w in raw_lower for w in ["good", "mostly", "largely"]): + return 0.7 + if any(w in raw_lower for w in ["partial", "somewhat", "moderate"]): + return 0.5 + if any(w in raw_lower for w in ["poor", "barely", "little"]): + return 0.3 + if any(w in raw_lower for w in ["no", "none", "not", "fail"]): + return 0.1 + + return 0.5 + + +def _parse_result(raw: str) -> tuple[float, str]: + score = _extract_score(raw) + reason = "No reason provided." + try: + cleaned = re.sub(r'```(?:json)?\s*|```', '', raw).strip() + data = json.loads(cleaned) + reason = data.get("reason", reason) + except Exception: + m = re.search(r'"reason"\s*:\s*"([^"]+)"', raw) + if m: + reason = m.group(1) + return round(score, 2), reason + + +# ── Evaluation functions ────────────────────────────────────────────────────── + +def evaluate_faithfulness(question: str, context: str, answer: str) -> dict: + prompt = f"""[INST] Tu es un évaluateur RAG expert. Évalue la FIDÉLITÉ de la réponse. +La fidélité mesure si la réponse est entièrement fondée sur le contexte fourni. + +Question : {question} +Contexte : {context[:2000]} +Réponse : {answer[:1000]} + +Note de 0.0 à 1.0 (1.0 = entièrement fondée sur le contexte, 0.0 = totalement hallucinée). +Réponds UNIQUEMENT avec : {{"score": , "reason": ""}} [/INST] +""" + raw = _call_hf(prompt) + score, reason = _parse_result(raw) + return {"score": score, "reason": reason, "raw": raw[:200]} + + +def evaluate_answer_relevancy(question: str, answer: str) -> dict: + prompt = f"""[INST] Tu es un évaluateur RAG expert. Évalue la PERTINENCE DE LA RÉPONSE. +La pertinence mesure si la réponse répond directement à la question posée. + +Question : {question} +Réponse : {answer[:1000]} + +Note de 0.0 à 1.0 (1.0 = répond parfaitement, 0.0 = hors sujet). +Réponds UNIQUEMENT avec : {{"score": , "reason": ""}} [/INST] +""" + raw = _call_hf(prompt) + score, reason = _parse_result(raw) + return {"score": score, "reason": reason, "raw": raw[:200]} + + +def evaluate_context_recall(question: str, context: str) -> dict: + prompt = f"""[INST] Tu es un évaluateur RAG expert. Évalue le RAPPEL DU CONTEXTE. +Mesure si le contexte récupéré contient les informations nécessaires pour répondre à la question. + +Question : {question} +Contexte récupéré : {context[:2000]} + +Note de 0.0 à 1.0 (1.0 = contexte idéal, 0.0 = contexte inutile). +Réponds UNIQUEMENT avec : {{"score": , "reason": ""}} [/INST] +""" + raw = _call_hf(prompt) + score, reason = _parse_result(raw) + return {"score": score, "reason": reason, "raw": raw[:200]} + + +def evaluate_hallucination(question: str, context: str, answer: str) -> dict: + prompt = f"""[INST] Tu es un évaluateur RAG expert. Détecte les HALLUCINATIONS dans la réponse. +Une hallucination = information présente dans la réponse mais ABSENTE du contexte et non-connaissance générale. + +Question : {question} +Contexte : {context[:2000]} +Réponse : {answer[:1000]} + +Note de 0.0 à 1.0 (1.0 = aucune hallucination, 0.0 = totalement hallucinée). +Réponds UNIQUEMENT avec : {{"score": , "reason": ""}} [/INST] +""" + raw = _call_hf(prompt) + score, reason = _parse_result(raw) + return {"score": score, "reason": reason, "raw": raw[:200]} + + +def evaluate_rag_response(question: str, context: str, answer: str) -> dict: + print(f"[RAG EVAL] Démarrage pour : {question[:80]}") + + results: dict[str, dict] = {} + + for key, fn, args in [ + ("faithfulness", evaluate_faithfulness, (question, context, answer)), + ("answer_relevancy", evaluate_answer_relevancy, (question, answer)), + ("context_recall", evaluate_context_recall, (question, context)), + ("hallucination", evaluate_hallucination, (question, context, answer)), + ]: + try: + results[key] = fn(*args) + print(f"[RAG EVAL] {key}: {results[key]['score']}") + except Exception as e: + results[key] = {"score": 0.0, "reason": str(e), "error": True} + + weights = { + "faithfulness": 0.35, + "answer_relevancy": 0.30, + "context_recall": 0.20, + "hallucination": 0.15, + } + overall = round(sum( + results[k]["score"] * w + for k, w in weights.items() + if not results[k].get("error") + ), 2) + + grade = "A" if overall >= 0.85 else "B" if overall >= 0.70 else "C" if overall >= 0.55 else "D" if overall >= 0.40 else "F" + print(f"[RAG EVAL] Overall: {overall} ({grade})") + + return { + "question": question, + "overall_score": overall, + "grade": grade, + "metrics": results, + "summary": _generate_summary(overall, results), + } + + +def _generate_summary(overall: float, results: dict) -> str: + label_map = { + "faithfulness": "Fidélité", + "answer_relevancy": "Pertinence", + "context_recall": "Rappel contexte", + "hallucination": "Hallucination", + } + weak = [label_map[k] for k, v in results.items() if v["score"] < 0.5 and not v.get("error")] + strong = [label_map[k] for k, v in results.items() if v["score"] >= 0.8 and not v.get("error")] + + if overall >= 0.85: + verdict = "Excellente réponse RAG." + elif overall >= 0.70: + verdict = "Bonne réponse avec quelques défauts mineurs." + elif overall >= 0.50: + verdict = "Réponse acceptable — qualité du contexte à améliorer." + else: + verdict = "Réponse insuffisante — uploadez des documents plus pertinents." + + parts = [] + if strong: + parts.append(f"Points forts : {', '.join(strong)}.") + if weak: + parts.append(f"À améliorer : {', '.join(weak)}.") + + return verdict + (" " + " ".join(parts) if parts else "") \ No newline at end of file diff --git a/backend/app/router_service.py b/backend/app/router_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e06641504bb90727d4531c69d35cd7caedba0140 --- /dev/null +++ b/backend/app/router_service.py @@ -0,0 +1,206 @@ +import os +import shutil +from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form +from sqlalchemy.orm import Session + +from app.db.database import get_db +from app.db import crud +from app.auth.jwt_handler import create_token +from app.auth.middleware import get_current_user +from app.schemas import ( + RegisterRequest, LoginRequest, ChatRequest, QuizRequest, + FlashcardRequest, ExplainRequest, ResumeRequest, RAGRequest, QuizResultRequest +) + +# ── Router (TOUJOURS en premier) +router = APIRouter() +UPLOAD_DIR = "./documents" +os.makedirs(UPLOAD_DIR, exist_ok=True) + + +# ══════════════════════════════════════════════════════════ +# AUTH +# ══════════════════════════════════════════════════════════ + +@router.post("/auth/register") +def register(req: RegisterRequest, db: Session = Depends(get_db)): + if crud.get_user_by_email(db, req.email): + raise HTTPException(400, "Email déjà utilisé") + if crud.get_user_by_username(db, req.username): + raise HTTPException(400, "Nom d'utilisateur déjà pris") + user = crud.create_user(db, req.username, req.email, req.password) + token = create_token({"sub": str(user.id), "username": user.username}) + return {"access_token": token, "username": user.username, "user_id": user.id} + + +@router.post("/auth/login") +def login(req: LoginRequest, db: Session = Depends(get_db)): + user = crud.get_user_by_email(db, req.email) + if not user or not crud.verify_password(req.password, user.password): + raise HTTPException(401, "Email ou mot de passe incorrect") + crud.update_streak(db, user) + token = create_token({"sub": str(user.id), "username": user.username}) + return {"access_token": token, "username": user.username, "user_id": user.id} + + +# ══════════════════════════════════════════════════════════ +# PROFILE +# ══════════════════════════════════════════════════════════ + +@router.get("/profile") +def get_profile(current_user=Depends(get_current_user), db: Session = Depends(get_db)): + return crud.get_student_profile(db, current_user.id) + + +@router.get("/progress") +def get_progress(current_user=Depends(get_current_user), db: Session = Depends(get_db)): + return crud.get_progress(db, current_user.id) + + +# ══════════════════════════════════════════════════════════ +# QUIZ RESULT +# ══════════════════════════════════════════════════════════ + +@router.post("/quiz/result") +def save_quiz_result(req: QuizResultRequest, current_user=Depends(get_current_user), db: Session = Depends(get_db)): + result = crud.save_quiz_result(db, current_user.id, req) + return {"message": "Résultat sauvegardé", "id": result.id} + + +# ══════════════════════════════════════════════════════════ +# UPLOAD & DOCUMENTS +# ══════════════════════════════════════════════════════════ + +@router.post("/upload") +async def upload_document( + file: UploadFile = File(...), + subject: str = Form(default="general") +): + allowed = [".pdf", ".txt", ".docx"] + ext = os.path.splitext(file.filename)[1].lower() + if ext not in allowed: + raise HTTPException(400, f"Format non supporté. Acceptés: {allowed}") + + file_path = os.path.join(UPLOAD_DIR, file.filename) + with open(file_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + try: + from app.ingest import ingest_document + chunks = ingest_document(file_path, subject) + return { + "message": f"Fichier '{file.filename}' ingéré avec succès", + "chunks": chunks, + "subject": subject, + "filename": file.filename + } + except Exception as e: + if os.path.exists(file_path): + os.remove(file_path) + raise HTTPException(500, f"Erreur ingestion: {str(e)}") + + +@router.get("/documents") +def list_documents(): + try: + from app.rag import get_collection + collection = get_collection() + results = collection.get() + sources = {} + for meta in results.get("metadatas", []): + if not meta: + continue + src = meta.get("source", "inconnu") + subj = meta.get("subject", "general") + if src not in sources: + sources[src] = {"filename": src, "subject": subj, "chunks": 0} + sources[src]["chunks"] += 1 + return {"documents": list(sources.values()), "total": len(sources)} + except Exception as e: + return {"documents": [], "total": 0, "error": str(e)} + + +@router.delete("/documents/{filename}") +def delete_document(filename: str): + try: + from app.rag import get_collection + collection = get_collection() + results = collection.get(where={"source": filename}) + ids = results.get("ids", []) + if ids: + collection.delete(ids=ids) + file_path = os.path.join(UPLOAD_DIR, filename) + if os.path.exists(file_path): + os.remove(file_path) + return {"message": f"'{filename}' supprimé ({len(ids)} chunks)"} + except Exception as e: + raise HTTPException(500, str(e)) + + +# ══════════════════════════════════════════════════════════ +# AI ENDPOINTS +# ══════════════════════════════════════════════════════════ + +@router.post("/chat") +async def chat(req: ChatRequest): + try: + from app.agent import run_agent + result = await run_agent("chat", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/quiz") +async def generate_quiz(req: QuizRequest): + try: + from app.agent import run_agent + result = await run_agent("quiz", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/flashcards") +async def generate_flashcards(req: FlashcardRequest): + try: + from app.agent import run_agent + result = await run_agent("flashcards", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/explain") +async def explain(req: ExplainRequest): + try: + from app.agent import run_agent + result = await run_agent("explain", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/resume") +async def resume(req: ResumeRequest): + try: + from app.agent import run_agent + result = await run_agent("resume", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/rag-qa") +async def rag_qa_endpoint(req: RAGRequest): + try: + from app.agent import run_agent + result = await run_agent("rag-qa", req.dict()) + return result + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.get("/health") +def health(): + return {"status": "ok", "service": "SmartStudyAI"} \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..12136437dc4c71181ac7c9bd693289ed1ae68e3e --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel +from typing import Optional + +# ── Auth +class RegisterRequest(BaseModel): + username: str + email: str + password: str + +class LoginRequest(BaseModel): + email: str + password: str + +# ── AI Requests +class ChatRequest(BaseModel): + query: str + user_id: Optional[str] = "anonymous" + +class QuizRequest(BaseModel): + topic: str + num_questions: Optional[int] = 5 + difficulty: Optional[str] = "medium" + +class FlashcardRequest(BaseModel): + topic: str + num_cards: Optional[int] = 8 + +class ExplainRequest(BaseModel): + concept: str + level: Optional[str] = "intermediate" + +class ResumeRequest(BaseModel): + text: str + +class RAGRequest(BaseModel): + query: str + user_id: Optional[str] = "anonymous" + +# ── Quiz Result +class QuizResultRequest(BaseModel): + topic: str + score: int + total_questions: int + correct_answers: int + difficulty: Optional[str] = "medium" + duration_sec: Optional[int] = 0 +class RAGEvalRequest(BaseModel): + question: str + context: str + answer: str \ No newline at end of file diff --git a/backend/app/schemas_new.py b/backend/app/schemas_new.py new file mode 100644 index 0000000000000000000000000000000000000000..5a1e26b7d117730720290d27c5ec04495aed1ef5 --- /dev/null +++ b/backend/app/schemas_new.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel +from typing import Optional + +class RegisterRequest(BaseModel): + username: str + email: str + password: str + +class LoginRequest(BaseModel): + email: str + password: str + +class ChatRequest(BaseModel): + query: str + user_id: str = "anonymous" + +class QuizRequest(BaseModel): + topic: str + num_questions: int = 5 + difficulty: str = "medium" + +class FlashcardRequest(BaseModel): + topic: str + num_cards: int = 8 + +class ExplainRequest(BaseModel): + concept: str + level: str = "intermediate" + +class ResumeRequest(BaseModel): + text: str + +class RAGRequest(BaseModel): + query: str + user_id: str = "anonymous" + +class QuizResultRequest(BaseModel): + topic: str + score: int + total_questions: int + correct_answers: int + difficulty: str = "medium" + duration_sec: int = 0 diff --git a/backend/app/tools/tool_flashcards.py b/backend/app/tools/tool_flashcards.py new file mode 100644 index 0000000000000000000000000000000000000000..66f1b67022d166bee6a39fc6a439479ada222ba7 --- /dev/null +++ b/backend/app/tools/tool_flashcards.py @@ -0,0 +1,77 @@ +import json +import re +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +_client = None + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +def _call_hf(system: str, user: str, max_tokens: int = 1024, temperature: float = 0.3) -> str: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + + +def _extract_json_array(raw: str) -> list: + cleaned = re.sub(r'```(?:json)?\s*|```', '', raw).strip() + try: + result = json.loads(cleaned) + if isinstance(result, list): + return result + except Exception: + pass + start = cleaned.find('[') + if start != -1: + depth = 0 + for i, ch in enumerate(cleaned[start:], start): + if ch == '[': depth += 1 + elif ch == ']': + depth -= 1 + if depth == 0: + candidate = re.sub(r',\s*([}\]])', r'\1', cleaned[start:i+1]) + try: + return json.loads(candidate) + except Exception: + pass + break + return [] + + +def generate_flashcards(topic: str, num_cards: int = 10) -> list[dict]: + system = ( + "Tu es un générateur de flashcards pédagogiques. " + "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après." + ) + user = ( + f"Génère {num_cards} flashcards sur : \"{topic}\".\n" + "Chaque objet : front (question/terme) et back (réponse/définition).\n" + "Réponds UNIQUEMENT avec le tableau JSON." + ) + + raw = _call_hf(system, user) + cards = _extract_json_array(raw) + + if cards: + return [ + {"front": str(c.get("front", "")), "back": str(c.get("back", ""))} + for c in cards + if isinstance(c, dict) and c.get("front") and c.get("back") + ] + return [{"front": topic, "back": raw[:300]}] \ No newline at end of file diff --git a/backend/app/tools/tool_quiz.py b/backend/app/tools/tool_quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..56633a753a02c1962bff74634ad0f6a4e90dc99d --- /dev/null +++ b/backend/app/tools/tool_quiz.py @@ -0,0 +1,92 @@ +import json +import re +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +_client = None + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +def _call_hf(system: str, user: str, max_tokens: int = 1500, temperature: float = 0.3) -> str: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + + +def _extract_json_array(raw: str) -> list: + cleaned = re.sub(r'```(?:json)?\s*|```', '', raw).strip() + try: + result = json.loads(cleaned) + if isinstance(result, list): + return result + except Exception: + pass + start = cleaned.find('[') + if start != -1: + depth = 0 + for i, ch in enumerate(cleaned[start:], start): + if ch == '[': depth += 1 + elif ch == ']': + depth -= 1 + if depth == 0: + candidate = re.sub(r',\s*([}\]])', r'\1', cleaned[start:i+1]) + try: + return json.loads(candidate) + except Exception: + pass + break + return [] + + +def generate_quiz(topic: str, num_questions: int = 5, difficulty: str = "medium") -> list[dict]: + difficulty_map = { + "easy": "simples et directes, pour débutants", + "medium": "de difficulté intermédiaire", + "hard": "difficiles et approfondies, pour experts", + } + level_desc = difficulty_map.get(difficulty, "de difficulté intermédiaire") + + system = ( + "Tu es un générateur de quiz pédagogique. " + "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après." + ) + user = ( + f"Génère {num_questions} questions QCM ({level_desc}) sur : \"{topic}\".\n" + "Chaque objet : question, options (4 chaînes A/B/C/D), correct_answer (A/B/C/D), explanation.\n" + "Réponds UNIQUEMENT avec le tableau JSON." + ) + + raw = _call_hf(system, user) + questions = _extract_json_array(raw) + + if questions: + clean = [ + { + "question": str(q.get("question", "")), + "options": list(q.get("options", [])), + "correct_answer": str(q.get("correct_answer", "A")), + "explanation": str(q.get("explanation", "")), + } + for q in questions + if isinstance(q, dict) and q.get("question") and q.get("options") + ] + if clean: + return clean + + return [{"question": f"Question sur {topic}", "options": ["A) -", "B) -", "C) -", "D) -"], "correct_answer": "A", "explanation": "Erreur de génération."}] \ No newline at end of file diff --git a/backend/app/tools/tool_rag_qa.py b/backend/app/tools/tool_rag_qa.py new file mode 100644 index 0000000000000000000000000000000000000000..d901f6b5870bf46f33ba17274678d8431f31616f --- /dev/null +++ b/backend/app/tools/tool_rag_qa.py @@ -0,0 +1,61 @@ +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") +RELEVANCE_THRESHOLD = 0.4 + +_client = None + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +def _call_hf(system: str, user: str, max_tokens: int = 1024, temperature: float = 0.4) -> str: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + + +def rag_qa(query: str, history_text: str = "") -> tuple[str, list[str]]: + from app.rag import query_documents + + results = query_documents(query, n_results=3) + documents = results.get("documents", [[]])[0] + metadatas = results.get("metadatas", [[]])[0] + distances = results.get("distances", [[]])[0] + + relevant_docs = [ + (doc, meta) + for doc, meta, dist in zip(documents, metadatas, distances) + if dist < RELEVANCE_THRESHOLD + ] + + if not relevant_docs: + return ("Je n'ai pas trouvé d'information pertinente dans vos cours.", []) + + context = "\n\n---\n\n".join([doc for doc, _ in relevant_docs]) + sources = list(set([meta.get("source", "inconnu") for _, meta in relevant_docs])) + + system = ( + "Tu es un assistant pédagogique RAG. " + "Réponds à la question en te basant UNIQUEMENT sur le contexte fourni. " + "Si la réponse n'est pas dans le contexte, dis-le clairement. " + "Réponds dans la même langue que la question." + ) + history_section = f"Historique:\n{history_text}\n\n" if history_text else "" + user = f"{history_section}Contexte :\n{context[:3000]}\n\nQuestion : {query}" + + answer = _call_hf(system, user) + return answer, sources \ No newline at end of file diff --git a/backend/app/tools/tool_resume.py b/backend/app/tools/tool_resume.py new file mode 100644 index 0000000000000000000000000000000000000000..51ce88bde71d08aaa47eccc70e563f7a8e73ea06 --- /dev/null +++ b/backend/app/tools/tool_resume.py @@ -0,0 +1,51 @@ +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +_client = None + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +def _call_hf(system: str, user: str, max_tokens: int = 1024, temperature: float = 0.4) -> str: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + + +def generate_resume(text: str = None, file_path: str = None) -> str: + if file_path and not text: + try: + with open(file_path, "r", encoding="utf-8") as f: + text = f.read() + except Exception as e: + return f"Erreur lors de la lecture du fichier : {e}" + + if not text: + return "Aucun texte ou fichier fourni." + + system = ( + "Tu es un assistant pédagogique expert en synthèse de documents. " + "Réponds dans la même langue que le texte fourni." + ) + user = ( + "Résume le texte suivant de façon claire et structurée. " + "Utilise des titres et des points clés.\n\n" + f"Texte :\n{text[:4000]}" + ) + + return _call_hf(system, user, max_tokens=1024) \ No newline at end of file diff --git a/backend/app/tools/tool_simple_explain.py b/backend/app/tools/tool_simple_explain.py new file mode 100644 index 0000000000000000000000000000000000000000..ab03a17235e629962d750a1e33069da0df066100 --- /dev/null +++ b/backend/app/tools/tool_simple_explain.py @@ -0,0 +1,53 @@ +import os +from huggingface_hub import InferenceClient + +HF_TOKEN = os.getenv("HF_TOKEN", "") +MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") + +LEVEL_DESCRIPTIONS = { + "beginner": "de manière très simple, comme si tu expliquais à un lycéen, avec des analogies du quotidien", + "intermediate": "de manière claire avec les concepts essentiels, pour un étudiant universitaire", + "advanced": "de manière approfondie et technique, pour un expert du domaine", +} + +_client = None + +def _get_client() -> InferenceClient: + global _client + if _client is None: + _client = InferenceClient(token=HF_TOKEN or None) + return _client + + +def _call_hf(system: str, user: str, max_tokens: int = 1024, temperature: float = 0.5) -> str: + client = _get_client() + response = client.chat_completion( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + max_tokens=max_tokens, + temperature=temperature, + ) + return response.choices[0].message.content.strip() + + +def simple_explain(concept: str, level: str = "intermediate") -> str: + level_desc = LEVEL_DESCRIPTIONS.get(level, LEVEL_DESCRIPTIONS["intermediate"]) + + system = ( + "Tu es un professeur pédagogue expert. " + "Réponds dans la même langue que le concept demandé." + ) + user = ( + f"Explique le concept suivant {level_desc}.\n\n" + "Structure ta réponse avec :\n" + "1. Une définition courte et claire\n" + "2. Les points clés à retenir\n" + "3. Un exemple concret\n" + "4. Les applications pratiques\n\n" + f"Concept : {concept}" + ) + + return _call_hf(system, user, max_tokens=1024) \ No newline at end of file diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000000000000000000000000000000000000..f5f10a6b00f2e280084290431edfb57f795bfaed --- /dev/null +++ b/backend/database.py @@ -0,0 +1,25 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "sqlite:///./smartstudy.db" + +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def create_tables(): + from app.db.models import User, QuizResult, StudySession, FlashcardProgress + Base.metadata.create_all(bind=engine) + print("✅ Tables créées avec succès") \ No newline at end of file diff --git a/backend/migrate.py b/backend/migrate.py new file mode 100644 index 0000000000000000000000000000000000000000..4d8ec83ea3b812d71d6c3ebba92e3f2918fed1c7 --- /dev/null +++ b/backend/migrate.py @@ -0,0 +1,20 @@ +import sqlite3 +import os + +db_path = os.path.join(os.path.dirname(__file__), "smartstudy.db") + +conn = sqlite3.connect(db_path) +cursor = conn.cursor() + +# Check if column already exists +cursor.execute("PRAGMA table_info(study_sessions)") +columns = [col[1] for col in cursor.fetchall()] + +if "duration" not in columns: + cursor.execute("ALTER TABLE study_sessions ADD COLUMN duration INTEGER DEFAULT 0") + conn.commit() + print("✅ Column 'duration' added successfully.") +else: + print("ℹ️ Column 'duration' already exists, nothing to do.") + +conn.close() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f17ba0478056ce3b9432622a5f1e1aefdd0a2572 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,30 @@ +# API & serveur +fastapi==0.111.0 +uvicorn[standard]==0.29.0 +python-multipart==0.0.9 + +# Auth & sécurité +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 + +# Base de données +sqlalchemy==2.0.30 + +# HuggingFace +huggingface-hub>=0.31.0 + +# NumPy — forcer 1.x pour compatibilité ChromaDB +numpy<2.0 + +# RAG / ChromaDB +chromadb==0.5.0 +onnxruntime==1.18.0 + +# Lecture de documents +pdfplumber==0.11.1 +python-docx==1.1.2 + +# Utilitaires +pydantic==2.7.1 +python-dotenv==1.0.1 +requests==2.32.3 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6fc5daaf98b864b295cc40c7a8778c1283a2389c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + + backend: + build: + context: ./backend # Docker lit les fichiers DEPUIS ./backend + dockerfile: Dockerfile + container_name: smartstudy-backend + ports: + - "8000:8000" + environment: + HF_TOKEN: ${HF_TOKEN} + HF_MODEL: ${HF_MODEL:-mistralai/Mistral-7B-Instruct-v0.3} + volumes: + - chroma_data:/app/chroma_db + - documents_data:/app/documents + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + + frontend: + build: + context: ./frontend # Docker lit les fichiers DEPUIS ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: http://localhost:8000/api + container_name: smartstudy-frontend + ports: + - "5173:80" + depends_on: + backend: + condition: service_healthy + restart: unless-stopped + +volumes: + chroma_data: + documents_data: \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ccbecedcf39089f435808d9d330893db2cd096cb --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +ARG VITE_API_URL=http://localhost:8000/api +ENV VITE_API_URL=$VITE_API_URL + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# ── Runtime Nginx ───────────────────────────────────────────────────────────── +FROM nginx:alpine + +COPY --from=builder /app/dist /usr/share/nginx/html + +RUN echo 'server { \ + listen 80; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..18bc70ebe277fbfe6e55e6f9a0ae7e2c3e4bdd83 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4fa125da29e01fa85529cfa06a83a7c0ce240d55 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..296631c31de1059daa3375eb391c472bcada9493 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + smartstudy-frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..2d38b9bdcc25ec4a5f62bd0db00bc7a0f20b77b6 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2929 @@ +{ + "name": "smartstudy-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartstudy-frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..7dcfeb820de1f9e940b080aff4389abe477b6de3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "smartstudy-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/Background.jpg b/frontend/public/Background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..51d9a513aa930ab2fb38719b7a2f867a98122e36 --- /dev/null +++ b/frontend/public/Background.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63dfe46b92048479fa3a78cd097d3512e47a0b2ebd29ebb7c8d918234e3a9ddf +size 4144133 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7a48f51a2f5dcbf04b9eb64388c0b183b3d7107e --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,1106 @@ +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 }) => ( +
+

{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 */} +
+
+
+
+ +
+ PaperBrain +
+

{t.tagline}

+
+ +
+
+ {["login", "register"].map(tab => ( + + ))} +
+ +
+ {mode === "register" && ( +
+
{t.username}
+ setForm({ ...form, username: e.target.value })} /> +
+ )} +
+
{t.email}
+ setForm({ ...form, email: e.target.value })} /> +
+
+
{t.password}
+ setForm({ ...form, password: e.target.value })} + onKeyDown={e => e.key === "Enter" && handle()} /> +
+ + {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 && ( +
+
+ P +
+
+ {[0, 1, 2].map(i => )} +
+
+ )} +
+
+ +
+
+