diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2bb8a312d16f3eccd192990cfab80313ff24d5b3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,53 @@
+# General OS & Environment
+.DS_Store
+Thumbs.db
+*.log
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+!.env.example
+!.env.local.example
+
+COPILOT_PROMPT.md
+
+# Frontend (Node.js & Next.js)
+frontend/node_modules/
+frontend/.pnp
+frontend/.pnp.js
+frontend/.next/
+frontend/out/
+frontend/build/
+frontend/coverage/
+frontend/.vercel
+
+# Backend (Python)
+backend/__pycache__/
+backend/*.py[cod]
+backend/*$py.class
+backend/.pytest_cache/
+backend/.coverage
+backend/htmlcov/
+backend/venv/
+backend/env/
+backend/ENV/
+backend/env.bak/
+backend/venv.bak/
+backend/.venv/
+backend/build/
+backend/develop-eggs/
+backend/dist/
+backend/downloads/
+backend/eggs/
+backend/.eggs/
+backend/lib/
+backend/lib64/
+backend/parts/
+backend/sdist/
+backend/var/
+backend/wheels/
+backend/share/python-wheels/
+backend/*.egg-info/
+backend/.installed.cfg
+backend/*.egg
diff --git a/README.md b/README.md
index ff9c6dde8bd6d225bd6fec198decb1ba5def3c39..d7648c2660ca42fa342139507934b2d39055a553 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,99 @@
-# SkillForge
\ No newline at end of file
+# SkillForge AI
+
+> **From resume claims to real capability.**
+
+A next-generation AI agent that takes a Job Description and a candidate's resume, conversationally assesses real proficiency on each required skill, identifies gaps, and generates a personalised learning plan focused on adjacent skills the candidate can realistically acquire — with curated resources and time estimates.
+
+No simple keyword matching. No generic RAG. Pure **Knowledge Graph traversal** + **multi-agent real-time conversation**.
+
+---
+
+## ⚡ Architecture & Tech Stack
+
+SkillForge AI is built as a full-stack monorepo featuring a powerful Python/AI backend and a blazing fast Next.js frontend.
+
+### Backend (AI & Logic)
+- **Framework:** FastAPI
+- **Agent Orchestration:** LangGraph (Multi-agent state machines)
+- **LLM Engine:** LangChain + Groq (`llama-3.3-70b-versatile`) for lightning-fast inference
+- **Knowledge Graph:** NetworkX (Directed graphs with custom edge weights)
+- **PDF Processing:** `pdfplumber` for clean JD/Resume extraction
+
+### Frontend (User Interface)
+- **Framework:** Next.js 14 (App Router)
+- **Styling:** Tailwind CSS + Custom Industrial Dark Theme
+- **State Management:** Zustand
+- **Streaming:** Server-Sent Events (SSE) via native Web API `EventSource`
+
+---
+
+## 🧠 How it Works
+
+1. **Extraction (`extractor.py`):** An LLM extracts structured capabilities from the candidate's resume and the target job description.
+2. **Gap Analysis (`claim_vs_reality.py`):** Cross-references resume claims against job requirements to flag high-priority gaps.
+3. **Conversational Assessment (`interviewer.py` & `scorer.py`):** The agent engages the candidate in a dynamic, progressive Q&A to verify actual competency rather than just stated experience.
+4. **Targeted Challenges (`challenger.py`):** For intermediate/senior skills, the agent generates grounded "find-the-bug" style micro-challenges.
+5. **Roadmap Generation (`roadmap_generator.py`):** The Knowledge Graph engine (`graph_engine.py`) calculates the shortest learning path from what the candidate *already knows* to what they *need to know*, packaging it into a multi-tiered weekly syllabus.
+
+---
+
+## 🚀 Running Locally
+
+### 1. Start the FastAPI Backend
+You will need a [Groq API Key](https://console.groq.com/keys) to run the LLM models.
+
+```bash
+cd backend
+python -m venv venv
+
+# Windows
+.\venv\Scripts\Activate.ps1
+# Mac/Linux
+source venv/bin/activate
+
+pip install -r requirements.txt
+
+# Set up your environment variables
+cp .env.example .env
+# Edit .env and add your GROQ_API_KEY
+
+uvicorn main:app --reload --port 8000
+```
+*The backend API will be running at `http://localhost:8000`*
+
+### 2. Start the Next.js Frontend
+In a new terminal window:
+
+```bash
+cd frontend
+npm install
+
+# Ensure your local environment is pointing to the backend
+cp .env.local.example .env.local
+
+npm run dev
+```
+*The frontend will be running at `http://localhost:3000`*
+
+---
+
+## 📂 Repository Structure
+
+```text
+skillforge-ai/
+├── backend/ # Python FastAPI Backend
+│ ├── agents/ # LangChain/Groq agent definitions
+│ ├── data/ # Sample JDs and Resumes
+│ ├── graph_pipeline/ # LangGraph state machine & nodes
+│ ├── knowledge_graph/ # NetworkX implementation & static graph JSON
+│ ├── models/ # Pydantic data schemas
+│ ├── output/ # Roadmap synthesis logic
+│ ├── routers/ # FastAPI endpoints (Upload, Assess, Roadmap)
+│ └── scoring/ # Algorithmic mismatch and gap scoring
+│
+└── frontend/ # Next.js 14 Frontend
+ ├── app/ # Next.js App Router pages
+ ├── components/ # React components (Upload, Assessment, Results)
+ ├── hooks/ # Custom React hooks (e.g. SSE streaming)
+ └── lib/ # API clients, Zustand store, and TS Types
+```
\ No newline at end of file
diff --git a/backend/agents/__pycache__/challenger.cpython-312.pyc b/backend/agents/__pycache__/challenger.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ae7cecf2b5c65065a908f855664aa84e4313f073
Binary files /dev/null and b/backend/agents/__pycache__/challenger.cpython-312.pyc differ
diff --git a/backend/agents/__pycache__/extractor.cpython-312.pyc b/backend/agents/__pycache__/extractor.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e65aa0594b9ce70b6a8579bcc1c43df7fdae4bdc
Binary files /dev/null and b/backend/agents/__pycache__/extractor.cpython-312.pyc differ
diff --git a/backend/agents/__pycache__/interviewer.cpython-312.pyc b/backend/agents/__pycache__/interviewer.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cb9ca7a1360a1e166b385e1c5c63038d0055c027
Binary files /dev/null and b/backend/agents/__pycache__/interviewer.cpython-312.pyc differ
diff --git a/backend/agents/__pycache__/scorer.cpython-312.pyc b/backend/agents/__pycache__/scorer.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..06ab2d1bfcb5e013a6f671cf19450082bd4f13f0
Binary files /dev/null and b/backend/agents/__pycache__/scorer.cpython-312.pyc differ
diff --git a/backend/agents/challenger.py b/backend/agents/challenger.py
new file mode 100644
index 0000000000000000000000000000000000000000..480752685995149dc79b86ed71c42d92b8ddf494
--- /dev/null
+++ b/backend/agents/challenger.py
@@ -0,0 +1,59 @@
+from langchain_groq import ChatGroq
+from langchain_core.messages import SystemMessage, HumanMessage
+import json, os
+
+GROQ_MODEL = "llama-3.3-70b-versatile"
+
+CHALLENGER_SYSTEM = """You are generating a technical debugging challenge.
+The challenge must:
+1. Be directly relevant to the target skill
+2. Contain exactly ONE bug or design flaw
+3. Be solvable in 3–5 minutes by someone with genuine intermediate knowledge
+4. Include a brief context sentence before the code/scenario
+5. Use markdown with a fenced code block if code is involved
+
+Return ONLY valid JSON (no markdown fences):
+{
+ "challenge_type": "find_the_bug",
+ "skill_id": "docker",
+ "context": "Your teammate pushed this Dockerfile. The container starts but immediately exits.",
+ "task": "```dockerfile\\nFROM python:3.11\\nWORKDIR /app\\nCOPY . .\\nRUN pip install -r requirements.txt\\nCMD uvicorn main:app --host 0.0.0.0 --port 8000\\n```",
+ "expected_answer": "CMD uses shell form — uvicorn won't receive SIGTERM. Fix: CMD [\\"uvicorn\\", \\"main:app\\", \\"--host\\", \\"0.0.0.0\\", \\"--port\\", \\"8000\\"]",
+ "difficulty": "intermediate"
+}"""
+
+def get_llm() -> ChatGroq:
+ return ChatGroq(
+ model=GROQ_MODEL,
+ api_key=os.getenv("GROQ_API_KEY", "dummy"),
+ temperature=0.3,
+ )
+
+def generate_challenge(
+ skill_id: str,
+ graph_path: list[str],
+ resume_context: str,
+) -> dict:
+ llm = get_llm()
+ prompt = f"Skill: {skill_id}\nGraph Path: {graph_path}\nResume context: {resume_context}"
+ messages = [
+ SystemMessage(content=CHALLENGER_SYSTEM),
+ HumanMessage(content=prompt)
+ ]
+ response = llm.invoke(messages)
+ try:
+ content = response.content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.endswith("```"):
+ content = content[:-3]
+ return json.loads(content)
+ except Exception:
+ return {
+ "challenge_type": "find_the_bug",
+ "skill_id": skill_id,
+ "context": "Explain a common bug in this context.",
+ "task": "No task generated",
+ "expected_answer": "Any reasonable explanation",
+ "difficulty": "intermediate"
+ }
diff --git a/backend/agents/extractor.py b/backend/agents/extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..82a8e31bda7d2969a5caddf107024dbc6eda5256
--- /dev/null
+++ b/backend/agents/extractor.py
@@ -0,0 +1,76 @@
+from langchain_groq import ChatGroq
+from langchain_core.messages import SystemMessage, HumanMessage
+import json, os
+from models.schemas import ExtractionResult
+
+GROQ_MODEL = "llama-3.3-70b-versatile"
+
+EXTRACTION_SYSTEM = """You are a skill extraction engine for a technical interview platform.
+Extract structured information from a job description and a candidate resume.
+
+Return ONLY valid JSON matching this exact schema — no markdown fences, no explanation:
+{
+ "jd_skills": [
+ {
+ "skill_id": "python",
+ "label": "Python",
+ "priority": "high",
+ "years_required": 3,
+ "context": "3+ years Python required"
+ }
+ ],
+ "resume_skills": [
+ {
+ "skill_id": "python",
+ "label": "Python",
+ "evidence_strength": 0.7,
+ "years_mentioned": 2,
+ "context": "2 years Python scripting"
+ }
+ ],
+ "seniority_level": "mid",
+ "domain": "backend"
+}
+
+skill_id rules: lowercase, underscores, no spaces. "GitHub Actions" → "github_actions"
+evidence_strength rubric:
+ 0.0 = not mentioned
+ 0.4 = mentioned in passing
+ 0.7 = mentioned with project/years
+ 1.0 = specific metrics or production outcomes
+priority: "high" if required/must-have, "medium" if preferred, "low" if nice-to-have
+domain: one of backend | data_engineering | ml | devops"""
+
+def get_llm() -> ChatGroq:
+ return ChatGroq(
+ model=GROQ_MODEL,
+ api_key=os.getenv("GROQ_API_KEY", "dummy"),
+ temperature=0.1,
+ )
+
+def extract_skills(jd_text: str, resume_text: str) -> ExtractionResult:
+ llm = get_llm()
+ messages = [
+ SystemMessage(content=EXTRACTION_SYSTEM),
+ HumanMessage(content=f"JD: {jd_text}\n\nResume: {resume_text}")
+ ]
+ response = llm.invoke(messages)
+ try:
+ content = response.content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.endswith("```"):
+ content = content[:-3]
+ data = json.loads(content)
+ return ExtractionResult.model_validate(data)
+ except Exception:
+ return ExtractionResult(
+ jd_skills=[],
+ resume_skills=[],
+ seniority_level="junior",
+ domain="backend"
+ )
+
+def normalize_skill_id(label: str) -> str:
+ import re
+ return re.sub(r"[^a-z0-9]+", "_", label.lower()).strip("_")
diff --git a/backend/agents/interviewer.py b/backend/agents/interviewer.py
new file mode 100644
index 0000000000000000000000000000000000000000..4888f5f96d43bf282f0af8fa911ae9cd3a778507
--- /dev/null
+++ b/backend/agents/interviewer.py
@@ -0,0 +1,55 @@
+from langchain_groq import ChatGroq
+from langchain_core.messages import SystemMessage, HumanMessage
+import os
+
+GROQ_MODEL = "llama-3.3-70b-versatile"
+
+INTERVIEWER_SYSTEM = """You are a senior technical interviewer.
+Generate ONE question for the given skill and difficulty level.
+
+Rules:
+- If difficulty is 'conceptual' or 'scenario', MUST generate a Multiple Choice Question (with A, B, C, D options).
+- If difficulty is 'applied' or 'debugging', MUST generate an open-ended descriptive question (NO options).
+- For MCQs, clearly list the options but DO NOT include the correct answer in your output.
+- Reference the candidate's resume context if available
+- Return ONLY the question text (and choices if MCQ) — no numbering, no preamble, and NO answer keys."""
+
+difficulty_ladder = {
+ 1: "conceptual",
+ 2: "applied",
+ 3: "scenario",
+}
+
+def get_llm() -> ChatGroq:
+ return ChatGroq(
+ model=GROQ_MODEL,
+ api_key=os.getenv("GROQ_API_KEY", "dummy"),
+ temperature=0.7,
+ )
+
+def get_difficulty(question_number: int, current_score: float) -> str:
+ if question_number == 1:
+ return "conceptual"
+ if question_number == 2:
+ return "applied" if current_score > 0.5 else "conceptual"
+ if question_number >= 3:
+ return "scenario" if current_score > 0.6 else "applied"
+ return "conceptual"
+
+def generate_question(
+ skill_label: str,
+ resume_context: str,
+ jd_requirement_level: str,
+ current_score: float,
+ question_number: int,
+ previous_questions: list[str],
+) -> str:
+ difficulty = get_difficulty(question_number, current_score)
+ llm = get_llm()
+ prompt = f"Skill: {skill_label}\nDifficulty: {difficulty}\nResume Context: {resume_context}\nPrevious Questions: {previous_questions}"
+ messages = [
+ SystemMessage(content=INTERVIEWER_SYSTEM),
+ HumanMessage(content=prompt)
+ ]
+ response = llm.invoke(messages)
+ return response.content.strip()
diff --git a/backend/agents/scorer.py b/backend/agents/scorer.py
new file mode 100644
index 0000000000000000000000000000000000000000..50a8e848a84224021c997e3a493e2768a6187209
--- /dev/null
+++ b/backend/agents/scorer.py
@@ -0,0 +1,62 @@
+from langchain_groq import ChatGroq
+from langchain_core.messages import SystemMessage, HumanMessage
+import json, os
+
+GROQ_MODEL = "llama-3.3-70b-versatile"
+
+SCORER_SYSTEM = """You are a silent technical interview rubric evaluator.
+Score the candidate answer on a 0.0–1.0 scale.
+
+Rubric:
+0.0–0.2 = No knowledge. Vague or wrong.
+0.3–0.4 = Surface awareness only. No applied depth.
+0.5–0.6 = Working knowledge. Correct but generic.
+0.7–0.8 = Applied knowledge. Specific examples or tradeoffs.
+0.9–1.0 = Expert. Edge cases, alternatives, nuanced reasoning.
+
+Note: If the question was a Multiple Choice Question (MCQ), evaluate if their chosen option is correct. Give 0.8–1.0 for the correct choice (1.0 if they also briefly explain why), and 0.0 for an incorrect choice.
+
+Return ONLY valid JSON (no markdown):
+{
+ "score": 0.72,
+ "signal": "move_on",
+ "reasoning": "Candidate described specific tradeoffs in FastAPI dependency injection."
+}
+
+signal rules:
+ "probe_deeper" → (score < 0.5 OR questions_asked == 1) AND questions_asked < 3
+ "fire_challenge" → score < 0.5 AND questions_asked >= 2
+ "move_on" → (score >= 0.5 AND questions_asked >= 2) OR questions_asked >= 3"""
+
+def get_llm() -> ChatGroq:
+ return ChatGroq(
+ model=GROQ_MODEL,
+ api_key=os.getenv("GROQ_API_KEY", "dummy"),
+ temperature=0.0,
+ )
+
+def score_answer(
+ skill: str,
+ question: str,
+ answer: str,
+ questions_asked: int,
+) -> dict:
+ llm = get_llm()
+ prompt = f"Skill: {skill}\nQuestion: {question}\nAnswer: {answer}\nQuestions asked: {questions_asked}"
+ messages = [
+ SystemMessage(content=SCORER_SYSTEM),
+ HumanMessage(content=prompt)
+ ]
+ response = llm.invoke(messages)
+ try:
+ content = response.content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.endswith("```"):
+ content = content[:-3]
+ return json.loads(content)
+ except Exception:
+ return {"score": 0.0, "signal": "move_on", "reasoning": "Failed to parse response."}
+
+def compute_final_score(resume_evidence: float, conversation_score: float) -> float:
+ return round(0.35 * resume_evidence + 0.65 * conversation_score, 3)
diff --git a/backend/data/sample_jd_resume.json b/backend/data/sample_jd_resume.json
new file mode 100644
index 0000000000000000000000000000000000000000..62715d6cb7fe9120095688ae520886f652cab82d
--- /dev/null
+++ b/backend/data/sample_jd_resume.json
@@ -0,0 +1,4 @@
+{
+ "jd": "Mid-Level Data Engineer. Requirements: 3-5 years of experience in data engineering. Strong SQL skills. Experience with Snowflake and dbt for data transformations. Proficiency in Python for scripting and automation. Familiarity with Airflow for orchestrating data pipelines. Bachelor's degree in Computer Science or related field.",
+ "resume": "Junior Data Engineer passionate about data. I have 1 year of experience working with Python and SQL to build basic reports. I've taken a few courses on Pandas and have touched Airflow in a personal project. Looking for an opportunity to grow."
+}
diff --git a/backend/graph_pipeline/__pycache__/edges.cpython-312.pyc b/backend/graph_pipeline/__pycache__/edges.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..59433ae2c4c55c6e9c03985e71ee112ef6154025
Binary files /dev/null and b/backend/graph_pipeline/__pycache__/edges.cpython-312.pyc differ
diff --git a/backend/graph_pipeline/__pycache__/nodes.cpython-312.pyc b/backend/graph_pipeline/__pycache__/nodes.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ee1231d65dac58be915d608269cf3e5c75d8b338
Binary files /dev/null and b/backend/graph_pipeline/__pycache__/nodes.cpython-312.pyc differ
diff --git a/backend/graph_pipeline/__pycache__/pipeline.cpython-312.pyc b/backend/graph_pipeline/__pycache__/pipeline.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2e8fecf51518b1921a316be9edcacd490070cf3b
Binary files /dev/null and b/backend/graph_pipeline/__pycache__/pipeline.cpython-312.pyc differ
diff --git a/backend/graph_pipeline/__pycache__/state.cpython-312.pyc b/backend/graph_pipeline/__pycache__/state.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..eb27d94255d25621df0d028c8c2dfe68c5363a58
Binary files /dev/null and b/backend/graph_pipeline/__pycache__/state.cpython-312.pyc differ
diff --git a/backend/graph_pipeline/edges.py b/backend/graph_pipeline/edges.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2a4e2c5150d57e69689d972ae6528f37077a0b7
--- /dev/null
+++ b/backend/graph_pipeline/edges.py
@@ -0,0 +1,28 @@
+from graph_pipeline.state import AssessmentState
+
+def route_after_scorer(state: AssessmentState) -> str:
+ records = state.get("question_records", [])
+ if not records:
+ return "output_node"
+
+ last_record = records[-1]
+ signal = last_record.signal
+
+ if signal == "probe_deeper":
+ return "interviewer_node"
+ elif signal == "fire_challenge":
+ return "challenger_node"
+ else:
+ idx = state.get("current_skill_index", 0)
+ gap_skills = state.get("gap_skill_ids", [])
+ if idx < len(gap_skills):
+ return "interviewer_node"
+ else:
+ return "output_node"
+
+def route_after_challenger(state: AssessmentState) -> str:
+ idx = state.get("current_skill_index", 0)
+ gap_skills = state.get("gap_skill_ids", [])
+ if idx < len(gap_skills):
+ return "interviewer_node"
+ return "output_node"
diff --git a/backend/graph_pipeline/nodes.py b/backend/graph_pipeline/nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dcb5cdfcea34331af5c382939e6b6837a54e03e
--- /dev/null
+++ b/backend/graph_pipeline/nodes.py
@@ -0,0 +1,176 @@
+from graph_pipeline.state import AssessmentState
+from agents.extractor import extract_skills
+from agents.interviewer import generate_question, get_difficulty
+from agents.scorer import score_answer, compute_final_score
+from agents.challenger import generate_challenge
+from scoring.claim_vs_reality import detect_mismatch
+from scoring.gap_classifier import classify_gap
+from output.roadmap_generator import generate_roadmap
+from knowledge_graph.graph_engine import get_engine
+from models.schemas import QuestionRecord, SkillScore, ChallengeTask
+
+def extract_node(state: AssessmentState) -> dict:
+ jd_text = state["jd_text"]
+ resume_text = state["resume_text"]
+ extraction = extract_skills(jd_text, resume_text)
+
+ gap_skill_ids = []
+ resume_skills_dict = {s.skill_id: s for s in extraction.resume_skills}
+
+ for jd_skill in extraction.jd_skills:
+ rs = resume_skills_dict.get(jd_skill.skill_id)
+ if not rs or rs.evidence_strength < 0.6:
+ gap_skill_ids.append(jd_skill.skill_id)
+
+ engine = get_engine()
+ graph_paths = {}
+ known_skills = [s.skill_id for s in extraction.resume_skills if s.evidence_strength >= 0.6]
+ if not known_skills:
+ known_skills = ["python"] # fallback
+
+ for gap_skill in gap_skill_ids:
+ path = engine.find_shortest_path(known_skills, gap_skill)
+ if path:
+ graph_paths[gap_skill] = path
+ else:
+ graph_paths[gap_skill] = [gap_skill]
+
+ return {
+ "extraction": extraction,
+ "gap_skill_ids": gap_skill_ids,
+ "graph_paths": graph_paths,
+ "stream_events": [{"event": "extraction_complete", "data": extraction.model_dump()}]
+ }
+
+def interviewer_node(state: AssessmentState) -> dict:
+ idx = state.get("current_skill_index", 0)
+ gap_skills = state.get("gap_skill_ids", [])
+ if idx >= len(gap_skills):
+ return {}
+
+ skill_id = gap_skills[idx]
+ extraction = state["extraction"]
+
+ jd_skill = next((s for s in extraction.jd_skills if s.skill_id == skill_id), None)
+ rs_skill = next((s for s in extraction.resume_skills if s.skill_id == skill_id), None)
+
+ resume_context = rs_skill.context if rs_skill else "No resume context"
+ jd_req_level = jd_skill.priority if jd_skill else "medium"
+
+ questions_asked = state.get("questions_asked", 0)
+ current_score = state.get("conversation_scores", {}).get(skill_id, 0.5)
+
+ previous_questions = [q.question for q in state.get("question_records", []) if q.skill_id == skill_id]
+
+ skill_label = jd_skill.label if jd_skill else skill_id
+ question = generate_question(skill_label, resume_context, jd_req_level, current_score, questions_asked + 1, previous_questions)
+
+ qr = QuestionRecord(skill_id=skill_id, question=question, answer="", score=0.0, signal="move_on")
+
+ return {
+ "stream_events": [{"event": "question", "skill_id": skill_id, "content": question}],
+ "questions_asked": questions_asked + 1,
+ "question_records": [qr]
+ }
+
+def scorer_node(state: AssessmentState) -> dict:
+ records = state.get("question_records", [])
+ if not records:
+ return {}
+
+ last_record = records[-1]
+ pending_answer = state.get("pending_answer", "")
+ questions_asked = state.get("questions_asked", 0)
+
+ score_result = score_answer(last_record.skill_id, last_record.question, pending_answer, questions_asked)
+
+ # Python lists are mutable, we just mutate the record in place
+ last_record.answer = pending_answer
+ last_record.score = score_result.get("score", 0.0)
+ last_record.signal = score_result.get("signal", "move_on")
+
+ conversation_scores = state.get("conversation_scores", {})
+ skill_id = last_record.skill_id
+
+ skill_records = [r for r in records if r.skill_id == skill_id and r.answer]
+ avg_score = sum(r.score for r in skill_records) / len(skill_records) if skill_records else 0.0
+
+ conversation_scores[skill_id] = avg_score
+
+ update = {
+ "conversation_scores": conversation_scores,
+ "pending_answer": None,
+ "stream_events": []
+ }
+
+ if last_record.signal == "move_on":
+ idx = state.get("current_skill_index", 0)
+ update["current_skill_index"] = idx + 1
+ update["questions_asked"] = 0
+
+ return update
+
+def challenger_node(state: AssessmentState) -> dict:
+ idx = state.get("current_skill_index", 0)
+ gap_skills = state.get("gap_skill_ids", [])
+ if idx >= len(gap_skills):
+ return {}
+ skill_id = gap_skills[idx]
+
+ graph_path = state.get("graph_paths", {}).get(skill_id, [])
+ extraction = state["extraction"]
+ rs_skill = next((s for s in extraction.resume_skills if s.skill_id == skill_id), None)
+ resume_context = rs_skill.context if rs_skill else "No resume context"
+
+ challenge = generate_challenge(skill_id, graph_path, resume_context)
+ ct = ChallengeTask(**challenge)
+
+ markdown_task = challenge.get("task", "")
+
+ return {
+ "challenge_task": ct,
+ "stream_events": [{"event": "challenge", "skill_id": skill_id, "content": markdown_task}],
+ "current_skill_index": idx + 1,
+ "questions_asked": 0
+ }
+
+def output_node(state: AssessmentState) -> dict:
+ extraction = state["extraction"]
+ conversation_scores = state.get("conversation_scores", {})
+
+ resume_skills_dict = {s.skill_id: s for s in extraction.resume_skills}
+ skill_scores = []
+
+ for jd_skill in extraction.jd_skills:
+ skill_id = jd_skill.skill_id
+ rs = resume_skills_dict.get(skill_id)
+ resume_evidence = rs.evidence_strength if rs else 0.0
+
+ conv_score = conversation_scores.get(skill_id, resume_evidence)
+ final_score = compute_final_score(resume_evidence, conv_score)
+
+ mismatch_data = detect_mismatch(skill_id, resume_evidence, conv_score)
+ gap_level = classify_gap(final_score)
+
+ skill_scores.append(SkillScore(
+ skill_id=skill_id,
+ label=jd_skill.label,
+ resume_evidence=resume_evidence,
+ conversation_score=conv_score,
+ final_score=final_score,
+ gap_level=gap_level,
+ mismatch=mismatch_data["mismatch"],
+ mismatch_severity=mismatch_data["severity"]
+ ))
+
+ graph_paths = state.get("graph_paths", {})
+ hours_per_day = state.get("hours_per_day", 2.0)
+
+ roadmap = generate_roadmap(skill_scores, graph_paths, hours_per_day)
+
+ return {
+ "skill_scores": skill_scores,
+ "roadmap": roadmap,
+ "assessment_complete": True,
+ "stream_events": [{"event": "assessment_complete", "data": {"status": "done"}}]
+ }
diff --git a/backend/graph_pipeline/pipeline.py b/backend/graph_pipeline/pipeline.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccdd80ac56db539f7a77526c91835030218ae27a
--- /dev/null
+++ b/backend/graph_pipeline/pipeline.py
@@ -0,0 +1,50 @@
+from langgraph.graph import StateGraph, END
+from langgraph.checkpoint.memory import MemorySaver
+from graph_pipeline.state import AssessmentState
+from graph_pipeline.nodes import (
+ extract_node, interviewer_node, scorer_node,
+ challenger_node, output_node
+)
+from graph_pipeline.edges import route_after_scorer, route_after_challenger
+
+def build_pipeline():
+ builder = StateGraph(AssessmentState)
+
+ builder.add_node("extract_node", extract_node)
+ builder.add_node("interviewer_node", interviewer_node)
+ builder.add_node("scorer_node", scorer_node)
+ builder.add_node("challenger_node", challenger_node)
+ builder.add_node("output_node", output_node)
+
+ builder.add_edge("extract_node", "interviewer_node")
+ builder.add_edge("interviewer_node", "scorer_node")
+
+ builder.add_conditional_edges(
+ "scorer_node",
+ route_after_scorer,
+ {
+ "interviewer_node": "interviewer_node",
+ "challenger_node": "challenger_node",
+ "output_node": "output_node"
+ }
+ )
+
+ builder.add_conditional_edges(
+ "challenger_node",
+ route_after_challenger,
+ {
+ "interviewer_node": "interviewer_node",
+ "output_node": "output_node"
+ }
+ )
+
+ builder.add_edge("output_node", END)
+ builder.set_entry_point("extract_node")
+
+ memory = MemorySaver()
+ return builder.compile(
+ checkpointer=memory,
+ interrupt_before=["scorer_node"],
+ )
+
+pipeline = build_pipeline()
diff --git a/backend/graph_pipeline/state.py b/backend/graph_pipeline/state.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ad3dc7be258fac13a1da31f4aa572324ec5b579
--- /dev/null
+++ b/backend/graph_pipeline/state.py
@@ -0,0 +1,30 @@
+from typing import TypedDict, Annotated
+import operator
+from models.schemas import (
+ ExtractionResult, QuestionRecord, ChallengeTask, SkillScore, RoadmapWeek
+)
+
+class AssessmentState(TypedDict):
+ assessment_id: str
+ jd_text: str
+ resume_text: str
+ hours_per_day: float
+
+ extraction: ExtractionResult | None
+ gap_skill_ids: list[str]
+ graph_paths: dict[str, list[str]]
+
+ current_skill_index: int
+ questions_asked: int
+ questions_per_skill: int
+
+ question_records: Annotated[list[QuestionRecord], operator.add]
+ conversation_scores: dict[str, float]
+ challenge_task: ChallengeTask | None
+ pending_answer: str | None
+
+ stream_events: Annotated[list[dict], operator.add]
+
+ skill_scores: list[SkillScore]
+ roadmap: list[RoadmapWeek]
+ assessment_complete: bool
diff --git a/backend/knowledge_graph/__pycache__/graph_engine.cpython-312.pyc b/backend/knowledge_graph/__pycache__/graph_engine.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..61c6869515829d7e833be155793a40328f368cb0
Binary files /dev/null and b/backend/knowledge_graph/__pycache__/graph_engine.cpython-312.pyc differ
diff --git a/backend/knowledge_graph/graph_engine.py b/backend/knowledge_graph/graph_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c794b0c3cf7ad64cace7fafa72963896fd38723
--- /dev/null
+++ b/backend/knowledge_graph/graph_engine.py
@@ -0,0 +1,120 @@
+import networkx as nx
+import json
+import re
+from pathlib import Path
+from typing import List, Dict, Optional
+
+class SkillGraphEngine:
+ def __init__(self, graph_path: str = "backend/knowledge_graph/skills_graph.json"):
+ self.graph = nx.DiGraph()
+ path = Path(graph_path)
+ if path.exists():
+ with open(path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ for node in data.get("nodes", []):
+ self.graph.add_node(
+ node["id"],
+ label=node.get("label", node["id"]),
+ domain=node.get("domain", "unknown"),
+ level=node.get("level", "intermediate"),
+ tags=node.get("tags", [])
+ )
+ for edge in data.get("edges", []):
+ self.graph.add_edge(
+ edge["source"],
+ edge["target"],
+ type=edge.get("type", "PREREQUISITE"),
+ weight=edge.get("weight", 1.0),
+ hop_cost=edge.get("hop_cost", 1)
+ )
+ if edge.get("type") == "COUSIN":
+ # Bidirectional
+ self.graph.add_edge(
+ edge["target"],
+ edge["source"],
+ type=edge.get("type", "COUSIN"),
+ weight=edge.get("weight", 1.0),
+ hop_cost=edge.get("hop_cost", 1)
+ )
+
+ def find_shortest_path(self, source_skills: List[str], target_skill: str, max_hops: int = 4) -> Optional[List[str]]:
+ best_path = None
+ best_cost = float('inf')
+
+ if not self.graph.has_node(target_skill):
+ return None
+
+ for source in source_skills:
+ if not self.graph.has_node(source):
+ continue
+ try:
+ path = nx.shortest_path(self.graph, source=source, target=target_skill, weight="hop_cost")
+ if len(path) - 1 <= max_hops:
+ cost = sum(self.graph[path[i]][path[i+1]]['hop_cost'] for i in range(len(path)-1))
+ if cost < best_cost:
+ best_cost = cost
+ best_path = path
+ except nx.NetworkXNoPath:
+ continue
+ return best_path
+
+ def get_adjacent(self, skill_id: str, hops: int = 2) -> List[Dict]:
+ if not self.graph.has_node(skill_id):
+ return []
+ lengths = nx.single_source_dijkstra_path_length(self.graph, skill_id, cutoff=hops, weight="hop_cost")
+ paths = nx.single_source_dijkstra_path(self.graph, skill_id, cutoff=hops, weight="hop_cost")
+
+ results = []
+ for target, cost in lengths.items():
+ if target == skill_id:
+ continue
+ path = paths[target]
+ edge_types = [self.graph[path[i]][path[i+1]]['type'] for i in range(len(path)-1)]
+ results.append({
+ "skill": target,
+ "path": path,
+ "edge_types": edge_types,
+ "total_cost": float(cost)
+ })
+ results.sort(key=lambda x: x["total_cost"])
+ return results
+
+ def get_prerequisites(self, skill_id: str) -> List[str]:
+ if not self.graph.has_node(skill_id):
+ return []
+ return [p for p in self.graph.predecessors(skill_id) if self.graph[p][skill_id]['type'] == 'PREREQUISITE']
+
+ def get_domain(self, skill_id: str) -> str:
+ if not self.graph.has_node(skill_id):
+ return "unknown"
+ return self.graph.nodes[skill_id].get("domain", "unknown")
+
+ def path_to_steps(self, path: List[str]) -> List[Dict]:
+ steps = []
+ for i in range(len(path) - 1):
+ source = path[i]
+ target = path[i+1]
+ edge_type = self.graph[source][target]['type']
+ weeks = 1.5
+ if edge_type == 'COUSIN':
+ weeks = 0.75
+ elif edge_type == 'BRIDGES':
+ weeks = 2.5
+ steps.append({
+ "from": source,
+ "to": target,
+ "edge_type": edge_type,
+ "weeks": weeks
+ })
+ return steps
+
+ def normalize_skill_id(self, label: str) -> str:
+ return re.sub(r"[^a-z0-9]+", "_", label.lower()).strip("_")
+
+_engine = None
+
+def get_engine() -> SkillGraphEngine:
+ global _engine
+ if _engine is None:
+ _engine = SkillGraphEngine()
+ return _engine
diff --git a/backend/knowledge_graph/resources.json b/backend/knowledge_graph/resources.json
new file mode 100644
index 0000000000000000000000000000000000000000..2dbfddd6be2de78d73e9718300b2f6341457982a
--- /dev/null
+++ b/backend/knowledge_graph/resources.json
@@ -0,0 +1,62 @@
+{
+ "fastapi": {
+ "courses": [
+ {
+ "title": "FastAPI - The Complete Course",
+ "url": "https://www.udemy.com/course/fastapi-the-complete-course/",
+ "hours": 12,
+ "type": "course"
+ }
+ ]
+ },
+ "docker": {
+ "courses": [
+ {
+ "title": "Docker Mastery: with Kubernetes +Swarm from a Docker Captain",
+ "url": "https://www.udemy.com/course/docker-mastery/",
+ "hours": 20,
+ "type": "course"
+ }
+ ]
+ },
+ "dbt": {
+ "courses": [
+ {
+ "title": "Data Engineering with dbt",
+ "url": "https://www.datacamp.com/courses/introduction-to-dbt",
+ "hours": 8,
+ "type": "course"
+ }
+ ]
+ },
+ "airflow": {
+ "courses": [
+ {
+ "title": "Apache Airflow: The Hands-On Guide",
+ "url": "https://www.udemy.com/course/the-complete-hands-on-course-to-master-apache-airflow/",
+ "hours": 10,
+ "type": "course"
+ }
+ ]
+ },
+ "pytorch": {
+ "courses": [
+ {
+ "title": "PyTorch for Deep Learning in 2024: Zero to Mastery",
+ "url": "https://www.udemy.com/course/pytorch-for-deep-learning/",
+ "hours": 25,
+ "type": "course"
+ }
+ ]
+ },
+ "kubernetes": {
+ "courses": [
+ {
+ "title": "Kubernetes for the Absolute Beginners - Hands-on",
+ "url": "https://www.udemy.com/course/learn-kubernetes/",
+ "hours": 15,
+ "type": "course"
+ }
+ ]
+ }
+}
diff --git a/backend/knowledge_graph/skills_graph.json b/backend/knowledge_graph/skills_graph.json
new file mode 100644
index 0000000000000000000000000000000000000000..7075bc7a25754369f16fdbbd8609d4ddeba66b61
--- /dev/null
+++ b/backend/knowledge_graph/skills_graph.json
@@ -0,0 +1,517 @@
+{
+ "nodes": [
+ {
+ "id": "python",
+ "label": "Python",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "fastapi",
+ "label": "Fastapi",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "flask",
+ "label": "Flask",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "django",
+ "label": "Django",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "nodejs",
+ "label": "Nodejs",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "expressjs",
+ "label": "Expressjs",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "rest_api",
+ "label": "Rest Api",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "graphql",
+ "label": "Graphql",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "grpc",
+ "label": "Grpc",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "websockets",
+ "label": "Websockets",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "celery",
+ "label": "Celery",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "redis",
+ "label": "Redis",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "postgresql",
+ "label": "Postgresql",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "mysql",
+ "label": "Mysql",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "mongodb",
+ "label": "Mongodb",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "docker",
+ "label": "Docker",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "kubernetes",
+ "label": "Kubernetes",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "github_actions",
+ "label": "Github Actions",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "git",
+ "label": "Git",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "linux_cli",
+ "label": "Linux Cli",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "nginx",
+ "label": "Nginx",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "jwt_auth",
+ "label": "Jwt Auth",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "pytest",
+ "label": "Pytest",
+ "domain": "backend",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "sql",
+ "label": "Sql",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "dbt",
+ "label": "Dbt",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "airflow",
+ "label": "Airflow",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "prefect",
+ "label": "Prefect",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "spark",
+ "label": "Spark",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "kafka",
+ "label": "Kafka",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "pandas",
+ "label": "Pandas",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "polars",
+ "label": "Polars",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "numpy",
+ "label": "Numpy",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "pyspark",
+ "label": "Pyspark",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "snowflake",
+ "label": "Snowflake",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "bigquery",
+ "label": "Bigquery",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "duckdb",
+ "label": "Duckdb",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "great_expectations",
+ "label": "Great Expectations",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "dagster",
+ "label": "Dagster",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "data_modeling",
+ "label": "Data Modeling",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "etl_pipelines",
+ "label": "Etl Pipelines",
+ "domain": "data_engineering",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "scikit_learn",
+ "label": "Scikit Learn",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "pytorch",
+ "label": "Pytorch",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "tensorflow",
+ "label": "Tensorflow",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "huggingface_transformers",
+ "label": "Huggingface Transformers",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "mlflow",
+ "label": "Mlflow",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "bentoml",
+ "label": "Bentoml",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "onnx",
+ "label": "Onnx",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "feature_engineering",
+ "label": "Feature Engineering",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "model_evaluation",
+ "label": "Model Evaluation",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "hyperparameter_tuning",
+ "label": "Hyperparameter Tuning",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "nlp_basics",
+ "label": "Nlp Basics",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "llm_prompting",
+ "label": "Llm Prompting",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "langchain",
+ "label": "Langchain",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "langgraph",
+ "label": "Langgraph",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "vector_databases",
+ "label": "Vector Databases",
+ "domain": "ml",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "terraform",
+ "label": "Terraform",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "aws_ec2",
+ "label": "Aws Ec2",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "aws_s3",
+ "label": "Aws S3",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "aws_lambda",
+ "label": "Aws Lambda",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "aws_sagemaker",
+ "label": "Aws Sagemaker",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "gcp_vertex",
+ "label": "Gcp Vertex",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "azure_ml",
+ "label": "Azure Ml",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "ci_cd",
+ "label": "Ci Cd",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "prometheus",
+ "label": "Prometheus",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "grafana",
+ "label": "Grafana",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "helm",
+ "label": "Helm",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ },
+ {
+ "id": "ansible",
+ "label": "Ansible",
+ "domain": "devops",
+ "level": "intermediate",
+ "tags": []
+ }
+ ],
+ "edges": [
+ {
+ "source": "python",
+ "target": "fastapi",
+ "type": "PREREQUISITE",
+ "weight": 0.8,
+ "hop_cost": 1
+ },
+ {
+ "source": "python",
+ "target": "pandas",
+ "type": "PREREQUISITE",
+ "weight": 0.8,
+ "hop_cost": 1
+ },
+ {
+ "source": "pandas",
+ "target": "polars",
+ "type": "COUSIN",
+ "weight": 0.9,
+ "hop_cost": 1
+ },
+ {
+ "source": "fastapi",
+ "target": "docker",
+ "type": "BRIDGES",
+ "weight": 0.5,
+ "hop_cost": 2
+ },
+ {
+ "source": "git",
+ "target": "github_actions",
+ "type": "PREREQUISITE",
+ "weight": 0.8,
+ "hop_cost": 1
+ },
+ {
+ "source": "sql",
+ "target": "dbt",
+ "type": "PREREQUISITE",
+ "weight": 0.8,
+ "hop_cost": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..55a6bed5eee008b44f8d0797fe85f6aaf4ee6654
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,27 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from routers import assessment, upload, roadmap
+from dotenv import load_dotenv
+
+import os
+os.environ["LANGGRAPH_STRICT_MSGPACK"] = "false"
+
+load_dotenv()
+
+app = FastAPI(title="SkillForge AI", version="1.0.0")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:3000"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(upload.router)
+app.include_router(assessment.router)
+app.include_router(roadmap.router)
+
+@app.get("/health")
+async def health():
+ return {"status": "ok"}
diff --git a/backend/models/__pycache__/schemas.cpython-312.pyc b/backend/models/__pycache__/schemas.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cb5ea488a3971e8fe40157bac853ff21911ce133
Binary files /dev/null and b/backend/models/__pycache__/schemas.cpython-312.pyc differ
diff --git a/backend/models/schemas.py b/backend/models/schemas.py
new file mode 100644
index 0000000000000000000000000000000000000000..a324026870fcf08b69d303b10f78ee164217e654
--- /dev/null
+++ b/backend/models/schemas.py
@@ -0,0 +1,91 @@
+from pydantic import BaseModel, Field
+from typing import Literal
+
+class JDSkill(BaseModel):
+ skill_id: str
+ label: str
+ priority: Literal["high", "medium", "low"]
+ years_required: int | None
+ context: str
+
+class ResumeSkill(BaseModel):
+ skill_id: str
+ label: str
+ evidence_strength: float = Field(ge=0.0, le=1.0)
+ years_mentioned: int | None
+ context: str
+
+class ExtractionResult(BaseModel):
+ jd_skills: list[JDSkill]
+ resume_skills: list[ResumeSkill]
+ seniority_level: Literal["junior", "mid", "senior", "lead"]
+ domain: Literal["backend", "data_engineering", "ml", "devops"]
+
+class QuestionRecord(BaseModel):
+ skill_id: str
+ question: str
+ answer: str
+ score: float
+ signal: Literal["probe_deeper", "fire_challenge", "move_on"]
+
+class ChallengeTask(BaseModel):
+ challenge_type: Literal["find_the_bug", "design_flaw", "sql_query"]
+ skill_id: str
+ context: str
+ task: str
+ expected_answer: str
+ difficulty: Literal["junior", "intermediate", "senior"]
+
+class SkillScore(BaseModel):
+ skill_id: str
+ label: str
+ resume_evidence: float
+ conversation_score: float
+ final_score: float
+ gap_level: Literal["high_gap", "medium_gap", "ready"]
+ mismatch: bool
+ mismatch_severity: Literal["none", "mild", "significant"]
+
+class RoadmapWeek(BaseModel):
+ week: int
+ skill_id: str
+ label: str
+ tier: Literal[1, 2, 3]
+ resources: list[dict]
+ mini_project: str
+ graph_path: list[str]
+ why: str
+
+class AssessmentResult(BaseModel):
+ assessment_id: str
+ extraction: ExtractionResult
+ skill_scores: list[SkillScore]
+ roadmap: list[RoadmapWeek]
+ time_to_ready_weeks: float
+ domain: str
+
+class UploadResponse(BaseModel):
+ text: str
+ filename: str
+
+class StartAssessmentRequest(BaseModel):
+ jd_text: str
+ resume_text: str
+ hours_per_day: float = 2.0
+ assessment_id: str | None = None
+
+class StartAssessmentResponse(BaseModel):
+ assessment_id: str
+ extraction: ExtractionResult
+ message: str
+
+class StreamEvent(BaseModel):
+ event: Literal[
+ "question", "challenge", "skill_complete",
+ "assessment_complete", "error", "extraction_complete"
+ ]
+ skill_id: str | None = None
+ skill_label: str | None = None
+ content: str | None = None
+ progress: float | None = None
+ data: dict | None = None
diff --git a/backend/output/__pycache__/roadmap_generator.cpython-312.pyc b/backend/output/__pycache__/roadmap_generator.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..59fd609881259911657891a160c15c6ca64ae105
Binary files /dev/null and b/backend/output/__pycache__/roadmap_generator.cpython-312.pyc differ
diff --git a/backend/output/roadmap_generator.py b/backend/output/roadmap_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc5267dcfabefad1b723a6f7f1099dfbd979fe19
--- /dev/null
+++ b/backend/output/roadmap_generator.py
@@ -0,0 +1,89 @@
+import json
+from pathlib import Path
+from models.schemas import RoadmapWeek, SkillScore
+from knowledge_graph.graph_engine import get_engine
+
+def generate_roadmap(
+ skill_scores: list[SkillScore],
+ graph_paths: dict[str, list[str]],
+ hours_per_day: float = 2.0,
+) -> list[RoadmapWeek]:
+ engine = get_engine()
+ resources_path = Path("backend/knowledge_graph/resources.json")
+ if resources_path.exists():
+ with open(resources_path, "r", encoding="utf-8") as f:
+ resources_db = json.load(f)
+ else:
+ resources_db = {}
+
+ roadmap = []
+ base_hours_per_week = hours_per_day * 5
+ week_counter = 1
+
+ # Sort skills: high_gap first, then medium_gap
+ gap_skills = [s for s in skill_scores if s.gap_level in ("high_gap", "medium_gap")]
+ gap_skills.sort(key=lambda s: 0 if s.gap_level == "high_gap" else 1)
+
+ for skill in gap_skills:
+ skill_id = skill.skill_id
+ path = graph_paths.get(skill_id, [])
+ path_str = " → ".join(path)
+
+ resources = []
+ if skill_id in resources_db:
+ resources = resources_db[skill_id].get("courses", [])
+
+ why_msg = "Direct learning path"
+ if len(path) > 1:
+ source = path[0]
+ target = path[-1]
+ hops = len(path) - 1
+ has_edges = True if engine.graph.has_node(source) and engine.graph.has_node(target) else False
+ if has_edges:
+ edge_type = "path"
+ try:
+ if hops == 1:
+ edge_type = engine.graph[source][target]["type"]
+ except:
+ pass
+ why_msg = f"You already know {source} → {target} is {edge_type} ({hops} hop)"
+ else:
+ why_msg = f"Building up from {source} to {target}"
+
+ roadmap.append(RoadmapWeek(
+ week=week_counter,
+ skill_id=skill_id,
+ label=skill.label,
+ tier=1,
+ resources=resources,
+ mini_project=f"Build a basic script using {skill.label} core features",
+ graph_path=path,
+ why=why_msg
+ ))
+ week_counter += 1
+
+ roadmap.append(RoadmapWeek(
+ week=week_counter,
+ skill_id=skill_id,
+ label=skill.label,
+ tier=2,
+ resources=[],
+ mini_project=f"Integrate {skill.label} into a broader project",
+ graph_path=path,
+ why=why_msg
+ ))
+ week_counter += 1
+
+ roadmap.append(RoadmapWeek(
+ week=week_counter,
+ skill_id=skill_id,
+ label=skill.label,
+ tier=3,
+ resources=[],
+ mini_project=f"Solve a role-specific scenario with {skill.label}",
+ graph_path=path,
+ why=why_msg
+ ))
+ week_counter += 1
+
+ return roadmap
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1a0c2370180866b51351961a41194a9481492328
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,12 @@
+fastapi>=0.111.0
+uvicorn[standard]>=0.30.0
+langgraph>=0.1.0
+langchain-groq>=0.1.0
+langchain-core>=0.2.0
+networkx>=3.3
+pdfplumber>=0.11.0
+python-dotenv>=1.0.0
+python-multipart>=0.0.9
+pydantic>=2.7.0
+pytest>=8.0.0
+httpx>=0.27.0
diff --git a/backend/routers/__pycache__/assessment.cpython-312.pyc b/backend/routers/__pycache__/assessment.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9711f7fdb70030e4a6adee4c6f6da5297bec8a9f
Binary files /dev/null and b/backend/routers/__pycache__/assessment.cpython-312.pyc differ
diff --git a/backend/routers/__pycache__/roadmap.cpython-312.pyc b/backend/routers/__pycache__/roadmap.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e797edffe8a86a8de38451fcaf036df79514c0ef
Binary files /dev/null and b/backend/routers/__pycache__/roadmap.cpython-312.pyc differ
diff --git a/backend/routers/__pycache__/upload.cpython-312.pyc b/backend/routers/__pycache__/upload.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4ad44f0d5021891a6dcc1023c7b001b4850d40fe
Binary files /dev/null and b/backend/routers/__pycache__/upload.cpython-312.pyc differ
diff --git a/backend/routers/assessment.py b/backend/routers/assessment.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1ad8400b2db9337a087e07239b7790e260c79f9
--- /dev/null
+++ b/backend/routers/assessment.py
@@ -0,0 +1,99 @@
+from fastapi import APIRouter, HTTPException
+from fastapi.responses import StreamingResponse
+from models.schemas import (
+ StartAssessmentRequest, StartAssessmentResponse, StreamEvent
+)
+from graph_pipeline.pipeline import pipeline
+from graph_pipeline.state import AssessmentState
+import uuid, json, asyncio
+from typing import Dict
+
+router = APIRouter(prefix="/assess", tags=["assessment"])
+
+active_assessments: Dict[str, dict] = {}
+
+@router.post("/", response_model=StartAssessmentResponse)
+async def start_assessment(body: StartAssessmentRequest):
+ assessment_id = body.assessment_id if body.assessment_id else str(uuid.uuid4())
+
+ config = {"configurable": {"thread_id": assessment_id}}
+ active_assessments[assessment_id] = config
+
+ state = AssessmentState(
+ assessment_id=assessment_id,
+ jd_text=body.jd_text,
+ resume_text=body.resume_text,
+ hours_per_day=body.hours_per_day,
+ extraction=None,
+ gap_skill_ids=[],
+ graph_paths={},
+ current_skill_index=0,
+ questions_asked=0,
+ questions_per_skill=3,
+ question_records=[],
+ conversation_scores={},
+ challenge_task=None,
+ pending_answer=None,
+ stream_events=[],
+ skill_scores=[],
+ roadmap=[],
+ assessment_complete=False
+ )
+
+ pipeline.invoke(state, config)
+
+ current_state = pipeline.get_state(config)
+ st = current_state.values
+
+ extraction = st.get("extraction")
+
+ return StartAssessmentResponse(
+ assessment_id=assessment_id,
+ extraction=extraction,
+ message="Assessment started"
+ )
+
+@router.post("/{assessment_id}/answer")
+async def submit_answer(assessment_id: str, answer: str):
+ if assessment_id not in active_assessments:
+ raise HTTPException(status_code=404, detail="Assessment not found")
+
+ config = active_assessments[assessment_id]
+
+ pipeline.update_state(config, {"pending_answer": answer})
+ pipeline.invoke(None, config)
+
+ return {"status": "ok"}
+
+@router.get("/{assessment_id}/stream")
+async def stream_events(assessment_id: str):
+ if assessment_id not in active_assessments:
+ raise HTTPException(status_code=404, detail="Assessment not found")
+
+ config = active_assessments[assessment_id]
+
+ async def event_generator():
+ last_event_idx = 0
+ while True:
+ current_state = pipeline.get_state(config)
+ st = current_state.values
+ if not st:
+ await asyncio.sleep(0.2)
+ continue
+
+ events = st.get("stream_events", [])
+ while last_event_idx < len(events):
+ event = events[last_event_idx]
+ yield f"data: {json.dumps(event)}\n\n"
+ last_event_idx += 1
+
+ if st.get("assessment_complete"):
+ break
+
+ await asyncio.sleep(0.2)
+
+ return StreamingResponse(
+ event_generator(),
+ media_type="text/event-stream",
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
+ )
diff --git a/backend/routers/roadmap.py b/backend/routers/roadmap.py
new file mode 100644
index 0000000000000000000000000000000000000000..40f6fc312c83e80bfec4c961da8ff57bbb4dcc52
--- /dev/null
+++ b/backend/routers/roadmap.py
@@ -0,0 +1,34 @@
+from fastapi import APIRouter, HTTPException
+from models.schemas import AssessmentResult
+from graph_pipeline.pipeline import pipeline
+from routers.assessment import active_assessments
+from output.roadmap_generator import generate_roadmap
+
+router = APIRouter(prefix="/roadmap", tags=["roadmap"])
+
+@router.get("/{assessment_id}", response_model=AssessmentResult)
+async def get_roadmap(assessment_id: str, hours_per_day: float = 2.0):
+ if assessment_id not in active_assessments:
+ raise HTTPException(status_code=404, detail="Assessment not found")
+
+ config = active_assessments[assessment_id]
+ current_state = pipeline.get_state(config)
+ st = current_state.values
+
+ if not st.get("assessment_complete"):
+ raise HTTPException(status_code=400, detail="Assessment not complete")
+
+ roadmap = generate_roadmap(
+ st.get("skill_scores", []),
+ st.get("graph_paths", {}),
+ hours_per_day
+ )
+
+ return AssessmentResult(
+ assessment_id=assessment_id,
+ extraction=st["extraction"],
+ skill_scores=st.get("skill_scores", []),
+ roadmap=roadmap,
+ time_to_ready_weeks=len(roadmap),
+ domain=st["extraction"].domain
+ )
diff --git a/backend/routers/upload.py b/backend/routers/upload.py
new file mode 100644
index 0000000000000000000000000000000000000000..47e6ee4f4672ccfb8ca4cad596fd14c847113454
--- /dev/null
+++ b/backend/routers/upload.py
@@ -0,0 +1,24 @@
+from fastapi import APIRouter, UploadFile, File, HTTPException
+from models.schemas import UploadResponse
+import pdfplumber, io
+
+router = APIRouter(prefix="/upload", tags=["upload"])
+
+def extract_text_from_upload(file: UploadFile) -> str:
+ content = file.file.read()
+ if file.filename.endswith(".pdf"):
+ with pdfplumber.open(io.BytesIO(content)) as pdf:
+ text = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
+ return text
+ else:
+ return content.decode("utf-8", errors="ignore")
+
+@router.post("/jd", response_model=UploadResponse)
+async def upload_jd(file: UploadFile = File(...)):
+ text = extract_text_from_upload(file)
+ return UploadResponse(text=text, filename=file.filename)
+
+@router.post("/resume", response_model=UploadResponse)
+async def upload_resume(file: UploadFile = File(...)):
+ text = extract_text_from_upload(file)
+ return UploadResponse(text=text, filename=file.filename)
diff --git a/backend/scoring/__pycache__/claim_vs_reality.cpython-312.pyc b/backend/scoring/__pycache__/claim_vs_reality.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..baf0a58a2dfe1dc82a7258f447524889f4872c54
Binary files /dev/null and b/backend/scoring/__pycache__/claim_vs_reality.cpython-312.pyc differ
diff --git a/backend/scoring/__pycache__/gap_classifier.cpython-312.pyc b/backend/scoring/__pycache__/gap_classifier.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0ba3eae6d5ec635ad23b738d3508d979493c81bf
Binary files /dev/null and b/backend/scoring/__pycache__/gap_classifier.cpython-312.pyc differ
diff --git a/backend/scoring/claim_vs_reality.py b/backend/scoring/claim_vs_reality.py
new file mode 100644
index 0000000000000000000000000000000000000000..cffc4642b70d70997f3bf63c0c5906415f2fbc90
--- /dev/null
+++ b/backend/scoring/claim_vs_reality.py
@@ -0,0 +1,26 @@
+def detect_mismatch(
+ skill_id: str,
+ resume_evidence: float,
+ conversation_score: float,
+ threshold: float = 0.3,
+) -> dict:
+ delta = resume_evidence - conversation_score
+ mismatch = delta > threshold
+
+ if delta < 0.3:
+ severity = "none"
+ label = "Verified"
+ elif delta <= 0.5:
+ severity = "mild"
+ label = "Slight overstatement"
+ else:
+ severity = "significant"
+ label = "Significant inflation detected"
+
+ return {
+ "skill_id": skill_id,
+ "mismatch": mismatch,
+ "delta": round(delta, 3),
+ "severity": severity,
+ "label": label
+ }
diff --git a/backend/scoring/gap_classifier.py b/backend/scoring/gap_classifier.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf4b0d18bd6055581daf9d94eec0c05cb43ee950
--- /dev/null
+++ b/backend/scoring/gap_classifier.py
@@ -0,0 +1,7 @@
+def classify_gap(final_score: float) -> str:
+ if final_score < 0.3:
+ return "high_gap"
+ elif final_score < 0.6:
+ return "medium_gap"
+ else:
+ return "ready"
diff --git a/frontend/app/assess/[id]/page.tsx b/frontend/app/assess/[id]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..18cb6f49d4c67e9c3fcbe818e42e470443c370e7
--- /dev/null
+++ b/frontend/app/assess/[id]/page.tsx
@@ -0,0 +1,99 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useParams, useRouter } from "next/navigation"
+import { useStore } from "@/lib/store"
+import { submitAnswer } from "@/lib/api"
+import { useAssessmentStream } from "@/hooks/useAssessmentStream"
+import ChatInterface from "@/components/assessment/ChatInterface"
+import ProgressBar from "@/components/assessment/ProgressBar"
+
+export default function AssessPage() {
+ const params = useParams()
+ const id = typeof params.id === "string" ? params.id : null
+ const router = useRouter()
+ const [mounted, setMounted] = useState(false)
+ useEffect(() => setMounted(true), [])
+
+ const { isConnected, error, isComplete } = useAssessmentStream(id)
+
+ const { messages, extraction, addMessage, currentSkillId } = useStore()
+ const [isWaiting, setIsWaiting] = useState(false)
+
+ const handleSend = async (answer: string) => {
+ if (!id) return
+ setIsWaiting(true)
+ addMessage({
+ id: Math.random().toString(36).substr(2, 9),
+ role: "user",
+ content: answer,
+ timestamp: new Date()
+ })
+ try {
+ await submitAnswer(id, answer)
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ useEffect(() => {
+ const lastMsg = messages[messages.length - 1]
+ if (lastMsg && (lastMsg.role === "agent" || lastMsg.role === "challenge")) {
+ setIsWaiting(false)
+ }
+ }, [messages])
+
+ useEffect(() => {
+ if (isComplete) {
+ router.push(`/results/${id}`)
+ }
+ }, [isComplete, id, router])
+
+ const jdSkills = extraction?.jd_skills || []
+ const resumeSkills = extraction?.resume_skills || []
+
+ const gapSkills = jdSkills.filter(jd => {
+ const rs = resumeSkills.find(r => r.skill_id === jd.skill_id)
+ return !rs || rs.evidence_strength < 0.6
+ })
+
+ const currentIdx = currentSkillId ? gapSkills.findIndex(s => s.skill_id === currentSkillId) : 0
+ const progressPct = gapSkills.length > 0 ? ((currentIdx) / gapSkills.length) * 100 : 0
+
+ if (!mounted) return
+
+ return (
+
+
+
SkillForge Assessment
+
+
+
+
+
Skills to verify
+ {gapSkills.map((s, idx) => (
+
+
+ {s.label}
+ {idx < currentIdx && Complete }
+ {s.skill_id === currentSkillId && }
+
+
+ ))}
+
+
+
+
+ {error ? (
+
+ {error}. window.location.reload()} className="ml-4 underline">Retry
+ router.push(`/results/${id}`)} className="ml-4 underline text-accent">Go to Results
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/frontend/app/candidate/job/[id]/page.tsx b/frontend/app/candidate/job/[id]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e180bca2bac26c92b929633eb986932e346796d1
--- /dev/null
+++ b/frontend/app/candidate/job/[id]/page.tsx
@@ -0,0 +1,129 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useParams, useRouter } from "next/navigation"
+import { createClient } from "@/utils/supabase/client"
+import { useStore } from "@/lib/store"
+import { startAssessment } from "@/lib/api"
+import UploadZone from "@/components/upload/UploadZone"
+
+export default function CandidateJobPage() {
+ const params = useParams()
+ const id = typeof params.id === "string" ? params.id : null
+ const router = useRouter()
+ const supabase = createClient()
+
+ const { hoursPerDay, setHoursPerDay, setJdText, resumeText, setResumeText, setAssessmentId, setExtraction } = useStore()
+
+ const [job, setJob] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [starting, setStarting] = useState(false)
+ const [error, setError] = useState("")
+
+ useEffect(() => {
+ if (!id) return
+ fetchJob()
+ }, [id])
+
+ const fetchJob = async () => {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+
+ const { data } = await supabase.from('jobs').select('*, profiles(email)').eq('id', id).single()
+ if (data) {
+ setJob(data)
+ setJdText(data.jd_text)
+ }
+ setLoading(false)
+ }
+
+ const handleBegin = async () => {
+ if (!job || !resumeText) return
+ setStarting(true)
+ setError("")
+
+ try {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) throw new Error("Not authenticated")
+
+ // 1. Create Assessment Row in Supabase
+ const { data: assessment, error: dbError } = await supabase.from('assessments').insert({
+ job_id: job.id,
+ candidate_id: user.id,
+ resume_text: resumeText,
+ status: 'in_progress'
+ }).select().single()
+
+ if (dbError) throw dbError
+
+ // 2. Start Python Backend Assessment with that same Supabase ID
+ const res = await startAssessment(job.jd_text, resumeText, hoursPerDay, assessment.id)
+
+ // 3. Save to Zustand store and navigate
+ setAssessmentId(assessment.id)
+ setExtraction(res.extraction)
+ router.push(`/assess/${assessment.id}`)
+
+ } catch (err: any) {
+ setError(err.message)
+ setStarting(false)
+ }
+ }
+
+ if (loading || !job) return Loading Job...
+
+ return (
+
+
+
router.push('/candidate')} className="text-sm font-mono text-muted hover:text-accent transition-colors">
+ ← Back to Portal
+
+
+
+
+
+
+
+
+ Apply & Test
+
+
+ Upload your resume below to begin. Our AI will instantly evaluate your resume against this job description and conduct a dynamic technical interview.
+
+
+
+
+
+
+
+
+
+ Availability (hours/day)
+ {hoursPerDay}h
+
+ setHoursPerDay(parseFloat(e.target.value))}
+ className="w-full h-1.5 bg-background rounded-lg appearance-none cursor-pointer accent-accent transition-all hover:h-2"
+ />
+
+
+ {error &&
{error}
}
+
+
+ {starting ? "Initializing Engine..." : "Begin AI Interview →"}
+
+
+
+
+ )
+}
diff --git a/frontend/app/candidate/page.tsx b/frontend/app/candidate/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b69f865bc77872cfa110246c8b57a45be969fe12
--- /dev/null
+++ b/frontend/app/candidate/page.tsx
@@ -0,0 +1,83 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useRouter } from "next/navigation"
+import { createClient } from "@/utils/supabase/client"
+
+export default function CandidateDashboard() {
+ const router = useRouter()
+ const supabase = createClient()
+ const [jobs, setJobs] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ fetchJobs()
+ }, [])
+
+ const fetchJobs = async () => {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
+ if (profile?.role !== 'candidate') {
+ router.push('/employer')
+ return
+ }
+
+ // Fetch all jobs
+ const { data } = await supabase.from('jobs').select('*, profiles(email)').order('created_at', { ascending: false })
+ if (data) setJobs(data)
+ setLoading(false)
+ }
+
+ const handleLogout = async () => {
+ await supabase.auth.signOut()
+ router.push('/')
+ }
+
+ if (loading) return Loading Opportunities...
+
+ return (
+
+
+
+
+
+ Open Positions
+
+ {jobs.length === 0 ? (
+
+ No open positions at the moment. Check back soon!
+
+ ) : (
+
+ {jobs.map(job => (
+
+
+
{job.title}
+
Posted by: {job.profiles?.email}
+
{new Date(job.created_at).toLocaleDateString()}
+
+
router.push(`/candidate/job/${job.id}`)}
+ className="bg-accent/10 text-accent border border-accent/30 px-6 py-2 rounded-lg font-mono font-bold hover:bg-accent hover:text-background transition-all"
+ >
+ Apply & Assess →
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/app/employer/job/[id]/page.tsx b/frontend/app/employer/job/[id]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b4dbb9e2f2d3fd0f54552175f32194077e673e57
--- /dev/null
+++ b/frontend/app/employer/job/[id]/page.tsx
@@ -0,0 +1,118 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useParams, useRouter } from "next/navigation"
+import { createClient } from "@/utils/supabase/client"
+
+export default function EmployerJobPage() {
+ const params = useParams()
+ const id = typeof params.id === "string" ? params.id : null
+ const router = useRouter()
+ const supabase = createClient()
+
+ const [job, setJob] = useState(null)
+ const [assessments, setAssessments] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ if (!id) return
+ fetchJobAndCandidates()
+ }, [id])
+
+ const fetchJobAndCandidates = async () => {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+
+ // Fetch job details
+ const { data: jobData } = await supabase.from('jobs').select('*').eq('id', id).single()
+ if (jobData) setJob(jobData)
+
+ // Fetch assessments for this job
+ const { data: assessmentData } = await supabase.from('assessments')
+ .select('id, status, created_at, result_data, profiles(email)')
+ .eq('job_id', id)
+
+ if (assessmentData) {
+ const mapped = assessmentData.map((a: any) => {
+ let score = 0;
+ if (a.result_data && a.result_data.skill_scores && a.result_data.skill_scores.length > 0) {
+ score = Math.round((a.result_data.skill_scores.reduce((sum: number, s: any) => sum + s.final_score, 0) / a.result_data.skill_scores.length) * 100)
+ }
+ return { ...a, score }
+ })
+ // Sort by score descending
+ mapped.sort((a, b) => b.score - a.score)
+ setAssessments(mapped)
+ }
+
+ setLoading(false)
+ }
+
+ if (loading || !job) return Loading Job Details...
+
+ return (
+
+
+
router.push('/employer')} className="text-sm font-mono text-muted hover:text-accent transition-colors">
+ ← Back to Dashboard
+
+
+
+
+
+ Candidate Assessments
+
+ {assessments.length === 0 ? (
+
+ No candidates have applied to this job yet.
+
+ ) : (
+
+ {assessments.map((assessment) => (
+
+
+
{assessment.profiles?.email}
+
+
+ {assessment.status === 'completed' ? 'COMPLETED' : 'IN PROGRESS'}
+
+ {assessment.status === 'completed' && (
+
+ SCORE: {assessment.score}/100
+
+ )}
+ {new Date(assessment.created_at).toLocaleString()}
+
+
+
+
router.push(`/results/${assessment.id}`)}
+ className="bg-background border border-border text-text px-6 py-2 rounded-lg font-mono font-bold hover:border-accent hover:text-accent transition-all disabled:opacity-50 disabled:hover:border-border disabled:hover:text-text"
+ >
+ {assessment.status === 'completed' ? "View Results →" : "Awaiting Completion"}
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/app/employer/page.tsx b/frontend/app/employer/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7933fb1106c27603d2c67bb84b0a41445dababdd
--- /dev/null
+++ b/frontend/app/employer/page.tsx
@@ -0,0 +1,136 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useRouter } from "next/navigation"
+import { createClient } from "@/utils/supabase/client"
+import UploadZone from "@/components/upload/UploadZone"
+
+export default function EmployerDashboard() {
+ const router = useRouter()
+ const supabase = createClient()
+ const [jobs, setJobs] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [jdText, setJdText] = useState("")
+ const [jobTitle, setJobTitle] = useState("")
+ const [creating, setCreating] = useState(false)
+
+ useEffect(() => {
+ fetchJobs()
+ }, [])
+
+ const fetchJobs = async () => {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
+ if (profile?.role !== 'employer') {
+ router.push('/candidate')
+ return
+ }
+
+ const { data } = await supabase.from('jobs').select('*').order('created_at', { ascending: false })
+ if (data) setJobs(data)
+ setLoading(false)
+ }
+
+ const handleCreateJob = async () => {
+ if (!jobTitle || !jdText) return
+ setCreating(true)
+
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return
+
+ const { error } = await supabase.from('jobs').insert({
+ employer_id: user.id,
+ title: jobTitle,
+ jd_text: jdText
+ })
+
+ setCreating(false)
+ if (!error) {
+ setJobTitle("")
+ setJdText("")
+ fetchJobs()
+ }
+ }
+
+ const handleLogout = async () => {
+ await supabase.auth.signOut()
+ router.push('/')
+ }
+
+ if (loading) return Loading Dashboard...
+
+ return (
+
+
+
+
+
+
+
+
Post New Job
+
+
+
+ Job Title
+ setJobTitle(e.target.value)}
+ className="w-full bg-background border border-border/50 rounded-xl p-3 text-sm focus:border-accent outline-none"
+ placeholder="e.g. Senior Frontend Engineer"
+ />
+
+
+
+
+
+
+
+ {creating ? "Posting..." : "Create Job"}
+
+
+
+
+
+
+
Your Active Jobs
+
+ {jobs.length === 0 ? (
+
+ No jobs posted yet. Post your first job to start receiving candidates!
+
+ ) : (
+
+ {jobs.map(job => (
+
router.push(`/employer/job/${job.id}`)}>
+
+
{job.title}
+
Posted {new Date(job.created_at).toLocaleDateString()}
+
+
+ View Candidates →
+
+
+ ))}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
new file mode 100644
index 0000000000000000000000000000000000000000..03b1b205d68aa3ce5509d0a8b76f4c32283d8ddc
--- /dev/null
+++ b/frontend/app/globals.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --bg: #030014;
+ --surface: rgba(13, 10, 31, 0.7);
+ --border: #221c46;
+ --accent: #00f0ff;
+ --text: #ffffff;
+ --muted: #8e8b9f;
+}
+
+body {
+ background-color: var(--bg);
+ color: var(--text);
+ background-image:
+ radial-gradient(circle at 15% 0%, rgba(112, 0, 255, 0.15) 0%, transparent 40%),
+ radial-gradient(circle at 85% 100%, rgba(0, 240, 255, 0.15) 0%, transparent 40%);
+ background-attachment: fixed;
+ min-height: 100vh;
+}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..83f416698c69631220ec5a83d209525fc93a1a0d
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,22 @@
+import { IBM_Plex_Mono, IBM_Plex_Sans } from "next/font/google"
+import type { Metadata } from "next"
+import "./globals.css"
+
+const plexMono = IBM_Plex_Mono({ subsets: ["latin"], weight: ["400", "500", "700"], variable: "--font-mono" })
+const plexSans = IBM_Plex_Sans({ subsets: ["latin"], weight: ["400", "500", "700"], variable: "--font-sans" })
+
+export const metadata: Metadata = {
+ title: "SkillForge AI",
+ description: "From resume claims to real capability.",
+ colorScheme: "dark",
+}
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e744dea58d042759c0ce3490a18690c8014a7cad
--- /dev/null
+++ b/frontend/app/login/page.tsx
@@ -0,0 +1,151 @@
+"use client"
+import { useState } from "react"
+import { useRouter } from "next/navigation"
+import { createClient } from "@/utils/supabase/client"
+
+export default function LoginPage() {
+ const router = useRouter()
+ const supabase = createClient()
+
+ const [isLogin, setIsLogin] = useState(true)
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [role, setRole] = useState("candidate")
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState("")
+
+ const handleAuth = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ setError("")
+
+ // Strict check for allowed email domains
+ const allowedDomains = ["@gmail.com", "@yahoo.com", "@outlook.com", "@hotmail.com", "@icloud.com"]
+ const hasValidDomain = allowedDomains.some(domain => email.toLowerCase().endsWith(domain))
+
+ if (!hasValidDomain) {
+ setError("Please use a standard email provider (e.g., @gmail.com, @yahoo.com)")
+ setLoading(false)
+ return
+ }
+
+ if (isLogin) {
+ const { data, error } = await supabase.auth.signInWithPassword({ email, password })
+ if (error) {
+ setError(error.message)
+ setLoading(false)
+ return
+ }
+
+ // Fetch role
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', data.user.id).single()
+ if (profile?.role === 'employer') {
+ router.push('/employer')
+ } else {
+ router.push('/candidate')
+ }
+ } else {
+ const { data, error } = await supabase.auth.signUp({ email, password })
+ if (error) {
+ setError(error.message)
+ setLoading(false)
+ return
+ }
+ if (data.user) {
+ const { error: profileError } = await supabase.from('profiles').insert({
+ id: data.user.id,
+ email,
+ role
+ })
+ if (profileError) {
+ setError(profileError.message)
+ setLoading(false)
+ return
+ }
+ if (role === 'employer') {
+ router.push('/employer')
+ } else {
+ router.push('/candidate')
+ }
+ }
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ SkillForge.ai
+
+
{isLogin ? "Access your portal" : "Create your account"}
+
+ {error &&
{error}
}
+
+
+
+
+ {isLogin ? "Don't have an account? " : "Already have an account? "}
+ setIsLogin(!isLogin)} className="text-accent hover:underline">
+ {isLogin ? "Sign Up" : "Log In"}
+
+
+
+
+ )
+}
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..53d5ad61d6afbc9455309f7f4fae680d0ab0ae20
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,43 @@
+"use client"
+import { useRouter } from "next/navigation"
+
+export default function LandingPage() {
+ const router = useRouter()
+
+ return (
+
+
+
+
+
SkillForge.ai
+
+ The next-generation technical assessment platform. Move beyond basic resume parsing and test real capability.
+
+
+
+
+
For Employers
+
Post jobs, set requirements, and let AI conduct the first-round technical interviews for you.
+
router.push('/login')}
+ className="w-full bg-background border border-border text-text px-6 py-3 rounded-xl font-mono hover:border-accent hover:text-accent transition-colors"
+ >
+ Employer Portal →
+
+
+
+
+
For Candidates
+
Apply to open roles, take interactive AI-driven technical assessments, and prove your skills.
+
router.push('/login')}
+ className="w-full bg-accent text-background px-6 py-3 rounded-xl font-mono font-bold hover:scale-[1.02] shadow-[0_0_20px_rgba(0,240,255,0.2)] transition-all"
+ >
+ Candidate Portal →
+
+
+
+
+
+ )
+}
diff --git a/frontend/app/results/[id]/page.tsx b/frontend/app/results/[id]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a1589021afbf73a7c78ca7079297718a8967a47
--- /dev/null
+++ b/frontend/app/results/[id]/page.tsx
@@ -0,0 +1,195 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useParams, useRouter } from "next/navigation"
+import { useStore } from "@/lib/store"
+import { getRoadmap } from "@/lib/api"
+import { createClient } from "@/utils/supabase/client"
+import SkillHeatmap from "@/components/results/SkillHeatmap"
+import RoadmapTimeline from "@/components/results/RoadmapTimeline"
+import GraphPathViz from "@/components/results/GraphPathViz"
+
+export default function ResultsPage() {
+ const params = useParams()
+ const id = typeof params.id === "string" ? params.id : null
+ const router = useRouter()
+ const { hoursPerDay, setHoursPerDay, result, setResult, resetAssessment } = useStore()
+ const [mounted, setMounted] = useState(false)
+ const [error, setError] = useState("")
+ const [resumeText, setResumeText] = useState("")
+
+ useEffect(() => {
+ setMounted(true)
+ if (!id) return
+
+ const loadResults = async () => {
+ const supabase = createClient()
+
+ // 1. Try to load completed result from Supabase
+ const { data: assessment } = await supabase.from('assessments').select('status, result_data, resume_text').eq('id', id).single()
+
+ if (assessment) {
+ setResumeText(assessment.resume_text || "")
+ if (assessment.status === 'completed' && assessment.result_data) {
+ setResult(assessment.result_data)
+ return
+ }
+ }
+
+ // 2. If not completed or not in Supabase, try to fetch from Python backend (live computation)
+ try {
+ const res = await getRoadmap(id, hoursPerDay)
+ setResult(res)
+
+ // 3. Save the newly computed result to Supabase
+ await supabase.from('assessments').update({
+ status: 'completed',
+ result_data: res
+ }).eq('id', id)
+ } catch (err) {
+ console.error("Failed to load roadmap:", err)
+ setError("This assessment session was interrupted or wiped from memory before completion.")
+ }
+ }
+
+ loadResults()
+ }, [id, hoursPerDay, setResult, router])
+
+ if (error) {
+ return (
+
+
Session Expired
+
{error}
+
router.back()} className="text-accent border border-accent/50 px-6 py-2 rounded-lg hover:bg-accent/10">Go Back
+
+ )
+ }
+
+ if (!mounted || !result) {
+ return (
+
+ Compiling Results...
+
+ }
+
+ const overallScore = result?.skill_scores?.length > 0
+ ? Math.round((result.skill_scores.reduce((sum: number, s: any) => sum + s.final_score, 0) / result.skill_scores.length) * 100)
+ : 0;
+
+ return (
+
+ {/* Background Gradients */}
+
+
+
+
+ {/* Header */}
+
+
+
+ Assessment Results
+
+
+
+ Profile: {result.extraction.seniority_level} {result.extraction.domain}
+
+
+
+
+
+ Availability
+ {hoursPerDay}h/day
+
+ setHoursPerDay(parseFloat(e.target.value))}
+ className="w-full h-1.5 bg-background rounded-lg appearance-none cursor-pointer accent-accent transition-all hover:h-2"
+ />
+
+
+
+ {/* Score Overview */}
+
+ Overall Capability Score
+
+
+
+
+
+
+ {overallScore}
+ / 100
+
+
+
+
+ {/* Skill Map */}
+
+
+
+ Skill Map
+ Performance Overview
+
+
+
+
+ {/* Learning Path */}
+ {result.roadmap.filter(r => r.tier === 1).length > 0 && (
+
+
+
+ Prerequisite Pathways
+
+
+ {result.roadmap.filter(r => r.tier === 1).map(r => (
+
+ ))}
+
+
+ )}
+
+ {/* Roadmap */}
+
+
+
+ Personalized Roadmap
+
+
+
+
+ {/* Resume View */}
+ {resumeText && (
+
+
+
+ Candidate Resume
+
+
+ {resumeText}
+
+
+ )}
+
+
+ {
+ resetAssessment()
+ router.push('/')
+ }}
+ className="group relative inline-flex items-center justify-center px-8 py-4 font-mono font-bold text-background bg-accent rounded-xl overflow-hidden transition-all hover:scale-105 shadow-[0_0_20px_rgba(0,240,255,0.2)] hover:shadow-[0_0_40px_rgba(0,240,255,0.4)]"
+ >
+
+ Start New Assessment
+ →
+
+
+
+
+
+ )
+}
diff --git a/frontend/components/assessment/ChatInterface.tsx b/frontend/components/assessment/ChatInterface.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8dc6da8b57c88e0f12d94505b602d611a1b3e38b
--- /dev/null
+++ b/frontend/components/assessment/ChatInterface.tsx
@@ -0,0 +1,142 @@
+"use client"
+import { useState, useRef, useEffect } from "react"
+import type { ChatMessage } from "@/lib/types"
+import ReactMarkdown from "react-markdown"
+
+interface Props {
+ messages: ChatMessage[]
+ onSend: (answer: string) => void
+ isWaiting: boolean
+}
+
+export default function ChatInterface({ messages, onSend, isWaiting }: Props) {
+ const [input, setInput] = useState("")
+ const endRef = useRef(null)
+
+ useEffect(() => {
+ endRef.current?.scrollIntoView({ behavior: "smooth" })
+ }, [messages])
+
+ const handleSend = () => {
+ if (!input.trim() || isWaiting) return
+ onSend(input)
+ setInput("")
+ }
+
+ const renderAgentMessage = (content: string) => {
+ const lines = content.split('\n')
+ const options: string[] = []
+ const mainText: string[] = []
+
+ const optionRegex = /^[-*]?\s*([A-D])[\)\.]\s+(.*)/i;
+
+ for (const line of lines) {
+ const match = line.trim().match(optionRegex)
+ if (match) {
+ options.push(line.trim())
+ } else {
+ mainText.push(line)
+ }
+ }
+
+ return (
+
+
+ {mainText.join('\n')}
+
+ {options.length > 0 && (
+
+ {options.map((opt, i) => {
+ const cleanOpt = opt.replace(/^[-*]?\s*/, '')
+ const match = cleanOpt.match(/^([A-D])[\)\.]/i)
+ const letter = match ? match[1] : cleanOpt
+ return (
+ {
+ if (!isWaiting) onSend(letter.toUpperCase())
+ }}
+ disabled={isWaiting}
+ className="text-left p-3 rounded-lg border border-border bg-surface hover:border-accent hover:bg-accent/5 transition-colors text-sm font-mono text-text disabled:opacity-50 disabled:cursor-not-allowed text-wrap break-words"
+ >
+ {cleanOpt}
+
+ )
+ })}
+
+ )}
+
+ )
+ }
+
+ return (
+
+
+ {messages.map((m) => (
+
+ {m.role === "agent" && (
+
SF
+ )}
+
+ {m.role === "challenge" ? (
+
+
Challenge
+
+ {m.content}
+
+
Submit your answer below
+
+ ) : m.role === "system" ? (
+
{m.content}
+ ) : m.role === "user" ? (
+
+ {m.content}
+
+ ) : (
+
+ {renderAgentMessage(m.content)}
+
+ )}
+
+ ))}
+ {isWaiting && (
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/frontend/components/assessment/ProgressBar.tsx b/frontend/components/assessment/ProgressBar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3160340d551242b7754638d5b0bb8101c2f2a2e4
--- /dev/null
+++ b/frontend/components/assessment/ProgressBar.tsx
@@ -0,0 +1,20 @@
+interface Props {
+ current: number
+ total: number
+ label: string
+}
+
+export default function ProgressBar({ current, total, label }: Props) {
+ const pct = total > 0 ? (current / total) * 100 : 0
+ return (
+
+
+ {label}
+ {current}/{total}
+
+
+
+ )
+}
diff --git a/frontend/components/results/GraphPathViz.tsx b/frontend/components/results/GraphPathViz.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5ecc178b1c7a034f63349dab94d88f8f1e5fdb6b
--- /dev/null
+++ b/frontend/components/results/GraphPathViz.tsx
@@ -0,0 +1,39 @@
+import type { FC } from "react"
+import { ArrowRight } from "lucide-react"
+
+interface Props {
+ path: string[]
+ edgeTypes: string[]
+}
+
+const GraphPathViz: FC = ({ path, edgeTypes }) => {
+ if (!path || path.length === 0) return null
+
+ return (
+
+ {path.map((node, idx) => (
+
+
+ {node}
+
+ {idx < path.length - 1 && (
+
+ )}
+
+ ))}
+
+ )
+}
+
+export default GraphPathViz
diff --git a/frontend/components/results/MismatchBadge.tsx b/frontend/components/results/MismatchBadge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8639600b4b2e0b54b8d1270ce2150655d2343881
--- /dev/null
+++ b/frontend/components/results/MismatchBadge.tsx
@@ -0,0 +1,20 @@
+import { AlertTriangle, XCircle } from "lucide-react"
+
+export default function MismatchBadge({ severity }: { severity: "mild" | "significant" | "none" }) {
+ if (severity === "none") return null
+
+ if (severity === "mild") {
+ return (
+
+
+ Slight overstatement
+
+ )
+ }
+ return (
+
+
+ Inflation detected
+
+ )
+}
diff --git a/frontend/components/results/RoadmapTimeline.tsx b/frontend/components/results/RoadmapTimeline.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..590034735e05950530fd0bf2bc314d8b53b50fff
--- /dev/null
+++ b/frontend/components/results/RoadmapTimeline.tsx
@@ -0,0 +1,52 @@
+import type { RoadmapWeek } from "@/lib/types"
+import { BookOpen, Video, Code, Box } from "lucide-react"
+import GraphPathViz from "./GraphPathViz"
+
+export default function RoadmapTimeline({ roadmap }: { roadmap: RoadmapWeek[] }) {
+ return (
+
+ {roadmap.map((item, idx) => (
+
+
+ {item.week}
+
+
+
+
{item.label}
+
+ Tier {item.tier}
+
+
+
+
"{item.why}"
+
+
+
+
+
+ {item.resources && item.resources.length > 0 && (
+
+ )}
+
+
+
Mini Project
+
+
+ {item.mini_project}
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/frontend/components/results/SkillHeatmap.tsx b/frontend/components/results/SkillHeatmap.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f295789a68946ac5574a58d4eae5a95f974b7187
--- /dev/null
+++ b/frontend/components/results/SkillHeatmap.tsx
@@ -0,0 +1,60 @@
+import type { SkillScore } from "@/lib/types"
+import MismatchBadge from "./MismatchBadge"
+
+interface Props {
+ skillScores: SkillScore[]
+}
+
+export default function SkillHeatmap({ skillScores }: Props) {
+ const sorted = [...skillScores].sort((a, b) => {
+ const order = { high_gap: 0, medium_gap: 1, ready: 2 }
+ return order[a.gap_level] - order[b.gap_level]
+ })
+
+ return (
+
+ {sorted.map((s) => (
+
+ {s.mismatch && (
+
+
+
+ )}
+
{s.label}
+
+
+
+
+ Claimed
+ {Math.round(s.resume_evidence * 100)}%
+
+
+
+
+
+
+ Assessed
+ {Math.round(s.conversation_score * 100)}%
+
+
+
+
+
+
+
+ {s.gap_level === "ready" ? "READY" : s.gap_level === "medium_gap" ? "UPSKILL" : "HIGH GAP"}
+
+
+
+ ))}
+
+ )
+}
diff --git a/frontend/components/upload/SkillPreviewTable.tsx b/frontend/components/upload/SkillPreviewTable.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5309a49a18517bcc9a277c4648d2f4f2463515de
--- /dev/null
+++ b/frontend/components/upload/SkillPreviewTable.tsx
@@ -0,0 +1,17 @@
+import type { ExtractionResult } from "@/lib/types"
+
+export default function SkillPreviewTable({ extraction }: { extraction: ExtractionResult }) {
+ return (
+
+
Extracted Profile
+
+
+ Domain: {extraction.domain}
+
+
+ Level: {extraction.seniority_level}
+
+
+
+ )
+}
diff --git a/frontend/components/upload/UploadZone.tsx b/frontend/components/upload/UploadZone.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6c94102325fdfe56183f8d6ca140338d7545b47c
--- /dev/null
+++ b/frontend/components/upload/UploadZone.tsx
@@ -0,0 +1,75 @@
+"use client"
+import { useState, useRef } from "react"
+import { uploadFile } from "@/lib/api"
+import { UploadCloud, CheckCircle, AlertCircle } from "lucide-react"
+
+interface Props {
+ label: string
+ endpoint: "jd" | "resume"
+ onUpload: (text: string) => void
+}
+
+export default function UploadZone({ label, endpoint, onUpload }: Props) {
+ const [state, setState] = useState<"idle" | "dragging" | "uploading" | "done" | "error">("idle")
+ const [filename, setFilename] = useState("")
+ const [errorMsg, setErrorMsg] = useState("")
+ const fileInputRef = useRef(null)
+
+ const handleFile = async (file: File) => {
+ setState("uploading")
+ try {
+ const res = await uploadFile(endpoint, file)
+ setFilename(res.filename)
+ onUpload(res.text)
+ setState("done")
+ } catch (err: any) {
+ setErrorMsg(err.message)
+ setState("error")
+ }
+ }
+
+ const onDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ setState("idle")
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ handleFile(e.dataTransfer.files[0])
+ }
+ }
+
+ return (
+ { e.preventDefault(); setState("dragging") }}
+ onDragLeave={() => setState("idle")}
+ onDrop={onDrop}
+ onClick={() => fileInputRef.current?.click()}
+ >
+
{
+ if (e.target.files?.[0]) handleFile(e.target.files[0])
+ }} />
+
+ {state === "idle" || state === "dragging" ? (
+ <>
+
+
{label}
+
Drop PDF or .txt here
+ >
+ ) : state === "uploading" ? (
+
Extracting text...
+ ) : state === "done" ? (
+ <>
+
+
{filename}
+
Click to replace
+ >
+ ) : (
+ <>
+
+
{errorMsg}
+ >
+ )}
+
+ )
+}
diff --git a/frontend/hooks/useAssessmentStream.ts b/frontend/hooks/useAssessmentStream.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19f838b93c3ae10d20a0a85741624d7ff86a113d
--- /dev/null
+++ b/frontend/hooks/useAssessmentStream.ts
@@ -0,0 +1,35 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useStore } from "@/lib/store"
+import { getStreamUrl } from "@/lib/api"
+import type { SSEEvent } from "@/lib/types"
+
+export function useAssessmentStream(assessmentId: string | null) {
+ const handleSSEEvent = useStore((s) => s.handleSSEEvent)
+ const [isConnected, setIsConnected] = useState(false)
+ const [error, setError] = useState(null)
+ const [isComplete, setIsComplete] = useState(false)
+
+ useEffect(() => {
+ if (!assessmentId) return
+ const es = new EventSource(getStreamUrl(assessmentId))
+ es.onopen = () => setIsConnected(true)
+ es.onmessage = (e) => {
+ try {
+ const event: SSEEvent = JSON.parse(e.data)
+ handleSSEEvent(event)
+ if (event.event === "assessment_complete") {
+ setIsComplete(true)
+ es.close()
+ }
+ } catch (err) {}
+ }
+ es.onerror = () => {
+ setError("Stream disconnected")
+ es.close()
+ }
+ return () => es.close()
+ }, [assessmentId, handleSSEEvent])
+
+ return { isConnected, error, isComplete }
+}
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c2ceb66ce6a03703eda02aec92998930eac47b00
--- /dev/null
+++ b/frontend/lib/api.ts
@@ -0,0 +1,55 @@
+import { ExtractionResult, AssessmentResult } from "./types"
+
+const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"
+
+export async function uploadFile(
+ endpoint: "jd" | "resume",
+ file: File
+): Promise<{ text: string; filename: string }> {
+ const formData = new FormData()
+ formData.append("file", file)
+ const res = await fetch(`${BASE}/upload/${endpoint}`, {
+ method: "POST",
+ body: formData,
+ })
+ if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`)
+ return res.json()
+}
+
+export async function startAssessment(
+ jdText: string,
+ resumeText: string,
+ hoursPerDay: number,
+ assessmentId?: string
+): Promise<{ assessment_id: string; extraction: ExtractionResult }> {
+ const res = await fetch(`${BASE}/assess/`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ jd_text: jdText, resume_text: resumeText, hours_per_day: hoursPerDay, assessment_id: assessmentId })
+ })
+ if (!res.ok) throw new Error(`Failed to start assessment: ${res.statusText}`)
+ return res.json()
+}
+
+export async function submitAnswer(
+ assessmentId: string,
+ answer: string
+): Promise {
+ const res = await fetch(`${BASE}/assess/${assessmentId}/answer?answer=${encodeURIComponent(answer)}`, {
+ method: "POST",
+ })
+ if (!res.ok) throw new Error(`Failed to submit answer: ${res.statusText}`)
+}
+
+export async function getRoadmap(
+ assessmentId: string,
+ hoursPerDay: number
+): Promise {
+ const res = await fetch(`${BASE}/roadmap/${assessmentId}?hours_per_day=${hoursPerDay}`)
+ if (!res.ok) throw new Error(`Failed to fetch roadmap: ${res.statusText}`)
+ return res.json()
+}
+
+export function getStreamUrl(assessmentId: string): string {
+ return `${BASE}/assess/${assessmentId}/stream`
+}
diff --git a/frontend/lib/store.ts b/frontend/lib/store.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3895605017679da35c50876f35c5ffd8d990b957
--- /dev/null
+++ b/frontend/lib/store.ts
@@ -0,0 +1,88 @@
+import { create } from "zustand"
+import { persist, createJSONStorage } from "zustand/middleware"
+import type {
+ ExtractionResult, AssessmentResult, ChatMessage, SSEEvent
+} from "./types"
+
+interface SkillForgeStore {
+ jdText: string
+ resumeText: string
+ hoursPerDay: number
+ setJdText: (t: string) => void
+ setResumeText: (t: string) => void
+ setHoursPerDay: (h: number) => void
+
+ assessmentId: string | null
+ extraction: ExtractionResult | null
+ messages: ChatMessage[]
+ currentSkillId: string | null
+ progress: number
+ isAssessing: boolean
+ setAssessmentId: (id: string) => void
+ setExtraction: (e: ExtractionResult) => void
+ addMessage: (m: ChatMessage) => void
+ handleSSEEvent: (e: SSEEvent) => void
+
+ result: AssessmentResult | null
+ setResult: (r: AssessmentResult) => void
+ resetAssessment: () => void
+}
+
+export const useStore = create()(
+ persist(
+ (set, get) => ({
+ jdText: "",
+ resumeText: "",
+ hoursPerDay: 2,
+ setJdText: (t) => set({ jdText: t }),
+ setResumeText: (t) => set({ resumeText: t }),
+ setHoursPerDay: (h) => set({ hoursPerDay: h }),
+
+ assessmentId: null,
+ extraction: null,
+ messages: [],
+ currentSkillId: null,
+ progress: 0,
+ isAssessing: false,
+
+ setAssessmentId: (id) => set({
+ assessmentId: id,
+ isAssessing: true,
+ messages: [],
+ currentSkillId: null,
+ result: null
+ }),
+ setExtraction: (e) => set({ extraction: e }),
+ addMessage: (m) => set((state) => ({ messages: [...state.messages, m] })),
+
+ handleSSEEvent: (e) => {
+ if (e.event === "question" || e.event === "challenge") {
+ set({ currentSkillId: e.skill_id })
+ get().addMessage({
+ id: Math.random().toString(36).substr(2, 9),
+ role: e.event === "challenge" ? "challenge" : "agent",
+ content: e.content || "",
+ skill_id: e.skill_id,
+ timestamp: new Date()
+ })
+ }
+ },
+
+ result: null,
+ setResult: (r) => set({ result: r, isAssessing: false }),
+ resetAssessment: () => set({
+ assessmentId: null,
+ extraction: null,
+ messages: [],
+ currentSkillId: null,
+ progress: 0,
+ isAssessing: false,
+ result: null
+ }),
+ }),
+ {
+ name: 'skillforge-storage',
+ storage: createJSONStorage(() => localStorage),
+ }
+ )
+)
diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..577e541d84638babec1dcffe054e4b82ba3fd459
--- /dev/null
+++ b/frontend/lib/types.ts
@@ -0,0 +1,83 @@
+export type GapLevel = "high_gap" | "medium_gap" | "ready"
+export type MismatchSeverity = "none" | "mild" | "significant"
+export type Domain = "backend" | "data_engineering" | "ml" | "devops"
+export type Priority = "high" | "medium" | "low"
+
+export interface JDSkill {
+ skill_id: string
+ label: string
+ priority: Priority
+ years_required: number | null
+ context: string
+}
+
+export interface ResumeSkill {
+ skill_id: string
+ label: string
+ evidence_strength: number
+ years_mentioned: number | null
+ context: string
+}
+
+export interface ExtractionResult {
+ jd_skills: JDSkill[]
+ resume_skills: ResumeSkill[]
+ seniority_level: string
+ domain: Domain
+}
+
+export interface SkillScore {
+ skill_id: string
+ label: string
+ resume_evidence: number
+ conversation_score: number
+ final_score: number
+ gap_level: GapLevel
+ mismatch: boolean
+ mismatch_severity: MismatchSeverity
+}
+
+export interface RoadmapWeek {
+ week: number
+ skill_id: string
+ label: string
+ tier: 1 | 2 | 3
+ resources: { title: string; url: string; hours: number; type: string }[]
+ mini_project: string
+ graph_path: string[]
+ why: string
+}
+
+export interface AssessmentResult {
+ assessment_id: string
+ extraction: ExtractionResult
+ skill_scores: SkillScore[]
+ roadmap: RoadmapWeek[]
+ time_to_ready_weeks: number
+ domain: Domain
+}
+
+export type SSEEventType =
+ | "question"
+ | "challenge"
+ | "skill_complete"
+ | "assessment_complete"
+ | "error"
+ | "extraction_complete"
+
+export interface SSEEvent {
+ event: SSEEventType
+ skill_id?: string
+ skill_label?: string
+ content?: string
+ progress?: number
+ data?: any
+}
+
+export interface ChatMessage {
+ id: string
+ role: "agent" | "user" | "system" | "challenge"
+ content: string
+ skill_id?: string
+ timestamp: Date
+}
diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4f11a03dc6cc37f2b5105c08f2e7b24c603ab2f4
--- /dev/null
+++ b/frontend/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..e09fdf0f3d59f89910075e6937a8cdafb9aaa67c
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2989 @@
+{
+ "name": "skillforge-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "skillforge-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "@supabase/ssr": "^0.10.2",
+ "@supabase/supabase-js": "^2.104.1",
+ "@tailwindcss/typography": "^0.5.19",
+ "lucide-react": "^0.383.0",
+ "next": "14.2.0",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-markdown": "^9.0.0",
+ "swr": "^2.2.0",
+ "zustand": "^4.5.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "autoprefixer": "^10",
+ "postcss": "^8",
+ "tailwindcss": "^3.4.0",
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@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==",
+ "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==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.0.tgz",
+ "integrity": "sha512-4+70ELtSbRtYUuyRpAJmKC8NHBW2x1HMje9KO2Xd7IkoyucmV9SjgO+qeWMC0JWkRQXgydv1O7yKOK8nu/rITQ==",
+ "license": "MIT"
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.0.tgz",
+ "integrity": "sha512-kHktLlw0AceuDnkVljJ/4lTJagLzDiO3klR1Fzl2APDFZ8r+aTxNaNcPmpp0xLMkgRwwk6sggYeqq0Rz9K4zzA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.0.tgz",
+ "integrity": "sha512-HFSDu7lb1U3RDxXNeKH3NGRR5KyTPBSUTuIOr9jXoAso7i76gNYvnTjbuzGVWt2X5izpH908gmOYWtI7un+JrA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.0.tgz",
+ "integrity": "sha512-iQsoWziO5ZMxDWZ4ZTCAc7hbJ1C9UDj/gATSqTaMjW2bJFwAsvf9UM79AKnljBl73uPZ+V0kH4rvnHTco4Ps2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.0.tgz",
+ "integrity": "sha512-0JOk2uzLUt8fJK5LpsKKZa74zAch7bJjjgJzR9aOMs231AlE4gPYzsSm430ckZitjPGKeH5bgDZjqwqJQKIS2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.0.tgz",
+ "integrity": "sha512-uYHkuTzX0NM6biKNp7hdKTf+BF0iMV254SxO0B8PgrQkxUBKGmk5ysHKB+FYBfdf9xei/t8OIKlXJs9ckD943A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.0.tgz",
+ "integrity": "sha512-paN89nLs2dTBDtfXWty1/NVPit+q6ldwdktixYSVwiiAz647QDCd+EIYqoiS+/rPG3oXs/A7rWcJK9HVqfnMVg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.0.tgz",
+ "integrity": "sha512-j1oiidZisnymYjawFqEfeGNcE22ZQ7lGUaa4pGOCVWrWeIDkPSj8zYgS9TzMNlg17Q3wSWCQC/F5uJAhSh7qcA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.0.tgz",
+ "integrity": "sha512-6ff6F4xb+QGD1jhx/dOT9Ot7PQ/GAYekV9ykwEh2EFS/cLTyU4Y3cXkX5cNtNIhpctS5NvyjW9gIksRNErYE0A==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.0.tgz",
+ "integrity": "sha512-09DbG5vXAxz0eTFSf1uebWD36GF3D5toynRkgo2AlSrxwGZkWtJ1RhmrczRYQ17eD5bdo4FZ0ibiffdq5kc4vg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@supabase/auth-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.104.1.tgz",
+ "integrity": "sha512-pqFnDKekq1isqlqnzqzyJ3mzmho+o+FjfVTqhKY3PFlwj2anx3OPznO1kbo1ZEwD8zg1r4EAFf/7pplLyX0ocQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.104.1.tgz",
+ "integrity": "sha512-JjAH4JN9rZzxh4plQnILPrQZXAG6ccoRS6z9hQAGmXpRSwJA+7CWbsDV2R82I8MROlGDsjqj1Ot/cWpTfdf6xg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz",
+ "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.104.1.tgz",
+ "integrity": "sha512-RqlLpvgXsjcc27fLyHNGm3zN0KDWXbkdTdaFtaEdX83RsTEqH7BAmshH7zoUMml5lL04naUeRjS3B81O6jZcJw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.104.1.tgz",
+ "integrity": "sha512-dVJHhFB2ErBd0/2qE9G8CedCrGoAtBfL9Q4zbSMXO7b1Cpld916ljSiX21mURUqijPf1WoPQG4Bp/averUzk/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.0",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/ssr": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.2.tgz",
+ "integrity": "sha512-JFbchN63CXLFHJRNT7udec4/RoD9PmXkSGko3QSO6vUuqGBtSzdmxR7FPfQNr7SuFd65I7Xv46q66ALjEN1cgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.2"
+ },
+ "peerDependencies": {
+ "@supabase/supabase-js": "^2.102.1"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.104.1.tgz",
+ "integrity": "sha512-2bQaLbkRshctkUVuqamwYZDEd+0cGSc9DY9sjh92DcA5hu1F/1AP8p6gxGr76sgdK9Ngi0rh+2Kdh+uC4hcnGA==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.104.1",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.104.1.tgz",
+ "integrity": "sha512-E0H/CtVmaGjiAy+ieZ5ZB/1EqxXcGdaFaAc23AE5zaYfz6NtCNDcmaEdoGPYMPFH5pE6drGG6e3ljPmkFoGVxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.104.1",
+ "@supabase/functions-js": "2.104.1",
+ "@supabase/postgrest-js": "2.104.1",
+ "@supabase/realtime-js": "2.104.1",
+ "@supabase/storage-js": "2.104.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
+ "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+ }
+ },
+ "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.39",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.21",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
+ "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "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": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001790",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
+ "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
+ "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/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.383.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.383.0.tgz",
+ "integrity": "sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/next": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.0.tgz",
+ "integrity": "sha512-2T41HqJdKPqheR27ll7MFZ3gtTYvGew7cUc0PwPSyK9Ao5vvwpf9bYfP4V5YBGLckHF2kEGvrLte5BqLSv0s8g==",
+ "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "14.2.0",
+ "@swc/helpers": "0.5.5",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001579",
+ "graceful-fs": "^4.2.11",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=18.17.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "14.2.0",
+ "@next/swc-darwin-x64": "14.2.0",
+ "@next/swc-linux-arm64-gnu": "14.2.0",
+ "@next/swc-linux-arm64-musl": "14.2.0",
+ "@next/swc-linux-x64-gnu": "14.2.0",
+ "@next/swc-linux-x64-musl": "14.2.0",
+ "@next/swc-win32-arm64-msvc": "14.2.0",
+ "@next/swc-win32-ia32-msvc": "14.2.0",
+ "@next/swc-win32-x64-msvc": "14.2.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.41.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "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.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "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/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "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==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swr": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
+ "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "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/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..17a7107f61eb98d0c74d1e9ba30983dde8dae1a2
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "skillforge-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@supabase/ssr": "^0.10.2",
+ "@supabase/supabase-js": "^2.104.1",
+ "@tailwindcss/typography": "^0.5.19",
+ "lucide-react": "^0.383.0",
+ "next": "14.2.0",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-markdown": "^9.0.0",
+ "swr": "^2.2.0",
+ "zustand": "^4.5.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "autoprefixer": "^10",
+ "postcss": "^8",
+ "tailwindcss": "^3.4.0",
+ "typescript": "^5"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..33ad091d26d8a9dc95ebdf616e217d985ec215b8
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..308430256231768efd33e7da1cb39ee0627c7cc0
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,26 @@
+import type { Config } from "tailwindcss"
+
+const config: Config = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ background: "var(--bg)",
+ surface: "var(--surface)",
+ border: "var(--border)",
+ accent: "var(--accent)",
+ text: "var(--text)",
+ muted: "var(--muted)",
+ },
+ },
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
+}
+
+export default config
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..afb79176c7687e6329e5959ca435ca024564622a
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/frontend/utils/supabase/client.ts b/frontend/utils/supabase/client.ts
new file mode 100644
index 0000000000000000000000000000000000000000..891134bc161ad079551c3d5d9b2c544b64141987
--- /dev/null
+++ b/frontend/utils/supabase/client.ts
@@ -0,0 +1,10 @@
+import { createBrowserClient } from "@supabase/ssr";
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
+
+export const createClient = () =>
+ createBrowserClient(
+ supabaseUrl!,
+ supabaseKey!,
+ );
diff --git a/frontend/utils/supabase/middleware.ts b/frontend/utils/supabase/middleware.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3866faa56f67960c6100536a3d0def04d310c597
--- /dev/null
+++ b/frontend/utils/supabase/middleware.ts
@@ -0,0 +1,37 @@
+import { createServerClient } from "@supabase/ssr";
+import { type NextRequest, NextResponse } from "next/server";
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
+
+export const createClient = (request: NextRequest) => {
+ // Create an unmodified response
+ let supabaseResponse = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+
+ const supabase = createServerClient(
+ supabaseUrl!,
+ supabaseKey!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
+ supabaseResponse = NextResponse.next({
+ request,
+ })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ )
+ },
+ },
+ },
+ );
+
+ return supabaseResponse
+};
diff --git a/frontend/utils/supabase/server.ts b/frontend/utils/supabase/server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..43d0aa04f94ff8e7ce40ab4a29c04f44b2ba667c
--- /dev/null
+++ b/frontend/utils/supabase/server.ts
@@ -0,0 +1,28 @@
+import { createServerClient } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
+
+export const createClient = (cookieStore: Awaited>) => {
+ return createServerClient(
+ supabaseUrl!,
+ supabaseKey!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll()
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
+ } catch {
+ // The `setAll` method was called from a Server Component.
+ // This can be ignored if you have middleware refreshing
+ // user sessions.
+ }
+ },
+ },
+ },
+ );
+};