diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..b8c8ece9cad180b5b427a60df007e90e5e271c25 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Gemini API key (required for repository analysis, chat, and semantic search) +GEMINI_API_KEY=your_gemini_api_key_here + +# Optional: comma-separated CORS origins (default: *) +# CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +# Frontend (local dev) — leave empty to use Vite proxy +# VITE_API_URL= +# VITE_API_PROXY=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ec2bfd72a8f454d2a40465cc1d86424b6aca4e49 Binary files /dev/null and b/.gitignore differ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e9fb8b425573138b6bd6e76d071560f0d6ed7d5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# --- Stage 1: Build Frontend --- +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# --- Stage 2: Serve Backend & Frontend --- +FROM python:3.11-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy backend requirements and install +COPY backend/requirements.txt ./backend/ +RUN pip install --no-cache-dir -r backend/requirements.txt + +# Copy backend source +COPY backend/ ./backend/ + +# Copy compiled frontend build +COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist + +# Expose port (Hugging Face Spaces requires port 7860) +EXPOSE 7860 + +# Run FastAPI from the backend directory so imports resolve locally +WORKDIR /app/backend +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/README.md b/README.md index 2b0fc88d0f7c7a6d5053026e58b66af7c61d2ca2..660378db2bf47ecd3fbed5e4ab21beec653b90cd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,221 @@ --- -title: Software Engineer Agent -emoji: 🐠 -colorFrom: pink -colorTo: yellow +title: Repository Intelligence Layer +emoji: 🔍 +colorFrom: green +colorTo: blue sdk: docker +app_port: 7860 pinned: false +license: mit --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Repository Intelligence Layer + +AI-powered repository analysis platform that clones or uploads codebases, generates structured intelligence artifacts with **Gemini 2.5 Flash**, and provides an interactive dashboard with graph visualization, semantic search, and multi-agent chat. + +![Dashboard Screenshot](./docs/screenshots/dashboard.png) + + +## Features + +- **GitHub cloning** — public and private repos (PAT authentication) +- **ZIP upload** — drag-and-drop repository archives +- **Repository scanner** — file tree, manifest parsing, static profiling +- **Graph builder** — static import/dependency graph + LLM architecture graph +- **Gemini analysis** — markdown report, profile, summary, and graph JSON +- **Interactive dashboard** — report, summary, profile, graph viewer, repo tree +- **AI Assistant** — multi-agent orchestration with RAG context +- **Knowledge Explorer** — ChromaDB semantic search and conversation history +- **Download endpoints** — export all intelligence artifacts +- **Persistent memory** — artifacts saved to disk; ChromaDB vector index + +## Architecture + +```mermaid +flowchart LR + subgraph Input + URL[GitHub URL + PAT] + ZIP[ZIP Upload] + end + subgraph Pipeline + Scan[Repository Scanner] + Profile[Repository Profiler] + Graph[Graph Builder] + LLM[Gemini Analyzer] + Mem[Memory Layer] + Chroma[ChromaDB Index] + end + subgraph Frontend + Tree[Repo Tree] + Dash[Dashboard] + GraphV[Graph Viewer] + Chat[AI Assistant] + end + URL --> Scan + ZIP --> Scan + Scan --> Profile + Profile --> Graph + Graph --> LLM + LLM --> Mem + LLM --> Chroma + Mem --> Dash + Scan --> Tree + Mem --> GraphV + Chroma --> Chat +``` + +## Project Structure + +``` +├── backend/ # FastAPI application +│ ├── main.py # API routes +│ ├── services/ # Scanner, profiler, graph builder, LLM, memory +│ ├── agents/ # Multi-agent orchestration +│ ├── memory/ # ChromaDB, RAG, conversations +│ └── tools/ # MCP-ready tool registry +├── frontend/ # React + Vite dashboard +├── Dockerfile # Unified build for Hugging Face Spaces (port 7860) +└── docker-compose.yml # Local split-stack development +``` + +## Installation + +### Prerequisites + +- Python 3.11+ +- Node.js 20+ +- Git (for repository cloning) +- Gemini API key from [Google AI Studio](https://aistudio.google.com/) + +### Local Setup + +1. **Clone the repository** + +```bash +git clone +cd "Software Engineer Agent" +``` + +2. **Configure environment** + +```bash +cp .env.example backend/.env +# Edit backend/.env and set GEMINI_API_KEY +``` + +3. **Install backend dependencies** + +```bash +cd backend +pip install -r requirements.txt +``` + +4. **Install frontend dependencies** + +```bash +cd ../frontend +npm install +``` + +5. **Run locally (two terminals)** + +Terminal 1 — Backend: +```bash +cd backend +uvicorn main:app --reload --host 127.0.0.1 --port 8000 +``` + +Terminal 2 — Frontend: +```bash +cd frontend +npm run dev +``` + +Open **http://localhost:5173** — the Vite dev server proxies `/api` to the backend. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `GEMINI_API_KEY` | Yes | Google Gemini API key for analysis, chat, and embeddings | +| `CORS_ORIGINS` | No | Comma-separated allowed origins (default: `*`) | +| `VITE_API_URL` | No | Frontend API base URL (empty = same origin / Vite proxy) | +| `VITE_API_PROXY` | No | Vite dev proxy target (default: `http://localhost:8000`) | + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/health` | Health check | +| `POST` | `/api/analyze-url` | Clone and analyze a GitHub repository | +| `POST` | `/api/analyze-zip` | Upload and analyze a ZIP archive | +| `GET` | `/api/download/{repo_id}/{type}` | Download artifact (`profile`, `graph`, `summary`, `report`) | +| `POST` | `/api/chat` | Multi-agent chat with RAG | +| `POST` | `/api/search` | Semantic search over indexed knowledge | +| `GET` | `/api/memory?repo_id=` | Vector index statistics | +| `GET` | `/api/conversations?repo_id=` | List chat sessions | +| `GET` | `/api/conversations/{session_id}` | Session message history | +| `GET` | `/api/tools` | Tool catalog | + +## Docker + +### Unified (Hugging Face / production) + +```bash +docker build -t repo-intelligence . +docker run -p 7860:7860 -e GEMINI_API_KEY=your_key repo-intelligence +``` + +Open **http://localhost:7860** + +### Split stack (development) + +```bash +export GEMINI_API_KEY=your_key +docker compose up --build +``` + +- Frontend: **http://localhost:5173** +- Backend: **http://localhost:8000** + +## Hugging Face Spaces Deployment + +This project is ready for **free deployment** on [Hugging Face Spaces](https://huggingface.co/spaces) using the Docker SDK. + +1. Create a new Space → select **Docker** as the SDK +2. Push this repository (or connect GitHub) +3. Ensure the root `Dockerfile` is used (builds frontend + serves backend on port **7860**) +4. Add a Space secret: `GEMINI_API_KEY` = your Gemini API key +5. Wait for the build to complete + +The Space will serve both the React dashboard and FastAPI backend from a single container. + +### HF Space Settings + +- **SDK:** Docker +- **App port:** 7860 +- **Secrets:** `GEMINI_API_KEY` + +## Generated Artifacts + +After analysis, the platform produces: + +| File | Description | +|------|-------------| +| `repository_report.md` | Full markdown intelligence report | +| `repository_profile.json` | Languages, frameworks, APIs, modules, auth | +| `repository_summary.json` | Elevator pitch, features, workflows, risks | +| `repository_graph.json` | Architecture nodes, edges, flows, concepts | + +Artifacts are stored in `backend/storage/repos/{repo_id}/` and available via the dashboard download buttons. + +## Security + +- ZIP extraction includes path-traversal protection +- GitHub PAT tokens are redacted from error messages +- Temporary clone/extract workspaces are cleaned up after analysis +- Private repos require a valid GitHub Personal Access Token + +## License + +MIT diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000000000000000000000000000000000000..4ed442be8416e61ca94d484009d1b061736dbad9 --- /dev/null +++ b/backend/.env @@ -0,0 +1 @@ +GEMINI_API_KEY=AQ.Ab8RN6JCtHvyMAUiC4ldysHtPKp2vzTXWHjWFeke1frUha3BAA diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..bd28d32a0258948c1cb09156c5623da8fcea52bb --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +# Install system dependencies (Git is required for cloning repos) +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY . . + +# Expose port +EXPOSE 8000 + +# Start FastAPI application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b212fcd6cba07ca1f14b8b44a6e9cc3633b7b3c1 Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/test_agent_flow.cpython-312.pyc b/backend/__pycache__/test_agent_flow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..948adfc4c1f9bfd8f975b5e0f916e5293088263e Binary files /dev/null and b/backend/__pycache__/test_agent_flow.cpython-312.pyc differ diff --git a/backend/__pycache__/test_rag_pipeline.cpython-312.pyc b/backend/__pycache__/test_rag_pipeline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d37d096fb04c1b91e8bea61124803e51007ccf8a Binary files /dev/null and b/backend/__pycache__/test_rag_pipeline.cpython-312.pyc differ diff --git a/backend/adapters/__init__.py b/backend/adapters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..380f449abde8a502c3fcdf80768bf12c191a901e --- /dev/null +++ b/backend/adapters/__init__.py @@ -0,0 +1,3 @@ +from adapters.adk_adapter import GoogleADKPlatformAdapter +from adapters.agent_adapter import ADKAgentAdapter +from adapters.planner_adapter import ADKPlannerAdapter diff --git a/backend/adapters/__pycache__/__init__.cpython-312.pyc b/backend/adapters/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c17d3c3c632a99a44e3008db816194820f61bdeb Binary files /dev/null and b/backend/adapters/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/adapters/__pycache__/adk_adapter.cpython-312.pyc b/backend/adapters/__pycache__/adk_adapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8865e136b32887e743afea7e27fafee6384a0b1e Binary files /dev/null and b/backend/adapters/__pycache__/adk_adapter.cpython-312.pyc differ diff --git a/backend/adapters/__pycache__/agent_adapter.cpython-312.pyc b/backend/adapters/__pycache__/agent_adapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a75f6b38a399bc1a6e0f24442ab676ec8fd7f462 Binary files /dev/null and b/backend/adapters/__pycache__/agent_adapter.cpython-312.pyc differ diff --git a/backend/adapters/__pycache__/planner_adapter.cpython-312.pyc b/backend/adapters/__pycache__/planner_adapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..093ba206f9902d7c9c5dd3e0aad9707121dd7d4c Binary files /dev/null and b/backend/adapters/__pycache__/planner_adapter.cpython-312.pyc differ diff --git a/backend/adapters/adk_adapter.py b/backend/adapters/adk_adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..cf7e9f348c2fd59a2cd0e45ffe30ddc2447373bc --- /dev/null +++ b/backend/adapters/adk_adapter.py @@ -0,0 +1,36 @@ +from typing import Dict, Any +from adapters.agent_adapter import ADKAgentAdapter +from adapters.planner_adapter import ADKPlannerAdapter + +class GoogleADKPlatformAdapter: + """ + High-level adapter class representing a Google ADK Application context. + Integrates the multi-agent layers and provides entry points for task execution. + """ + def __init__(self, orchestrator: Any): + self.orchestrator = orchestrator + self.planner_adapter = ADKPlannerAdapter(orchestrator.planner) + self.agent_adapters = { + name: ADKAgentAdapter(agent) + for name, agent in orchestrator.agents.items() + } + + async def dispatch_query( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + """ + Executes queries by forwarding them to the underlying orchestrator, + representing a standard ADK execution cycle. + """ + return await self.orchestrator.execute( + profile=profile, + graph=graph, + summary=summary, + report=report, + query=query + ) diff --git a/backend/adapters/agent_adapter.py b/backend/adapters/agent_adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..0a880f93171713f08c84ed5b2b0c9be342b6fa62 --- /dev/null +++ b/backend/adapters/agent_adapter.py @@ -0,0 +1,26 @@ +from typing import Dict, Any +from agents.base_agent import BaseAgent + +class ADKAgentAdapter: + """ + Adapter wrapping existing BaseAgent specialized agents to conform to Google ADK + agent invocation schemas. + """ + def __init__(self, agent: BaseAgent): + self.agent = agent + self.name = agent.__class__.__name__ + + async def execute_task(self, context: Dict[str, Any], query: str) -> Dict[str, Any]: + profile = context.get("profile", {}) + graph = context.get("graph", {}) + summary = context.get("summary", {}) + report = context.get("report", "") + + # Call the underlying agent + return await self.agent.run( + profile=profile, + graph=graph, + summary=summary, + report=report, + query=query + ) diff --git a/backend/adapters/planner_adapter.py b/backend/adapters/planner_adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..0a9e9b9bd60d3c98c2c2da4153f3f1b8ce8ad302 --- /dev/null +++ b/backend/adapters/planner_adapter.py @@ -0,0 +1,38 @@ +import time +from typing import Dict, Any +from agents.planner_agent import PlannerAgent + +class ADKPlannerAdapter: + """ + Adapter wrapping the PlannerAgent to match ADK planner expectations. + """ + def __init__(self, planner: PlannerAgent): + self.planner = planner + + async def create_plan(self, context: Dict[str, Any], query: str) -> Dict[str, Any]: + profile = context.get("profile", {}) + graph = context.get("graph", {}) + summary = context.get("summary", {}) + report = context.get("report", "") + + # Call planner + plan = await self.planner.run( + profile=profile, + graph=graph, + summary=summary, + report=report, + query=query + ) + return { + "plan_id": f"plan_{int(time.time())}", + "steps": [ + { + "stage": idx + 1, + "agents": stage, + "parameters": {"query": query} + } + for idx, stage in enumerate(plan.get("execution_order", [])) + ], + "reasoning": plan.get("reasoning", ""), + "selected_agents": plan.get("selected_agents", []) + } diff --git a/backend/agents/__init__.py b/backend/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..13aec5de254c7f868565cb7dc33d9c41f6bf151a --- /dev/null +++ b/backend/agents/__init__.py @@ -0,0 +1,2 @@ +from agents.llm_client import LLMClient, GeminiLLMClient +from agents.orchestrator import AgentOrchestrator diff --git a/backend/agents/api_agent.py b/backend/agents/api_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..16bfae86323c55bff36b7aa7308f3e7209721d32 --- /dev/null +++ b/backend/agents/api_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class ApiAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the API Agent. Your responsibility is to analyze the API design of the repository, including HTTP methods, routes/endpoints, payload structures, external API dependencies, and data exchange/request-response flow. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous analysis of the endpoints and API flow to answer this query. Your citations must specify specific routes or lines where endpoints are declared or configured. +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/agents/architecture_agent.py b/backend/agents/architecture_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..eb2f46520e32299febd83b1ebc1ff563dd075671 --- /dev/null +++ b/backend/agents/architecture_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class ArchitectureAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Architecture Agent. Your responsibility is to analyze the codebase structure, architectural design patterns, component relationships, data flow, business workflows, and entry points to answer the user query. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous architectural analysis to answer this query. Your citations must specify files, code blocks, or components (e.g. "backend/main.py:L10-L40", "Graph Node: App.jsx"). +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..da48f227ef17324fd7c490f65db3206b9389a867 --- /dev/null +++ b/backend/agents/base_agent.py @@ -0,0 +1,31 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, List +import asyncio +from pydantic import BaseModel, Field +from agents.llm_client import LLMClient + +class AgentResponseSchema(BaseModel): + agent: str = Field(description="Name of the agent, e.g., SecurityAgent") + confidence: float = Field(description="Confidence score between 0.0 and 1.0 based on relevance and sufficiency of data") + answer: str = Field(description="Detailed answer or analysis regarding the user query") + citations: List[str] = Field(description="Source files, line numbers, or endpoints cited as reference") + reasoning: List[str] = Field(description="Step-by-step reasoning steps the agent took") + +class BaseAgent(ABC): + def __init__(self, llm_client: LLMClient): + self.llm_client = llm_client + + @abstractmethod + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + """Runs the agent's analysis.""" + pass + + async def _call_llm_json(self, prompt: str, schema: Any, temperature: float = 0.2) -> Dict[str, Any]: + return await asyncio.to_thread(self.llm_client.generate_json, prompt, schema, temperature) diff --git a/backend/agents/dependency_agent.py b/backend/agents/dependency_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..36c387a1ab3940c518f84a391bb93c0a0dc04468 --- /dev/null +++ b/backend/agents/dependency_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class DependencyAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Dependency Agent. Your responsibility is to analyze libraries, package configurations (e.g. package.json, requirements.txt), framework selections, database integrations, cloud stack, and deployment environment setups. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous analysis of project dependencies and frameworks to answer this query. Your citations must reference the package manifest files or configurations (e.g., "requirements.txt:L3", "docker-compose.yml:L5"). +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/agents/llm_client.py b/backend/agents/llm_client.py new file mode 100644 index 0000000000000000000000000000000000000000..6a6113fa7482c4ef00f627c0135609f9a6ba17c3 --- /dev/null +++ b/backend/agents/llm_client.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, Type +from pydantic import BaseModel + +class LLMClient(ABC): + @abstractmethod + def generate_json( + self, + prompt: str, + response_schema: Type[BaseModel], + temperature: float = 0.2 + ) -> Dict[str, Any]: + """Generates a structured JSON response matching the given response_schema.""" + pass + +class GeminiLLMClient(LLMClient): + def __init__(self, api_key: str): + from google import genai + self._client = genai.Client(api_key=api_key) + + def generate_json( + self, + prompt: str, + response_schema: Type[BaseModel], + temperature: float = 0.2 + ) -> Dict[str, Any]: + import json + + response = self._client.models.generate_content( + model='gemini-2.5-flash', + contents=prompt, + config={ + 'response_mime_type': 'application/json', + 'response_schema': response_schema, + 'temperature': temperature + } + ) + if not response.text: + raise ValueError("Gemini returned empty response text.") + return json.loads(response.text) diff --git a/backend/agents/onboarding_agent.py b/backend/agents/onboarding_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..ed70788fec3d294c8ae15676dd6253dda4737cea --- /dev/null +++ b/backend/agents/onboarding_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class OnboardingAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Onboarding Agent. Your responsibility is to guide new developers in understanding the project execution flow, configuring local developer environments, locating entry point scripts, outlining recommended learning paths, and listing the key folders/files to read first. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous developer-focused onboarding walkthrough to answer this query. Your citations must point out documentation, entry files, or configuration variables that speed up developer setup. +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/agents/orchestrator.py b/backend/agents/orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..031d1c1e3fdc40076b50b5f1f546354a056936b1 --- /dev/null +++ b/backend/agents/orchestrator.py @@ -0,0 +1,351 @@ +import time +import asyncio +import logging +import json +from typing import Dict, Any, List, Optional +from agents.llm_client import LLMClient +from agents.planner_agent import PlannerAgent +from agents.architecture_agent import ArchitectureAgent +from agents.security_agent import SecurityAgent +from agents.api_agent import ApiAgent +from agents.dependency_agent import DependencyAgent +from agents.quality_agent import QualityAgent +from agents.onboarding_agent import OnboardingAgent +from agents.response_synthesizer import ResponseSynthesizer +from tools.tool_registry import tool_registry, setup_default_registry +from memory.memory_cache import memory_cache +from memory.conversation_manager import conversation_manager + +logger = logging.getLogger("orchestrator") + +class AgentOrchestrator: + def __init__(self, llm_client: LLMClient): + self.llm_client = llm_client + self.planner = PlannerAgent(llm_client) + self.synthesizer = ResponseSynthesizer(llm_client) + + # Register specialized agents + self.agents = { + "ArchitectureAgent": ArchitectureAgent(llm_client), + "SecurityAgent": SecurityAgent(llm_client), + "ApiAgent": ApiAgent(llm_client), + "DependencyAgent": DependencyAgent(llm_client), + "QualityAgent": QualityAgent(llm_client), + "OnboardingAgent": OnboardingAgent(llm_client) + } + + async def execute( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str, + repo_id: str, + session_id: Optional[str] = None, + vector_store: Optional[Any] = None, + retriever: Optional[Any] = None + ) -> Dict[str, Any]: + timeline = [] + agents_used = [] + start_total = time.time() + + # Initialize tools registry if empty and vector dependencies are available + if not tool_registry.list_tools() and vector_store and retriever: + setup_default_registry(vector_store, retriever) + + # 1. Run Planner Agent to decide execution steps + logger.info(f"Running PlannerAgent for query: '{query}'") + planner_start = time.time() + try: + planner_res = await self.planner.run(profile, graph, summary, report, query) + planner_latency = time.time() - planner_start + logger.info(f"PlannerAgent completed in {planner_latency:.2f}s. Plan: {planner_res}") + timeline.append({ + "agent": "PlannerAgent", + "execution_time_ms": int(planner_latency * 1000), + "status": "success", + "message": f"Pipeline: memory={planner_res.get('retrieve_memory')}, search={planner_res.get('run_semantic_search')}, tools={planner_res.get('invoke_tools')}, agents={planner_res.get('selected_agents')}" + }) + except Exception as e: + planner_latency = time.time() - planner_start + logger.error(f"PlannerAgent failed: {e}") + timeline.append({ + "agent": "PlannerAgent", + "execution_time_ms": int(planner_latency * 1000), + "status": "failure", + "message": f"Planner failed: {str(e)}" + }) + # Fallback plan: enable all pipeline elements and run all agents + planner_res = { + "retrieve_memory": True, + "run_semantic_search": True, + "invoke_tools": [], + "run_agents": True, + "selected_agents": list(self.agents.keys()), + "execution_order": [list(self.agents.keys())], + "synthesize_final_answer": True, + "reasoning": "Fallback plan due to planner failure." + } + + retrieve_memory = planner_res.get("retrieve_memory", True) + run_semantic_search = planner_res.get("run_semantic_search", True) + invoke_tools = planner_res.get("invoke_tools", []) + run_agents = planner_res.get("run_agents", True) + selected_agents = planner_res.get("selected_agents", []) + execution_order = planner_res.get("execution_order", []) + synthesize_final_answer = planner_res.get("synthesize_final_answer", True) + + # 2. Retrieve Memory if requested + memory_context = "" + if retrieve_memory and session_id: + logger.info(f"Retrieving conversation memory for session {session_id}") + mem_start = time.time() + session = conversation_manager.get_session(session_id) + if session and session.history: + past_turns = [] + for msg in session.history[:-1]: # Exclude current question which was already added + past_turns.append(f"{msg.role.capitalize()}: {msg.content}") + memory_context = "\n".join(past_turns) + timeline.append({ + "agent": "MemoryRetrieval", + "execution_time_ms": int((time.time() - mem_start) * 1000), + "status": "success", + "message": "Retrieved past turns" if memory_context else "No past turns found" + }) + + # 3. Run Semantic Search if requested + search_results = [] + if run_semantic_search and retriever: + logger.info("Running semantic search retrieval") + search_start = time.time() + try: + search_results = retriever.retrieve(repo_id=repo_id, query=query, top_k=5) + timeline.append({ + "agent": "SemanticSearch", + "execution_time_ms": int((time.time() - search_start) * 1000), + "status": "success", + "message": f"Found {len(search_results)} relevant chunks" + }) + except Exception as e: + logger.error(f"Semantic search failed: {e}") + timeline.append({ + "agent": "SemanticSearch", + "execution_time_ms": int((time.time() - search_start) * 1000), + "status": "failure", + "message": str(e) + }) + + # 4. Invoke Tools (with caching) if requested + tool_outputs = {} + if invoke_tools: + logger.info(f"Invoking tools: {invoke_tools}") + tool_start = time.time() + for tool_name in invoke_tools: + # Check memory cache first + cache_key = f"tool:{repo_id}:{tool_name}:{query}" + cached_res = memory_cache.get(cache_key) + if cached_res is not None: + logger.info(f"Cache hit for tool '{tool_name}'") + tool_outputs[tool_name] = cached_res + else: + logger.info(f"Cache miss for tool '{tool_name}'. Executing...") + res = tool_registry.execute_tool(tool_name, repo_id=repo_id, query=query) + memory_cache.set(cache_key, res, ttl=300) # Cache for 5 mins + tool_outputs[tool_name] = res + timeline.append({ + "agent": "ToolInvocation", + "execution_time_ms": int((time.time() - tool_start) * 1000), + "status": "success", + "message": f"Executed {len(tool_outputs)} tools" + }) + + # 5. Run Specialized Agents if requested + agent_responses = [] + if run_agents and selected_agents: + # Construct augmented query containing retrieved context and tool outputs + augmented_context_parts = [] + if memory_context: + augmented_context_parts.append(f"--- Conversation History ---\n{memory_context}") + if search_results: + search_text = "\n\n".join( + f"[Relevant Code Chunk | similarity={r['similarity']:.2f}]\n{r['content']}" + for r in search_results + ) + augmented_context_parts.append(f"--- Codebase Semantic Search Results ---\n{search_text}") + if tool_outputs: + tools_text = json.dumps(tool_outputs, indent=2) + augmented_context_parts.append(f"--- Direct Tool Outputs ---\n{tools_text}") + + augmented_query = query + if augmented_context_parts: + augmented_query = f"{query}\n\n" + "\n\n".join(augmented_context_parts) + + for stage_idx, stage in enumerate(execution_order): + tasks = [] + agent_names = [] + + for agent_name in stage: + if agent_name in self.agents and agent_name in selected_agents: + agent_names.append(agent_name) + tasks.append(self._run_agent_with_retry(agent_name, profile, graph, summary, report, augmented_query)) + + if not tasks: + continue + + logger.info(f"Executing Stage {stage_idx + 1} with agents in parallel: {agent_names}") + stage_results = await asyncio.gather(*tasks, return_exceptions=True) + + for agent_name, result in zip(agent_names, stage_results): + agents_used.append(agent_name) + + if isinstance(result, Exception): + logger.error(f"Agent {agent_name} failed execution: {result}") + timeline.append({ + "agent": agent_name, + "execution_time_ms": 0, + "status": "failure", + "message": str(result), + "confidence": 0.0 + }) + else: + agent_responses.append(result["response"]) + timeline.append({ + "agent": agent_name, + "execution_time_ms": result["latency_ms"], + "status": "success", + "confidence": result["response"].get("confidence", 0.0), + "answer": result["response"].get("answer", "") + }) + + # 6. Run Response Synthesizer if requested + synth_res = {} + if synthesize_final_answer: + logger.info("Running ResponseSynthesizer...") + synth_start = time.time() + try: + synth_res = await self.synthesizer.synthesize( + query=query, + agent_responses=agent_responses, + memory_context=memory_context, + search_results=search_results, + tool_outputs=tool_outputs + ) + synth_latency = time.time() - synth_start + logger.info(f"ResponseSynthesizer completed in {synth_latency:.2f}s") + timeline.append({ + "agent": "ResponseSynthesizer", + "execution_time_ms": int(synth_latency * 1000), + "status": "success" + }) + except Exception as e: + synth_latency = time.time() - synth_start + logger.error(f"ResponseSynthesizer failed: {e}") + timeline.append({ + "agent": "ResponseSynthesizer", + "execution_time_ms": int(synth_latency * 1000), + "status": "failure", + "message": str(e) + }) + # Fallback synthesis + fallback_answer = "\n\n".join([f"### {r.get('agent')}\n{r.get('answer')}" for r in agent_responses]) + synth_res = { + "summary": "Fallback summary compiled from individual agents.", + "detailed_explanation": fallback_answer, + "agent_contributions": [f"{r.get('agent')} (direct contribution)" for r in agent_responses], + "confidence_score": 0.5 + } + else: + # Synthesis bypassed, compile direct report from tools/search + logger.info("Bypassing ResponseSynthesizer per Planner decision") + direct_parts = [] + if tool_outputs: + direct_parts.append("### Direct Tool Outputs") + for t_name, val in tool_outputs.items(): + direct_parts.append(f"**{t_name}**:\n```json\n{json.dumps(val, indent=2)}\n```") + if search_results: + direct_parts.append("### Codebase Search Results") + for r in search_results: + direct_parts.append(f"- **{r['metadata'].get('path', 'unknown')}** (similarity={r['similarity']:.2f}):\n{r['content']}") + + detailed_explanation = "\n\n".join(direct_parts) if direct_parts else "No tools or search results were requested, and LLM synthesis was bypassed." + synth_res = { + "summary": "Direct result compiled from tools/search.", + "detailed_explanation": detailed_explanation, + "agent_contributions": ["Direct tool output execution."], + "confidence_score": 1.0 + } + timeline.append({ + "agent": "ResponseSynthesizer", + "execution_time_ms": 0, + "status": "success", + "message": "Bypassed synthesizer" + }) + + total_time_ms = int((time.time() - start_total) * 1000) + + # Merge citations from all agent responses and search results + references = [] + for r in agent_responses: + references.extend(r.get("citations", [])) + for r in search_results: + path = r["metadata"].get("path") + if path and path not in references: + references.append(path) + + unique_references = [] + for ref in references: + if ref not in unique_references: + unique_references.append(ref) + + answer = synth_res.get("detailed_explanation", "") or "" + if not answer: + logger.warning("Synthesized response was empty. Falling back to agent answers or summary.") + if agent_responses: + fallback_answer = "\n\n".join( + f"### {r.get('agent', 'UnnamedAgent')}\n{r.get('answer', '') or 'No answer generated.'}" + for r in agent_responses + ).strip() + answer = fallback_answer or synth_res.get("summary", "") + else: + answer = synth_res.get("summary", "No answer could be generated from the agents.") + + return { + "answer": answer, + "summary": synth_res.get("summary", ""), + "agents_used": agents_used, + "confidence": synth_res.get("confidence_score", 0.0), + "references": unique_references, + "agent_contributions": synth_res.get("agent_contributions", []), + "planner_decision": planner_res, + "timeline": timeline, + "total_time_ms": total_time_ms, + "retrieved_context": search_results + } + + async def _run_agent_with_retry( + self, + agent_name: str, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str, + retries: int = 2 + ) -> Dict[str, Any]: + agent = self.agents[agent_name] + + for attempt in range(retries + 1): + start = time.time() + try: + response = await agent.run(profile, graph, summary, report, query) + latency_ms = int((time.time() - start) * 1000) + return { + "response": response, + "latency_ms": latency_ms + } + except Exception as e: + logger.warning(f"Agent {agent_name} attempt {attempt + 1} failed: {e}") + if attempt == retries: + raise e + await asyncio.sleep(0.5) diff --git a/backend/agents/planner_agent.py b/backend/agents/planner_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..91aa1ae495afea0c7e2ae3de1be4e035733808c5 --- /dev/null +++ b/backend/agents/planner_agent.py @@ -0,0 +1,83 @@ +import json +from typing import Dict, Any, List +from pydantic import BaseModel, Field +from agents.base_agent import BaseAgent + +class PlannerDecision(BaseModel): + retrieve_memory: bool = Field( + description="Whether to retrieve past conversation history or context for the current query." + ) + run_semantic_search: bool = Field( + description="Whether to query the vector database for matching chunks from the codebase." + ) + invoke_tools: List[str] = Field( + description="List of tool names to invoke, from: ['repository_search', 'graph_query', 'dependency_lookup', 'file_reader', 'architecture_lookup', 'api_lookup']." + ) + run_agents: bool = Field( + description="Whether specialized agents should be executed." + ) + selected_agents: List[str] = Field( + description="List of agent names selected to run if run_agents is true, from: ['ArchitectureAgent', 'SecurityAgent', 'ApiAgent', 'DependencyAgent', 'QualityAgent', 'OnboardingAgent']." + ) + execution_order: List[List[str]] = Field( + description="Execution order for agents (e.g., [['SecurityAgent', 'ApiAgent'], ['ArchitectureAgent']]) if run_agents is true." + ) + synthesize_final_answer: bool = Field( + description="Whether the response synthesizer should combine everything into the final answer. Set to True unless a direct tool/search result is sufficient." + ) + reasoning: str = Field( + description="Explanation of why these pipeline decisions and agent executions were chosen." + ) + +class PlannerAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Agent Orchestration Planner. Your task is to analyze the user's query and decide the best execution pipeline to resolve it. + +The execution pipeline consists of: +1. retrieve_memory: Fetching past chat history/turns. +2. run_semantic_search: Finding relevant code snippets via vector embeddings. +3. invoke_tools: Executing direct lookup/search tools. +4. run_agents: Launching specialized agents in parallel or sequential stages. +5. synthesize_final_answer: Combining all info into a clean final markdown explanation. + +Available Tools: +- repository_search: Semantic search over indexed repo chunks. +- graph_query: Query dependency graph, business flows, entry points. +- dependency_lookup: Look up project languages, frameworks, packages. +- file_reader: Retrieve specific source code file content. +- architecture_lookup: Query high-level architecture pattern and major folders. +- api_lookup: Look up HTTP endpoints, authentication details. + +Available Agents: +1. ArchitectureAgent: Focuses on architectural patterns, component interaction, data flow, component responsibilities, and business flows. +2. SecurityAgent: Focuses on authentication, authorization, API keys, secrets, security risks, vulnerability findings, and unsafe practices. +3. ApiAgent: Focuses on endpoints, routes, HTTP methods, request/response models, and external APIs. +4. DependencyAgent: Focuses on libraries, frameworks, cloud stack, dependencies, and infrastructure setup. +5. QualityAgent: Focuses on complexity, maintainability, dead code, refactoring suggestions, and testing hints. +6. OnboardingAgent: Focuses on developer onboarding walkthrough, where to start reading, and execution setup. + +Repository Profile: +{json.dumps(profile, indent=2)} + +Repository Summary: +{json.dumps(summary, indent=2)} + +User Query: +{query} + +Determine: +1. Which pipeline components should run (retrieve_memory, run_semantic_search, invoke_tools, run_agents, synthesize_final_answer). +2. Which agents and tools are needed and their execution plan. +3. The reasoning behind your plan. + +Return your decision in structured JSON format matching the schema. +""" + return await self._call_llm_json(prompt, PlannerDecision, temperature=0.1) diff --git a/backend/agents/quality_agent.py b/backend/agents/quality_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..f19b4088355672e1861680495f3dfc699211756f --- /dev/null +++ b/backend/agents/quality_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class QualityAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Quality Agent. Your responsibility is to analyze the codebase for code quality, maintainability, architectural complexity, indicators of dead code, large/bloated modules, potential code smells, and testing/coverage strategies. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous analysis of the code quality and maintainability indicators to answer this query. Your citations must specify modules or files containing smells, complex blocks, or missing test configurations. +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/agents/response_synthesizer.py b/backend/agents/response_synthesizer.py new file mode 100644 index 0000000000000000000000000000000000000000..b1b2dfd994a766d270e99846dbef0bb2230afc77 --- /dev/null +++ b/backend/agents/response_synthesizer.py @@ -0,0 +1,63 @@ +import json +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field +from agents.llm_client import LLMClient + +class SynthesizedResponse(BaseModel): + summary: str = Field(description="A concise summary of all findings across agents") + detailed_explanation: str = Field(description="A comprehensive, detailed markdown explanation combining all insights, resolving duplicates, and directly answering the user query") + agent_contributions: List[str] = Field(description="List of strings explaining what each agent contributed (e.g. 'SecurityAgent: identified lack of rate limiting on the login route.')") + confidence_score: float = Field(description="A combined confidence score representing the quality and consensus of the generated answer (0.0 to 1.0)") + +class ResponseSynthesizer: + def __init__(self, llm_client: LLMClient): + self.llm_client = llm_client + + async def synthesize( + self, + query: str, + agent_responses: List[Dict[str, Any]], + memory_context: Optional[str] = None, + search_results: Optional[List[Dict[str, Any]]] = None, + tool_outputs: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + import asyncio + responses_str = json.dumps(agent_responses, indent=2) + + context_parts = [] + if memory_context: + context_parts.append(f"--- Past Conversation turns ---\n{memory_context}") + if search_results: + search_text = "\n\n".join( + f"[Search similarity={r['similarity']:.2f}]\n{r['content']}" + for r in search_results + ) + context_parts.append(f"--- Semantic Search Context ---\n{search_text}") + if tool_outputs: + tools_text = json.dumps(tool_outputs, indent=2) + context_parts.append(f"--- Direct Tool Outputs ---\n{tools_text}") + + extra_context = "\n\n".join(context_parts) + + prompt = f""" +You are the Principal Systems Integrator and Response Synthesizer. +Your goal is to digest all specialized agent reports, retrieve context, tool outputs, and compile them into a single, cohesive, premium response answering the user's query. + +User Query: +{query} + +{extra_context} + +Specialized Agent Responses: +{responses_str} + +Tasks: +1. Synthesize a single coherent, authoritative detailed explanation in markdown. +2. Merge duplicate findings. +3. Call out specific contributions or viewpoints of the agents/tools. +4. Calculate an overall confidence score based on individual agent confidence levels, tool relevance, and consensus. +5. Create a concise summary. + +Return the result matching the schema in structured JSON format. +""" + return await asyncio.to_thread(self.llm_client.generate_json, prompt, SynthesizedResponse, 0.2) diff --git a/backend/agents/security_agent.py b/backend/agents/security_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..ef44e8aa92dff01ba1bff224ed70d7915a4848cb --- /dev/null +++ b/backend/agents/security_agent.py @@ -0,0 +1,33 @@ +import json +from typing import Dict, Any +from agents.base_agent import BaseAgent, AgentResponseSchema + +class SecurityAgent(BaseAgent): + async def run( + self, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report: str, + query: str + ) -> Dict[str, Any]: + prompt = f""" +You are the Security Agent. Your responsibility is to analyze the codebase for authentication methods, authorization mechanisms, handling of secrets/API keys, safety vulnerabilities, unsafe coding practices, and package dependencies that pose risks. + +Here is the repository context: +1. Profile: +{json.dumps(profile, indent=2)} +2. Graph Structure: +{json.dumps(graph, indent=2)} +3. Summary: +{json.dumps(summary, indent=2)} +4. Intelligence Report: +{report} + +User Query: +{query} + +Perform a rigorous security analysis to answer this query. Your citations must specify files, lines, or configurations where security measures are implemented, missing, or potentially vulnerable. +Return your structured answer matching the AgentResponseSchema. +""" + return await self._call_llm_json(prompt, AgentResponseSchema) diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000000000000000000000000000000000000..da5b9a2af7f829886c90ebba6fb03c0672ca83f0 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,561 @@ +import os +import json +import shutil +import tempfile +import uuid +import logging +from typing import Optional +from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Header, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from starlette.background import BackgroundTask +from pydantic import BaseModel + +# Load .env variables manually if the file is present +env_path = os.path.join(os.path.dirname(__file__), ".env") +if os.path.exists(env_path): + with open(env_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, val = line.split("=", 1) + os.environ[key.strip()] = val.strip() + +# Import services +from services.repositoryScanner import ( + check_repository_privacy, + clone_repository, + extract_zip, + scan_directory, + handle_remove_readonly +) +from services.repositoryProfiler import profile_repository +from services.graphBuilder import build_initial_graph +from services.llmAnalyzer import analyze_repository +from services.repositoryMemory import memory_service + +# Phase 3: Memory, RAG, Tools +from memory.embedding_service import EmbeddingService +from memory.vector_store import VectorStore +from memory.knowledge_index import KnowledgeIndexBuilder +from memory.retriever import KnowledgeRetriever +from memory.conversation_manager import conversation_manager +from memory.session_manager import session_manager +from memory.memory_cache import memory_cache +# Singleton memory infrastructure (initialised lazily per-request) +_vector_store: VectorStore = None + +def get_vector_store() -> VectorStore: + global _vector_store + if _vector_store is None: + _vector_store = VectorStore() + return _vector_store + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("main") + +app = FastAPI( + title="Repository Intelligence API", + description="Foundational Memory and Intelligence Layer for Multi-Agent AI Software Engineering", + version="1.0.0" +) + +# Enable CORS for frontend integration (credentials disabled when using wildcard origins) +_cors_origins = [o.strip() for o in os.environ.get("CORS_ORIGINS", "*").split(",") if o.strip()] +app.add_middleware( + CORSMiddleware, + allow_origins=_cors_origins, + allow_credentials="*" not in _cors_origins, + allow_methods=["*"], + allow_headers=["*"], +) + +# Request schema for analyzing git repository +class AnalyzeUrlRequest(BaseModel): + url: str + token: Optional[str] = None + +@app.get("/api/health") +def health_check(): + return {"status": "healthy"} + +@app.post("/api/analyze-url") +async def analyze_git_url( + request: AnalyzeUrlRequest, + x_gemini_key: Optional[str] = Header(None) +): + """ + Clones a GitHub repository, validates access permissions, processes the pipeline, + calls Gemini 2.5 Flash, and returns structured intelligence outputs. + """ + repo_url = request.url + token = request.token + gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY") + + if not gemini_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Gemini API Key is missing. Please provide it in the headers (x-gemini-key) or configure it on the server." + ) + + # 1. Validate repository privacy and access + logger.info(f"Validating access to repository: {repo_url}") + privacy_info = await check_repository_privacy(repo_url, token) + + if privacy_info["status"] in ["private_requires_auth", "private_denied"]: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=privacy_info["message"] + ) + elif privacy_info["status"] == "invalid": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=privacy_info["message"] + ) + elif privacy_info["status"] == "error": + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=privacy_info["message"] + ) + + owner_repo = privacy_info["owner_repo"] or "cloned_repo" + repo_id = str(uuid.uuid4()) + + # Create a temporary directory in a secure, OS-agnostic manner + temp_dir = tempfile.mkdtemp(prefix="repo_intel_") + + try: + # 2. Clone the repository + logger.info(f"Cloning repository into temporary directory: {temp_dir}") + clone_repository(repo_url, temp_dir, token) + + # 3. Scan the repository file tree and text files + logger.info("Scanning directory structure...") + scan_results = scan_directory(temp_dir) + + # 4. Generate static profile and basic relationship graph + logger.info("Generating static profiles...") + static_profile = profile_repository(scan_results["files"]) + static_graph = build_initial_graph(scan_results["files"]) + static_profile["static_graph"] = static_graph + + # 5. Call LLM for deep reasoning and structured outputs + logger.info("Triggering Gemini 2.5 Flash intelligence analysis...") + analysis_result = await analyze_repository( + repo_name=owner_repo, + tree_structure=scan_results["tree"], + static_profile=static_profile, + flat_files=scan_results["files"], + api_key=gemini_key + ) + + # 6. Save outputs inside the repositoryMemory service + logger.info("Storing generated artifacts in memory layer...") + stored = memory_service.store( + repo_id=repo_id, + profile=analysis_result["profile"], + graph=analysis_result["graph"], + summary=analysis_result["summary"], + report_markdown=analysis_result["report"] + ) + + # 7. Phase 3: Build semantic vector index asynchronously + gemini_key_for_embed = gemini_key + try: + logger.info("Building semantic knowledge index (ChromaDB)...") + import asyncio + embedder = EmbeddingService(api_key=gemini_key_for_embed) + vs = get_vector_store() + indexer = KnowledgeIndexBuilder(embedder, vs) + await asyncio.to_thread( + indexer.build_index, + repo_id, + analysis_result["profile"], + analysis_result["summary"], + analysis_result["graph"], + analysis_result["report"], + scan_results["files"] + ) + logger.info(f"Knowledge index built for repo {repo_id}.") + except Exception as idx_e: + logger.warning(f"Knowledge index build failed (non-fatal): {idx_e}") + + return { + "success": True, + "repo_id": repo_id, + "project_name": owner_repo, + "tree": scan_results["tree"], + "data": stored + } + + except Exception as e: + logger.error(f"Error during repository analysis: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Analysis failed: {str(e)}" + ) + finally: + # Clean up temporary directory (safe rmtree for Windows/Linux read-only files) + logger.info(f"Cleaning up temporary workspace directory: {temp_dir}") + shutil.rmtree(temp_dir, onerror=handle_remove_readonly) + +@app.post("/api/analyze-zip") +async def analyze_uploaded_zip( + file: UploadFile = File(...), + x_gemini_key: Optional[str] = Header(None) +): + """ + Extracts an uploaded repository ZIP file, runs structural scanning, + generates static/dynamic profile schemas, and runs the LLM analysis. + """ + gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY") + + if not gemini_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Gemini API Key is missing. Please provide it in the headers (x-gemini-key) or configure it on the server." + ) + + if not file.filename.endswith(".zip"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid file format. Only ZIP archives are supported." + ) + + repo_id = str(uuid.uuid4()) + project_name = file.filename[:-4] # Strip .zip + + # Set up temporary directory and paths + temp_dir = tempfile.mkdtemp(prefix="zip_intel_") + fd, zip_path = tempfile.mkstemp(suffix=".zip") + + try: + # Save ZIP upload chunk by chunk + with os.fdopen(fd, 'wb') as tmp_zip: + shutil.copyfileobj(file.file, tmp_zip) + + # 1. Extract ZIP securely with path traversal protection + logger.info(f"Extracting zip archive: {file.filename}") + extract_zip(zip_path, temp_dir) + + # 2. Scan directory + logger.info("Scanning unzipped directory structure...") + scan_results = scan_directory(temp_dir) + + # 3. Generate static profiles + logger.info("Generating static profiles...") + static_profile = profile_repository(scan_results["files"]) + static_graph = build_initial_graph(scan_results["files"]) + static_profile["static_graph"] = static_graph + + # 4. Trigger Gemini analysis + logger.info("Analyzing unzipped codebase with Gemini 2.5 Flash...") + analysis_result = await analyze_repository( + repo_name=project_name, + tree_structure=scan_results["tree"], + static_profile=static_profile, + flat_files=scan_results["files"], + api_key=gemini_key + ) + + # 5. Store generated artifacts + logger.info("Storing artifacts in memory service...") + stored = memory_service.store( + repo_id=repo_id, + profile=analysis_result["profile"], + graph=analysis_result["graph"], + summary=analysis_result["summary"], + report_markdown=analysis_result["report"] + ) + + # 6. Phase 3: Build semantic vector index + try: + logger.info("Building semantic knowledge index for ZIP repo...") + import asyncio + embedder = EmbeddingService(api_key=gemini_key) + vs = get_vector_store() + indexer = KnowledgeIndexBuilder(embedder, vs) + await asyncio.to_thread( + indexer.build_index, + repo_id, + analysis_result["profile"], + analysis_result["summary"], + analysis_result["graph"], + analysis_result["report"], + scan_results["files"] + ) + logger.info(f"Knowledge index built for ZIP repo {repo_id}.") + except Exception as idx_e: + logger.warning(f"Knowledge index build failed (non-fatal): {idx_e}") + + return { + "success": True, + "repo_id": repo_id, + "project_name": project_name, + "tree": scan_results["tree"], + "data": stored + } + + except Exception as e: + logger.error(f"Error processing uploaded zip file: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Analysis failed: {str(e)}" + ) + finally: + # Cleanup + logger.info(f"Cleaning up temporary workspace files...") + if os.path.exists(zip_path): + os.remove(zip_path) + shutil.rmtree(temp_dir, onerror=handle_remove_readonly) + +def _cleanup_temp_dir(path: str) -> None: + if os.path.exists(path): + shutil.rmtree(path, ignore_errors=True) + + +@app.get("/api/download/{repo_id}/{artifact_type}") +async def download_intelligence_artifact(repo_id: str, artifact_type: str): + """ + Downloads specific intelligence output as files (profile.json, graph.json, summary.json, report.md) + """ + data = memory_service.retrieve(repo_id) + if not data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Repository intelligence data not found or has expired." + ) + + artifact_map = { + "profile": ("repository_profile.json", "application/json", lambda d: json.dumps(d["profile"], indent=2)), + "graph": ("repository_graph.json", "application/json", lambda d: json.dumps(d["graph"], indent=2)), + "summary": ("repository_summary.json", "application/json", lambda d: json.dumps(d["summary"], indent=2)), + "report": ("repository_report.md", "text/markdown", lambda d: d["report"]), + } + + if artifact_type not in artifact_map: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid artifact type: {artifact_type}" + ) + + filename, media_type, content_fn = artifact_map[artifact_type] + temp_dir = tempfile.mkdtemp() + file_path = os.path.join(temp_dir, filename) + + try: + with open(file_path, "w", encoding="utf-8") as f: + f.write(content_fn(data)) + return FileResponse( + file_path, + media_type=media_type, + filename=filename, + background=BackgroundTask(_cleanup_temp_dir, temp_dir), + ) + except Exception as e: + _cleanup_temp_dir(temp_dir) + logger.error(f"Error compiling download file: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to generate download file." + ) + +class ChatRequest(BaseModel): + repo_id: str + question: str + session_id: Optional[str] = None + +@app.post("/api/chat") +async def chat_with_repo( + request: ChatRequest, + x_gemini_key: Optional[str] = Header(None) +): + """ + Phase 2+3: Executes the multi-agent orchestration pipeline with RAG context + retrieval and conversation memory. + """ + import time + import asyncio + from agents.llm_client import GeminiLLMClient + from agents.orchestrator import AgentOrchestrator + + gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY") + if not gemini_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Gemini API Key is missing." + ) + + repo_data = memory_service.retrieve(request.repo_id) + if not repo_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Repository intelligence data not found or has expired." + ) + + session_id = request.session_id or str(uuid.uuid4()) + + # Add user message to memory first + conversation_manager.add_message( + session_id=session_id, + repo_id=request.repo_id, + role="user", + content=request.question + ) + + try: + llm_client = GeminiLLMClient(api_key=gemini_key) + orchestrator = AgentOrchestrator(llm_client) + + embedder = EmbeddingService(api_key=gemini_key) + retriever = KnowledgeRetriever(embedder, get_vector_store()) + + logger.info(f"Orchestrating agents for repo {request.repo_id}: '{request.question}'") + + # Run pipeline + result = await orchestrator.execute( + profile=repo_data["profile"], + graph=repo_data["graph"], + summary=repo_data["summary"], + report=repo_data["report"], + query=request.question, + repo_id=request.repo_id, + session_id=session_id, + vector_store=get_vector_store(), + retriever=retriever + ) + + # Attach session and RAG details + result["session_id"] = session_id + + # Populate retrieved context on user message + session = conversation_manager.get_session(session_id) + if session and session.history: + session.history[-1].retrieved_context = result.get("retrieved_context", []) + + # Store assistant response in conversation memory + conversation_manager.add_message( + session_id=session_id, + repo_id=request.repo_id, + role="assistant", + content=result.get("answer", ""), + agent_decisions={ + "agents_used": result.get("agents_used", []), + "confidence": result.get("confidence", 0.0), + "planner_decision": result.get("planner_decision", {}) + } + ) + + # Persist conversation sessions to disk + session_manager.save_all() + + return result + + except Exception as e: + logger.error(f"Chat orchestration error: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Chat execution failed: {str(e)}" + ) + + +class SearchRequest(BaseModel): + repo_id: str + query: str + top_k: int = 5 + category: Optional[str] = None + +@app.post("/api/search") +async def semantic_search( + request: SearchRequest, + x_gemini_key: Optional[str] = Header(None) +): + """Phase 3: Semantic search against the repository knowledge vector index.""" + import time + gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY") + if not gemini_key: + raise HTTPException(status_code=400, detail="Gemini API Key required for semantic search.") + + repo_data = memory_service.retrieve(request.repo_id) + if not repo_data: + raise HTTPException(status_code=404, detail="Repository data not found.") + + try: + start = time.time() + embedder = EmbeddingService(api_key=gemini_key) + retriever = KnowledgeRetriever(embedder, get_vector_store()) + results = retriever.retrieve( + repo_id=request.repo_id, + query=request.query, + top_k=request.top_k, + category=request.category + ) + latency_ms = int((time.time() - start) * 1000) + return { + "query": request.query, + "results": results, + "result_count": len(results), + "latency_ms": latency_ms + } + except Exception as e: + logger.error(f"Semantic search error: {e}") + raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}") + + +@app.get("/api/memory") +async def get_memory_info(repo_id: str): + """Phase 3: Returns vector index stats for a repository.""" + try: + vs = get_vector_store() + count = vs.count_documents(repo_id) + return { + "repo_id": repo_id, + "indexed_chunks": count, + "storage_path": vs.storage_path + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/conversations") +async def list_conversations(repo_id: str): + """Phase 3: Returns conversation sessions for a repository.""" + sessions = conversation_manager.list_sessions_for_repo(repo_id) + return {"repo_id": repo_id, "sessions": sessions} + + +@app.get("/api/conversations/{session_id}") +async def get_conversation(session_id: str): + """Returns full message history for a conversation session.""" + history = session_manager.get_session_history(session_id) + if history is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Conversation session not found." + ) + return {"session_id": session_id, "history": history} + + +@app.get("/api/tools") +async def list_tools(): + """Phase 3: Returns the registered tool catalog (MCP-ready).""" + tools = [ + {"name": "repository_search", "description": "Semantic search over indexed repo chunks."}, + {"name": "graph_query", "description": "Query architecture graph, entry points, and flows."}, + {"name": "dependency_lookup", "description": "Lookup packages, frameworks, and databases."}, + {"name": "file_reader", "description": "Retrieve specific source file content."}, + {"name": "architecture_lookup", "description": "Query architecture pattern and key modules."}, + {"name": "api_lookup", "description": "Lookup HTTP routes and authentication methods."} + ] + return {"tools": tools, "count": len(tools)} + +# Serve static frontend build if present +frontend_dist = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")) +if os.path.exists(frontend_dist): + logger.info(f"Serving static frontend files from: {frontend_dist}") + app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="static") +else: + logger.warning(f"Frontend dist folder not found at {frontend_dist}. Running in API-only mode.") + diff --git a/backend/memory/__init__.py b/backend/memory/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a71cc217827ff7567f9741f572d6ac7fe1c511d8 --- /dev/null +++ b/backend/memory/__init__.py @@ -0,0 +1,7 @@ +from memory.embedding_service import EmbeddingService +from memory.vector_store import VectorStore +from memory.knowledge_index import KnowledgeIndexBuilder +from memory.retriever import KnowledgeRetriever +from memory.conversation_manager import conversation_manager +from memory.session_manager import session_manager +from memory.memory_cache import memory_cache diff --git a/backend/memory/__pycache__/__init__.cpython-312.pyc b/backend/memory/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6b141992086152c5b4b81493c3224d2c9c80aca Binary files /dev/null and b/backend/memory/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/conversation_manager.cpython-312.pyc b/backend/memory/__pycache__/conversation_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d2e3bf0ca049f65ad462eccfe5f93b55eb85160 Binary files /dev/null and b/backend/memory/__pycache__/conversation_manager.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/embedding_service.cpython-312.pyc b/backend/memory/__pycache__/embedding_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..251895d37759b7f44d74b5630cafa91981c08e0d Binary files /dev/null and b/backend/memory/__pycache__/embedding_service.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/knowledge_index.cpython-312.pyc b/backend/memory/__pycache__/knowledge_index.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fd923d1e0033bc2f1b1cd4a2416a6e21329668a Binary files /dev/null and b/backend/memory/__pycache__/knowledge_index.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/memory_cache.cpython-312.pyc b/backend/memory/__pycache__/memory_cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55f3b8d1e4d5c6ad835ff24053d07b6b1b6d0acd Binary files /dev/null and b/backend/memory/__pycache__/memory_cache.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/retriever.cpython-312.pyc b/backend/memory/__pycache__/retriever.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ab254ea09f13e42bd799abe18554366d1ecce3b Binary files /dev/null and b/backend/memory/__pycache__/retriever.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/session_manager.cpython-312.pyc b/backend/memory/__pycache__/session_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36658a8db00c691ec77ddc5b304f7725692237a6 Binary files /dev/null and b/backend/memory/__pycache__/session_manager.cpython-312.pyc differ diff --git a/backend/memory/__pycache__/vector_store.cpython-312.pyc b/backend/memory/__pycache__/vector_store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b59eca5f98b6fe7f054ca7594a830fda5ce1c83d Binary files /dev/null and b/backend/memory/__pycache__/vector_store.cpython-312.pyc differ diff --git a/backend/memory/conversation_manager.py b/backend/memory/conversation_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..41a422c63f18a4cd8b854e65ccd73096e94d8b9c --- /dev/null +++ b/backend/memory/conversation_manager.py @@ -0,0 +1,80 @@ +import time +from typing import List, Dict, Any, Optional +from pydantic import BaseModel, Field + +class MessageRecord(BaseModel): + role: str # 'user' | 'assistant' + content: str + timestamp: float = Field(default_factory=time.time) + retrieved_context: Optional[List[Dict[str, Any]]] = None + agent_decisions: Optional[Dict[str, Any]] = None + +class ConversationSession(BaseModel): + session_id: str + repo_id: str + history: List[MessageRecord] = Field(default_factory=list) + summary: str = "" + +class ConversationManager: + """ + Manages multi-session conversation tracking, caching history in-memory. + Persists context, questions, answers, and internal timeline decisions. + """ + def __init__(self): + self._sessions: Dict[str, ConversationSession] = {} + + def get_or_create_session(self, session_id: str, repo_id: str) -> ConversationSession: + if session_id not in self._sessions: + self._sessions[session_id] = ConversationSession(session_id=session_id, repo_id=repo_id) + return self._sessions[session_id] + + def add_message( + self, + session_id: str, + repo_id: str, + role: str, + content: str, + retrieved_context: Optional[List[Dict[str, Any]]] = None, + agent_decisions: Optional[Dict[str, Any]] = None + ) -> MessageRecord: + session = self.get_or_create_session(session_id, repo_id) + record = MessageRecord( + role=role, + content=content, + timestamp=time.time(), + retrieved_context=retrieved_context, + agent_decisions=agent_decisions + ) + session.history.append(record) + + # Keep summary updated with the last user prompt summary or simple description + if role == "user" and not session.summary: + # First query acts as session title/summary + session.summary = content[:40] + ("..." if len(content) > 40 else "") + + return record + + def update_summary(self, session_id: str, summary: str): + if session_id in self._sessions: + self._sessions[session_id].summary = summary + + def get_session(self, session_id: str) -> Optional[ConversationSession]: + return self._sessions.get(session_id) + + def list_sessions_for_repo(self, repo_id: str) -> List[Dict[str, Any]]: + return [ + { + "session_id": s.session_id, + "repo_id": s.repo_id, + "summary": s.summary, + "message_count": len(s.history), + "last_updated": s.history[-1].timestamp if s.history else time.time() + } + for s in self._sessions.values() if s.repo_id == repo_id + ] + + def clear_sessions(self): + self._sessions.clear() + +# Global conversation manager singleton +conversation_manager = ConversationManager() diff --git a/backend/memory/embedding_service.py b/backend/memory/embedding_service.py new file mode 100644 index 0000000000000000000000000000000000000000..dd9a3d7d8d5c11da5885dc8fe792b9f2ff0a5973 --- /dev/null +++ b/backend/memory/embedding_service.py @@ -0,0 +1,46 @@ +import os +import logging +from typing import List, Optional + +logger = logging.getLogger("embedding_service") + +class EmbeddingService: + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key or os.environ.get("GEMINI_API_KEY") + if not self.api_key: + raise ValueError("Gemini API key is required to generate embeddings.") + from google import genai + self.client = genai.Client(api_key=self.api_key) + + def embed_text(self, text: str) -> List[float]: + """Generates embedding for a single text string.""" + try: + response = self.client.models.embed_content( + model='text-embedding-004', + contents=text + ) + if response.embeddings and len(response.embeddings) > 0: + return response.embeddings[0].values + raise ValueError("No embeddings returned from Gemini API.") + except Exception as e: + logger.error(f"Error generating embedding: {e}") + raise e + + def embed_texts(self, texts: List[str]) -> List[List[float]]: + """Generates embeddings for a batch list of text strings.""" + if not texts: + return [] + try: + # Check length to prevent massive batch issues; text-embedding-004 supports bulk + response = self.client.models.embed_content( + model='text-embedding-004', + contents=texts + ) + if response.embeddings and len(response.embeddings) == len(texts): + return [e.values for e in response.embeddings] + elif response.embeddings: + return [e.values for e in response.embeddings] + raise ValueError("No embeddings returned from batch API call.") + except Exception as e: + logger.error(f"Error generating batch embeddings: {e}") + raise e diff --git a/backend/memory/knowledge_index.py b/backend/memory/knowledge_index.py new file mode 100644 index 0000000000000000000000000000000000000000..9334f0387bad0e8bfce35b0c5a6f98df3ad1ce6e --- /dev/null +++ b/backend/memory/knowledge_index.py @@ -0,0 +1,185 @@ +import json +import logging +from typing import List, Dict, Any, Tuple +from memory.embedding_service import EmbeddingService +from memory.vector_store import VectorStore + +logger = logging.getLogger("knowledge_index") + +class KnowledgeIndexBuilder: + def __init__(self, embedding_service: EmbeddingService, vector_store: VectorStore): + self.embedder = embedding_service + self.store = vector_store + + def chunk_text(self, text: str, chunk_size: int = 1000, overlap: int = 100) -> List[str]: + """Simple sliding window text splitter.""" + if not text: + return [] + chunks = [] + start = 0 + text_len = len(text) + + while start < text_len: + end = min(start + chunk_size, text_len) + chunks.append(text[start:end]) + start += chunk_size - overlap + + # Prevent infinite loop if overlap >= chunk_size + if chunk_size - overlap <= 0: + break + + return chunks + + def build_index( + self, + repo_id: str, + profile: Dict[str, Any], + summary: Dict[str, Any], + graph: Dict[str, Any], + report: str, + flat_files: List[Dict[str, Any]] + ): + """Builds and indexes a repository's code, structure, and profile metadata into ChromaDB.""" + logger.info(f"Starting index build for repository: {repo_id}") + + # Clear existing collection if any + self.store.delete_collection(repo_id) + + documents: List[str] = [] + metadatas: List[Dict[str, Any]] = [] + ids: List[str] = [] + + # Helper to generate unique IDs + def add_chunk(content: str, metadata: Dict[str, Any], prefix: str): + chunk_id = f"{prefix}_{len(documents)}" + documents.append(content) + metadatas.append(metadata) + ids.append(chunk_id) + + # 1. Index the Repository Intelligence Report (Markdown) + report_chunks = self.chunk_text(report, chunk_size=1000, overlap=100) + for i, chunk in enumerate(report_chunks): + add_chunk( + content=chunk, + metadata={"category": "report", "chunk_index": i}, + prefix="report" + ) + + # 2. Index the Summary details + elevator_pitch = summary.get("elevator_pitch", "") + if elevator_pitch: + add_chunk( + content=f"Project Elevator Pitch:\n{elevator_pitch}", + metadata={"category": "summary", "subcategory": "elevator_pitch"}, + prefix="summary_pitch" + ) + + for feat in summary.get("core_features", []): + add_chunk( + content=f"Core Feature: {feat}", + metadata={"category": "summary", "subcategory": "core_feature"}, + prefix="summary_feat" + ) + + for start_point in summary.get("developer_start_points", []): + add_chunk( + content=f"Developer Starting Point File: {start_point}", + metadata={"category": "summary", "subcategory": "developer_start_point"}, + prefix="summary_start" + ) + + # 3. Index Profile Metadata (Architecture, APIs, Auth, Dependencies) + arch_pattern = profile.get("architecture_pattern", "") + if arch_pattern: + add_chunk( + content=f"Architecture Pattern: {arch_pattern}", + metadata={"category": "architecture", "pattern": arch_pattern}, + prefix="profile_arch" + ) + + for auth_method in profile.get("authentication_methods", []): + add_chunk( + content=f"Authentication / Security Method: {auth_method}", + metadata={"category": "authentication", "method": auth_method}, + prefix="profile_auth" + ) + + for endpoint in profile.get("api_endpoints", []): + add_chunk( + content=f"API Endpoint / Route: {endpoint}", + metadata={"category": "api", "endpoint": endpoint}, + prefix="profile_api" + ) + + for dep in profile.get("dependencies", []): + add_chunk( + content=f"Package Dependency: {dep}", + metadata={"category": "dependency", "dependency": dep}, + prefix="profile_dep" + ) + + # 4. Index Business Flows & Concepts from Graph + for flow in graph.get("business_flows", []): + flow_name = flow.get("flow_name", "") + flow_desc = flow.get("description", "") + flow_steps = ", ".join(flow.get("steps", [])) + add_chunk( + content=f"Business Flow: {flow_name}\nDescription: {flow_desc}\nSteps: {flow_steps}", + metadata={"category": "business_flow", "flow_name": flow_name}, + prefix="graph_flow" + ) + + for concept in graph.get("concepts", []): + concept_name = concept.get("name", "") + concept_desc = concept.get("description", "") + concept_files = ", ".join(concept.get("files", [])) + add_chunk( + content=f"Core Concept: {concept_name}\nDescription: {concept_desc}\nFiles: {concept_files}", + metadata={"category": "concept", "concept_name": concept_name}, + prefix="graph_concept" + ) + + # 5. Index Source Code Files Content + for f in flat_files: + file_path = f.get("path", "") + content = f.get("content", "") + file_size = f.get("size", 0) + + # Skip massive files to avoid cluttering vector space + if file_size > 100 * 1024 or not content: + continue + + file_chunks = self.chunk_text(content, chunk_size=1000, overlap=100) + for j, chunk in enumerate(file_chunks): + # Include path info inside the document text to maintain retrieval association + chunk_content = f"File: {file_path} (Chunk {j+1}/{len(file_chunks)})\n\n{chunk}" + add_chunk( + content=chunk_content, + metadata={"category": "file", "path": file_path, "chunk_index": j}, + prefix="file_chunk" + ) + + if not documents: + logger.info("No documents found to index.") + return + + # 6. Generate Embeddings in Batches of 50 to respect API rate limits and connection pooling + batch_size = 50 + logger.info(f"Generating embeddings for {len(documents)} document chunks in batches of {batch_size}...") + + all_embeddings: List[List[float]] = [] + for start_idx in range(0, len(documents), batch_size): + end_idx = min(start_idx + batch_size, len(documents)) + batch_docs = documents[start_idx:end_idx] + batch_embeddings = self.embedder.embed_texts(batch_docs) + all_embeddings.extend(batch_embeddings) + + # 7. Write to Vector Store + self.store.add_documents( + repo_id=repo_id, + documents=documents, + metadatas=metadatas, + ids=ids, + embeddings=all_embeddings + ) + logger.info(f"Vector database indexing complete. Indexed {len(documents)} chunks.") diff --git a/backend/memory/memory_cache.py b/backend/memory/memory_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..cb748dd4eee79aeb3e7e9ec1523118e5d72950ad --- /dev/null +++ b/backend/memory/memory_cache.py @@ -0,0 +1,42 @@ +import time +from typing import Dict, Any, Optional + +class CacheEntry: + def __init__(self, value: Any, ttl: Optional[float] = None): + self.value = value + self.expires_at = time.time() + ttl if ttl is not None else None + + def is_expired(self) -> bool: + if self.expires_at is None: + return False + return time.time() > self.expires_at + +class MemoryCache: + """ + In-memory cache with TTL support for accelerating AI agent executions + and caching heavy search/graph query results. + """ + def __init__(self): + self._cache: Dict[str, CacheEntry] = {} + + def get(self, key: str) -> Optional[Any]: + entry = self._cache.get(key) + if not entry: + return None + if entry.is_expired(): + del self._cache[key] + return None + return entry.value + + def set(self, key: str, value: Any, ttl: Optional[float] = None): + self._cache[key] = CacheEntry(value, ttl) + + def delete(self, key: str): + if key in self._cache: + del self._cache[key] + + def clear(self): + self._cache.clear() + +# Global memory cache singleton +memory_cache = MemoryCache() diff --git a/backend/memory/retriever.py b/backend/memory/retriever.py new file mode 100644 index 0000000000000000000000000000000000000000..a60d0b854aa55ee304ff69be28d3dd5829217e35 --- /dev/null +++ b/backend/memory/retriever.py @@ -0,0 +1,43 @@ +import logging +from typing import List, Dict, Any, Optional +from memory.embedding_service import EmbeddingService +from memory.vector_store import VectorStore + +logger = logging.getLogger("retriever") + +class KnowledgeRetriever: + def __init__(self, embedding_service: EmbeddingService, vector_store: VectorStore): + self.embedder = embedding_service + self.store = vector_store + + def retrieve( + self, + repo_id: str, + query: str, + top_k: int = 5, + category: Optional[str] = None + ) -> List[Dict[str, Any]]: + """ + Runs semantic search on ChromaDB for the repo using the query. + Supports filtering by metadata 'category'. + """ + logger.info(f"Retrieving context for repo {repo_id}, query: '{query}' (category filter: {category}, top_k: {top_k})") + + try: + # Generate query vector using embedder + query_embedding = self.embedder.embed_text(query) + + # Setup filter + where_filter = None + if category: + where_filter = {"category": category} + + return self.store.query_documents( + repo_id=repo_id, + query_embedding=query_embedding, + top_k=top_k, + where_filter=where_filter + ) + except Exception as e: + logger.error(f"Failed to retrieve vector search results: {e}") + return [] diff --git a/backend/memory/session_manager.py b/backend/memory/session_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..e873c94f424937f2dfc8b7df292d737e57186fa0 --- /dev/null +++ b/backend/memory/session_manager.py @@ -0,0 +1,65 @@ +import os +import json +import logging +from typing import Dict, Any, List, Optional +from pydantic import BaseModel +from memory.conversation_manager import conversation_manager, ConversationSession, MessageRecord + +logger = logging.getLogger("session_manager") + +class SessionManager: + """ + Manages persistence and high-level metadata for multiple repository sessions. + Saves and loads conversation history and repository state to/from disk. + """ + def __init__(self, storage_dir: str = None): + if storage_dir is None: + storage_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "storage") + self.storage_dir = storage_dir + os.makedirs(self.storage_dir, exist_ok=True) + self.conversations_file = os.path.join(self.storage_dir, "conversations.json") + self.load_all() + + def load_all(self): + if os.path.exists(self.conversations_file): + try: + with open(self.conversations_file, "r", encoding="utf-8") as f: + data = json.load(f) + for session_id, s_data in data.items(): + session = ConversationSession( + session_id=session_id, + repo_id=s_data.get("repo_id"), + summary=s_data.get("summary", ""), + history=[ + MessageRecord(**msg) for msg in s_data.get("history", []) + ] + ) + conversation_manager._sessions[session_id] = session + logger.info(f"Loaded conversations from {self.conversations_file}") + except Exception as e: + logger.error(f"Error loading conversations: {e}") + + def save_all(self): + data = {} + for session_id, session in conversation_manager._sessions.items(): + data[session_id] = { + "session_id": session.session_id, + "repo_id": session.repo_id, + "summary": session.summary, + "history": [msg.model_dump() for msg in session.history] + } + try: + with open(self.conversations_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + logger.info(f"Saved conversations to {self.conversations_file}") + except Exception as e: + logger.error(f"Error saving conversations: {e}") + + def get_session_history(self, session_id: str) -> Optional[List[Dict[str, Any]]]: + session = conversation_manager.get_session(session_id) + if not session: + return None + return [msg.model_dump() for msg in session.history] + +# Global session manager singleton +session_manager = SessionManager() diff --git a/backend/memory/vector_store.py b/backend/memory/vector_store.py new file mode 100644 index 0000000000000000000000000000000000000000..0d257e1340de0d2a1a373c53ac4ef24bb8f6aa90 --- /dev/null +++ b/backend/memory/vector_store.py @@ -0,0 +1,95 @@ +import os +import logging +from typing import Dict, Any, List, Optional +import chromadb + +logger = logging.getLogger("vector_store") + +class VectorStore: + def __init__(self, storage_path: Optional[str] = None): + self.storage_path = storage_path or os.environ.get("CHROMA_DB_PATH", "./chroma_db") + os.makedirs(self.storage_path, exist_ok=True) + # Initialize persistent ChromaDB client + self.client = chromadb.PersistentClient(path=self.storage_path) + + def get_collection(self, repo_id: str): + # Convert UUID repo_id to a valid Chroma collection name (alphanumeric and underscores) + coll_name = f"repo_{repo_id.replace('-', '_')}" + return self.client.get_or_create_collection( + name=coll_name, + metadata={"hnsw:space": "cosine"} + ) + + def add_documents( + self, + repo_id: str, + documents: List[str], + metadatas: List[Dict[str, Any]], + ids: List[str], + embeddings: List[List[float]] + ): + """Adds documents with their corresponding embeddings and metadata.""" + if not documents: + return + collection = self.get_collection(repo_id) + collection.add( + documents=documents, + metadatas=metadatas, + ids=ids, + embeddings=embeddings + ) + logger.info(f"Added {len(documents)} chunks to vector store collection for repo {repo_id}") + + def query_documents( + self, + repo_id: str, + query_embedding: List[float], + top_k: int = 5, + where_filter: Optional[Dict[str, Any]] = None + ) -> List[Dict[str, Any]]: + """Queries ChromaDB using the query vector, returning matching chunks with similarity scores.""" + collection = self.get_collection(repo_id) + + # Query ChromaDB + results = collection.query( + query_embeddings=[query_embedding], + n_results=top_k, + where=where_filter + ) + + formatted = [] + if results and "documents" in results and len(results["documents"]) > 0: + docs = results["documents"][0] + metas = results["metadatas"][0] if results.get("metadatas") else [{} for _ in range(len(docs))] + ids = results["ids"][0] if results.get("ids") else [str(i) for i in range(len(docs))] + distances = results["distances"][0] if results.get("distances") else [0.0 for _ in range(len(docs))] + + for doc, meta, doc_id, dist in zip(docs, metas, ids, distances): + # Cosine distance to similarity: 1 - distance + similarity = 1.0 - dist + formatted.append({ + "id": doc_id, + "content": doc, + "metadata": meta, + "similarity": round(similarity, 4), + "distance": round(dist, 4) + }) + + # Sort by similarity descending + formatted.sort(key=lambda x: x["similarity"], reverse=True) + return formatted + + def delete_collection(self, repo_id: str): + coll_name = f"repo_{repo_id.replace('-', '_')}" + try: + self.client.delete_collection(name=coll_name) + logger.info(f"Deleted vector store collection: {coll_name}") + except Exception as e: + logger.warning(f"Could not delete collection {coll_name}: {e}") + + def count_documents(self, repo_id: str) -> int: + try: + collection = self.get_collection(repo_id) + return collection.count() + except Exception: + return 0 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2222fd8ad2ae18bc2e182dae5c4b32d87117d2ae --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.115.6 +uvicorn==0.34.0 +pydantic==2.10.4 +google-genai==1.14.0 +python-multipart==0.0.20 +httpx==0.28.1 +chromadb==1.5.9 diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a70b3029a5ce0e22988567ea083fca68daa2141b --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1 @@ +# Services package diff --git a/backend/services/__pycache__/__init__.cpython-312.pyc b/backend/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84ff6dc4683835b57bec71e10523937a2d3929db Binary files /dev/null and b/backend/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/services/__pycache__/graphBuilder.cpython-312.pyc b/backend/services/__pycache__/graphBuilder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85c09f6e18adaca6f1147211803f46c9be6c3a42 Binary files /dev/null and b/backend/services/__pycache__/graphBuilder.cpython-312.pyc differ diff --git a/backend/services/__pycache__/llmAnalyzer.cpython-312.pyc b/backend/services/__pycache__/llmAnalyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cd8e2307349f04fbb9c5ab57e28749bc0822fa1 Binary files /dev/null and b/backend/services/__pycache__/llmAnalyzer.cpython-312.pyc differ diff --git a/backend/services/__pycache__/repositoryMemory.cpython-312.pyc b/backend/services/__pycache__/repositoryMemory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20f837044fa3d06cb69698698fbdec5424309a11 Binary files /dev/null and b/backend/services/__pycache__/repositoryMemory.cpython-312.pyc differ diff --git a/backend/services/__pycache__/repositoryProfiler.cpython-312.pyc b/backend/services/__pycache__/repositoryProfiler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c46af2638fc1b2b02e61d34130945e54cfe5be5c Binary files /dev/null and b/backend/services/__pycache__/repositoryProfiler.cpython-312.pyc differ diff --git a/backend/services/__pycache__/repositoryScanner.cpython-312.pyc b/backend/services/__pycache__/repositoryScanner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86cf498171008b64fb288c1d3ca29b246ad63f59 Binary files /dev/null and b/backend/services/__pycache__/repositoryScanner.cpython-312.pyc differ diff --git a/backend/services/graphBuilder.py b/backend/services/graphBuilder.py new file mode 100644 index 0000000000000000000000000000000000000000..29834fffe66ac3b581a0aa680566d0297bfa99f9 --- /dev/null +++ b/backend/services/graphBuilder.py @@ -0,0 +1,126 @@ +import os +import re +from typing import Dict, List, Any + +# Simple regexes to find imports +PYTHON_IMPORT_RE = re.compile(r'^\s*(?:import\s+([\w\.,\s]+)|from\s+([\w\.]+)\s+import\s+([\w\.,\s\*]+))') +JS_IMPORT_RE = re.compile(r'(?:import\s+(?:[\w\s\{\}\*\,]+from\s+)?[\'"]([^\'"]+)[\'"]|require\([\'"]([^\'"]+)[\'"]\))') + +def clean_python_import(imp_str: str) -> List[str]: + """ + Cleans Python import strings. E.g. 'sys, os' -> ['sys', 'os'] + """ + if not imp_str: + return [] + return [i.strip().split('.')[0] for i in imp_str.split(',') if i.strip()] + +def extract_python_imports(content: str) -> List[str]: + """ + Finds Python imports in code content. + """ + imports = [] + for line in content.splitlines(): + match = PYTHON_IMPORT_RE.match(line) + if match: + group1, group2, _ = match.groups() + if group1: + # import X, Y + imports.extend(clean_python_import(group1)) + if group2: + # from X import Y + # extract X + parts = group2.split('.') + if parts: + imports.append(parts[0]) + return list(set(imports)) + +def extract_js_imports(content: str) -> List[str]: + """ + Finds JavaScript/TypeScript imports in code content. + """ + imports = [] + matches = JS_IMPORT_RE.findall(content) + for match in matches: + group1, group2 = match + imported = group1 or group2 + if imported: + # Clean up paths (e.g. "./utils" or "lodash") + # If it starts with . or .., it is local, otherwise it's a library/module + imports.append(imported) + return list(set(imports)) + +def build_initial_graph(files: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Statically analyzes files to generate nodes (files) and edges (imports/references). + """ + nodes = [] + edges = [] + + # 1. Add file nodes + for f in files: + path = f["path"] + _, ext = os.path.splitext(path.lower()) + + # Determine node type based on extension + node_type = "file" + if ext in [".js", ".ts", ".jsx", ".tsx", ".py", ".go", ".rs", ".java"]: + node_type = "module" + elif ext in [".json", ".yaml", ".yml"]: + node_type = "config" + elif ext in [".html", ".css", ".scss"]: + node_type = "ui" + + nodes.append({ + "id": path, + "label": os.path.basename(path), + "type": node_type, + "properties": { + "path": path, + "size": f["size"] + } + }) + + # 2. Extract dependencies (edges) + for f in files: + path = f["path"] + content = f.get("content", "") + _, ext = os.path.splitext(path.lower()) + + imports = [] + if ext == ".py": + imports = extract_python_imports(content) + elif ext in [".js", ".jsx", ".ts", ".tsx"]: + imports = extract_js_imports(content) + + for imp in imports: + # Check if this import resolves to a local file in the scanned repository + # Simple matching: see if the import string is in any file path or matches file basenames + target_path = None + + # Case 1: Import matches local file name directly or path-wise + for other_f in files: + other_path = other_f["path"] + other_base, _ = os.path.splitext(os.path.basename(other_path)) + + # Check relative match or direct name match + if imp == other_base or imp.endswith(other_base) or other_path.endswith(imp): + target_path = other_path + break + + if target_path and target_path != path: + edges.append({ + "source": path, + "target": target_path, + "type": "imports", + "label": "imports" + }) + else: + # Case 2: Package/Library import (not in local files) + # We can add a node for the external library if it's important, + # but for Phase 1 skeleton we just skip external imports or let LLM do the mapping. + pass + + return { + "nodes": nodes, + "edges": edges + } diff --git a/backend/services/llmAnalyzer.py b/backend/services/llmAnalyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..3969fc72a0d29c50b01afa6f019628aeb27cc116 --- /dev/null +++ b/backend/services/llmAnalyzer.py @@ -0,0 +1,243 @@ +import os +import json +import logging +from typing import List, Dict, Any, Optional +from pydantic import BaseModel, Field + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# Define Pydantic response schemas to guarantee valid JSON structure from Gemini +class ProfileSchema(BaseModel): + project_name: str = Field(description="Name of the software project") + project_type: str = Field(description="Type of the project, e.g. web app, CLI, library, etc.") + languages: List[str] = Field(description="Programming languages used in the repository") + frameworks: List[str] = Field(description="Frameworks used (e.g. Django, FastAPI, React)") + databases: List[str] = Field(description="Databases detected (e.g. PostgreSQL, Redis, MongoDB)") + authentication_methods: List[str] = Field(description="Security/authentication methods (e.g. JWT, OAuth, session)") + major_modules: List[str] = Field(description="Key modules or packages in the codebase") + api_endpoints: List[str] = Field(description="List of core API routes / endpoints discovered") + important_files: List[str] = Field(description="Important files for configuring or understanding the app") + architecture_pattern: str = Field(description="The primary architectural pattern (e.g. MVC, Clean Architecture, Monolith)") + dependencies: List[str] = Field(description="Main dependencies or libraries used") + + +class SummarySchema(BaseModel): + elevator_pitch: str = Field(description="A 2-3 sentence overview of the codebase and its purpose") + core_features: List[str] = Field(description="List of core features implemented in the repository") + main_workflows: List[str] = Field(description="Major developer or user workflows identified") + key_components: List[str] = Field(description="Key code components or classes") + key_risks: List[str] = Field(description="Potential architectural risks or legacy blocks") + developer_start_points: List[str] = Field(description="Suggested files or modules where a developer should start reading") + + +class NodeSchema(BaseModel): + id: str = Field(description="Unique ID for the node, e.g. the path or the identifier") + label: str = Field(description="Human-readable label for the node") + type: str = Field(description="Type of the node (module, api, database, entrypoint, file)") + properties: Dict[str, Any] = Field(default_factory=dict, description="Custom properties metadata") + + +class EdgeSchema(BaseModel): + source: str = Field(description="ID of the source node") + target: str = Field(description="ID of the target node") + type: str = Field(description="Type of relation (imports, calls, data_flow, ownership)") + label: str = Field(description="Human-readable label for the relation") + + +class EntryPointSchema(BaseModel): + file_path: str = Field(description="Path to the execution entry point file") + type: str = Field(description="Type of execution path, e.g. script, dev server, wsgi") + description: str = Field(description="Description of what this entry point runs") + + +class BusinessFlowSchema(BaseModel): + flow_name: str = Field(description="Name of the business process flow") + description: str = Field(description="Summary of what the workflow does") + steps: List[str] = Field(description="Ordered list of node IDs involved in the workflow") + + +class CriticalPathSchema(BaseModel): + path_name: str = Field(description="Name of the performance-critical path") + description: str = Field(description="Explanation of why this path is critical") + nodes: List[str] = Field(description="List of node IDs in this path") + + +class ConceptSchema(BaseModel): + name: str = Field(description="Conceptual area, e.g. State Management, Caching") + description: str = Field(description="How this concept is implemented") + files: List[str] = Field(description="Files corresponding to this concept") + + +class GraphSchema(BaseModel): + nodes: List[NodeSchema] = Field(description="Nodes in the topology graph") + edges: List[EdgeSchema] = Field(description="Directed edges in the topology graph") + entry_points: List[EntryPointSchema] = Field(description="Identified application entry points") + business_flows: List[BusinessFlowSchema] = Field(description="Primary user/business flows") + critical_paths: List[CriticalPathSchema] = Field(description="Performance-critical execution paths") + concepts: List[ConceptSchema] = Field(description="Core concepts mapped to code files") + + +class AnalysisResponse(BaseModel): + report: str = Field(description="A beautifully formatted Markdown Repository Intelligence Report") + profile: ProfileSchema = Field(description="Structured metadata profiling the project tech stack and components") + summary: SummarySchema = Field(description="High-level summaries and onboarding metadata") + graph: GraphSchema = Field(description="Topology graph of file and module relations") + + +def select_important_files(files: List[Dict[str, Any]], max_tokens: int = 150000) -> List[Dict[str, Any]]: + """ + Selects files that are most structurally important first (README, manifests, entry points, + routes, controllers, services, models) to prevent exceeding prompt token/context limits on + large repositories. + """ + def get_file_priority(f: Dict[str, Any]) -> int: + path_lower = f["path"].lower() + parts = [p.strip() for p in path_lower.split("/") if p.strip()] + filename = parts[-1] if parts else "" + + # Priority 0: Critical manifests, config registries and high-level summaries + p0_exact = { + "readme.md", "readme.txt", "package.json", "requirements.txt", + "pyproject.toml", "go.mod", "cargo.toml", "pom.xml", "build.gradle" + } + if filename in p0_exact: + return 0 + + # Priority 1: Key execution entry points + p1_exact = { + "main.py", "app.py", "server.js", "index.js", "wsgi.py", "asgi.py" + } + if filename in p1_exact: + return 1 + + # Priority 2: Key architectural components / directories + p2_dirs = { + "routes", "controllers", "services", "models", "api", "handlers", "views" + } + if any(d in parts for d in p2_dirs): + return 2 + + # Priority 3: Other general code files + return 3 + + sorted_files = sorted(files, key=get_file_priority) + + selected = [] + current_size = 0 + # Estimate: roughly 4 characters per token + char_limit = max_tokens * 4 + + for f in sorted_files: + content_len = len(f.get("content", "")) + # Filter files larger than 50KB to protect context window spacing + if f["size"] > 50 * 1024: + logger.info(f"Skipping file content for {f['path']} - file size is larger than 50KB.") + continue + + if current_size + content_len <= char_limit: + selected.append(f) + current_size += content_len + else: + logger.info(f"Skipping content of file {f['path']} due to context size limit.") + + return selected + + +async def analyze_repository( + repo_name: str, + tree_structure: Dict[str, Any], + static_profile: Dict[str, Any], + flat_files: List[Dict[str, Any]], + api_key: Optional[str] = None +) -> Dict[str, Any]: + """ + Generates a Repository Intelligence Report, profile.json, summary.json, and graph.json + using Google Gemini via the new google-genai SDK. + """ + # API Key selection: Try parameter first, then environment variable + key = api_key or os.environ.get("GEMINI_API_KEY") + if not key: + raise ValueError("Gemini API Key is missing. Please configure it in the backend or frontend.") + + # Import and configure the new google-genai SDK + from google import genai + + client = genai.Client(api_key=key) + + # Select important files content to keep under budget + context_files = select_important_files(flat_files) + + # Format files for prompt ingestion + formatted_code = "" + for f in context_files: + formatted_code += f"\n\n--- File: {f['path']} ---\n" + formatted_code += f.get("content", "") + formatted_code += "\n--- End File ---" + + # Convert tree structure to JSON string + tree_str = json.dumps(tree_structure, indent=2) + # Convert static profile to JSON string + profile_str = json.dumps(static_profile, indent=2) + + prompt = f""" +You are an expert Principal Software Engineer analyzing the source code repository of the project named '{repo_name}'. + +Your goal is to perform a deep structural and conceptual analysis of this repository, thinking like a senior engineer who has spent 30 minutes reading the codebase. Do not just summarize files. Infer business logic, workflows, entrypoints, database models, user flows, service interactions, authentication paths, and key concepts. + +Below is the repository context: + +1. STATIC PROFILE: +{profile_str} + +2. FILE DIRECTORY STRUCTURE: +{tree_str} + +3. PRIMARY CODE FILES CONTENT: +{formatted_code} + +You must analyze the repository context and return the structured outputs matching the schema. +""" + + try: + logger.info("Sending request to Gemini via google-genai SDK with response_schema...") + + # Use the new client-based API with the response_schema configuration parameter + response = client.models.generate_content( + model='gemini-2.5-flash', + contents=prompt, + config={ + 'response_mime_type': 'application/json', + 'response_schema': AnalysisResponse, + 'temperature': 0.2 + } + ) + + # Parse the structured JSON response + response_text = response.text + if not response_text: + raise Exception("Gemini returned an empty response. Please check your API key and quota.") + + result_json = json.loads(response_text) + return result_json + + except json.JSONDecodeError as e: + logger.error(f"Failed to decode response from Gemini as JSON: {e}") + raise Exception(f"Gemini API returned an invalid JSON response structure. Please retry. Error: {str(e)}") + except Exception as e: + error_msg = str(e) + logger.error(f"Error communicating with Gemini: {error_msg}") + # Provide more helpful error messages + if "API_KEY_INVALID" in error_msg or "401" in error_msg: + raise Exception( + "Invalid Gemini API Key. Please get a valid key from https://aistudio.google.com/apikey " + "and set it in the .env file or pass it via the frontend." + ) + elif "RESOURCE_EXHAUSTED" in error_msg or "429" in error_msg: + raise Exception( + "Gemini API rate limit exceeded. Please wait a moment and try again, " + "or upgrade your API quota at https://aistudio.google.com." + ) + raise Exception(f"Gemini analysis execution failed: {error_msg}") diff --git a/backend/services/repositoryMemory.py b/backend/services/repositoryMemory.py new file mode 100644 index 0000000000000000000000000000000000000000..de05e31c294f4b1052d77ca5ace5da4b08bfed1a --- /dev/null +++ b/backend/services/repositoryMemory.py @@ -0,0 +1,118 @@ +import os +import json +from typing import Dict, Any, Optional + +class RepositoryMemoryService: + """ + Foundational data and memory layer for repository intelligence. + Manages in-memory caching with disk persistence for structured + artifacts (profile, graph, summary, and report). + """ + + def __init__(self): + self._memory_db: Dict[str, Dict[str, Any]] = {} + self.storage_dir = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "storage", "repos" + ) + os.makedirs(self.storage_dir, exist_ok=True) + self._load_persisted() + + def _repo_dir(self, repo_id: str) -> str: + return os.path.join(self.storage_dir, repo_id) + + def _load_persisted(self) -> None: + if not os.path.isdir(self.storage_dir): + return + for repo_id in os.listdir(self.storage_dir): + repo_dir = self._repo_dir(repo_id) + if not os.path.isdir(repo_dir): + continue + try: + profile_path = os.path.join(repo_dir, "repository_profile.json") + graph_path = os.path.join(repo_dir, "repository_graph.json") + summary_path = os.path.join(repo_dir, "repository_summary.json") + report_path = os.path.join(repo_dir, "repository_report.md") + if not all(os.path.exists(p) for p in (profile_path, graph_path, summary_path, report_path)): + continue + with open(profile_path, "r", encoding="utf-8") as f: + profile = json.load(f) + with open(graph_path, "r", encoding="utf-8") as f: + graph = json.load(f) + with open(summary_path, "r", encoding="utf-8") as f: + summary = json.load(f) + with open(report_path, "r", encoding="utf-8") as f: + report = f.read() + self._memory_db[repo_id] = { + "repo_id": repo_id, + "profile": profile, + "graph": graph, + "summary": summary, + "report": report, + } + except (json.JSONDecodeError, OSError): + continue + + def store( + self, + repo_id: str, + profile: Dict[str, Any], + graph: Dict[str, Any], + summary: Dict[str, Any], + report_markdown: str + ) -> Dict[str, Any]: + """ + Stores repository intelligence artifacts in memory and on disk. + """ + intelligence_payload = { + "repo_id": repo_id, + "profile": profile, + "graph": graph, + "summary": summary, + "report": report_markdown + } + self._memory_db[repo_id] = intelligence_payload + self.export_as_json_files(repo_id, self._repo_dir(repo_id)) + return intelligence_payload + + def retrieve(self, repo_id: str) -> Optional[Dict[str, Any]]: + """ + Retrieves repository intelligence artifacts by repo ID. + """ + return self._memory_db.get(repo_id) + + def export_as_json_files(self, repo_id: str, output_dir: str) -> Dict[str, str]: + """ + Writes structured intelligence files to a target directory. + """ + data = self.retrieve(repo_id) + if not data: + raise ValueError(f"No intelligence artifacts found for repo: {repo_id}") + + os.makedirs(output_dir, exist_ok=True) + + profile_path = os.path.join(output_dir, "repository_profile.json") + graph_path = os.path.join(output_dir, "repository_graph.json") + summary_path = os.path.join(output_dir, "repository_summary.json") + report_path = os.path.join(output_dir, "repository_report.md") + + with open(profile_path, "w", encoding="utf-8") as f: + json.dump(data["profile"], f, indent=2) + + with open(graph_path, "w", encoding="utf-8") as f: + json.dump(data["graph"], f, indent=2) + + with open(summary_path, "w", encoding="utf-8") as f: + json.dump(data["summary"], f, indent=2) + + with open(report_path, "w", encoding="utf-8") as f: + f.write(data["report"]) + + return { + "profile": profile_path, + "graph": graph_path, + "summary": summary_path, + "report": report_path + } + +# Global singleton service instance for easy access across the controllers +memory_service = RepositoryMemoryService() diff --git a/backend/services/repositoryProfiler.py b/backend/services/repositoryProfiler.py new file mode 100644 index 0000000000000000000000000000000000000000..81d60c9b17f01a1cfa88e6182808b34a4908a4ca --- /dev/null +++ b/backend/services/repositoryProfiler.py @@ -0,0 +1,278 @@ +import os +import json +import re +from typing import Dict, List, Any + +# Map extension to programming languages +EXTENSION_MAP = { + ".py": "Python", + ".js": "JavaScript", + ".jsx": "JavaScript (React)", + ".ts": "TypeScript", + ".tsx": "TypeScript (React)", + ".go": "Go", + ".java": "Java", + ".kt": "Kotlin", + ".cs": "C#", + ".cpp": "C++", + ".c": "C", + ".h": "C/C++ Header", + ".rs": "Rust", + ".rb": "Ruby", + ".php": "PHP", + ".swift": "Swift", + ".sh": "Shell Script", + ".bat": "Batch Script", + ".ps1": "PowerShell Script", + ".sql": "SQL", + ".html": "HTML", + ".css": "CSS", + ".scss": "SCSS", + ".yaml": "YAML Config", + ".yml": "YAML Config", + ".json": "JSON Config", + ".md": "Markdown", + ".tf": "Terraform", + ".dockerfile": "Dockerfile" +} + +def detect_languages_from_files(files: List[Dict[str, Any]]) -> Dict[str, int]: + """ + Counts frequency of detected languages based on file extensions. + """ + lang_counts = {} + for f in files: + path = f["path"] + _, ext = os.path.splitext(path.lower()) if 'os' in globals() else (None, "." + path.split(".")[-1] if "." in path else "") + lang = EXTENSION_MAP.get(ext) + if lang: + lang_counts[lang] = lang_counts.get(lang, 0) + 1 + return lang_counts + +def parse_package_json(content: str) -> Dict[str, Any]: + """ + Parses Node.js dependencies and frameworks from package.json. + """ + result = { + "frameworks": [], + "libraries": [], + "databases": [], + "package_manager": "npm" + } + try: + data = json.loads(content) + deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})} + + # Detect Frameworks & key libraries + framework_signatures = { + "react": "React", + "vue": "Vue", + "angular": "Angular", + "@angular/core": "Angular", + "next": "Next.js", + "nuxt": "Nuxt.js", + "express": "Express", + "koa": "Koa", + "nest": "NestJS", + "@nestjs/core": "NestJS", + "svelte": "Svelte", + "gatsby": "Gatsby", + "fastify": "Fastify" + } + + db_signatures = { + "mongoose": "MongoDB (Mongoose)", + "mongodb": "MongoDB", + "pg": "PostgreSQL (pg)", + "mysql": "MySQL", + "mysql2": "MySQL (mysql2)", + "sequelize": "Sequelize ORM", + "prisma": "Prisma ORM", + "redis": "Redis", + "ioredis": "Redis (ioredis)", + "sqlite3": "SQLite" + } + + for dep_name in deps: + for sig, label in framework_signatures.items(): + if sig == dep_name or dep_name.startswith(sig + "/"): + if label not in result["frameworks"]: + result["frameworks"].append(label) + + for sig, label in db_signatures.items(): + if sig == dep_name: + if label not in result["databases"]: + result["databases"].append(label) + + # Standard dependencies of interest + if dep_name in ["axios", "lodash", "rxjs", "dotenv", "webpack", "vite", "typescript"]: + result["libraries"].append(dep_name) + + except Exception: + pass + return result + +def parse_requirements_txt(content: str) -> Dict[str, Any]: + """ + Parses Python dependencies and frameworks from requirements.txt. + """ + result = { + "frameworks": [], + "libraries": [], + "databases": [], + "package_manager": "pip" + } + + # Simple regex to split lines and strip versions + lines = content.splitlines() + deps = [] + for line in lines: + line = line.strip() + if not line or line.startswith("#"): + continue + # Remove version requirements e.g. fastapi>=0.100.0 or django==4.0 + parts = re.split(r'[=<>~]', line) + if parts: + dep_name = parts[0].strip().lower() + deps.append(dep_name) + + framework_signatures = { + "django": "Django", + "flask": "Flask", + "fastapi": "FastAPI", + "tornado": "Tornado", + "pyramid": "Pyramid", + "sanic": "Sanic" + } + + db_signatures = { + "psycopg2": "PostgreSQL (psycopg2)", + "psycopg2-binary": "PostgreSQL", + "pymongo": "MongoDB", + "redis": "Redis", + "sqlalchemy": "SQLAlchemy ORM", + "tortoise-orm": "Tortoise ORM", + "peewee": "Peewee ORM", + "mysqlclient": "MySQL", + "pymysql": "MySQL (PyMySQL)" + } + + for dep in deps: + for sig, label in framework_signatures.items(): + if sig == dep: + if label not in result["frameworks"]: + result["frameworks"].append(label) + + for sig, label in db_signatures.items(): + if sig == dep: + if label not in result["databases"]: + result["databases"].append(label) + + if dep in ["requests", "numpy", "pandas", "scipy", "celery", "pydantic", "jinja2", "cryptography"]: + result["libraries"].append(dep) + + return result + +def profile_repository(files: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Orchestrates scan file inspection and compiles framework, database, language, + and build metadata profiles. + """ + profile = { + "languages": {}, + "frameworks": [], + "libraries": [], + "databases": [], + "package_managers": [], + "infrastructure": [] + } + + # Calculate languages + for f in files: + path = f["path"] + # Extract extension + ext = "." + path.split(".")[-1] if "." in path else "" + lang = EXTENSION_MAP.get(ext.lower()) + if lang: + profile["languages"][lang] = profile["languages"].get(lang, 0) + 1 + + # Normalize language frequencies to sorted list + sorted_langs = sorted(profile["languages"].items(), key=lambda x: x[1], reverse=True) + profile["languages"] = [l[0] for l in sorted_langs] + + # Look for files of interest + for f in files: + path = f["path"].lower() + content = f.get("content", "") + + # package.json (Node) + if path.endswith("package.json"): + js_profile = parse_package_json(content) + for fw in js_profile["frameworks"]: + if fw not in profile["frameworks"]: + profile["frameworks"].append(fw) + for lib in js_profile["libraries"]: + if lib not in profile["libraries"]: + profile["libraries"].append(lib) + for db in js_profile["databases"]: + if db not in profile["databases"]: + profile["databases"].append(db) + if "npm" not in profile["package_managers"]: + profile["package_managers"].append("npm") + + # requirements.txt (Python) + elif path.endswith("requirements.txt") or path.endswith("pipfile"): + py_profile = parse_requirements_txt(content) + for fw in py_profile["frameworks"]: + if fw not in profile["frameworks"]: + profile["frameworks"].append(fw) + for lib in py_profile["libraries"]: + if lib not in profile["libraries"]: + profile["libraries"].append(lib) + for db in py_profile["databases"]: + if db not in profile["databases"]: + profile["databases"].append(db) + if "pip" not in profile["package_managers"]: + profile["package_managers"].append("pip") + + # go.mod (Go) + elif path.endswith("go.mod"): + if "Go modules" not in profile["package_managers"]: + profile["package_managers"].append("Go modules") + # Basic static checks + if "gin-gonic" in content or "github.com/gin-gonic/gin" in content: + profile["frameworks"].append("Gin (Go)") + if "gorm.io/gorm" in content: + profile["databases"].append("GORM ORM") + if "go.mongodb.org/mongo-driver" in content: + profile["databases"].append("MongoDB") + + # Cargo.toml (Rust) + elif path.endswith("cargo.toml"): + if "Cargo" not in profile["package_managers"]: + profile["package_managers"].append("Cargo") + if "tokio" in content: + profile["libraries"].append("tokio (async)") + if "actix-web" in content: + profile["frameworks"].append("Actix-web") + if "axum" in content: + profile["frameworks"].append("Axum") + + # Infrastructure files + if "dockerfile" in path or path.endswith("/dockerfile"): + if "Docker" not in profile["infrastructure"]: + profile["infrastructure"].append("Docker") + elif path.endswith("docker-compose.yml") or path.endswith("docker-compose.yaml"): + if "Docker Compose" not in profile["infrastructure"]: + profile["infrastructure"].append("Docker Compose") + elif ".github/workflows" in path: + if "GitHub Actions CI/CD" not in profile["infrastructure"]: + profile["infrastructure"].append("GitHub Actions CI/CD") + elif "kubernetes" in path or path.endswith(".k8s.yml") or path.endswith(".k8s.yaml"): + if "Kubernetes" not in profile["infrastructure"]: + profile["infrastructure"].append("Kubernetes") + elif path.endswith("serverless.yml") or path.endswith("serverless.yaml"): + if "Serverless Framework" not in profile["infrastructure"]: + profile["infrastructure"].append("Serverless Framework") + + return profile diff --git a/backend/services/repositoryScanner.py b/backend/services/repositoryScanner.py new file mode 100644 index 0000000000000000000000000000000000000000..cf9b0beaf0508480ee539c7a771de1ac4c434c4a --- /dev/null +++ b/backend/services/repositoryScanner.py @@ -0,0 +1,288 @@ +import os +import stat +import shutil +import tempfile +import zipfile +import subprocess +import urllib.parse +import httpx +from typing import Dict, List, Tuple, Any + +# Directories to completely ignore during scanning +IGNORED_DIRS = { + "node_modules", "venv", ".git", "dist", "build", "__pycache__", + ".venv", "env", ".env", "bin", "obj", "target", "out" +} + +# File extensions to ignore (binary and build artifacts) +IGNORED_EXTS = { + ".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".zip", ".tar", + ".gz", ".db", ".sqlite", ".exe", ".dll", ".so", ".dylib", ".class", + ".pyc", ".pyd", ".woff", ".woff2", ".ttf", ".eot", ".svg", ".mp4", + ".mp3", ".wav", ".avi", ".mov", ".zip", ".rar", ".7z", ".tar.gz", + ".DS_Store", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock" +} + +# Maximum size for a single file to be read for LLM analysis (100 KB) +MAX_FILE_SIZE_BYTES = 100 * 1024 + +def handle_remove_readonly(func, path, excinfo): + """ + Error handler for shutil.rmtree on Windows to remove read-only attributes. + """ + try: + os.chmod(path, stat.S_IWRITE) + func(path) + except Exception: + pass + +def parse_github_url(url: str) -> Tuple[str, str]: + """ + Parses owner and repository name from a GitHub URL. + Supports formats like: + - https://github.com/owner/repo + - https://github.com/owner/repo.git + - git@github.com:owner/repo.git + """ + url = url.strip() + if url.endswith(".git"): + url = url[:-4] + + if url.startswith("git@github.com:"): + path = url.split("git@github.com:")[1] + elif "github.com/" in url: + path = url.split("github.com/")[1] + else: + # Fallback if it is just "owner/repo" + path = url + + parts = [p for p in path.split("/") if p] + if len(parts) >= 2: + return parts[0], parts[1] + raise ValueError("Invalid GitHub URL format. Expected 'https://github.com/owner/repo'") + +async def check_repository_privacy(url: str, token: str = None) -> Dict[str, Any]: + """ + Checks if a GitHub repository is public or private. + Returns a dict with 'status' (public/private/invalid), 'message', and 'owner_repo'. + """ + try: + owner, repo = parse_github_url(url) + owner_repo = f"{owner}/{repo}" + except Exception as e: + return { + "status": "invalid", + "message": f"Could not parse GitHub URL: {str(e)}", + "owner_repo": None + } + + api_url = f"https://api.github.com/repos/{owner}/{repo}" + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "Repository-Intelligence-App" + } + + # Check without token first + async with httpx.AsyncClient() as client: + try: + response = await client.get(api_url, headers=headers) + if response.status_code == 200: + return { + "status": "public", + "message": "Repository is public.", + "owner_repo": owner_repo + } + elif response.status_code == 404: + # If a token is provided, verify access with the token + if token: + headers["Authorization"] = f"token {token}" + token_response = await client.get(api_url, headers=headers) + if token_response.status_code == 200: + return { + "status": "private", + "message": "Private repository access validated successfully.", + "owner_repo": owner_repo + } + else: + return { + "status": "private_denied", + "message": "Access denied. Please check your GitHub Personal Access Token.", + "owner_repo": owner_repo + } + return { + "status": "private_requires_auth", + "message": "Repository is private or does not exist. A GitHub Personal Access Token is required.", + "owner_repo": owner_repo + } + else: + return { + "status": "error", + "message": f"GitHub API returned HTTP {response.status_code}", + "owner_repo": owner_repo + } + except Exception as e: + return { + "status": "error", + "message": f"Failed to connect to GitHub: {str(e)}", + "owner_repo": owner_repo + } + +def sanitize_git_error(error_msg: str, token: str) -> str: + """ + Removes Personal Access Tokens from git output logs and errors. + """ + if not token: + return error_msg + return error_msg.replace(token, "[REDACTED]") + +def clone_repository(url: str, dest_dir: str, token: str = None) -> None: + """ + Clones a repository into a destination directory. Sanitizes token output. + """ + try: + owner, repo = parse_github_url(url) + except Exception as e: + raise Exception(f"Failed to parse repository URL: {str(e)}") + + if token: + # Build authenticated URL + # Format: https://x-access-token:@github.com/owner/repo.git + encoded_token = urllib.parse.quote(token) + clone_url = f"https://x-access-token:{encoded_token}@github.com/{owner}/{repo}.git" + else: + clone_url = f"https://github.com/{owner}/{repo}.git" + + # git clone requires the destination to be empty. Since tempfile.mkdtemp + # creates the directory, we clone into it using '.' which works when empty. + cmd = ["git", "clone", "--depth", "1", clone_url, "."] + + try: + # Run clone command inside dest_dir. Divert stderr to capture execution errors. + result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=dest_dir) + except subprocess.CalledProcessError as e: + stderr_sanitized = sanitize_git_error(e.stderr, token) + raise Exception(f"Git clone failed: {stderr_sanitized}") + except Exception as e: + raise Exception(f"Git execution error: {str(e)}") + +def extract_zip(zip_path: str, dest_dir: str) -> None: + """ + Extracts an uploaded zip file into a target directory. + Includes security protection against path traversal. + """ + target_dir = os.path.abspath(dest_dir) + + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + for member in zip_ref.infolist(): + # Resolve target path and verify it remains within target directory bounds + target_path = os.path.abspath(os.path.join(target_dir, member.filename)) + if not target_path.startswith(target_dir + os.sep) and target_path != target_dir: + raise Exception(f"Security Warning: Path traversal attempt detected in zip file: {member.filename}") + + # Safe to extract + zip_ref.extractall(target_dir) + +def is_text_file(file_path: str) -> bool: + """ + Heuristically checks if a file is a text file by scanning its initial bytes. + Also respects files that are purely empty as text files. + """ + # Check extension first + _, ext = os.path.splitext(file_path) + if ext.lower() in IGNORED_EXTS: + return False + + try: + with open(file_path, 'rb') as f: + chunk = f.read(1024) + if b'\x00' in chunk: # Binary files typically contain null bytes + return False + # Check if it can be decoded as utf-8 or ascii + try: + chunk.decode('utf-8') + except UnicodeDecodeError: + try: + chunk.decode('latin-1') + except UnicodeDecodeError: + return False + return True + except Exception: + return False + +def scan_directory(dir_path: str) -> Dict[str, Any]: + """ + Recursively scans the directory and returns: + 1. A nested file tree structure for visualization. + 2. A flat list of code files with their relative path and partial text contents (if key). + """ + file_tree = {} + flat_files = [] + + # Resolve the absolute path + abs_dir_path = os.path.abspath(dir_path) + + # Let's check if the unzipped repository structure has a single root folder wrapping the project + # (common in GitHub source code ZIPs like repo-name-main/) + scan_root = abs_dir_path + subdirs = os.listdir(abs_dir_path) + # If the folder contains only a single directory and no other files, we dive in + if len(subdirs) == 1: + single_path = os.path.join(abs_dir_path, subdirs[0]) + if os.path.isdir(single_path) and subdirs[0] not in IGNORED_DIRS: + scan_root = single_path + + # Helper to recursively build tree + def build_tree(current_dir: str, tree_node: Dict[str, Any]) -> None: + try: + entries = os.listdir(current_dir) + except Exception: + return + + for entry in entries: + if entry in IGNORED_DIRS: + continue + + full_path = os.path.join(current_dir, entry) + rel_path = os.path.relpath(full_path, scan_root).replace("\\", "/") + + if os.path.isdir(full_path): + tree_node[entry] = { + "type": "directory", + "path": rel_path, + "children": {} + } + build_tree(full_path, tree_node[entry]["children"]) + # If directory has no children, we still keep it + else: + _, ext = os.path.splitext(entry) + if ext.lower() in IGNORED_EXTS: + continue + + size = os.path.getsize(full_path) + tree_node[entry] = { + "type": "file", + "path": rel_path, + "size": size + } + + # Check if it is a text file and size is within limits + if size <= MAX_FILE_SIZE_BYTES and is_text_file(full_path): + try: + with open(full_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + flat_files.append({ + "path": rel_path, + "size": size, + "content": content + }) + except Exception: + pass + + root_tree = {} + build_tree(scan_root, root_tree) + + return { + "tree": root_tree, + "files": flat_files, + "scan_root": scan_root + } diff --git a/backend/storage/conversations.json b/backend/storage/conversations.json new file mode 100644 index 0000000000000000000000000000000000000000..40897dae7d16d7b464b44b71ba64ae30a72c386f --- /dev/null +++ b/backend/storage/conversations.json @@ -0,0 +1,120 @@ +{ + "9ca25aaf-d76f-4355-9ace-71ce59da998c": { + "session_id": "9ca25aaf-d76f-4355-9ace-71ce59da998c", + "repo_id": "f86159c4-07a9-4ea4-b32c-c99ace8a0ded", + "summary": "Which API endpoints exist?", + "history": [ + { + "role": "user", + "content": "Which API endpoints exist?", + "timestamp": 1782487809.031455, + "retrieved_context": [], + "agent_decisions": null + }, + { + "role": "assistant", + "content": "The `api_lookup` tool has identified the following API endpoints:\n\n* **Dictionary API:** `https://api.dictionaryapi.dev/api/v2/entries/en/{term}`\n * This endpoint appears to be part of a dictionary service, where `{term}` would be a placeholder for a word to look up.\n* **Google Gemini API:** `models/gemini-1.5-pro`\n * This refers to the Gemini 1.5 Pro model, indicating an endpoint for interacting with Google's AI model.", + "timestamp": 1782487822.3272474, + "retrieved_context": null, + "agent_decisions": { + "agents_used": [], + "confidence": 0.95, + "planner_decision": { + "retrieve_memory": false, + "run_semantic_search": false, + "invoke_tools": [ + "api_lookup" + ], + "run_agents": false, + "selected_agents": [], + "execution_order": [], + "synthesize_final_answer": true, + "reasoning": "The user is asking for API endpoints. The 'api_lookup' tool is specifically designed to retrieve information about HTTP endpoints and API details. This is a direct lookup query that can be efficiently handled by the tool without needing semantic search or specialized agents. The final answer should be synthesized to present the retrieved API endpoints clearly." + } + } + } + ] + }, + "4fd646e5-f098-4555-a7aa-d827ef2c18d8": { + "session_id": "4fd646e5-f098-4555-a7aa-d827ef2c18d8", + "repo_id": "0c360c45-e6a5-44f9-8b2a-c336ed3156ba", + "summary": "is there authentication in this repo?", + "history": [ + { + "role": "user", + "content": "is there authentication in this repo?", + "timestamp": 1782487991.8081007, + "retrieved_context": [], + "agent_decisions": null + }, + { + "role": "assistant", + "content": "The repository incorporates an authentication mechanism, but it is primarily for **external service integration** rather than direct user authentication for the Streamlit application. The DocuBot AI application authenticates with the Google Generative AI service (Gemini 1.5 Pro) to facilitate chatbot interactions and leverage external AI models.\n\n**Key Details of the Authentication Mechanism:**\n\n* **Method**: API Key authentication.\n* **Target**: Google Generative AI service (Gemini 1.5 Pro).\n* **Implementation**: The API key is intended to be loaded from an environment variable. This is typically managed through a `.env` file, which is then read by `app.py` using the `python-dotenv` library. This approach ensures that sensitive credentials are not hardcoded directly into the application's source code.\n* **Purpose**: To securely access and utilize the capabilities of the Google Generative AI API.\n\n**Absence of User Authentication:**\n\nIt is important to note that there is **no explicit user authentication mechanism** for accessing the Streamlit application itself. Access to the Streamlit interface is assumed to be open or managed by the deployment environment, rather than requiring individual user logins within the application's codebase.\n\n**Security Considerations:**\n\nWhile storing API keys in `.env` files is a standard practice for development, proper deployment considerations are crucial for securing these keys in production environments. This typically involves using platform-specific secret management services or environment variables provided by the hosting infrastructure.", + "timestamp": 1782488011.3974092, + "retrieved_context": null, + "agent_decisions": { + "agents_used": [ + "SecurityAgent" + ], + "confidence": 1.0, + "planner_decision": { + "retrieve_memory": false, + "run_semantic_search": true, + "invoke_tools": [], + "run_agents": true, + "selected_agents": [ + "SecurityAgent" + ], + "execution_order": [ + [ + "SecurityAgent" + ] + ], + "synthesize_final_answer": true, + "reasoning": "The user is asking about authentication, which is a core concern of the SecurityAgent. The repository profile already indicates API Key authentication, and the SecurityAgent can elaborate on its implementation and implications. Running a semantic search will also help identify any code-level mentions or implementations related to authentication mechanisms. The final answer should synthesize this information into a clear explanation." + } + } + }, + { + "role": "user", + "content": "what is the architecture", + "timestamp": 1782488049.1804276, + "retrieved_context": [], + "agent_decisions": null + }, + { + "role": "assistant", + "content": "The DocuBot AI project employs a **Monolithic Streamlit Application** architecture. This design choice centralizes all functionalities within a single codebase, primarily the `app.py` file, which simplifies development and deployment, making it ideal for interactive data applications.\n\n### Key Architectural Aspects:\n\n1. **Monolithic Structure**: The entire application, including the user interface, backend logic, and integrations with various libraries and external services, resides within `app.py`. This consolidates all operations, from UI rendering to complex text processing and API calls.\n\n2. **Streamlit Framework**: Streamlit serves as the foundational framework, handling both the frontend user interface and the application's core logic. It manages user interactions, maintains application state, and displays processed results dynamically.\n\n3. **Core Modules and Libraries**: The application integrates a comprehensive set of Python libraries to deliver its features:\n * **Document Parsing**: `PyPDF2` for handling PDF files and `python-docx` for processing DOCX documents.\n * **Text Summarization**: Utilizes the `transformers` library, specifically the `distilbart-cnn-12-6` model, for generating concise summaries.\n * **Natural Language Processing (NLP)**: `nltk` is used for tasks such as tokenization and stopword removal, while `textstat` provides readability metrics. `collections.Counter` assists in word frequency analysis.\n * **Visualization**: `wordcloud` and `matplotlib` are employed for generating visual representations of text data, such as word clouds.\n * **AI Chatbot**: The `google-generativeai` library facilitates interaction with the `Gemini 1.5 Pro` model, enabling both general and file-contextual conversations.\n * **External API Interaction**: The `requests` library is used to fetch definitions from `dictionaryapi.dev`.\n\n4. **Data Flow**: User inputs, such as uploaded documents or text queries, are processed by `app.py`. This central script orchestrates calls to the relevant internal libraries and external APIs. The results are then dynamically rendered back to the user through the Streamlit UI.\n\n5. **Environment Configuration**: Sensitive information, such as the Google Generative AI API key, is securely managed using `python-dotenv`, which loads environment variables from a `.env` file.\n\n6. **Entry Point**: The application is launched and executed via the command `streamlit run app.py`.\n\n### Important Files:\n\n* `.env`: Stores environment variables, including API keys.\n* `app.py`: Contains the entire application logic, UI, and integrations.\n* `requirements.txt`: Lists all necessary Python dependencies for the project.\n* `README.md`: Provides an overview and instructions for the project.", + "timestamp": 1782488069.0401134, + "retrieved_context": null, + "agent_decisions": { + "agents_used": [ + "ArchitectureAgent" + ], + "confidence": 1.0, + "planner_decision": { + "retrieve_memory": false, + "run_semantic_search": false, + "invoke_tools": [ + "architecture_lookup" + ], + "run_agents": true, + "selected_agents": [ + "ArchitectureAgent" + ], + "execution_order": [ + [ + "architecture_lookup" + ], + [ + "ArchitectureAgent" + ] + ], + "synthesize_final_answer": true, + "reasoning": "The user is asking for the architecture of the project. The 'architecture_lookup' tool can provide the high-level architecture pattern. The 'ArchitectureAgent' is specifically designed to elaborate on architectural patterns, component interactions, and data flow, which is essential for a comprehensive answer. The information from both will be synthesized to provide a complete explanation." + } + } + } + ] + } +} \ No newline at end of file diff --git a/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_graph.json b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_graph.json new file mode 100644 index 0000000000000000000000000000000000000000..efb9e7cf006cba1a0b6698c7e53e7cc3d53f6af7 --- /dev/null +++ b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_graph.json @@ -0,0 +1,458 @@ +{ + "nodes": [ + { + "id": "app.py", + "label": "Main Application", + "type": "entrypoint", + "properties": {} + }, + { + "id": "streamlit", + "label": "Streamlit Framework", + "type": "framework", + "properties": {} + }, + { + "id": "transformers", + "label": "HuggingFace Transformers", + "type": "library", + "properties": {} + }, + { + "id": "distilbart-cnn-12-6", + "label": "DistilBART Summarization Model", + "type": "model", + "properties": {} + }, + { + "id": "textstat", + "label": "Textstat Library", + "type": "library", + "properties": {} + }, + { + "id": "wordcloud", + "label": "WordCloud Library", + "type": "library", + "properties": {} + }, + { + "id": "matplotlib", + "label": "Matplotlib Library", + "type": "library", + "properties": {} + }, + { + "id": "PyPDF2", + "label": "PyPDF2 Library", + "type": "library", + "properties": {} + }, + { + "id": "python-docx", + "label": "Python-Docx Library", + "type": "library", + "properties": {} + }, + { + "id": "requests", + "label": "Requests Library", + "type": "library", + "properties": {} + }, + { + "id": "google-generativeai", + "label": "Google Generative AI Library", + "type": "library", + "properties": {} + }, + { + "id": "gemini-1.5-pro", + "label": "Gemini 1.5 Pro Model", + "type": "model", + "properties": {} + }, + { + "id": "nltk", + "label": "NLTK Library", + "type": "library", + "properties": {} + }, + { + "id": "python-dotenv", + "label": "Python-Dotenv Library", + "type": "library", + "properties": {} + }, + { + "id": "dictionaryapi.dev", + "label": "Free Dictionary API", + "type": "api", + "properties": {} + }, + { + "id": "uploaded_file", + "label": "Uploaded Document", + "type": "data", + "properties": {} + }, + { + "id": "summary_output", + "label": "Generated Summary", + "type": "data", + "properties": {} + }, + { + "id": "text_analysis_output", + "label": "Text Analysis Results", + "type": "data", + "properties": {} + }, + { + "id": "word_cloud_image", + "label": "Word Cloud Image", + "type": "data", + "properties": {} + }, + { + "id": "legal_term_input", + "label": "Legal Term Input", + "type": "data", + "properties": {} + }, + { + "id": "chatbot_input", + "label": "Chatbot Query", + "type": "data", + "properties": {} + }, + { + "id": "file_chatbot_input", + "label": "File-Context Chatbot Query", + "type": "data", + "properties": {} + }, + { + "id": "requirements.txt", + "label": "Dependencies List", + "type": "file", + "properties": {} + }, + { + "id": ".env", + "label": "Environment Variables", + "type": "file", + "properties": {} + }, + { + "id": "README.md", + "label": "Project Documentation", + "type": "file", + "properties": {} + } + ], + "edges": [ + { + "source": "app.py", + "target": "streamlit", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "pandas", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "textstat", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "wordcloud", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "matplotlib", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "transformers", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "python-docx", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "PyPDF2", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "requests", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "google-generativeai", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "nltk", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "python-dotenv", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "distilbart-cnn-12-6", + "type": "calls", + "label": "summarizes with" + }, + { + "source": "app.py", + "target": "dictionaryapi.dev", + "type": "calls", + "label": "fetches definitions from" + }, + { + "source": "app.py", + "target": "gemini-1.5-pro", + "type": "calls", + "label": "generates responses with" + }, + { + "source": "uploaded_file", + "target": "app.py", + "type": "data_flow", + "label": "provides content to" + }, + { + "source": "app.py", + "target": "summary_output", + "type": "data_flow", + "label": "generates" + }, + { + "source": "app.py", + "target": "text_analysis_output", + "type": "data_flow", + "label": "produces" + }, + { + "source": "app.py", + "target": "word_cloud_image", + "type": "data_flow", + "label": "creates" + }, + { + "source": "legal_term_input", + "target": "app.py", + "type": "data_flow", + "label": "queries" + }, + { + "source": "chatbot_input", + "target": "app.py", + "type": "data_flow", + "label": "sends to" + }, + { + "source": "file_chatbot_input", + "target": "app.py", + "type": "data_flow", + "label": "sends to" + }, + { + "source": "requirements.txt", + "target": "app.py", + "type": "ownership", + "label": "defines dependencies for" + }, + { + "source": ".env", + "target": "app.py", + "type": "data_flow", + "label": "provides API key to" + }, + { + "source": "README.md", + "target": "app.py", + "type": "documentation", + "label": "describes" + } + ], + "entry_points": [ + { + "file_path": "app.py", + "type": "streamlit_app", + "description": "Main entry point for the Streamlit web application, executed via `streamlit run app.py`." + } + ], + "business_flows": [ + { + "flow_name": "Document Upload & Analysis", + "description": "User uploads a document, which is then parsed, analyzed for statistics, common words, readability, and a word cloud is generated.", + "steps": [ + "uploaded_file", + "app.py", + "PyPDF2", + "python-docx", + "textstat", + "nltk", + "wordcloud", + "text_analysis_output", + "word_cloud_image" + ] + }, + { + "flow_name": "Document Summarization", + "description": "User uploads a document and requests a summary, which is generated using a HuggingFace transformer model.", + "steps": [ + "uploaded_file", + "app.py", + "transformers", + "distilbart-cnn-12-6", + "summary_output" + ] + }, + { + "flow_name": "Legal Term Explanation", + "description": "User inputs a legal term, and the application fetches its definition from an external dictionary API.", + "steps": [ + "legal_term_input", + "app.py", + "requests", + "dictionaryapi.dev" + ] + }, + { + "flow_name": "General Chatbot Interaction", + "description": "User interacts with a general-purpose AI chatbot powered by Google Gemini.", + "steps": [ + "chatbot_input", + "app.py", + "google-generativeai", + "gemini-1.5-pro" + ] + }, + { + "flow_name": "File-Contextual Chatbot Interaction", + "description": "User asks questions about the content of an uploaded file, with the Gemini chatbot providing answers based on the document's context.", + "steps": [ + "uploaded_file", + "file_chatbot_input", + "app.py", + "google-generativeai", + "gemini-1.5-pro" + ] + } + ], + "critical_paths": [ + { + "path_name": "Text Summarization Performance", + "description": "Processing and summarizing large documents using the DistilBART model can be computationally intensive and time-consuming, especially with chunking logic.", + "nodes": [ + "uploaded_file", + "app.py", + "transformers", + "distilbart-cnn-12-6", + "summary_output" + ] + }, + { + "path_name": "External API Latency/Reliability", + "description": "Calls to the Free Dictionary API and Google Gemini API introduce external dependencies and potential latency or failure points.", + "nodes": [ + "app.py", + "requests", + "dictionaryapi.dev", + "google-generativeai", + "gemini-1.5-pro" + ] + }, + { + "path_name": "Large File Text Extraction", + "description": "Extracting text from very large PDF or DOCX files can consume significant memory and processing time, impacting user experience.", + "nodes": [ + "uploaded_file", + "app.py", + "PyPDF2", + "python-docx" + ] + } + ], + "concepts": [ + { + "name": "Document Parsing & Extraction", + "description": "Handling different document types (.txt, .pdf, .docx) and extracting their raw text content for further processing.", + "files": [ + "app.py" + ] + }, + { + "name": "Natural Language Processing (NLP)", + "description": "Core text analysis functionalities including tokenization, stopword removal, word frequency, and readability scoring.", + "files": [ + "app.py" + ] + }, + { + "name": "Abstractive Summarization", + "description": "Using a pre-trained transformer model (DistilBART) to generate concise summaries of potentially long documents, involving text chunking.", + "files": [ + "app.py" + ] + }, + { + "name": "AI Chatbot Integration", + "description": "Integrating Google Gemini Pro for both general conversational AI and contextual questioning based on uploaded document content.", + "files": [ + "app.py" + ] + }, + { + "name": "External API Interaction", + "description": "Making HTTP requests to third-party services like the Free Dictionary API for specific data lookups.", + "files": [ + "app.py" + ] + }, + { + "name": "Streamlit User Interface", + "description": "Building an interactive web application frontend with file upload, button actions, text display, and sidebar components.", + "files": [ + "app.py" + ] + }, + { + "name": "Environment Configuration", + "description": "Managing sensitive information like API keys using environment variables loaded from a `.env` file.", + "files": [ + "app.py", + ".env" + ] + } + ] +} \ No newline at end of file diff --git a/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_profile.json b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_profile.json new file mode 100644 index 0000000000000000000000000000000000000000..a98224d213a4b271909a609df67208c97fdcfd5b --- /dev/null +++ b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_profile.json @@ -0,0 +1,50 @@ +{ + "project_name": "DocuBot AI", + "project_type": "Web Application", + "languages": [ + "Python", + "Markdown" + ], + "frameworks": [ + "Streamlit" + ], + "databases": [], + "authentication_methods": [ + "API Key (Google Generative AI)" + ], + "major_modules": [ + "app.py", + "nltk", + "transformers", + "textstat", + "wordcloud", + "PyPDF2", + "python-docx", + "google-generativeai", + "requests", + "python-dotenv" + ], + "api_endpoints": [], + "important_files": [ + ".env", + "app.py", + "requirements.txt", + "README.md" + ], + "architecture_pattern": "Monolithic Streamlit Application", + "dependencies": [ + "streamlit", + "pandas", + "textstat", + "wordcloud", + "matplotlib", + "transformers", + "python-docx", + "PyPDF2", + "requests", + "google-generativeai", + "nltk", + "python-dotenv", + "torch" + ] +} \ No newline at end of file diff --git a/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_report.md b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_report.md new file mode 100644 index 0000000000000000000000000000000000000000..88ab47acc5ed239eef57e55c46e2979615fef607 --- /dev/null +++ b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_report.md @@ -0,0 +1,55 @@ +# Repository Intelligence Report: DocuBot AI + +## Overview +DocuBot AI is an intelligent document assistant built as a Streamlit web application. Its primary purpose is to help users extract key insights, generate summaries, and perform various text analyses on uploaded `.txt`, `.docx`, or `.pdf` files. The application integrates several NLP capabilities and leverages external AI models and APIs to provide a comprehensive suite of document processing tools. + +## Key Features +* **Document Upload & Parsing**: Supports `.txt`, `.docx`, and `.pdf` file formats. +* **Text Summarization**: Utilizes a HuggingFace `transformers` pipeline (DistilBART model) to generate concise summaries of lengthy documents, handling large texts by chunking. +* **Legal Term Explainer**: Fetches definitions for legal terms from the Free Dictionary API, with a Google search fallback. +* **AI Chatbot**: Integrates Google Gemini 1.5 Pro for general conversational queries and a specialized mode for asking questions directly related to the content of an uploaded document. +* **Text Analytics**: Provides word, sentence, paragraph, and character counts, identifies the most common words (after removing stopwords), and calculates the Flesch-Kincaid Grade readability score. +* **Word Cloud Visualization**: Generates visual word clouds from the document's content. + +## Architecture and Design +The project follows a **Monolithic Streamlit Application** pattern, with all core logic, UI components, and integrations residing within a single `app.py` file. This design is typical for smaller, interactive data applications built with Streamlit, prioritizing rapid development and ease of deployment. + +### Data Flow +1. **User Interaction**: Users upload files or input text/queries via the Streamlit frontend. +2. **File Processing**: Uploaded `.pdf` and `.docx` files are parsed using `PyPDF2` and `python-docx` respectively to extract raw text. +3. **NLP & AI Services**: The extracted text is then fed into various modules: + * `transformers` for summarization. + * `textstat` for readability scores. + * `nltk` for text cleaning and tokenization. + * `google-generativeai` (Gemini Pro) for chatbot interactions (both general and file-contextual). + * `requests` to `dictionaryapi.dev` for legal term definitions. +4. **Output Display**: Results (summaries, analytics, chatbot responses, word clouds) are rendered back to the user via the Streamlit interface. + +### External Integrations +* **HuggingFace Transformers**: For text summarization. +* **Free Dictionary API**: For legal term definitions. +* **Google Generative AI (Gemini 1.5 Pro)**: For chatbot functionalities. + +## Technical Stack +* **Frontend & Application Framework**: Streamlit +* **Core Language**: Python +* **NLP Libraries**: `transformers`, `textstat`, `nltk` +* **Document Parsing**: `PyPDF2`, `python-docx` +* **AI Integration**: `google-generativeai` +* **HTTP Requests**: `requests` +* **Visualization**: `wordcloud`, `matplotlib` +* **Environment Management**: `python-dotenv` + +## Development Insights +* **Simplicity**: The single-file structure makes the project easy to understand and run, ideal for a demonstration or personal tool. +* **Session Management**: Streamlit's `st.session_state` is effectively used to maintain application state, such as chat history and uploaded document content, across user interactions. +* **NLTK Data**: The application handles NLTK data downloads dynamically, ensuring necessary resources are available. + +## Potential Risks & Considerations +* **Scalability**: The monolithic `app.py` might become challenging to manage and scale for more complex features or a larger user base. +* **Performance**: Processing very large documents for summarization or text analysis could lead to performance bottlenecks or memory issues, especially given the chunking strategy for summarization. +* **External API Dependency**: Reliance on `dictionaryapi.dev` and Google Gemini means the application's core features are susceptible to external service outages or API changes. +* **Error Handling**: While basic `try-except` blocks are present, more robust error handling, retry mechanisms, and user feedback for API failures could improve resilience. + +## Getting Started for Developers +To understand the codebase, `app.py` is the central file. It defines the UI layout, handles file uploads, orchestrates calls to various NLP and AI services, and manages the application's state. `requirements.txt` lists all necessary dependencies, and `.env` is crucial for configuring the Google Generative AI API key. diff --git a/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_summary.json b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_summary.json new file mode 100644 index 0000000000000000000000000000000000000000..8ca99dbca296d7450818a0624e55b1dfa7391e67 --- /dev/null +++ b/backend/storage/repos/0c360c45-e6a5-44f9-8b2a-c336ed3156ba/repository_summary.json @@ -0,0 +1,46 @@ +{ + "elevator_pitch": "DocuBot AI is an intelligent Streamlit-based web application designed to assist users with document analysis. It provides features like text summarization, legal term explanations, comprehensive text analytics, word cloud generation, and a Gemini-powered chatbot for general and file-contextual queries.", + "core_features": [ + "Upload and analyze .txt, .docx, or .pdf documents", + "Summarize lengthy documents using HuggingFace transformers", + "Explain legal terms using dictionary API (with Google fallback)", + "Built-in Gemini-powered chatbot for general queries", + "Text analytics: word, sentence, paragraph, character counts", + "Most common words with stopwords removed", + "Word Cloud visualization", + "Flesch-Kincaid Grade readability score", + "File-contextual chatbot for uploaded documents" + ], + "main_workflows": [ + "User uploads a document (.txt, .docx, .pdf) for analysis.", + "User requests a summary of the uploaded document.", + "User inputs a legal term to get its definition.", + "User interacts with the general-purpose chatbot in the sidebar.", + "User asks questions about the content of the uploaded file using the file-aware chatbot.", + "User downloads generated summaries or text statistics." + ], + "key_components": [ + "Streamlit UI: Handles user interaction, file uploads, and displays results.", + "Text Extraction: `PyPDF2` for PDFs, `python-docx` for DOCX files.", + "HuggingFace Summarizer: `transformers` pipeline (DistilBART model) for text summarization.", + "Legal Term Explainer: `requests` to `dictionaryapi.dev` for definitions.", + "Google Gemini Chatbot: `google-generativeai` for general and file-contextual AI responses.", + "Text Analytics: `textstat` for readability, `nltk` for tokenization and stopword removal, `collections.Counter` for word frequency.", + "Word Cloud Generator: `wordcloud` and `matplotlib` for visualization.", + "Session State Management: Streamlit's `st.session_state` to maintain application state across reruns." + ], + "key_risks": [ + "Single-file monolithic architecture: Can become hard to maintain and scale as features grow.", + "Reliance on external APIs: Downtime or changes in `dictionaryapi.dev` or Google Gemini API could break core functionality.", + "Large file processing: Summarization and text extraction for very large documents might hit memory limits or timeout issues, especially on free Streamlit hosting.", + "NLTK data download at runtime: Can cause delays on first run or in ephemeral environments if not pre-cached.", + "Lack of robust error handling for external API calls: Basic try-except blocks, but no retry mechanisms or circuit breakers.", + "Security: API key stored in `.env` is standard, but deployment considerations for securing it are important." + ], + "developer_start_points": [ + "app.py: Contains all the application logic, UI definition, and integration points.", + "README.md: Provides a high-level overview, setup instructions, and feature list.", + "requirements.txt: Lists all Python dependencies, crucial for environment setup.", + ".env: To configure the Google Generative AI API key." + ] +} \ No newline at end of file diff --git a/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_graph.json b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_graph.json new file mode 100644 index 0000000000000000000000000000000000000000..1df9e8be5fe264074051098a8447f4ffd62eabf5 --- /dev/null +++ b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_graph.json @@ -0,0 +1,518 @@ +{ + "nodes": [ + { + "id": "app.py", + "label": "app.py", + "type": "module", + "properties": {} + }, + { + "id": "requirements.txt", + "label": "requirements.txt", + "type": "file", + "properties": {} + }, + { + "id": ".env", + "label": ".env", + "type": "file", + "properties": {} + }, + { + "id": "README.md", + "label": "README.md", + "type": "file", + "properties": {} + }, + { + "id": "streamlit", + "label": "Streamlit", + "type": "module", + "properties": {} + }, + { + "id": "pandas", + "label": "Pandas", + "type": "module", + "properties": {} + }, + { + "id": "textstat", + "label": "Textstat", + "type": "module", + "properties": {} + }, + { + "id": "wordcloud", + "label": "WordCloud", + "type": "module", + "properties": {} + }, + { + "id": "matplotlib.pyplot", + "label": "Matplotlib.pyplot", + "type": "module", + "properties": {} + }, + { + "id": "transformers", + "label": "HuggingFace Transformers", + "type": "module", + "properties": {} + }, + { + "id": "docx", + "label": "Python-Docx", + "type": "module", + "properties": {} + }, + { + "id": "PyPDF2", + "label": "PyPDF2", + "type": "module", + "properties": {} + }, + { + "id": "requests", + "label": "Requests", + "type": "module", + "properties": {} + }, + { + "id": "google.generativeai", + "label": "Google Generative AI", + "type": "module", + "properties": {} + }, + { + "id": "os", + "label": "OS", + "type": "module", + "properties": {} + }, + { + "id": "string", + "label": "String", + "type": "module", + "properties": {} + }, + { + "id": "nltk", + "label": "NLTK", + "type": "module", + "properties": {} + }, + { + "id": "dotenv", + "label": "Python-Dotenv", + "type": "module", + "properties": {} + }, + { + "id": "Free Dictionary API", + "label": "Free Dictionary API", + "type": "api", + "properties": {} + }, + { + "id": "Google Gemini API", + "label": "Google Gemini API", + "type": "api", + "properties": {} + }, + { + "id": "Document Upload", + "label": "Document Upload", + "type": "entrypoint", + "properties": {} + }, + { + "id": "Legal Term Input", + "label": "Legal Term Input", + "type": "entrypoint", + "properties": {} + }, + { + "id": "Chatbot Input", + "label": "Chatbot Input", + "type": "entrypoint", + "properties": {} + }, + { + "id": "File Context Chatbot Input", + "label": "File Context Chatbot Input", + "type": "entrypoint", + "properties": {} + }, + { + "id": "Text Extraction Logic", + "label": "Text Extraction Logic", + "type": "module", + "properties": {} + }, + { + "id": "Text Analysis Logic", + "label": "Text Analysis Logic", + "type": "module", + "properties": {} + }, + { + "id": "Summarization Logic", + "label": "Summarization Logic", + "type": "module", + "properties": {} + }, + { + "id": "Word Cloud Generation Logic", + "label": "Word Cloud Generation Logic", + "type": "module", + "properties": {} + }, + { + "id": "Readability Scoring Logic", + "label": "Readability Scoring Logic", + "type": "module", + "properties": {} + }, + { + "id": "Common Words Calculation Logic", + "label": "Common Words Calculation Logic", + "type": "module", + "properties": {} + }, + { + "id": "Chatbot Interaction Logic", + "label": "Chatbot Interaction Logic", + "type": "module", + "properties": {} + }, + { + "id": "Legal Term Lookup Logic", + "label": "Legal Term Lookup Logic", + "type": "module", + "properties": {} + }, + { + "id": "NLTK Stopwords Data", + "label": "NLTK Stopwords Data", + "type": "file", + "properties": {} + } + ], + "edges": [ + { + "source": "app.py", + "target": "streamlit", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "pandas", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "textstat", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "wordcloud", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "matplotlib.pyplot", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "transformers", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "docx", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "PyPDF2", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "requests", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "google.generativeai", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "os", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "string", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "nltk", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": "dotenv", + "type": "imports", + "label": "uses" + }, + { + "source": "app.py", + "target": ".env", + "type": "data_flow", + "label": "reads API_KEY from" + }, + { + "source": "app.py", + "target": "Free Dictionary API", + "type": "calls", + "label": "fetches definition from" + }, + { + "source": "app.py", + "target": "Google Gemini API", + "type": "calls", + "label": "generates content with" + }, + { + "source": "Document Upload", + "target": "app.py", + "type": "data_flow", + "label": "provides file to" + }, + { + "source": "app.py", + "target": "Text Extraction Logic", + "type": "calls", + "label": "uses" + }, + { + "source": "Text Extraction Logic", + "target": "Text Analysis Logic", + "type": "data_flow", + "label": "provides text to" + }, + { + "source": "Text Extraction Logic", + "target": "Summarization Logic", + "type": "data_flow", + "label": "provides text to" + }, + { + "source": "Text Extraction Logic", + "target": "Word Cloud Generation Logic", + "type": "data_flow", + "label": "provides text to" + }, + { + "source": "Text Extraction Logic", + "target": "Common Words Calculation Logic", + "type": "data_flow", + "label": "provides text to" + }, + { + "source": "Text Extraction Logic", + "target": "Readability Scoring Logic", + "type": "data_flow", + "label": "provides text to" + }, + { + "source": "Text Extraction Logic", + "target": "File Context Chatbot Input", + "type": "data_flow", + "label": "provides context to" + }, + { + "source": "app.py", + "target": "NLTK Stopwords Data", + "type": "data_flow", + "label": "uses" + }, + { + "source": "Legal Term Input", + "target": "Legal Term Lookup Logic", + "type": "data_flow", + "label": "provides term to" + }, + { + "source": "Legal Term Lookup Logic", + "target": "app.py", + "type": "calls", + "label": "called by" + }, + { + "source": "Chatbot Input", + "target": "Chatbot Interaction Logic", + "type": "data_flow", + "label": "provides query to" + }, + { + "source": "File Context Chatbot Input", + "target": "Chatbot Interaction Logic", + "type": "data_flow", + "label": "provides query with context to" + }, + { + "source": "Chatbot Interaction Logic", + "target": "app.py", + "type": "calls", + "label": "called by" + }, + { + "source": "requirements.txt", + "target": "app.py", + "type": "ownership", + "label": "defines dependencies for" + } + ], + "entry_points": [ + { + "file_path": "app.py", + "type": "streamlit_app", + "description": "Main entry point for the Streamlit web application, executed via `streamlit run app.py`." + } + ], + "business_flows": [ + { + "flow_name": "Document Analysis & Summarization", + "description": "User uploads a document, which is then parsed, analyzed for various metrics, visualized with a word cloud, and can be summarized using an NLP model.", + "steps": [ + "Document Upload", + "app.py", + "Text Extraction Logic", + "Text Analysis Logic", + "Common Words Calculation Logic", + "Readability Scoring Logic", + "Word Cloud Generation Logic", + "Summarization Logic" + ] + }, + { + "flow_name": "Legal Term Definition Lookup", + "description": "Users can input a legal term in the sidebar to get its definition from an external dictionary API, with a Google search fallback.", + "steps": [ + "Legal Term Input", + "app.py", + "Legal Term Lookup Logic", + "Free Dictionary API" + ] + }, + { + "flow_name": "AI Chatbot Interaction", + "description": "Users can interact with a Gemini-powered chatbot for general queries or questions specifically related to the content of an uploaded document.", + "steps": [ + "Chatbot Input", + "File Context Chatbot Input", + "app.py", + "Chatbot Interaction Logic", + "Google Gemini API" + ] + } + ], + "critical_paths": [ + { + "path_name": "Document Summarization", + "description": "This path involves chunking potentially large documents and performing multiple inferences with the DistilBART model, which can be computationally intensive and time-consuming.", + "nodes": [ + "Document Upload", + "app.py", + "Text Extraction Logic", + "Summarization Logic", + "transformers" + ] + }, + { + "path_name": "Large Document Parsing", + "description": "Extracting text from very large or complex PDF/DOCX files can consume significant memory and CPU, potentially leading to performance bottlenecks.", + "nodes": [ + "Document Upload", + "app.py", + "Text Extraction Logic", + "PyPDF2", + "docx" + ] + }, + { + "path_name": "External API Calls", + "description": "Reliance on external services like Free Dictionary API and Google Gemini API means that the responsiveness of these features is subject to the latency and availability of those APIs.", + "nodes": [ + "app.py", + "Free Dictionary API", + "Google Gemini API" + ] + } + ], + "concepts": [ + { + "name": "Document Parsing", + "description": "Handles the extraction of raw text content from various document formats (.txt, .docx, .pdf) for further processing.", + "files": [ + "app.py" + ] + }, + { + "name": "Natural Language Processing (NLP)", + "description": "Encompasses text analysis (word/sentence counts, common words), readability scoring, and text summarization using specialized libraries.", + "files": [ + "app.py" + ] + }, + { + "name": "Generative AI Chatbot", + "description": "Integrates Google Gemini Pro for conversational AI, providing answers to general queries and file-contextual questions.", + "files": [ + "app.py" + ] + }, + { + "name": "User Interface & Interaction", + "description": "Built with Streamlit, providing an interactive web interface for file uploads, input fields, and displaying analytical results and chatbot responses.", + "files": [ + "app.py" + ] + }, + { + "name": "External Service Integration", + "description": "Manages communication with third-party APIs for dictionary definitions and generative AI capabilities.", + "files": [ + "app.py" + ] + }, + { + "name": "Environment Configuration", + "description": "Manages sensitive information like API keys using environment variables loaded from a `.env` file.", + "files": [ + "app.py", + ".env" + ] + } + ] +} \ No newline at end of file diff --git a/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_profile.json b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_profile.json new file mode 100644 index 0000000000000000000000000000000000000000..a4273eb5071ddf656dfa23402ae63a1aa23ecd7e --- /dev/null +++ b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_profile.json @@ -0,0 +1,51 @@ +{ + "project_name": "DocuBot AI", + "project_type": "Web App", + "languages": [ + "Python", + "Markdown" + ], + "frameworks": [ + "Streamlit" + ], + "databases": [], + "authentication_methods": [], + "major_modules": [ + "app.py", + "transformers", + "textstat", + "wordcloud", + "PyPDF2", + "python-docx", + "requests", + "google-generativeai", + "nltk", + "python-dotenv" + ], + "api_endpoints": [ + "https://api.dictionaryapi.dev/api/v2/entries/en/{term}", + "models/gemini-1.5-pro (Google Gemini API)" + ], + "important_files": [ + "app.py", + "requirements.txt", + ".env", + "README.md" + ], + "architecture_pattern": "Monolith", + "dependencies": [ + "streamlit", + "pandas", + "textstat", + "wordcloud", + "matplotlib", + "transformers", + "python-docx", + "PyPDF2", + "requests", + "google-generativeai", + "nltk", + "python-dotenv", + "torch" + ] +} \ No newline at end of file diff --git a/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_report.md b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_report.md new file mode 100644 index 0000000000000000000000000000000000000000..50dcc0c65b81ae21f5cae439a935b2077e7754ee --- /dev/null +++ b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_report.md @@ -0,0 +1,37 @@ +# Repository Intelligence Report: DocuBot AI + +## Overview +DocuBot AI is an intelligent document assistant built with Streamlit, designed to help users extract key insights, generate summaries, and analyze text data from various document formats (.txt, .docx, .pdf). It integrates advanced NLP capabilities, a legal term explainer, and a Google Gemini-powered chatbot, making it a versatile tool for document understanding. + +## 🚀 Features at a Glance +* **Document Upload & Parsing**: Supports `.txt`, `.docx`, and `.pdf` files. +* **Text Summarization**: Utilizes HuggingFace's DistilBART model for concise summaries. +* **Legal Term Explainer**: Fetches definitions from a dictionary API, with a Google search fallback. +* **AI Chatbot**: A Gemini 1.5 Pro-powered assistant for general queries and file-contextual questions. +* **Text Analytics**: Provides word, sentence, paragraph, and character counts. +* **Common Words**: Identifies and lists the most frequent words after removing stopwords. +* **Word Cloud Visualization**: Generates visual representations of common terms. +* **Readability Scoring**: Calculates the Flesch-Kincaid Grade level. +* **Downloadable Outputs**: Summaries and text statistics can be downloaded. + +## 🛠️ Tech Stack & Architecture +DocuBot AI is primarily a Python application leveraging the Streamlit framework for its interactive web interface. The core logic resides within a single `app.py` file, making it a monolithic application. It integrates several powerful libraries for NLP, document parsing, and AI capabilities, and relies on external APIs for dictionary lookups and generative AI. + +## 🗺️ Key Modules & Data Flows +1. **`app.py`**: The central hub, handling UI, file uploads, text extraction, NLP processing, and API integrations. +2. **Document Parsers (`PyPDF2`, `python-docx`)**: Extract raw text from uploaded files. +3. **NLP Pipeline (`transformers`, `textstat`, `nltk`, `collections.Counter`)**: Processes extracted text for summarization, readability, and word frequency analysis. +4. **External API Clients (`requests`, `google-generativeai`)**: Interact with the Free Dictionary API for legal terms and the Google Gemini API for chatbot functionalities. +5. **`python-dotenv`**: Manages environment variables, particularly the Google Gemini API key. + +## ⚠️ Potential Risks & Considerations +* **Monolithic Design**: While simple for this project size, a single `app.py` can become challenging to manage and scale for more complex features or team collaboration. +* **External API Dependency**: The functionality of the legal term explainer and chatbot is entirely dependent on the availability and performance of the Free Dictionary API and Google Gemini API. +* **Large File Processing**: Summarization and text extraction for very large documents might be resource-intensive, potentially leading to performance bottlenecks or timeouts. +* **NLTK Data Download**: The `nltk.download` call for stopwords runs at runtime if the data is not present, which could cause initial delays or issues in certain deployment environments. + +## 🧑‍💻 Getting Started for Developers +To understand and contribute to DocuBot AI, developers should start by examining `app.py` to grasp the overall application flow and UI structure. The `README.md` provides essential setup instructions, and `requirements.txt` lists all necessary dependencies. The `.env` file is crucial for configuring the Google Gemini API key. + +## Conclusion +DocuBot AI is a well-structured and functional prototype demonstrating the power of integrating various AI and NLP tools into a user-friendly Streamlit application. It effectively addresses common document analysis and summarization needs, offering a solid foundation for further development and expansion. \ No newline at end of file diff --git a/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_summary.json b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_summary.json new file mode 100644 index 0000000000000000000000000000000000000000..0daf14f07a1bc514e7f811aa65941c06bfc8c62a --- /dev/null +++ b/backend/storage/repos/f86159c4-07a9-4ea4-b32c-c99ace8a0ded/repository_summary.json @@ -0,0 +1,39 @@ +{ + "elevator_pitch": "DocuBot AI is a Streamlit-based intelligent document assistant that allows users to upload .txt, .docx, or .pdf files for summarization, comprehensive text analysis, legal term explanations, and interactive chatbot queries powered by Google Gemini.", + "core_features": [ + "Document Upload and Parsing (.txt, .docx, .pdf)", + "Text Summarization (using HuggingFace DistilBART)", + "Legal Term Explanation (via Free Dictionary API with Google fallback)", + "Gemini-powered Chatbot (general and file-context aware)", + "Comprehensive Text Analytics (word, sentence, paragraph, character counts)", + "Most Common Words identification (stopwords removed)", + "Word Cloud Visualization", + "Flesch-Kincaid Readability Scoring", + "Downloadable Summary and Text Statistics" + ], + "main_workflows": [ + "**Document Processing**: User uploads a document -> System extracts text -> Performs various analyses (counts, readability, common words, word cloud) -> User can trigger summarization -> User can download results.", + "**Legal Term Lookup**: User inputs a term in the sidebar -> System queries dictionary API -> Displays definition or Google search link.", + "**Chatbot Interaction**: User activates chatbot -> Asks general questions or questions specific to the uploaded document -> System queries Gemini API -> Displays AI response." + ], + "key_components": [ + "Streamlit UI for interactive web interface", + "Document Parsers (PyPDF2, python-docx) for text extraction", + "NLP Pipeline (transformers, textstat, nltk) for analysis and summarization", + "External API Clients (requests, google-generativeai) for dictionary and generative AI", + "Environment Management (python-dotenv) for API key handling" + ], + "key_risks": [ + "Monolithic architecture may hinder scalability and maintainability for future growth.", + "High reliance on external APIs (Free Dictionary, Google Gemini) for core functionalities.", + "Potential performance issues with large document processing and summarization.", + "Runtime NLTK data download can cause initial delays or deployment challenges.", + "Security considerations for API key management in production environments." + ], + "developer_start_points": [ + "app.py (main application logic and UI)", + "README.md (project overview and setup instructions)", + "requirements.txt (project dependencies)", + ".env (API key configuration)" + ] +} \ No newline at end of file diff --git a/backend/test_agent_flow.py b/backend/test_agent_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..f36aa3d5c5a35babfa133206cd784803260a0c12 --- /dev/null +++ b/backend/test_agent_flow.py @@ -0,0 +1,194 @@ +import asyncio +import os +import sys +from typing import Dict, Any, Type +from pydantic import BaseModel + +# Add backend directory to path if needed +sys.path.append(os.path.dirname(__file__)) + +from agents.llm_client import LLMClient +from agents.orchestrator import AgentOrchestrator +from agents.planner_agent import PlannerDecision +from agents.base_agent import AgentResponseSchema +from agents.response_synthesizer import SynthesizedResponse + +class MockLLMClient(LLMClient): + """ + Mock LLM client to run validation tests without hitting Gemini API endpoints. + """ + def generate_json( + self, + prompt: str, + response_schema: Type[BaseModel], + temperature: float = 0.2 + ) -> Dict[str, Any]: + print(f"[MockLLMClient] Called with schema: {response_schema.__name__}") + + if response_schema == PlannerDecision: + return { + "selected_agents": ["SecurityAgent", "ApiAgent"], + "execution_order": [["SecurityAgent", "ApiAgent"]], + "reasoning": "Query is about endpoints and credentials." + } + + elif response_schema == AgentResponseSchema: + prompt_lower = prompt.lower() + if "security" in prompt_lower: + return { + "agent": "SecurityAgent", + "confidence": 0.95, + "answer": "Checked endpoints. Found jwt authentication.", + "citations": ["backend/main.py:L48"], + "reasoning": ["Parsed authentication middleware."] + } + elif "api" in prompt_lower: + return { + "agent": "ApiAgent", + "confidence": 0.90, + "answer": "Endpoints found: POST /api/analyze-url.", + "citations": ["backend/main.py:L64"], + "reasoning": ["Scanned fastapi routes."] + } + else: + return { + "agent": "GenericAgent", + "confidence": 0.80, + "answer": "Generic analysis.", + "citations": [], + "reasoning": [] + } + + elif response_schema == SynthesizedResponse: + return { + "summary": "Coherent combined view of authentication and API endpoints.", + "detailed_explanation": "Combined: The app has jwt authentication on routes like POST /api/analyze-url.", + "agent_contributions": [ + "SecurityAgent: Analyzed JWT usage.", + "ApiAgent: Listed endpoints." + ], + "confidence_score": 0.93 + } + + return {} + +async def run_tests(): + print("=== Running Integration Tests for Phase 2 Agent Architecture ===") + + # Setup mock data + profile = { + "project_name": "Test Project", + "project_type": "web app", + "languages": ["Python"], + "frameworks": ["FastAPI"], + "databases": [], + "authentication_methods": ["JWT"], + "major_modules": ["main"], + "api_endpoints": ["/api/health", "/api/analyze-url"], + "important_files": ["main.py"], + "architecture_pattern": "Monolithic", + "dependencies": ["fastapi", "uvicorn"] + } + + graph = { + "nodes": [{"id": "main.py", "label": "main.py", "type": "file", "properties": {}}], + "edges": [], + "entry_points": [{"file_path": "main.py", "type": "uvicorn", "description": "Start app"}], + "business_flows": [], + "critical_paths": [], + "concepts": [] + } + + summary = { + "elevator_pitch": "A test repository scanning tool.", + "core_features": ["Cloning", "Scanning"], + "main_workflows": [], + "key_components": [], + "key_risks": [], + "developer_start_points": ["main.py"] + } + + report = "# Test Intelligence Report\nThis is a mock repository report." + + query = "How is security and routing configured?" + + mock_client = MockLLMClient() + orchestrator = AgentOrchestrator(mock_client) + + print("\nExecuting orchestrator...") + result = await orchestrator.execute(profile, graph, summary, report, query, repo_id="test_repo") + + print("\n--- Test Result ---") + print(f"Synthesized Answer: {result['answer']}") + print(f"Agents Used: {result['agents_used']}") + print(f"Confidence Score: {result['confidence']}") + print(f"Citations/References: {result['references']}") + print(f"Timeline Steps count: {len(result['timeline'])}") + print(f"Total Time MS: {result['total_time_ms']}ms") + print("-------------------") + + # Validate result fields + assert result['answer'] != "" + assert "SecurityAgent" in result['agents_used'] + assert "ApiAgent" in result['agents_used'] + assert result['confidence'] == 0.93 + assert len(result['references']) > 0 + assert len(result['timeline']) == 4 # Planner + 2 Parallel Agents + Synthesizer + print("\nIntegrity assertion checks passed successfully!") + + # Validate fallback behavior when synthesized answer is empty + print("\nTesting fallback behavior for empty synthesized answer...") + def generate_json_with_empty_answer(prompt: str, response_schema: Type[BaseModel], temperature: float = 0.2) -> Dict[str, Any]: + if response_schema == PlannerDecision: + return { + "selected_agents": ["SecurityAgent", "ApiAgent"], + "execution_order": [["SecurityAgent", "ApiAgent"]], + "reasoning": "Query is about endpoints and credentials." + } + elif response_schema == AgentResponseSchema: + prompt_lower = prompt.lower() + if "security agent" in prompt_lower or "architecture agent" in prompt_lower or "quality agent" in prompt_lower or "onboarding agent" in prompt_lower: + return { + "agent": "SecurityAgent", + "confidence": 0.95, + "answer": "Checked endpoints. Found jwt authentication.", + "citations": ["backend/main.py:L48"], + "reasoning": ["Parsed authentication middleware."] + } + elif "api agent" in prompt_lower or "api" in prompt_lower: + return { + "agent": "ApiAgent", + "confidence": 0.90, + "answer": "Endpoints found: POST /api/analyze-url.", + "citations": ["backend/main.py:L64"], + "reasoning": ["Scanned fastapi routes."] + } + else: + return { + "agent": "GenericAgent", + "confidence": 0.80, + "answer": "Generic analysis.", + "citations": [], + "reasoning": [] + } + elif response_schema == SynthesizedResponse: + return { + "summary": "Fallback summary compiled from individual agents.", + "detailed_explanation": "", + "agent_contributions": [ + "SecurityAgent: Analyzed JWT usage.", + "ApiAgent: Listed endpoints." + ], + "confidence_score": 0.50 + } + return {} + + mock_client.generate_json = generate_json_with_empty_answer # type: ignore + fallback_result = await orchestrator.execute(profile, graph, summary, report, query, repo_id="test_repo") + assert fallback_result['answer'] != "" + assert "SecurityAgent" in fallback_result['answer'] + assert "ApiAgent" in fallback_result['answer'] + print("Fallback behavior validation passed successfully!") + +if __name__ == "__main__": + asyncio.run(run_tests()) diff --git a/backend/test_rag_pipeline.py b/backend/test_rag_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..4dd3149321bd6d34b7c78d9c48dcffa023cd9d9f --- /dev/null +++ b/backend/test_rag_pipeline.py @@ -0,0 +1,184 @@ +""" +Phase 3 integration test: validates RAG pipeline, tool execution, +conversation manager, and vector store operations without hitting live APIs. +""" +import sys, os, asyncio +sys.path.insert(0, os.path.dirname(__file__)) + +# ── Mock EmbeddingService ──────────────────────────────────────────────────── +from memory.embedding_service import EmbeddingService +from memory.vector_store import VectorStore +from memory.knowledge_index import KnowledgeIndexBuilder +from memory.retriever import KnowledgeRetriever +from memory.conversation_manager import ConversationManager +from tools.repository_search_tool import RepositorySearchTool +from tools.graph_query_tool import GraphQueryTool +from tools.dependency_lookup_tool import DependencyLookupTool +from tools.architecture_lookup_tool import ArchitectureLookupTool +from tools.api_lookup_tool import ApiLookupTool +import unittest.mock as mock +import tempfile, shutil + +# ── Fixtures ───────────────────────────────────────────────────────────────── +REPO_ID = "test-repo-phase3" + +PROFILE = { + "project_name": "TestApp", "project_type": "web app", + "languages": ["Python"], "frameworks": ["FastAPI"], + "databases": ["SQLite"], "authentication_methods": ["JWT"], + "major_modules": ["main", "auth"], "api_endpoints": ["/api/login", "/api/data"], + "important_files": ["main.py"], "architecture_pattern": "Monolithic", + "dependencies": ["fastapi", "pyjwt"] +} + +SUMMARY = { + "elevator_pitch": "A test app with JWT auth.", + "core_features": ["Login", "Data access"], + "main_workflows": ["User authentication flow"], + "key_components": ["AuthRouter"], + "key_risks": ["No refresh tokens"], + "developer_start_points": ["main.py"] +} + +GRAPH = { + "nodes": [{"id": "main.py", "label": "main.py", "type": "file", "properties": {}}], + "edges": [], + "entry_points": [{"file_path": "main.py", "type": "uvicorn", "description": "Entry"}], + "business_flows": [{"flow_name": "Auth Flow", "description": "Login process", "steps": ["login_endpoint"]}], + "critical_paths": [], + "concepts": [{"name": "Authentication", "description": "JWT-based auth", "files": ["auth.py"]}] +} + +REPORT = "# Test Report\nThis app uses JWT authentication with FastAPI." + +FILES = [ + {"path": "main.py", "content": "from fastapi import FastAPI\napp = FastAPI()", "size": 50}, + {"path": "auth.py", "content": "import jwt\ndef verify(token): pass", "size": 40} +] + +def run_tests(): + print("=== Phase 3 Integration Tests ===\n") + tmp_dir = tempfile.mkdtemp(prefix="phase3_test_") + passed = 0 + failed = 0 + + try: + # ── Test 1: VectorStore create / count ──────────────────────────────── + print("Test 1: VectorStore initialization...") + vs = VectorStore(storage_path=tmp_dir) + count = vs.count_documents(REPO_ID) + assert count == 0, f"Expected 0, got {count}" + print(" ✓ VectorStore initialized, empty collection count = 0") + passed += 1 + + # ── Test 2: KnowledgeIndexBuilder with mocked embedder ──────────────── + print("Test 2: KnowledgeIndexBuilder with mocked embeddings...") + mock_embedder = mock.MagicMock() + # Return exactly N vectors where N = len of input texts + mock_embedder.embed_texts.side_effect = lambda texts: [[0.1] * 768 for _ in texts] + mock_embedder.embed_text.return_value = [0.1] * 768 + + indexer = KnowledgeIndexBuilder(mock_embedder, vs) + indexer.build_index(REPO_ID, PROFILE, SUMMARY, GRAPH, REPORT, FILES) + + count_after = vs.count_documents(REPO_ID) + assert count_after > 0, f"Expected chunks indexed, got {count_after}" + print(f" ✓ Indexed {count_after} chunks successfully") + passed += 1 + + # ── Test 3: KnowledgeRetriever ──────────────────────────────────────── + print("Test 3: KnowledgeRetriever semantic query...") + retriever = KnowledgeRetriever(mock_embedder, vs) + results = retriever.retrieve(REPO_ID, "JWT authentication", top_k=3) + assert isinstance(results, list), "Expected list of results" + assert len(results) <= 3, f"Expected at most 3, got {len(results)}" + if results: + assert "content" in results[0] + assert "similarity" in results[0] + print(f" ✓ Retrieved {len(results)} results with similarity scores") + passed += 1 + + # ── Test 4: Category-filtered retrieval ─────────────────────────────── + print("Test 4: Filtered retrieval by category 'authentication'...") + auth_results = retriever.retrieve(REPO_ID, "login security", top_k=5, category="authentication") + assert isinstance(auth_results, list) + print(f" ✓ Filtered retrieval returned {len(auth_results)} authentication chunks") + passed += 1 + + # ── Test 5: ConversationManager ─────────────────────────────────────── + print("Test 5: ConversationManager sessions...") + cm = ConversationManager() + cm.add_message("sess-1", REPO_ID, "user", "What is the auth method?") + cm.add_message("sess-1", REPO_ID, "assistant", "JWT is used.", agent_decisions={"agents_used": ["SecurityAgent"]}) + cm.add_message("sess-2", REPO_ID, "user", "List all endpoints.") + + session = cm.get_session("sess-1") + assert session is not None + assert len(session.history) == 2 + assert session.history[0].role == "user" + assert session.history[1].agent_decisions["agents_used"] == ["SecurityAgent"] + + sessions_for_repo = cm.list_sessions_for_repo(REPO_ID) + assert len(sessions_for_repo) == 2 + print(f" ✓ ConversationManager: {len(sessions_for_repo)} sessions, history tracked") + passed += 1 + + # ── Test 6: Tools execute ───────────────────────────────────────────── + print("Test 6: Tool execution (DependencyLookup, ArchitectureLookup)...") + from services.repositoryMemory import memory_service + memory_service.store(REPO_ID, PROFILE, GRAPH, SUMMARY, REPORT) + + dep_tool = DependencyLookupTool() + dep_result = dep_tool.execute(repo_id=REPO_ID) + assert "frameworks" in dep_result + assert "FastAPI" in dep_result["frameworks"] + + arch_tool = ArchitectureLookupTool() + arch_result = arch_tool.execute(repo_id=REPO_ID) + assert arch_result["architecture_pattern"] == "Monolithic" + + api_tool = ApiLookupTool() + api_result = api_tool.execute(repo_id=REPO_ID) + assert "/api/login" in api_result["api_endpoints"] + + graph_tool = GraphQueryTool() + graph_result = graph_tool.execute(repo_id=REPO_ID) + assert "business_flows" in graph_result + + search_tool = RepositorySearchTool(retriever) + search_result = search_tool.execute(repo_id=REPO_ID, query="authentication") + assert "results" in search_result + + print(" ✓ All 5 tools executed and returned correct data") + passed += 1 + + # ── Test 7: VectorStore delete ──────────────────────────────────────── + print("Test 7: VectorStore collection deletion...") + vs.delete_collection(REPO_ID) + count_deleted = vs.count_documents(REPO_ID) + assert count_deleted == 0 + print(" ✓ Collection deleted, count = 0") + passed += 1 + + except AssertionError as e: + print(f" ✗ ASSERTION FAILED: {e}") + failed += 1 + except Exception as e: + print(f" ✗ UNEXPECTED ERROR: {e}") + import traceback; traceback.print_exc() + failed += 1 + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + print(f"\n{'='*40}") + print(f"Results: {passed} passed, {failed} failed") + if failed == 0: + print("All Phase 3 integration tests passed! ✓") + else: + print("Some tests FAILED.") + return failed == 0 + +if __name__ == "__main__": + ok = run_tests() + sys.exit(0 if ok else 1) + diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7b81278dbc6461539c6e04a61b5fa77d2fd7ea35 --- /dev/null +++ b/backend/tools/__init__.py @@ -0,0 +1,7 @@ +from tools.base_tool import BaseTool +from tools.repository_search_tool import RepositorySearchTool +from tools.graph_query_tool import GraphQueryTool +from tools.dependency_lookup_tool import DependencyLookupTool +from tools.file_reader_tool import FileReaderTool +from tools.architecture_lookup_tool import ArchitectureLookupTool +from tools.api_lookup_tool import ApiLookupTool diff --git a/backend/tools/__pycache__/__init__.cpython-312.pyc b/backend/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06779bf23d062e86349798f1ea0e9c45483bba6b Binary files /dev/null and b/backend/tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/api_lookup_tool.cpython-312.pyc b/backend/tools/__pycache__/api_lookup_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c308f058b61060cf177b90c3149acbdeb99ad7bb Binary files /dev/null and b/backend/tools/__pycache__/api_lookup_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/architecture_lookup_tool.cpython-312.pyc b/backend/tools/__pycache__/architecture_lookup_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e8d26467a79909d0013d56d379b7c9c46c4b59c Binary files /dev/null and b/backend/tools/__pycache__/architecture_lookup_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/base_tool.cpython-312.pyc b/backend/tools/__pycache__/base_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0488ae40d34580a4fc3904d92c1b504363ec927 Binary files /dev/null and b/backend/tools/__pycache__/base_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/dependency_lookup_tool.cpython-312.pyc b/backend/tools/__pycache__/dependency_lookup_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5fb78a0d0c475ad930b7d08ac39963604c9d2e0 Binary files /dev/null and b/backend/tools/__pycache__/dependency_lookup_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/file_reader_tool.cpython-312.pyc b/backend/tools/__pycache__/file_reader_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17bc8b635fd0c4ab19a29faf24fc61dafdfeb916 Binary files /dev/null and b/backend/tools/__pycache__/file_reader_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/graph_query_tool.cpython-312.pyc b/backend/tools/__pycache__/graph_query_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c8b3827fde2187cc741d9e683c15047f8dd3fe6 Binary files /dev/null and b/backend/tools/__pycache__/graph_query_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/repository_search_tool.cpython-312.pyc b/backend/tools/__pycache__/repository_search_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3f4173911bbc15606e1e965a37d34affabc6d93 Binary files /dev/null and b/backend/tools/__pycache__/repository_search_tool.cpython-312.pyc differ diff --git a/backend/tools/__pycache__/tool_registry.cpython-312.pyc b/backend/tools/__pycache__/tool_registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5c535203bb670b490e923a0ae45bbc8a7377a0f Binary files /dev/null and b/backend/tools/__pycache__/tool_registry.cpython-312.pyc differ diff --git a/backend/tools/api_lookup_tool.py b/backend/tools/api_lookup_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..891ee9459af9efe082285bf3ac8b6d60ed91a68e --- /dev/null +++ b/backend/tools/api_lookup_tool.py @@ -0,0 +1,27 @@ +from tools.base_tool import BaseTool +from services.repositoryMemory import memory_service +from typing import Any + +class ApiLookupTool(BaseTool): + @property + def name(self) -> str: + return "api_lookup" + + @property + def description(self) -> str: + return "Queries project HTTP routes, api endpoints, and controllers. Inputs: repo_id (str)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + if not repo_id: + return {"error": "Missing required parameter: repo_id."} + + data = memory_service.retrieve(repo_id) + if not data or "profile" not in data: + return {"error": f"No profile found for repository {repo_id}."} + + profile = data["profile"] + return { + "api_endpoints": profile.get("api_endpoints", []), + "authentication_methods": profile.get("authentication_methods", []) + } diff --git a/backend/tools/architecture_lookup_tool.py b/backend/tools/architecture_lookup_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..7efe32547acef8e45b1e03dc698ae499d0ea65ba --- /dev/null +++ b/backend/tools/architecture_lookup_tool.py @@ -0,0 +1,28 @@ +from tools.base_tool import BaseTool +from services.repositoryMemory import memory_service +from typing import Any + +class ArchitectureLookupTool(BaseTool): + @property + def name(self) -> str: + return "architecture_lookup" + + @property + def description(self) -> str: + return "Queries project high-level architecture pattern and main folders/modules. Inputs: repo_id (str)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + if not repo_id: + return {"error": "Missing required parameter: repo_id."} + + data = memory_service.retrieve(repo_id) + if not data or "profile" not in data: + return {"error": f"No intelligence profile found for repository {repo_id}."} + + profile = data["profile"] + return { + "architecture_pattern": profile.get("architecture_pattern", ""), + "major_modules": profile.get("major_modules", []), + "important_files": profile.get("important_files", []) + } diff --git a/backend/tools/base_tool.py b/backend/tools/base_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..0203d7b4e6786745a4a59208845222f8f360f39c --- /dev/null +++ b/backend/tools/base_tool.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from typing import Any + +class BaseTool(ABC): + """ + Abstract Base Class for all LLM-callable tools. + Compatible with Model Context Protocol (MCP) and agent execution environments. + """ + @property + @abstractmethod + def name(self) -> str: + pass + + @property + @abstractmethod + def description(self) -> str: + pass + + @abstractmethod + def execute(self, **kwargs) -> Any: + """Executes the tool's action with provided arguments.""" + pass diff --git a/backend/tools/dependency_lookup_tool.py b/backend/tools/dependency_lookup_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..e8edfdadd68562d35cc0a02166f30647a59d72cc --- /dev/null +++ b/backend/tools/dependency_lookup_tool.py @@ -0,0 +1,30 @@ +from tools.base_tool import BaseTool +from services.repositoryMemory import memory_service +from typing import Any + +class DependencyLookupTool(BaseTool): + @property + def name(self) -> str: + return "dependency_lookup" + + @property + def description(self) -> str: + return "Queries libraries, dependencies, package configurations, and stack components. Inputs: repo_id (str)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + if not repo_id: + return {"error": "Missing required parameter: repo_id."} + + data = memory_service.retrieve(repo_id) + if not data or "profile" not in data: + return {"error": f"No profile found for repository {repo_id}."} + + profile = data["profile"] + return { + "languages": profile.get("languages", []), + "frameworks": profile.get("frameworks", []), + "databases": profile.get("databases", []), + "dependencies": profile.get("dependencies", []), + "project_type": profile.get("project_type", "") + } diff --git a/backend/tools/file_reader_tool.py b/backend/tools/file_reader_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..b8222a22450533cc09947fbb17e737b13d5cfe53 --- /dev/null +++ b/backend/tools/file_reader_tool.py @@ -0,0 +1,59 @@ +from tools.base_tool import BaseTool +from memory.vector_store import VectorStore +from typing import Any +import logging + +logger = logging.getLogger("file_reader_tool") + +class FileReaderTool(BaseTool): + def __init__(self, vector_store: VectorStore): + self.store = vector_store + + @property + def name(self) -> str: + return "file_reader" + + @property + def description(self) -> str: + return "Reads the content of a specific source code file by retrieving its chunks. Inputs: repo_id (str), path (str)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + path = kwargs.get("path") + if not repo_id or not path: + return {"error": "Missing required parameters: repo_id and path."} + + try: + collection = self.store.get_collection(repo_id) + results = collection.get(where={"path": path}) + + if not results or not results.get("documents"): + return {"error": f"File '{path}' not found in knowledge index."} + + docs = results["documents"] + metas = results["metadatas"] + + # Reconstruct sorting by chunk index + chunks = [] + for doc, meta in zip(docs, metas): + idx = meta.get("chunk_index", 0) + chunks.append((idx, doc)) + chunks.sort(key=lambda x: x[0]) + + # Rebuild file and remove the file header prefix + content_builder = [] + for idx, content in chunks: + if "\n\n" in content and content.startswith("File: "): + parts = content.split("\n\n", 1) + content_builder.append(parts[1]) + else: + content_builder.append(content) + + return { + "path": path, + "content": "".join(content_builder), + "chunks_found": len(chunks) + } + except Exception as e: + logger.error(f"Error executing file_reader tool: {e}") + return {"error": f"Failed to retrieve file contents: {str(e)}"} diff --git a/backend/tools/graph_query_tool.py b/backend/tools/graph_query_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..562815d5d09f6add28eb05b52f4e4a29e45ae5dd --- /dev/null +++ b/backend/tools/graph_query_tool.py @@ -0,0 +1,31 @@ +from tools.base_tool import BaseTool +from services.repositoryMemory import memory_service +from typing import Any + +class GraphQueryTool(BaseTool): + @property + def name(self) -> str: + return "graph_query" + + @property + def description(self) -> str: + return "Queries the repository code dependency graph, entry points, workflows, and concepts. Inputs: repo_id (str)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + if not repo_id: + return {"error": "Missing required parameter: repo_id."} + + data = memory_service.retrieve(repo_id) + if not data or "graph" not in data: + return {"error": f"No architecture graph found for repository {repo_id}."} + + graph = data["graph"] + return { + "entry_points": graph.get("entry_points", []), + "business_flows": graph.get("business_flows", []), + "critical_paths": graph.get("critical_paths", []), + "concepts": graph.get("concepts", []), + "node_count": len(graph.get("nodes", [])), + "edge_count": len(graph.get("edges", [])) + } diff --git a/backend/tools/repository_search_tool.py b/backend/tools/repository_search_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..ccfeb06bd8ee0f1b1ef66d1f8e80549e836ed309 --- /dev/null +++ b/backend/tools/repository_search_tool.py @@ -0,0 +1,35 @@ +from tools.base_tool import BaseTool +from memory.retriever import KnowledgeRetriever +from typing import Any + +class RepositorySearchTool(BaseTool): + def __init__(self, retriever: KnowledgeRetriever): + self.retriever = retriever + + @property + def name(self) -> str: + return "repository_search" + + @property + def description(self) -> str: + return "Searches the codebase semantically for matching text, logic, or functions. Inputs: repo_id (str), query (str), top_k (int, optional)." + + def execute(self, **kwargs) -> Any: + repo_id = kwargs.get("repo_id") + query = kwargs.get("query") + top_k = kwargs.get("top_k", 5) + + if not repo_id or not query: + return {"error": "Missing required parameters: repo_id and query."} + + results = self.retriever.retrieve(repo_id=repo_id, query=query, top_k=top_k) + return { + "results": [ + { + "content": r["content"], + "metadata": r["metadata"], + "similarity": r["similarity"] + } + for r in results + ] + } diff --git a/backend/tools/tool_registry.py b/backend/tools/tool_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..22549883bdf40835e7d59d98e934c3f2d6bcc775 --- /dev/null +++ b/backend/tools/tool_registry.py @@ -0,0 +1,49 @@ +from typing import Dict, Any, List +from tools.base_tool import BaseTool +from tools.repository_search_tool import RepositorySearchTool +from tools.graph_query_tool import GraphQueryTool +from tools.dependency_lookup_tool import DependencyLookupTool +from tools.file_reader_tool import FileReaderTool +from tools.architecture_lookup_tool import ArchitectureLookupTool +from tools.api_lookup_tool import ApiLookupTool + +class ToolRegistry: + """ + Registry for managing and executing repository intelligence tools. + Compatible with agent planners and the Model Context Protocol (MCP). + """ + def __init__(self): + self._tools: Dict[str, BaseTool] = {} + + def register(self, tool: BaseTool): + self._tools[tool.name] = tool + + def get_tool(self, name: str) -> BaseTool: + return self._tools.get(name) + + def list_tools(self) -> List[Dict[str, str]]: + return [ + {"name": tool.name, "description": tool.description} + for tool in self._tools.values() + ] + + def execute_tool(self, name: str, **kwargs) -> Any: + tool = self.get_tool(name) + if not tool: + return {"error": f"Tool '{name}' not found in registry."} + try: + return tool.execute(**kwargs) + except Exception as e: + return {"error": f"Error executing tool '{name}': {str(e)}"} + +# Global tool registry instance +tool_registry = ToolRegistry() + +def setup_default_registry(vector_store, retriever): + """Utility to register all standard repository tools with dependencies.""" + tool_registry.register(RepositorySearchTool(retriever)) + tool_registry.register(FileReaderTool(vector_store)) + tool_registry.register(GraphQueryTool()) + tool_registry.register(DependencyLookupTool()) + tool_registry.register(ArchitectureLookupTool()) + tool_registry.register(ApiLookupTool()) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..1488ba18cfca0f35693d5c9c48dd03980a52dde9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + - GEMINI_API_KEY=${GEMINI_API_KEY} + - CORS_ORIGINS=http://localhost:5173,http://localhost + volumes: + - ./backend:/app + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "5173:80" + depends_on: + - backend diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0c8b6e8a4aaf8d17f7b324c07e741aeb737290c9 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +# Build stage +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +# Serve stage +FROM nginx:stable-alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/dist/assets/index-BLWiEPV3.js b/frontend/dist/assets/index-BLWiEPV3.js new file mode 100644 index 0000000000000000000000000000000000000000..ed2239bf8997b5b55e03b028d01c54d5e6c9a926 --- /dev/null +++ b/frontend/dist/assets/index-BLWiEPV3.js @@ -0,0 +1,305 @@ +var Pd=Object.defineProperty;var Ro=e=>{throw TypeError(e)};var Ld=(e,t,n)=>t in e?Pd(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var V=(e,t,n)=>Ld(e,typeof t!="symbol"?t+"":t,n),Rd=(e,t,n)=>t.has(e)||Ro("Cannot "+n);var Mo=(e,t,n)=>t.has(e)?Ro("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n);var Nr=(e,t,n)=>(Rd(e,t,"access private method"),n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function Md(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Xa={exports:{}},Rl={},Ja={exports:{}},I={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var yr=Symbol.for("react.element"),Fd=Symbol.for("react.portal"),Ad=Symbol.for("react.fragment"),Id=Symbol.for("react.strict_mode"),$d=Symbol.for("react.profiler"),Dd=Symbol.for("react.provider"),Od=Symbol.for("react.context"),Bd=Symbol.for("react.forward_ref"),Hd=Symbol.for("react.suspense"),Ud=Symbol.for("react.memo"),Wd=Symbol.for("react.lazy"),Fo=Symbol.iterator;function Vd(e){return e===null||typeof e!="object"?null:(e=Fo&&e[Fo]||e["@@iterator"],typeof e=="function"?e:null)}var eu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},tu=Object.assign,nu={};function Cn(e,t,n){this.props=e,this.context=t,this.refs=nu,this.updater=n||eu}Cn.prototype.isReactComponent={};Cn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Cn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ru(){}ru.prototype=Cn.prototype;function Ss(e,t,n){this.props=e,this.context=t,this.refs=nu,this.updater=n||eu}var js=Ss.prototype=new ru;js.constructor=Ss;tu(js,Cn.prototype);js.isPureReactComponent=!0;var Ao=Array.isArray,lu=Object.prototype.hasOwnProperty,Ns={current:null},iu={key:!0,ref:!0,__self:!0,__source:!0};function su(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)lu.call(t,r)&&!iu.hasOwnProperty(r)&&(l[r]=t[r]);var a=arguments.length-2;if(a===1)l.children=n;else if(1>>1,J=T[G];if(0>>1;Gl(ce,F))del(Yt,ce)?(T[G]=Yt,T[de]=F,G=de):(T[G]=ce,T[U]=F,G=U);else if(del(Yt,F))T[G]=Yt,T[de]=F,G=de;else break e}}return M}function l(T,M){var F=T.sortIndex-M.sortIndex;return F!==0?F:T.id-M.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,a=s.now();e.unstable_now=function(){return s.now()-a}}var u=[],d=[],g=1,y=null,p=3,v=!1,w=!1,k=!1,R=typeof setTimeout=="function"?setTimeout:null,m=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(T){for(var M=n(d);M!==null;){if(M.callback===null)r(d);else if(M.startTime<=T)r(d),M.sortIndex=M.expirationTime,t(u,M);else break;M=n(d)}}function x(T){if(k=!1,h(T),!w)if(n(u)!==null)w=!0,Rt(f);else{var M=n(d);M!==null&&qt(x,M.startTime-T)}}function f(T,M){w=!1,k&&(k=!1,m(C),C=-1),v=!0;var F=p;try{for(h(M),y=n(u);y!==null&&(!(y.expirationTime>M)||T&&!D());){var G=y.callback;if(typeof G=="function"){y.callback=null,p=y.priorityLevel;var J=G(y.expirationTime<=M);M=e.unstable_now(),typeof J=="function"?y.callback=J:y===n(u)&&r(u),h(M)}else r(u);y=n(u)}if(y!==null)var E=!0;else{var U=n(d);U!==null&&qt(x,U.startTime-M),E=!1}return E}finally{y=null,p=F,v=!1}}var N=!1,z=null,C=-1,S=5,_=-1;function D(){return!(e.unstable_now()-_T||125G?(T.sortIndex=F,t(d,T),n(u)===null&&T===n(d)&&(k?(m(C),C=-1):k=!0,qt(x,F-G))):(T.sortIndex=J,t(u,T),w||v||(w=!0,Rt(f))),T},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(T){var M=p;return function(){var F=p;p=M;try{return T.apply(this,arguments)}finally{p=F}}}})(du);cu.exports=du;var tf=cu.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var nf=L,Le=tf;function j(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ci=Object.prototype.hasOwnProperty,rf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,$o={},Do={};function lf(e){return Ci.call(Do,e)?!0:Ci.call($o,e)?!1:rf.test(e)?Do[e]=!0:($o[e]=!0,!1)}function sf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function of(e,t,n,r){if(t===null||typeof t>"u"||sf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function xe(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var ue={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ue[e]=new xe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ue[t]=new xe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ue[e]=new xe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ue[e]=new xe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ue[e]=new xe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ue[e]=new xe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ue[e]=new xe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ue[e]=new xe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ue[e]=new xe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Cs=/[\-:]([a-z])/g;function Es(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Cs,Es);ue[t]=new xe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Cs,Es);ue[t]=new xe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Cs,Es);ue[t]=new xe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ue[e]=new xe(e,1,!1,e.toLowerCase(),null,!1,!1)});ue.xlinkHref=new xe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ue[e]=new xe(e,1,!1,e.toLowerCase(),null,!0,!0)});function Ts(e,t,n,r){var l=ue.hasOwnProperty(t)?ue[t]:null;(l!==null?l.type!==0:r||!(2a||l[s]!==i[a]){var u=` +`+l[s].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=s&&0<=a);break}}}finally{Jl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?On(e):""}function af(e){switch(e.tag){case 5:return On(e.type);case 16:return On("Lazy");case 13:return On("Suspense");case 19:return On("SuspenseList");case 0:case 2:case 15:return e=ei(e.type,!1),e;case 11:return e=ei(e.type.render,!1),e;case 1:return e=ei(e.type,!0),e;default:return""}}function Li(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case tn:return"Fragment";case en:return"Portal";case Ei:return"Profiler";case Ps:return"StrictMode";case Ti:return"Suspense";case Pi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case hu:return(e.displayName||"Context")+".Consumer";case pu:return(e._context.displayName||"Context")+".Provider";case Ls:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Rs:return t=e.displayName||null,t!==null?t:Li(e.type)||"Memo";case dt:t=e._payload,e=e._init;try{return Li(e(t))}catch{}}return null}function uf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Li(t);case 8:return t===Ps?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function _t(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function gu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function cf(e){var t=gu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Cr(e){e._valueTracker||(e._valueTracker=cf(e))}function yu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=gu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function nl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Ri(e,t){var n=t.checked;return Y({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Bo(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=_t(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function vu(e,t){t=t.checked,t!=null&&Ts(e,"checked",t,!1)}function Mi(e,t){vu(e,t);var n=_t(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Fi(e,t.type,n):t.hasOwnProperty("defaultValue")&&Fi(e,t.type,_t(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Ho(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Fi(e,t,n){(t!=="number"||nl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Bn=Array.isArray;function pn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Er.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function tr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Wn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},df=["Webkit","ms","Moz","O"];Object.keys(Wn).forEach(function(e){df.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Wn[t]=Wn[e]})});function Su(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Wn.hasOwnProperty(e)&&Wn[e]?(""+t).trim():t+"px"}function ju(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Su(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var ff=Y({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function $i(e,t){if(t){if(ff[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(j(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(j(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(j(61))}if(t.style!=null&&typeof t.style!="object")throw Error(j(62))}}function Di(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Oi=null;function Ms(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Bi=null,hn=null,mn=null;function Vo(e){if(e=kr(e)){if(typeof Bi!="function")throw Error(j(280));var t=e.stateNode;t&&(t=$l(t),Bi(e.stateNode,e.type,t))}}function Nu(e){hn?mn?mn.push(e):mn=[e]:hn=e}function zu(){if(hn){var e=hn,t=mn;if(mn=hn=null,Vo(e),t)for(e=0;e>>=0,e===0?32:31-(jf(e)/Nf|0)|0}var Tr=64,Pr=4194304;function Hn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function sl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var a=s&~l;a!==0?r=Hn(a):(i&=s,i!==0&&(r=Hn(i)))}else s=n&~l,s!==0?r=Hn(s):i!==0&&(r=Hn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function vr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ve(t),e[t]=n}function Ef(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Qn),Jo=" ",ea=!1;function Qu(e,t){switch(e){case"keyup":return np.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function bu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var nn=!1;function lp(e,t){switch(e){case"compositionend":return bu(t);case"keypress":return t.which!==32?null:(ea=!0,Jo);case"textInput":return e=t.data,e===Jo&&ea?null:e;default:return null}}function ip(e,t){if(nn)return e==="compositionend"||!Hs&&Qu(e,t)?(e=Wu(),Kr=Ds=mt=null,nn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=la(n)}}function qu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?qu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Yu(){for(var e=window,t=nl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=nl(e.document)}return t}function Us(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function hp(e){var t=Yu(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&qu(n.ownerDocument.documentElement,n)){if(r!==null&&Us(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=ia(n,i);var s=ia(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,rn=null,bi=null,Kn=null,Ki=!1;function sa(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ki||rn==null||rn!==nl(r)||(r=rn,"selectionStart"in r&&Us(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Kn&&or(Kn,r)||(Kn=r,r=ul(bi,"onSelect"),0on||(e.current=Ji[on],Ji[on]=null,on--)}function W(e,t){on++,Ji[on]=e.current,e.current=t}var Ct={},me=Tt(Ct),Ne=Tt(!1),Bt=Ct;function wn(e,t){var n=e.type.contextTypes;if(!n)return Ct;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ze(e){return e=e.childContextTypes,e!=null}function dl(){b(Ne),b(me)}function pa(e,t,n){if(me.current!==Ct)throw Error(j(168));W(me,t),W(Ne,n)}function sc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(j(108,uf(e)||"Unknown",l));return Y({},n,r)}function fl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ct,Bt=me.current,W(me,e),W(Ne,Ne.current),!0}function ha(e,t,n){var r=e.stateNode;if(!r)throw Error(j(169));n?(e=sc(e,t,Bt),r.__reactInternalMemoizedMergedChildContext=e,b(Ne),b(me),W(me,e)):b(Ne),W(Ne,n)}var tt=null,Dl=!1,hi=!1;function oc(e){tt===null?tt=[e]:tt.push(e)}function _p(e){Dl=!0,oc(e)}function Pt(){if(!hi&&tt!==null){hi=!0;var e=0,t=B;try{var n=tt;for(B=1;e>=s,l-=s,nt=1<<32-Ve(t)+l|n<C?(S=z,z=null):S=z.sibling;var _=p(m,z,h[C],x);if(_===null){z===null&&(z=S);break}e&&z&&_.alternate===null&&t(m,z),c=i(_,c,C),N===null?f=_:N.sibling=_,N=_,z=S}if(C===h.length)return n(m,z),K&&Mt(m,C),f;if(z===null){for(;CC?(S=z,z=null):S=z.sibling;var D=p(m,z,_.value,x);if(D===null){z===null&&(z=S);break}e&&z&&D.alternate===null&&t(m,z),c=i(D,c,C),N===null?f=D:N.sibling=D,N=D,z=S}if(_.done)return n(m,z),K&&Mt(m,C),f;if(z===null){for(;!_.done;C++,_=h.next())_=y(m,_.value,x),_!==null&&(c=i(_,c,C),N===null?f=_:N.sibling=_,N=_);return K&&Mt(m,C),f}for(z=r(m,z);!_.done;C++,_=h.next())_=v(z,m,C,_.value,x),_!==null&&(e&&_.alternate!==null&&z.delete(_.key===null?C:_.key),c=i(_,c,C),N===null?f=_:N.sibling=_,N=_);return e&&z.forEach(function(ke){return t(m,ke)}),K&&Mt(m,C),f}function R(m,c,h,x){if(typeof h=="object"&&h!==null&&h.type===tn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case _r:e:{for(var f=h.key,N=c;N!==null;){if(N.key===f){if(f=h.type,f===tn){if(N.tag===7){n(m,N.sibling),c=l(N,h.props.children),c.return=m,m=c;break e}}else if(N.elementType===f||typeof f=="object"&&f!==null&&f.$$typeof===dt&&ya(f)===N.type){n(m,N.sibling),c=l(N,h.props),c.ref=An(m,N,h),c.return=m,m=c;break e}n(m,N);break}else t(m,N);N=N.sibling}h.type===tn?(c=Ot(h.props.children,m.mode,x,h.key),c.return=m,m=c):(x=tl(h.type,h.key,h.props,null,m.mode,x),x.ref=An(m,c,h),x.return=m,m=x)}return s(m);case en:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(m,c.sibling),c=l(c,h.children||[]),c.return=m,m=c;break e}else{n(m,c);break}else t(m,c);c=c.sibling}c=Si(h,m.mode,x),c.return=m,m=c}return s(m);case dt:return N=h._init,R(m,c,N(h._payload),x)}if(Bn(h))return w(m,c,h,x);if(Pn(h))return k(m,c,h,x);$r(m,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(m,c.sibling),c=l(c,h),c.return=m,m=c):(n(m,c),c=wi(h,m.mode,x),c.return=m,m=c),s(m)):n(m,c)}return R}var jn=dc(!0),fc=dc(!1),ml=Tt(null),gl=null,cn=null,bs=null;function Ks(){bs=cn=gl=null}function Gs(e){var t=ml.current;b(ml),e._currentValue=t}function ns(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function yn(e,t){gl=e,bs=cn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(je=!0),e.firstContext=null)}function De(e){var t=e._currentValue;if(bs!==e)if(e={context:e,memoizedValue:t,next:null},cn===null){if(gl===null)throw Error(j(308));cn=e,gl.dependencies={lanes:0,firstContext:e}}else cn=cn.next=e;return t}var It=null;function Zs(e){It===null?It=[e]:It.push(e)}function pc(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Zs(t)):(n.next=l.next,l.next=n),t.interleaved=n,ot(e,r)}function ot(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var ft=!1;function qs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function hc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function lt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function St(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,ot(e,n)}return l=r.interleaved,l===null?(t.next=t,Zs(r)):(t.next=l.next,l.next=t),r.interleaved=t,ot(e,n)}function Zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,As(e,n)}}function va(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function yl(e,t,n,r){var l=e.updateQueue;ft=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,a=l.shared.pending;if(a!==null){l.shared.pending=null;var u=a,d=u.next;u.next=null,s===null?i=d:s.next=d,s=u;var g=e.alternate;g!==null&&(g=g.updateQueue,a=g.lastBaseUpdate,a!==s&&(a===null?g.firstBaseUpdate=d:a.next=d,g.lastBaseUpdate=u))}if(i!==null){var y=l.baseState;s=0,g=d=u=null,a=i;do{var p=a.lane,v=a.eventTime;if((r&p)===p){g!==null&&(g=g.next={eventTime:v,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var w=e,k=a;switch(p=t,v=n,k.tag){case 1:if(w=k.payload,typeof w=="function"){y=w.call(v,y,p);break e}y=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=k.payload,p=typeof w=="function"?w.call(v,y,p):w,p==null)break e;y=Y({},y,p);break e;case 2:ft=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,p=l.effects,p===null?l.effects=[a]:p.push(a))}else v={eventTime:v,lane:p,tag:a.tag,payload:a.payload,callback:a.callback,next:null},g===null?(d=g=v,u=y):g=g.next=v,s|=p;if(a=a.next,a===null){if(a=l.shared.pending,a===null)break;p=a,a=p.next,p.next=null,l.lastBaseUpdate=p,l.shared.pending=null}}while(!0);if(g===null&&(u=y),l.baseState=u,l.firstBaseUpdate=d,l.lastBaseUpdate=g,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Wt|=s,e.lanes=s,e.memoizedState=y}}function xa(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=gi.transition;gi.transition={};try{e(!1),t()}finally{B=n,gi.transition=r}}function Lc(){return Oe().memoizedState}function Pp(e,t,n){var r=Nt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Rc(e))Mc(t,n);else if(n=pc(e,t,n,r),n!==null){var l=ye();Qe(n,e,r,l),Fc(n,t,r)}}function Lp(e,t,n){var r=Nt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Rc(e))Mc(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,a=i(s,n);if(l.hasEagerState=!0,l.eagerState=a,be(a,s)){var u=t.interleaved;u===null?(l.next=l,Zs(t)):(l.next=u.next,u.next=l),t.interleaved=l;return}}catch{}finally{}n=pc(e,t,l,r),n!==null&&(l=ye(),Qe(n,e,r,l),Fc(n,t,r))}}function Rc(e){var t=e.alternate;return e===q||t!==null&&t===q}function Mc(e,t){Gn=xl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Fc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,As(e,n)}}var kl={readContext:De,useCallback:fe,useContext:fe,useEffect:fe,useImperativeHandle:fe,useInsertionEffect:fe,useLayoutEffect:fe,useMemo:fe,useReducer:fe,useRef:fe,useState:fe,useDebugValue:fe,useDeferredValue:fe,useTransition:fe,useMutableSource:fe,useSyncExternalStore:fe,useId:fe,unstable_isNewReconciler:!1},Rp={readContext:De,useCallback:function(e,t){return Ge().memoizedState=[e,t===void 0?null:t],e},useContext:De,useEffect:wa,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Yr(4194308,4,_c.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Yr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Yr(4,2,e,t)},useMemo:function(e,t){var n=Ge();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ge();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Pp.bind(null,q,e),[r.memoizedState,e]},useRef:function(e){var t=Ge();return e={current:e},t.memoizedState=e},useState:ka,useDebugValue:lo,useDeferredValue:function(e){return Ge().memoizedState=e},useTransition:function(){var e=ka(!1),t=e[0];return e=Tp.bind(null,e[1]),Ge().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=q,l=Ge();if(K){if(n===void 0)throw Error(j(407));n=n()}else{if(n=t(),se===null)throw Error(j(349));Ut&30||vc(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,wa(kc.bind(null,r,i,e),[e]),r.flags|=2048,mr(9,xc.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ge(),t=se.identifierPrefix;if(K){var n=rt,r=nt;n=(r&~(1<<32-Ve(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=pr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Ze]=t,e[cr]=r,Vc(e,t,!1,!1),t.stateNode=e;e:{switch(s=Di(n,r),n){case"dialog":Q("cancel",e),Q("close",e),l=r;break;case"iframe":case"object":case"embed":Q("load",e),l=r;break;case"video":case"audio":for(l=0;l_n&&(t.flags|=128,r=!0,In(i,!1),t.lanes=4194304)}else{if(!r)if(e=vl(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),In(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!K)return pe(t),null}else 2*ee()-i.renderingStartTime>_n&&n!==1073741824&&(t.flags|=128,r=!0,In(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=ee(),t.sibling=null,n=Z.current,W(Z,r?n&1|2:n&1),t):(pe(t),null);case 22:case 23:return co(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ce&1073741824&&(pe(t),t.subtreeFlags&6&&(t.flags|=8192)):pe(t),null;case 24:return null;case 25:return null}throw Error(j(156,t.tag))}function Bp(e,t){switch(Vs(t),t.tag){case 1:return ze(t.type)&&dl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Nn(),b(Ne),b(me),Js(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Xs(t),null;case 13:if(b(Z),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(j(340));Sn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return b(Z),null;case 4:return Nn(),null;case 10:return Gs(t.type._context),null;case 22:case 23:return co(),null;case 24:return null;default:return null}}var Or=!1,he=!1,Hp=typeof WeakSet=="function"?WeakSet:Set,P=null;function dn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){X(e,t,r)}else n.current=null}function ds(e,t,n){try{n()}catch(r){X(e,t,r)}}var Ra=!1;function Up(e,t){if(Gi=ol,e=Yu(),Us(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,a=-1,u=-1,d=0,g=0,y=e,p=null;t:for(;;){for(var v;y!==n||l!==0&&y.nodeType!==3||(a=s+l),y!==i||r!==0&&y.nodeType!==3||(u=s+r),y.nodeType===3&&(s+=y.nodeValue.length),(v=y.firstChild)!==null;)p=y,y=v;for(;;){if(y===e)break t;if(p===n&&++d===l&&(a=s),p===i&&++g===r&&(u=s),(v=y.nextSibling)!==null)break;y=p,p=y.parentNode}y=v}n=a===-1||u===-1?null:{start:a,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Zi={focusedElem:e,selectionRange:n},ol=!1,P=t;P!==null;)if(t=P,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,P=e;else for(;P!==null;){t=P;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,R=w.memoizedState,m=t.stateNode,c=m.getSnapshotBeforeUpdate(t.elementType===t.type?k:He(t.type,k),R);m.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(j(163))}}catch(x){X(t,t.return,x)}if(e=t.sibling,e!==null){e.return=t.return,P=e;break}P=t.return}return w=Ra,Ra=!1,w}function Zn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&ds(t,n,i)}l=l.next}while(l!==r)}}function Hl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function fs(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Kc(e){var t=e.alternate;t!==null&&(e.alternate=null,Kc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ze],delete t[cr],delete t[Xi],delete t[Np],delete t[zp])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Gc(e){return e.tag===5||e.tag===3||e.tag===4}function Ma(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Gc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ps(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=cl));else if(r!==4&&(e=e.child,e!==null))for(ps(e,t,n),e=e.sibling;e!==null;)ps(e,t,n),e=e.sibling}function hs(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(hs(e,t,n),e=e.sibling;e!==null;)hs(e,t,n),e=e.sibling}var oe=null,Ue=!1;function ct(e,t,n){for(n=n.child;n!==null;)Zc(e,t,n),n=n.sibling}function Zc(e,t,n){if(Xe&&typeof Xe.onCommitFiberUnmount=="function")try{Xe.onCommitFiberUnmount(Ml,n)}catch{}switch(n.tag){case 5:he||dn(n,t);case 6:var r=oe,l=Ue;oe=null,ct(e,t,n),oe=r,Ue=l,oe!==null&&(Ue?(e=oe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):oe.removeChild(n.stateNode));break;case 18:oe!==null&&(Ue?(e=oe,n=n.stateNode,e.nodeType===8?pi(e.parentNode,n):e.nodeType===1&&pi(e,n),ir(e)):pi(oe,n.stateNode));break;case 4:r=oe,l=Ue,oe=n.stateNode.containerInfo,Ue=!0,ct(e,t,n),oe=r,Ue=l;break;case 0:case 11:case 14:case 15:if(!he&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&ds(n,t,s),l=l.next}while(l!==r)}ct(e,t,n);break;case 1:if(!he&&(dn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){X(n,t,a)}ct(e,t,n);break;case 21:ct(e,t,n);break;case 22:n.mode&1?(he=(r=he)||n.memoizedState!==null,ct(e,t,n),he=r):ct(e,t,n);break;default:ct(e,t,n)}}function Fa(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Hp),t.forEach(function(r){var l=Yp.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Be(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=ee()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Vp(r/1960))-r,10e?16:e,gt===null)var r=!1;else{if(e=gt,gt=null,jl=0,$&6)throw Error(j(331));var l=$;for($|=4,P=e.current;P!==null;){var i=P,s=i.child;if(P.flags&16){var a=i.deletions;if(a!==null){for(var u=0;uee()-ao?Dt(e,0):oo|=n),_e(e,t)}function rd(e,t){t===0&&(e.mode&1?(t=Pr,Pr<<=1,!(Pr&130023424)&&(Pr=4194304)):t=1);var n=ye();e=ot(e,t),e!==null&&(vr(e,t,n),_e(e,n))}function qp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),rd(e,n)}function Yp(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(j(314))}r!==null&&r.delete(t),rd(e,n)}var ld;ld=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ne.current)je=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return je=!1,Dp(e,t,n);je=!!(e.flags&131072)}else je=!1,K&&t.flags&1048576&&ac(t,hl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Xr(e,t),e=t.pendingProps;var l=wn(t,me.current);yn(t,n),l=to(null,t,r,e,l,n);var i=no();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ze(r)?(i=!0,fl(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,qs(t),l.updater=Bl,t.stateNode=l,l._reactInternals=t,ls(t,r,e,n),t=os(null,t,r,!0,i,n)):(t.tag=0,K&&i&&Ws(t),ge(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Xr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Jp(r),e=He(r,e),l){case 0:t=ss(null,t,r,e,n);break e;case 1:t=Ta(null,t,r,e,n);break e;case 11:t=Ca(null,t,r,e,n);break e;case 14:t=Ea(null,t,r,He(r.type,e),n);break e}throw Error(j(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:He(r,l),ss(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:He(r,l),Ta(e,t,r,l,n);case 3:e:{if(Hc(t),e===null)throw Error(j(387));r=t.pendingProps,i=t.memoizedState,l=i.element,hc(e,t),yl(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=zn(Error(j(423)),t),t=Pa(e,t,r,n,l);break e}else if(r!==l){l=zn(Error(j(424)),t),t=Pa(e,t,r,n,l);break e}else for(Te=wt(t.stateNode.containerInfo.firstChild),Pe=t,K=!0,We=null,n=fc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Sn(),r===l){t=at(e,t,n);break e}ge(e,t,r,n)}t=t.child}return t;case 5:return mc(t),e===null&&ts(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,qi(r,l)?s=null:i!==null&&qi(r,i)&&(t.flags|=32),Bc(e,t),ge(e,t,s,n),t.child;case 6:return e===null&&ts(t),null;case 13:return Uc(e,t,n);case 4:return Ys(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=jn(t,null,r,n):ge(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:He(r,l),Ca(e,t,r,l,n);case 7:return ge(e,t,t.pendingProps,n),t.child;case 8:return ge(e,t,t.pendingProps.children,n),t.child;case 12:return ge(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,W(ml,r._currentValue),r._currentValue=s,i!==null)if(be(i.value,s)){if(i.children===l.children&&!Ne.current){t=at(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var a=i.dependencies;if(a!==null){s=i.child;for(var u=a.firstContext;u!==null;){if(u.context===r){if(i.tag===1){u=lt(-1,n&-n),u.tag=2;var d=i.updateQueue;if(d!==null){d=d.shared;var g=d.pending;g===null?u.next=u:(u.next=g.next,g.next=u),d.pending=u}}i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),ns(i.return,n,t),a.lanes|=n;break}u=u.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(j(341));s.lanes|=n,a=s.alternate,a!==null&&(a.lanes|=n),ns(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ge(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,yn(t,n),l=De(l),r=r(l),t.flags|=1,ge(e,t,r,n),t.child;case 14:return r=t.type,l=He(r,t.pendingProps),l=He(r.type,l),Ea(e,t,r,l,n);case 15:return Dc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:He(r,l),Xr(e,t),t.tag=1,ze(r)?(e=!0,fl(t)):e=!1,yn(t,n),Ac(t,r,l),ls(t,r,l,n),os(null,t,r,!0,e,n);case 19:return Wc(e,t,n);case 22:return Oc(e,t,n)}throw Error(j(156,t.tag))};function id(e,t){return Ru(e,t)}function Xp(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ie(e,t,n,r){return new Xp(e,t,n,r)}function po(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jp(e){if(typeof e=="function")return po(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ls)return 11;if(e===Rs)return 14}return 2}function zt(e,t){var n=e.alternate;return n===null?(n=Ie(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function tl(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")po(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case tn:return Ot(n.children,l,i,t);case Ps:s=8,l|=8;break;case Ei:return e=Ie(12,n,t,l|2),e.elementType=Ei,e.lanes=i,e;case Ti:return e=Ie(13,n,t,l),e.elementType=Ti,e.lanes=i,e;case Pi:return e=Ie(19,n,t,l),e.elementType=Pi,e.lanes=i,e;case mu:return Wl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case pu:s=10;break e;case hu:s=9;break e;case Ls:s=11;break e;case Rs:s=14;break e;case dt:s=16,r=null;break e}throw Error(j(130,e==null?e:typeof e,""))}return t=Ie(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Ot(e,t,n,r){return e=Ie(7,e,r,t),e.lanes=n,e}function Wl(e,t,n,r){return e=Ie(22,e,r,t),e.elementType=mu,e.lanes=n,e.stateNode={isHidden:!1},e}function wi(e,t,n){return e=Ie(6,e,null,t),e.lanes=n,e}function Si(e,t,n){return t=Ie(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function eh(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ni(0),this.expirationTimes=ni(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ni(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function ho(e,t,n,r,l,i,s,a,u){return e=new eh(e,t,n,a,u),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ie(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},qs(i),e}function th(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(ud)}catch(e){console.error(e)}}ud(),uu.exports=Re;var sh=uu.exports,Ua=sh;_i.createRoot=Ua.createRoot,_i.hydrateRoot=Ua.hydrateRoot;/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const oh=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),cd=(...e)=>e.filter((t,n,r)=>!!t&&r.indexOf(t)===n).join(" ");/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var ah={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const uh=L.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:l="",children:i,iconNode:s,...a},u)=>L.createElement("svg",{ref:u,...ah,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:cd("lucide",l),...a},[...s.map(([d,g])=>L.createElement(d,g)),...Array.isArray(i)?i:[i]]));/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const A=(e,t)=>{const n=L.forwardRef(({className:r,...l},i)=>L.createElement(uh,{ref:i,iconNode:t,className:cd(`lucide-${oh(e)}`,r),...l}));return n.displayName=`${e}`,n};/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Gl=A("Activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ch=A("ArrowLeft",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dd=A("ArrowRight",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dh=A("Award",[["path",{d:"m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526",key:"1yiouv"}],["circle",{cx:"12",cy:"8",r:"6",key:"1vp47v"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fd=A("BookOpen",[["path",{d:"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z",key:"vv98re"}],["path",{d:"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z",key:"1cyq3y"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ji=A("Bot",[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const pd=A("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const vo=A("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fh=A("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ph=A("CircleCheckBig",[["path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14",key:"g774vq"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xo=A("CircleHelp",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hh=A("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const mh=A("CloudUpload",[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242",key:"1pljnt"}],["path",{d:"M12 12v9",key:"192myk"}],["path",{d:"m16 16-4-4-4 4",key:"119tzi"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _l=A("Cpu",[["rect",{width:"16",height:"16",x:"4",y:"4",rx:"2",key:"14l7u7"}],["rect",{width:"6",height:"6",x:"9",y:"9",rx:"1",key:"5aljv4"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ko=A("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gh=A("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yh=A("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const vh=A("FileArchive",[["path",{d:"M10 12v-1",key:"v7bkov"}],["path",{d:"M10 18v-2",key:"1cjy8d"}],["path",{d:"M10 7V6",key:"dljcrl"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01",key:"gkbcor"}],["circle",{cx:"10",cy:"20",r:"2",key:"1xzdoj"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hd=A("FileCode",[["path",{d:"M10 12.5 8 15l2 2.5",key:"1tg20x"}],["path",{d:"m14 12.5 2 2.5-2 2.5",key:"yinavb"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z",key:"1mlx9k"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const md=A("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xh=A("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const kh=A("Folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gd=A("GitBranch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const wh=A("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Sh=A("HardDrive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xn=A("Layers",[["path",{d:"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z",key:"8b97xw"}],["path",{d:"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65",key:"dd6zsq"}],["path",{d:"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65",key:"ep9fru"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const jh=A("Link2",[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2",key:"8i5ue5"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2",key:"1b9ql8"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12",key:"1jonct"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Nh=A("ListFilter",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M7 12h10",key:"b7w52i"}],["path",{d:"M10 18h4",key:"1ulq68"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zh=A("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xs=A("MessageSquare",[["path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",key:"1lielz"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _h=A("Network",[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Zl=A("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Jt=A("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Cl=A("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ch=A("Send",[["path",{d:"m22 2-7 20-4-9-9-4Z",key:"1q3vgg"}],["path",{d:"M22 2 11 13",key:"nzbqef"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yd=A("Shield",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Eh=A("Sparkles",[["path",{d:"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z",key:"4pj2yx"}],["path",{d:"M20 3v4",key:"1olli1"}],["path",{d:"M22 5h-4",key:"1gvqau"}],["path",{d:"M4 17v2",key:"vumght"}],["path",{d:"M5 18H3",key:"zchphs"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const El=A("Terminal",[["polyline",{points:"4 17 10 11 4 5",key:"akl6gq"}],["line",{x1:"12",x2:"20",y1:"19",y2:"19",key:"q2wloq"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Th=A("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-react v0.395.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const wo=A("Zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]),Ph=["https://github.com/pallets/flask","https://github.com/tiangolo/fastapi","https://github.com/django/django"];function Wa({onSubmit:e,loading:t}){const[n,r]=L.useState("url"),[l,i]=L.useState(""),[s,a]=L.useState(""),[u,d]=L.useState(null),[g,y]=L.useState(!1),p=L.useRef(null),v=c=>{c.preventDefault(),c.stopPropagation(),y(c.type==="dragenter"||c.type==="dragover")},w=c=>{var x;c.preventDefault(),c.stopPropagation(),y(!1);const h=(x=c.dataTransfer.files)==null?void 0:x[0];h!=null&&h.name.endsWith(".zip")?d(h):h&&alert("Only .zip archives are supported.")},k=c=>{var x;const h=(x=c.target.files)==null?void 0:x[0];h!=null&&h.name.endsWith(".zip")?d(h):h&&alert("Only .zip archives are supported.")},R=c=>{if(c.preventDefault(),n==="url"){if(!l.trim())return;e({type:"url",url:l.trim(),token:s})}else{if(!u)return;e({type:"zip",file:u})}},m=n==="url"?!!l.trim():!!u;return o.jsxs("div",{className:"input-section",children:[o.jsxs("div",{style:{textAlign:"center",marginBottom:"2.5rem"},children:[o.jsxs("div",{style:{display:"inline-flex",alignItems:"center",gap:"0.5rem",background:"#E6F7F7",border:"1px solid var(--accent-teal-lt)",borderRadius:"var(--radius-full)",padding:"0.3rem 0.9rem",fontSize:"0.78rem",fontWeight:600,color:"var(--accent-teal-dk)",marginBottom:"1.25rem"},children:[o.jsx(Eh,{size:12}),"Powered by Gemini 2.5 Flash + RAG"]}),o.jsxs("h1",{className:"section-title",style:{fontSize:"2rem",lineHeight:1.2,marginBottom:"0.75rem"},children:["Understand any codebase",o.jsx("br",{}),"in seconds"]}),o.jsx("p",{className:"section-desc",style:{maxWidth:"480px",margin:"0 auto"},children:"Analyze repositories, map architecture, extract APIs, and build a semantic knowledge base ready for AI agents — no setup required."})]}),o.jsxs("div",{className:"card",style:{padding:"2rem"},children:[o.jsxs("div",{className:"dashboard-tabs",style:{marginBottom:"1.75rem"},children:[o.jsxs("button",{type:"button",className:`tab-btn ${n==="url"?"active":""}`,onClick:()=>r("url"),children:[o.jsx(jh,{size:15})," GitHub URL"]}),o.jsxs("button",{type:"button",className:`tab-btn ${n==="zip"?"active":""}`,onClick:()=>r("zip"),children:[o.jsx(vh,{size:15})," Upload ZIP"]})]}),o.jsxs("form",{onSubmit:R,children:[n==="url"?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"form-group",children:[o.jsx("label",{className:"form-label",children:"GitHub Repository URL"}),o.jsxs("div",{className:"input-wrapper",children:[o.jsx(gd,{className:"input-icon",size:17}),o.jsx("input",{type:"url",required:!0,className:"form-input",placeholder:"https://github.com/owner/repository",value:l,onChange:c=>i(c.target.value),disabled:t,id:"repo-url-input"})]})]}),o.jsxs("div",{className:"form-group",children:[o.jsxs("label",{className:"form-label",children:["GitHub Personal Access Token",o.jsx("span",{style:{textTransform:"none",color:"var(--text-muted)",marginLeft:"0.5rem",fontWeight:400},children:"(required for private repos)"})]}),o.jsxs("div",{className:"input-wrapper",children:[o.jsx(zh,{className:"input-icon",size:17}),o.jsx("input",{type:"password",className:"form-input",placeholder:"ghp_xxxxxxxxxxxxxxxx",value:s,onChange:c=>a(c.target.value),disabled:t,id:"github-token-input"})]})]}),o.jsxs("div",{style:{marginBottom:"1.5rem"},children:[o.jsx("div",{className:"form-label",style:{marginBottom:"0.5rem"},children:"Try an example"}),o.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem"},children:Ph.map(c=>o.jsx("button",{type:"button",className:"btn-secondary",style:{fontSize:"0.75rem",padding:"0.3rem 0.7rem"},onClick:()=>i(c),children:c.split("/").slice(-2).join("/")},c))})]})]}):o.jsxs("div",{className:"form-group",children:[o.jsx("label",{className:"form-label",children:"Upload ZIP Archive"}),o.jsxs("div",{className:`drag-drop-area ${g?"drag-active":""}`,onDragEnter:v,onDragOver:v,onDragLeave:v,onDrop:w,onClick:()=>{var c;return(c=p.current)==null?void 0:c.click()},id:"zip-drop-zone",children:[o.jsx("input",{ref:p,type:"file",style:{display:"none"},accept:".zip",onChange:k,disabled:t}),o.jsx(mh,{size:36,className:"upload-icon"}),u?o.jsxs("div",{children:[o.jsx("p",{style:{fontWeight:600,color:"var(--accent-teal-dk)"},children:u.name}),o.jsxs("p",{style:{fontSize:"0.8rem",color:"var(--text-muted)",marginTop:"0.25rem"},children:[(u.size/(1024*1024)).toFixed(2)," MB · Click to change"]})]}):o.jsxs("div",{children:[o.jsx("p",{style:{fontWeight:600,color:"var(--text-primary)"},children:"Drop your .zip file here"}),o.jsx("p",{style:{fontSize:"0.8rem",color:"var(--text-muted)",marginTop:"0.25rem"},children:"or click to browse files"})]})]})]}),o.jsx("button",{id:"submit-analysis-btn",className:"btn-primary",type:"submit",disabled:t||!m,children:t?o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"spinner",style:{width:16,height:16,borderWidth:2,margin:0}}),"Analyzing..."]}):o.jsxs(o.Fragment,{children:["Run Intelligence Analysis ",o.jsx(dd,{size:16})]})})]})]}),o.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"1rem",marginTop:"1.5rem"},children:[{icon:"🏗",title:"Architecture Map",desc:"Auto-detect patterns, entry points, and data flows"},{icon:"🔍",title:"Semantic Search",desc:"RAG-powered search across indexed code knowledge"},{icon:"🤖",title:"AI Agents",desc:"Multi-agent Q&A with confidence scoring"}].map(c=>o.jsxs("div",{style:{background:"var(--bg-muted)",border:"1px solid var(--border-color)",borderRadius:"var(--radius-lg)",padding:"1rem",textAlign:"center"},children:[o.jsx("div",{style:{fontSize:"1.5rem",marginBottom:"0.4rem"},children:c.icon}),o.jsx("div",{style:{fontWeight:600,fontSize:"0.82rem",color:"var(--text-primary)",marginBottom:"0.25rem"},children:c.title}),o.jsx("div",{style:{fontSize:"0.74rem",color:"var(--text-muted)",lineHeight:1.4},children:c.desc})]},c.title))})]})}function vd({name:e,node:t}){const n=t.type==="directory",[r,l]=L.useState(!1),i=s=>s?s<1024?`${s}B`:s<1024*1024?`${(s/1024).toFixed(1)}KB`:`${(s/(1024*1024)).toFixed(1)}MB`:"";return o.jsxs("div",{style:{marginBottom:1},children:[o.jsx("div",{className:"tree-item",onClick:()=>n&&l(!r),style:{paddingLeft:0},children:n?o.jsxs(o.Fragment,{children:[r?o.jsx(pd,{size:13,style:{color:"var(--text-muted)",flexShrink:0}}):o.jsx(vo,{size:13,style:{color:"var(--text-muted)",flexShrink:0}}),r?o.jsx(xh,{size:14,style:{color:"var(--accent-amber)",flexShrink:0}}):o.jsx(kh,{size:14,style:{color:"var(--accent-amber)",flexShrink:0}}),o.jsx("span",{style:{fontWeight:600,color:"var(--text-primary)",fontSize:"0.8rem"},children:e})]}):o.jsxs(o.Fragment,{children:[o.jsx("span",{style:{width:13,flexShrink:0}}),o.jsx(hd,{size:13,style:{color:"var(--accent-teal)",flexShrink:0}}),o.jsx("span",{className:"file",style:{fontSize:"0.78rem",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e}),t.size!==void 0&&o.jsx("span",{style:{fontSize:"0.66rem",color:"var(--text-muted)",marginLeft:"auto",flexShrink:0,paddingLeft:"0.25rem"},children:i(t.size)})]})}),n&&r&&t.children&&o.jsx("div",{style:{paddingLeft:"0.9rem",borderLeft:"1px solid var(--border-color)",marginLeft:"0.4rem"},children:Object.entries(t.children).sort(([,s],[,a])=>s.type==="directory"&&a.type!=="directory"?-1:s.type!=="directory"&&a.type==="directory"?1:0).map(([s,a])=>o.jsx(vd,{name:s,node:a},a.path||s))})]})}function Lh({tree:e,title:t}){return!e||Object.keys(e).length===0?o.jsx("div",{style:{color:"var(--text-muted)",fontSize:"0.85rem",textAlign:"center",padding:"2rem 0"},children:"No file tree available."}):o.jsxs("div",{children:[o.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.4rem",fontFamily:"var(--font-display)",fontSize:"0.82rem",fontWeight:700,textTransform:"uppercase",letterSpacing:"0.05em",color:"var(--text-muted)",marginBottom:"0.75rem",paddingBottom:"0.5rem",borderBottom:"1px solid var(--border-color)"},children:[o.jsx(gd,{size:13,style:{color:"var(--accent-teal)"}}),t||"Repository"]}),o.jsx("div",{style:{overflow:"auto",maxHeight:"calc(100vh - 220px)",paddingRight:"0.25rem"},children:Object.entries(e).sort(([,n],[,r])=>n.type==="directory"&&r.type!=="directory"?-1:n.type!=="directory"&&r.type==="directory"?1:0).map(([n,r])=>o.jsx(vd,{name:n,node:r},r.path||n))})]})}function So(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}let Zt=So();function xd(e){Zt=e}const kd=/[&<>"']/,Rh=new RegExp(kd.source,"g"),wd=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Mh=new RegExp(wd.source,"g"),Fh={"&":"&","<":"<",">":">",'"':""","'":"'"},Va=e=>Fh[e];function Ee(e,t){if(t){if(kd.test(e))return e.replace(Rh,Va)}else if(wd.test(e))return e.replace(Mh,Va);return e}const Ah=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function Ih(e){return e.replace(Ah,(t,n)=>(n=n.toLowerCase(),n==="colon"?":":n.charAt(0)==="#"?n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1)):""))}const $h=/(^|[^\[])\^/g;function H(e,t){let n=typeof e=="string"?e:e.source;t=t||"";const r={replace:(l,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace($h,"$1"),n=n.replace(l,s),r},getRegex:()=>new RegExp(n,t)};return r}function Qa(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch{return null}return e}const Xn={exec:()=>null};function ba(e,t){const n=e.replace(/\|/g,(i,s,a)=>{let u=!1,d=s;for(;--d>=0&&a[d]==="\\";)u=!u;return u?"|":" |"}),r=n.split(/ \|/);let l=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),t)if(r.length>t)r.splice(t);else for(;r.length{const i=l.match(/^\s+/);if(i===null)return l;const[s]=i;return s.length>=r.length?l.slice(r.length):l}).join(` +`)}class Tl{constructor(t){V(this,"options");V(this,"rules");V(this,"lexer");this.options=t||Zt}space(t){const n=this.rules.block.newline.exec(t);if(n&&n[0].length>0)return{type:"space",raw:n[0]}}code(t){const n=this.rules.block.code.exec(t);if(n){const r=n[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:n[0],codeBlockStyle:"indented",text:this.options.pedantic?r:Ur(r,` +`)}}}fences(t){const n=this.rules.block.fences.exec(t);if(n){const r=n[0],l=Oh(r,n[3]||"");return{type:"code",raw:r,lang:n[2]?n[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):n[2],text:l}}}heading(t){const n=this.rules.block.heading.exec(t);if(n){let r=n[2].trim();if(/#$/.test(r)){const l=Ur(r,"#");(this.options.pedantic||!l||/ $/.test(l))&&(r=l.trim())}return{type:"heading",raw:n[0],depth:n[1].length,text:r,tokens:this.lexer.inline(r)}}}hr(t){const n=this.rules.block.hr.exec(t);if(n)return{type:"hr",raw:n[0]}}blockquote(t){const n=this.rules.block.blockquote.exec(t);if(n){let r=n[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,` + $1`);r=Ur(r.replace(/^ *>[ \t]?/gm,""),` +`);const l=this.lexer.state.top;this.lexer.state.top=!0;const i=this.lexer.blockTokens(r);return this.lexer.state.top=l,{type:"blockquote",raw:n[0],tokens:i,text:r}}}list(t){let n=this.rules.block.list.exec(t);if(n){let r=n[1].trim();const l=r.length>1,i={type:"list",raw:"",ordered:l,start:l?+r.slice(0,-1):"",loose:!1,items:[]};r=l?`\\d{1,9}\\${r.slice(-1)}`:`\\${r}`,this.options.pedantic&&(r=l?r:"[*+-]");const s=new RegExp(`^( {0,3}${r})((?:[ ][^\\n]*)?(?:\\n|$))`);let a="",u="",d=!1;for(;t;){let g=!1;if(!(n=s.exec(t))||this.rules.block.hr.test(t))break;a=n[0],t=t.substring(a.length);let y=n[2].split(` +`,1)[0].replace(/^\t+/,m=>" ".repeat(3*m.length)),p=t.split(` +`,1)[0],v=0;this.options.pedantic?(v=2,u=y.trimStart()):(v=n[2].search(/[^ ]/),v=v>4?1:v,u=y.slice(v),v+=n[1].length);let w=!1;if(!y&&/^ *$/.test(p)&&(a+=p+` +`,t=t.substring(p.length+1),g=!0),!g){const m=new RegExp(`^ {0,${Math.min(3,v-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),c=new RegExp(`^ {0,${Math.min(3,v-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),h=new RegExp(`^ {0,${Math.min(3,v-1)}}(?:\`\`\`|~~~)`),x=new RegExp(`^ {0,${Math.min(3,v-1)}}#`);for(;t;){const f=t.split(` +`,1)[0];if(p=f,this.options.pedantic&&(p=p.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),h.test(p)||x.test(p)||m.test(p)||c.test(t))break;if(p.search(/[^ ]/)>=v||!p.trim())u+=` +`+p.slice(v);else{if(w||y.search(/[^ ]/)>=4||h.test(y)||x.test(y)||c.test(y))break;u+=` +`+p}!w&&!p.trim()&&(w=!0),a+=f+` +`,t=t.substring(f.length+1),y=p.slice(v)}}i.loose||(d?i.loose=!0:/\n *\n *$/.test(a)&&(d=!0));let k=null,R;this.options.gfm&&(k=/^\[[ xX]\] /.exec(u),k&&(R=k[0]!=="[ ] ",u=u.replace(/^\[[ xX]\] +/,""))),i.items.push({type:"list_item",raw:a,task:!!k,checked:R,loose:!1,text:u,tokens:[]}),i.raw+=a}i.items[i.items.length-1].raw=a.trimEnd(),i.items[i.items.length-1].text=u.trimEnd(),i.raw=i.raw.trimEnd();for(let g=0;gv.type==="space"),p=y.length>0&&y.some(v=>/\n.*\n/.test(v.raw));i.loose=p}if(i.loose)for(let g=0;g$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=n[3]?n[3].substring(1,n[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):n[3];return{type:"def",tag:r,raw:n[0],href:l,title:i}}}table(t){const n=this.rules.block.table.exec(t);if(!n||!/[:|]/.test(n[2]))return;const r=ba(n[1]),l=n[2].replace(/^\||\| *$/g,"").split("|"),i=n[3]&&n[3].trim()?n[3].replace(/\n[ \t]*$/,"").split(` +`):[],s={type:"table",raw:n[0],header:[],align:[],rows:[]};if(r.length===l.length){for(const a of l)/^ *-+: *$/.test(a)?s.align.push("right"):/^ *:-+: *$/.test(a)?s.align.push("center"):/^ *:-+ *$/.test(a)?s.align.push("left"):s.align.push(null);for(const a of r)s.header.push({text:a,tokens:this.lexer.inline(a)});for(const a of i)s.rows.push(ba(a,s.header.length).map(u=>({text:u,tokens:this.lexer.inline(u)})));return s}}lheading(t){const n=this.rules.block.lheading.exec(t);if(n)return{type:"heading",raw:n[0],depth:n[2].charAt(0)==="="?1:2,text:n[1],tokens:this.lexer.inline(n[1])}}paragraph(t){const n=this.rules.block.paragraph.exec(t);if(n){const r=n[1].charAt(n[1].length-1)===` +`?n[1].slice(0,-1):n[1];return{type:"paragraph",raw:n[0],text:r,tokens:this.lexer.inline(r)}}}text(t){const n=this.rules.block.text.exec(t);if(n)return{type:"text",raw:n[0],text:n[0],tokens:this.lexer.inline(n[0])}}escape(t){const n=this.rules.inline.escape.exec(t);if(n)return{type:"escape",raw:n[0],text:Ee(n[1])}}tag(t){const n=this.rules.inline.tag.exec(t);if(n)return!this.lexer.state.inLink&&/^/i.test(n[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(n[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(n[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:n[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:n[0]}}link(t){const n=this.rules.inline.link.exec(t);if(n){const r=n[2].trim();if(!this.options.pedantic&&/^$/.test(r))return;const s=Ur(r.slice(0,-1),"\\");if((r.length-s.length)%2===0)return}else{const s=Dh(n[2],"()");if(s>-1){const u=(n[0].indexOf("!")===0?5:4)+n[1].length+s;n[2]=n[2].substring(0,s),n[0]=n[0].substring(0,u).trim(),n[3]=""}}let l=n[2],i="";if(this.options.pedantic){const s=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(l);s&&(l=s[1],i=s[3])}else i=n[3]?n[3].slice(1,-1):"";return l=l.trim(),/^$/.test(r)?l=l.slice(1):l=l.slice(1,-1)),Ka(n,{href:l&&l.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},n[0],this.lexer)}}reflink(t,n){let r;if((r=this.rules.inline.reflink.exec(t))||(r=this.rules.inline.nolink.exec(t))){const l=(r[2]||r[1]).replace(/\s+/g," "),i=n[l.toLowerCase()];if(!i){const s=r[0].charAt(0);return{type:"text",raw:s,text:s}}return Ka(r,i,r[0],this.lexer)}}emStrong(t,n,r=""){let l=this.rules.inline.emStrongLDelim.exec(t);if(!l||l[3]&&r.match(/[\p{L}\p{N}]/u))return;if(!(l[1]||l[2]||"")||!r||this.rules.inline.punctuation.exec(r)){const s=[...l[0]].length-1;let a,u,d=s,g=0;const y=l[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(y.lastIndex=0,n=n.slice(-1*t.length+s);(l=y.exec(n))!=null;){if(a=l[1]||l[2]||l[3]||l[4]||l[5]||l[6],!a)continue;if(u=[...a].length,l[3]||l[4]){d+=u;continue}else if((l[5]||l[6])&&s%3&&!((s+u)%3)){g+=u;continue}if(d-=u,d>0)continue;u=Math.min(u,u+d+g);const p=[...l[0]][0].length,v=t.slice(0,s+l.index+p+u);if(Math.min(s,u)%2){const k=v.slice(1,-1);return{type:"em",raw:v,text:k,tokens:this.lexer.inlineTokens(k)}}const w=v.slice(2,-2);return{type:"strong",raw:v,text:w,tokens:this.lexer.inlineTokens(w)}}}}codespan(t){const n=this.rules.inline.code.exec(t);if(n){let r=n[2].replace(/\n/g," ");const l=/[^ ]/.test(r),i=/^ /.test(r)&&/ $/.test(r);return l&&i&&(r=r.substring(1,r.length-1)),r=Ee(r,!0),{type:"codespan",raw:n[0],text:r}}}br(t){const n=this.rules.inline.br.exec(t);if(n)return{type:"br",raw:n[0]}}del(t){const n=this.rules.inline.del.exec(t);if(n)return{type:"del",raw:n[0],text:n[2],tokens:this.lexer.inlineTokens(n[2])}}autolink(t){const n=this.rules.inline.autolink.exec(t);if(n){let r,l;return n[2]==="@"?(r=Ee(n[1]),l="mailto:"+r):(r=Ee(n[1]),l=r),{type:"link",raw:n[0],text:r,href:l,tokens:[{type:"text",raw:r,text:r}]}}}url(t){var r;let n;if(n=this.rules.inline.url.exec(t)){let l,i;if(n[2]==="@")l=Ee(n[0]),i="mailto:"+l;else{let s;do s=n[0],n[0]=((r=this.rules.inline._backpedal.exec(n[0]))==null?void 0:r[0])??"";while(s!==n[0]);l=Ee(n[0]),n[1]==="www."?i="http://"+n[0]:i=n[0]}return{type:"link",raw:n[0],text:l,href:i,tokens:[{type:"text",raw:l,text:l}]}}}inlineText(t){const n=this.rules.inline.text.exec(t);if(n){let r;return this.lexer.state.inRawBlock?r=n[0]:r=Ee(n[0]),{type:"text",raw:n[0],text:r}}}}const Bh=/^(?: *(?:\n|$))+/,Hh=/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,Uh=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,Sr=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Wh=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,Sd=/(?:[*+-]|\d{1,9}[.)])/,jd=H(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,Sd).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),jo=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Vh=/^[^\n]+/,No=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Qh=H(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",No).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),bh=H(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,Sd).getRegex(),ql="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",zo=/|$))/,Kh=H("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",zo).replace("tag",ql).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Nd=H(jo).replace("hr",Sr).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ql).getRegex(),Gh=H(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Nd).getRegex(),_o={blockquote:Gh,code:Hh,def:Qh,fences:Uh,heading:Wh,hr:Sr,html:Kh,lheading:jd,list:bh,newline:Bh,paragraph:Nd,table:Xn,text:Vh},Ga=H("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",Sr).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ql).getRegex(),Zh={..._o,table:Ga,paragraph:H(jo).replace("hr",Sr).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Ga).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ql).getRegex()},qh={..._o,html:H(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",zo).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Xn,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:H(jo).replace("hr",Sr).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",jd).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},zd=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Yh=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,_d=/^( {2,}|\\)\n(?!\s*$)/,Xh=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,tm=H(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,jr).getRegex(),nm=H("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,jr).getRegex(),rm=H("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,jr).getRegex(),lm=H(/\\([punct])/,"gu").replace(/punct/g,jr).getRegex(),im=H(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),sm=H(zo).replace("(?:-->|$)","-->").getRegex(),om=H("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",sm).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Pl=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,am=H(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",Pl).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Cd=H(/^!?\[(label)\]\[(ref)\]/).replace("label",Pl).replace("ref",No).getRegex(),Ed=H(/^!?\[(ref)\](?:\[\])?/).replace("ref",No).getRegex(),um=H("reflink|nolink(?!\\()","g").replace("reflink",Cd).replace("nolink",Ed).getRegex(),Co={_backpedal:Xn,anyPunctuation:lm,autolink:im,blockSkip:em,br:_d,code:Yh,del:Xn,emStrongLDelim:tm,emStrongRDelimAst:nm,emStrongRDelimUnd:rm,escape:zd,link:am,nolink:Ed,punctuation:Jh,reflink:Cd,reflinkSearch:um,tag:om,text:Xh,url:Xn},cm={...Co,link:H(/^!?\[(label)\]\((.*?)\)/).replace("label",Pl).getRegex(),reflink:H(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Pl).getRegex()},ks={...Co,escape:H(zd).replace("])","~|])").getRegex(),url:H(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\u+" ".repeat(d.length));let r,l,i,s;for(;t;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some(a=>(r=a.call({lexer:this},t,n))?(t=t.substring(r.raw.length),n.push(r),!0):!1))){if(r=this.tokenizer.space(t)){t=t.substring(r.raw.length),r.raw.length===1&&n.length>0?n[n.length-1].raw+=` +`:n.push(r);continue}if(r=this.tokenizer.code(t)){t=t.substring(r.raw.length),l=n[n.length-1],l&&(l.type==="paragraph"||l.type==="text")?(l.raw+=` +`+r.raw,l.text+=` +`+r.text,this.inlineQueue[this.inlineQueue.length-1].src=l.text):n.push(r);continue}if(r=this.tokenizer.fences(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.heading(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.hr(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.blockquote(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.list(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.html(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.def(t)){t=t.substring(r.raw.length),l=n[n.length-1],l&&(l.type==="paragraph"||l.type==="text")?(l.raw+=` +`+r.raw,l.text+=` +`+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=l.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title});continue}if(r=this.tokenizer.table(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.lheading(t)){t=t.substring(r.raw.length),n.push(r);continue}if(i=t,this.options.extensions&&this.options.extensions.startBlock){let a=1/0;const u=t.slice(1);let d;this.options.extensions.startBlock.forEach(g=>{d=g.call({lexer:this},u),typeof d=="number"&&d>=0&&(a=Math.min(a,d))}),a<1/0&&a>=0&&(i=t.substring(0,a+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){l=n[n.length-1],s&&l.type==="paragraph"?(l.raw+=` +`+r.raw,l.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=l.text):n.push(r),s=i.length!==t.length,t=t.substring(r.raw.length);continue}if(r=this.tokenizer.text(t)){t=t.substring(r.raw.length),l=n[n.length-1],l&&l.type==="text"?(l.raw+=` +`+r.raw,l.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=l.text):n.push(r);continue}if(t){const a="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(a);break}else throw new Error(a)}}return this.state.top=!0,n}inline(t,n=[]){return this.inlineQueue.push({src:t,tokens:n}),n}inlineTokens(t,n=[]){let r,l,i,s=t,a,u,d;if(this.tokens.links){const g=Object.keys(this.tokens.links);if(g.length>0)for(;(a=this.tokenizer.rules.inline.reflinkSearch.exec(s))!=null;)g.includes(a[0].slice(a[0].lastIndexOf("[")+1,-1))&&(s=s.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(a=this.tokenizer.rules.inline.blockSkip.exec(s))!=null;)s=s.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;(a=this.tokenizer.rules.inline.anyPunctuation.exec(s))!=null;)s=s.slice(0,a.index)+"++"+s.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;t;)if(u||(d=""),u=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some(g=>(r=g.call({lexer:this},t,n))?(t=t.substring(r.raw.length),n.push(r),!0):!1))){if(r=this.tokenizer.escape(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.tag(t)){t=t.substring(r.raw.length),l=n[n.length-1],l&&r.type==="text"&&l.type==="text"?(l.raw+=r.raw,l.text+=r.text):n.push(r);continue}if(r=this.tokenizer.link(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(r.raw.length),l=n[n.length-1],l&&r.type==="text"&&l.type==="text"?(l.raw+=r.raw,l.text+=r.text):n.push(r);continue}if(r=this.tokenizer.emStrong(t,s,d)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.codespan(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.br(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.del(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.autolink(t)){t=t.substring(r.raw.length),n.push(r);continue}if(!this.state.inLink&&(r=this.tokenizer.url(t))){t=t.substring(r.raw.length),n.push(r);continue}if(i=t,this.options.extensions&&this.options.extensions.startInline){let g=1/0;const y=t.slice(1);let p;this.options.extensions.startInline.forEach(v=>{p=v.call({lexer:this},y),typeof p=="number"&&p>=0&&(g=Math.min(g,p))}),g<1/0&&g>=0&&(i=t.substring(0,g+1))}if(r=this.tokenizer.inlineText(i)){t=t.substring(r.raw.length),r.raw.slice(-1)!=="_"&&(d=r.raw.slice(-1)),u=!0,l=n[n.length-1],l&&l.type==="text"?(l.raw+=r.raw,l.text+=r.text):n.push(r);continue}if(t){const g="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(g);break}else throw new Error(g)}}return n}}class Ll{constructor(t){V(this,"options");this.options=t||Zt}code(t,n,r){var i;const l=(i=(n||"").match(/^\S*/))==null?void 0:i[0];return t=t.replace(/\n$/,"")+` +`,l?'
'+(r?t:Ee(t,!0))+`
+`:"
"+(r?t:Ee(t,!0))+`
+`}blockquote(t){return`
+${t}
+`}html(t,n){return t}heading(t,n,r){return`${t} +`}hr(){return`
+`}list(t,n,r){const l=n?"ol":"ul",i=n&&r!==1?' start="'+r+'"':"";return"<"+l+i+`> +`+t+" +`}listitem(t,n,r){return`
  • ${t}
  • +`}checkbox(t){return"'}paragraph(t){return`

    ${t}

    +`}table(t,n){return n&&(n=`${n}`),` + +`+t+` +`+n+`
    +`}tablerow(t){return` +${t} +`}tablecell(t,n){const r=n.header?"th":"td";return(n.align?`<${r} align="${n.align}">`:`<${r}>`)+t+` +`}strong(t){return`${t}`}em(t){return`${t}`}codespan(t){return`${t}`}br(){return"
    "}del(t){return`${t}`}link(t,n,r){const l=Qa(t);if(l===null)return r;t=l;let i='
    ",i}image(t,n,r){const l=Qa(t);if(l===null)return r;t=l;let i=`${r}0&&p.tokens[0].type==="paragraph"?(p.tokens[0].text=R+" "+p.tokens[0].text,p.tokens[0].tokens&&p.tokens[0].tokens.length>0&&p.tokens[0].tokens[0].type==="text"&&(p.tokens[0].tokens[0].text=R+" "+p.tokens[0].tokens[0].text)):p.tokens.unshift({type:"text",text:R+" "}):k+=R+" "}k+=this.parse(p.tokens,d),g+=this.renderer.listitem(k,w,!!v)}r+=this.renderer.list(g,a,u);continue}case"html":{const s=i;r+=this.renderer.html(s.text,s.block);continue}case"paragraph":{const s=i;r+=this.renderer.paragraph(this.parseInline(s.tokens));continue}case"text":{let s=i,a=s.tokens?this.parseInline(s.tokens):s.text;for(;l+1{const d=a[u].flat(1/0);r=r.concat(this.walkTokens(d,n))}):a.tokens&&(r=r.concat(this.walkTokens(a.tokens,n)))}}return r}use(...t){const n=this.defaults.extensions||{renderers:{},childTokens:{}};return t.forEach(r=>{const l={...r};if(l.async=this.defaults.async||l.async||!1,r.extensions&&(r.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){const s=n.renderers[i.name];s?n.renderers[i.name]=function(...a){let u=i.renderer.apply(this,a);return u===!1&&(u=s.apply(this,a)),u}:n.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const s=n[i.level];s?s.unshift(i.tokenizer):n[i.level]=[i.tokenizer],i.start&&(i.level==="block"?n.startBlock?n.startBlock.push(i.start):n.startBlock=[i.start]:i.level==="inline"&&(n.startInline?n.startInline.push(i.start):n.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(n.childTokens[i.name]=i.childTokens)}),l.extensions=n),r.renderer){const i=this.defaults.renderer||new Ll(this.defaults);for(const s in r.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(s==="options")continue;const a=s,u=r.renderer[a],d=i[a];i[a]=(...g)=>{let y=u.apply(i,g);return y===!1&&(y=d.apply(i,g)),y||""}}l.renderer=i}if(r.tokenizer){const i=this.defaults.tokenizer||new Tl(this.defaults);for(const s in r.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;const a=s,u=r.tokenizer[a],d=i[a];i[a]=(...g)=>{let y=u.apply(i,g);return y===!1&&(y=d.apply(i,g)),y}}l.tokenizer=i}if(r.hooks){const i=this.defaults.hooks||new Jn;for(const s in r.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(s==="options")continue;const a=s,u=r.hooks[a],d=i[a];Jn.passThroughHooks.has(s)?i[a]=g=>{if(this.defaults.async)return Promise.resolve(u.call(i,g)).then(p=>d.call(i,p));const y=u.call(i,g);return d.call(i,y)}:i[a]=(...g)=>{let y=u.apply(i,g);return y===!1&&(y=d.apply(i,g)),y}}l.hooks=i}if(r.walkTokens){const i=this.defaults.walkTokens,s=r.walkTokens;l.walkTokens=function(a){let u=[];return u.push(s.call(this,a)),i&&(u=u.concat(i.call(this,a))),u}}this.defaults={...this.defaults,...l}}),this}setOptions(t){return this.defaults={...this.defaults,...t},this}lexer(t,n){return qe.lex(t,n??this.defaults)}parser(t,n){return Ye.parse(t,n??this.defaults)}}bt=new WeakSet,ws=function(t,n){return(r,l)=>{const i={...l},s={...this.defaults,...i};this.defaults.async===!0&&i.async===!1&&(s.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),s.async=!0);const a=Nr(this,bt,Td).call(this,!!s.silent,!!s.async);if(typeof r>"u"||r===null)return a(new Error("marked(): input parameter is undefined or null"));if(typeof r!="string")return a(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(r)+", string expected"));if(s.hooks&&(s.hooks.options=s),s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(r):r).then(u=>t(u,s)).then(u=>s.hooks?s.hooks.processAllTokens(u):u).then(u=>s.walkTokens?Promise.all(this.walkTokens(u,s.walkTokens)).then(()=>u):u).then(u=>n(u,s)).then(u=>s.hooks?s.hooks.postprocess(u):u).catch(a);try{s.hooks&&(r=s.hooks.preprocess(r));let u=t(r,s);s.hooks&&(u=s.hooks.processAllTokens(u)),s.walkTokens&&this.walkTokens(u,s.walkTokens);let d=n(u,s);return s.hooks&&(d=s.hooks.postprocess(d)),d}catch(u){return a(u)}}},Td=function(t,n){return r=>{if(r.message+=` +Please report this to https://github.com/markedjs/marked.`,t){const l="

    An error occurred:

    "+Ee(r.message+"",!0)+"
    ";return n?Promise.resolve(l):l}if(n)return Promise.reject(r);throw r}};const Qt=new fm;function O(e,t){return Qt.parse(e,t)}O.options=O.setOptions=function(e){return Qt.setOptions(e),O.defaults=Qt.defaults,xd(O.defaults),O};O.getDefaults=So;O.defaults=Zt;O.use=function(...e){return Qt.use(...e),O.defaults=Qt.defaults,xd(O.defaults),O};O.walkTokens=function(e,t){return Qt.walkTokens(e,t)};O.parseInline=Qt.parseInline;O.Parser=Ye;O.parser=Ye.parse;O.Renderer=Ll;O.TextRenderer=Eo;O.Lexer=qe;O.lexer=qe.lex;O.Tokenizer=Tl;O.Hooks=Jn;O.parse=O;O.options;O.setOptions;O.use;O.walkTokens;O.parseInline;Ye.parse;qe.lex;function pm({graphData:e}){var R,m,c,h,x;const[t,n]=L.useState(null),[r,l]=L.useState(null),[i,s]=L.useState(null),[a,u]=L.useState(null),d=800,g=500,y=L.useMemo(()=>{if(!e||!e.nodes)return{nodes:[],edges:[]};const f=[...e.nodes],N=[...e.edges],z={entrypoint:[],api:[],module:[],file:[],database:[],other:[]};f.forEach(_=>{const D=(_.type||"file").toLowerCase();z[D]?z[D].push(_):z.other.push(_)});const C={entrypoint:100,api:240,module:440,file:440,database:660,other:550},S={};return Object.entries(z).forEach(([_,D])=>{const ke=C[_]||380,ne=D.length;D.forEach((we,Lt)=>{const Rt=ne===1?g/2:(Lt+.5)/ne*g;S[we.id]={...we,x:ke,y:Rt,color:p(we.type)}})}),{nodes:Object.values(S),edges:N.map(_=>({..._,sourceNode:S[_.source],targetNode:S[_.target]})).filter(_=>_.sourceNode&&_.targetNode)}},[e]);function p(f){switch(f==null?void 0:f.toLowerCase()){case"entrypoint":return"#f97316";case"api":return"#10b981";case"database":return"#a855f7";case"module":return"#00f2fe";case"file":return"#3b82f6";default:return"#94a3b8"}}const v=L.useMemo(()=>{if(r){const f=e.business_flows.find(N=>N.flow_name===r);return new Set((f==null?void 0:f.steps)||[])}if(i){const f=e.critical_paths.find(N=>N.path_name===i);return new Set((f==null?void 0:f.nodes)||[])}return null},[r,i,e]),w=f=>{n(f)},k=()=>{n(null),l(null),s(null)};return!e||!e.nodes||e.nodes.length===0?o.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-secondary)"},children:"No relationship graph metadata available."}):o.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 340px",gap:"2rem"},children:[o.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[o.jsxs("div",{className:"graph-viewport",children:[o.jsxs("svg",{className:"graph-svg",viewBox:`0 0 ${d} ${g}`,children:[o.jsxs("defs",{children:[o.jsx("marker",{id:"arrow",viewBox:"0 0 10 10",refX:"18",refY:"5",markerWidth:"6",markerHeight:"6",orient:"auto-start-reverse",children:o.jsx("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"#475569"})}),o.jsx("marker",{id:"arrow-active",viewBox:"0 0 10 10",refX:"18",refY:"5",markerWidth:"8",markerHeight:"8",orient:"auto-start-reverse",children:o.jsx("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"#00f2fe"})})]}),y.edges.map((f,N)=>{const{sourceNode:z,targetNode:C}=f,S=C.x-z.x,_=C.y-z.y,D=z.x+S/2,ke=z.y+_/2-(S>0?30:-30),ne=v?v.has(f.source)&&v.has(f.target):!1;let we=.25;return a?we=f.source===a||f.target===a?.8:.05:v&&(we=ne?.9:.05),o.jsxs("g",{children:[o.jsx("path",{d:`M ${z.x} ${z.y} Q ${D} ${ke} ${C.x} ${C.y}`,fill:"none",stroke:ne?"var(--accent-cyan)":"#475569",strokeWidth:ne?2.5:1.25,strokeDasharray:ne?"5,5":"none",markerEnd:ne?"url(#arrow-active)":"url(#arrow)",style:{transition:"stroke-opacity 0.3s",strokeOpacity:we}}),ne&&o.jsx("text",{x:D,y:ke-10,fill:"var(--accent-cyan)",fontSize:"9px",fontWeight:"600",textAnchor:"middle",children:f.label})]},`edge-${N}`)}),y.nodes.map(f=>{const N=v?v.has(f.id):!0;let z=1;if(a){const S=f.id===a,_=y.edges.some(D=>D.source===a&&D.target===f.id||D.target===a&&D.source===f.id);z=S||_?1:.15}else v&&(z=N?1:.15);const C=(t==null?void 0:t.id)===f.id;return o.jsxs("g",{transform:`translate(${f.x}, ${f.y})`,style:{transition:"opacity 0.3s, transform 0.2s",opacity:z},onClick:()=>w(f),onMouseEnter:()=>u(f.id),onMouseLeave:()=>u(null),className:"node-circle",children:[C&&o.jsx("circle",{r:14,fill:"none",stroke:"var(--accent-cyan)",strokeWidth:2}),o.jsx("circle",{r:9,fill:f.color,stroke:"#0b0f19",strokeWidth:1.5,style:{filter:C?"drop-shadow(0 0 6px var(--accent-cyan))":"none"}}),o.jsx("text",{y:-14,fill:"#f1f5f9",fontSize:"10px",fontWeight:"500",textAnchor:"middle",style:{pointerEvents:"none",filter:"drop-shadow(0px 1px 2px rgba(0,0,0,0.9))"},children:f.label})]},f.id)})]}),o.jsxs("div",{className:"graph-legend",children:[o.jsxs("div",{className:"legend-item",children:[o.jsx("span",{className:"legend-color",style:{background:"#f97316"}}),o.jsx("span",{children:"Entry Points"})]}),o.jsxs("div",{className:"legend-item",children:[o.jsx("span",{className:"legend-color",style:{background:"#10b981"}}),o.jsx("span",{children:"APIs"})]}),o.jsxs("div",{className:"legend-item",children:[o.jsx("span",{className:"legend-color",style:{background:"#00f2fe"}}),o.jsx("span",{children:"Modules / Code"})]}),o.jsxs("div",{className:"legend-item",children:[o.jsx("span",{className:"legend-color",style:{background:"#a855f7"}}),o.jsx("span",{children:"Databases / Storage"})]})]})]}),t?o.jsxs("div",{className:"glass-panel",style:{padding:"1.25rem"},children:[o.jsxs("div",{style:{display:"flex",justifyContent:"space-between",marginBottom:"0.75rem"},children:[o.jsx("h4",{style:{fontFamily:"var(--font-display)",color:"var(--accent-cyan)",margin:0},children:t.label}),o.jsx("button",{onClick:k,style:{background:"none",border:"none",color:"var(--text-muted)",cursor:"pointer",fontSize:"0.8rem"},children:"Clear Focus"})]}),o.jsxs("p",{style:{fontSize:"0.9rem",color:"var(--text-secondary)"},children:[o.jsx("strong",{children:"Type:"})," ",o.jsx("span",{style:{textTransform:"capitalize"},children:t.type})]}),((R=t.properties)==null?void 0:R.path)&&o.jsx("p",{style:{fontSize:"0.85rem",color:"var(--text-muted)",marginTop:"0.25rem",fontFamily:"var(--font-mono)"},children:t.properties.path}),((m=t.properties)==null?void 0:m.db_type)&&o.jsxs("p",{style:{fontSize:"0.9rem",color:"var(--text-secondary)"},children:[o.jsx("strong",{children:"DB Technology:"})," ",t.properties.db_type]})]}):o.jsx("div",{className:"glass-panel",style:{padding:"1.25rem",color:"var(--text-muted)",fontSize:"0.85rem",textAlign:"center"},children:"Hover over nodes to inspect dependencies. Click a node to view properties."})]}),o.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1.5rem"},children:[o.jsxs("div",{className:"glass-panel",style:{padding:"1.25rem"},children:[o.jsxs("div",{className:"summary-title",style:{fontSize:"1.05rem",marginBottom:"0.75rem"},children:[o.jsx(Gl,{size:18})," Business Flows"]}),o.jsx("p",{style:{fontSize:"0.8rem",color:"var(--text-muted)",marginBottom:"1rem"},children:"Sequence steps mapping end-to-end user operations. Click to trace path in the graph."}),o.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:(c=e.business_flows)==null?void 0:c.map((f,N)=>o.jsxs("div",{onClick:()=>{l(r===f.flow_name?null:f.flow_name),s(null)},style:{padding:"0.75rem",borderRadius:"var(--radius-sm)",background:r===f.flow_name?"rgba(0, 242, 254, 0.08)":"rgba(255, 255, 255, 0.02)",border:`1px solid ${r===f.flow_name?"var(--accent-cyan)":"var(--border-color)"}`,cursor:"pointer",transition:"all 0.2s"},children:[o.jsx("div",{style:{fontWeight:600,fontSize:"0.85rem",color:r===f.flow_name?"var(--accent-cyan)":"#f1f5f9"},children:f.flow_name}),o.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-secondary)",marginTop:"0.25rem"},children:f.description}),r===f.flow_name&&o.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.25rem",alignItems:"center",marginTop:"0.5rem"},children:f.steps.map((z,C)=>o.jsxs(_s.Fragment,{children:[o.jsx("span",{style:{fontSize:"8px",background:"rgba(0,0,0,0.3)",padding:"2px 4px",borderRadius:"2px",color:"#fff",fontFamily:"var(--font-mono)"},children:z.split("/").pop()}),Co.jsxs("div",{onClick:()=>{s(i===f.path_name?null:f.path_name),l(null)},style:{padding:"0.75rem",borderRadius:"var(--radius-sm)",background:i===f.path_name?"rgba(249, 115, 22, 0.08)":"rgba(255, 255, 255, 0.02)",border:`1px solid ${i===f.path_name?"var(--accent-orange)":"var(--border-color)"}`,cursor:"pointer",transition:"all 0.2s"},children:[o.jsx("div",{style:{fontWeight:600,fontSize:"0.85rem",color:i===f.path_name?"var(--accent-orange)":"#f1f5f9"},children:f.path_name}),o.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-secondary)",marginTop:"0.25rem"},children:f.description})]},N))})]}),o.jsxs("div",{className:"glass-panel",style:{padding:"1.25rem"},children:[o.jsxs("div",{className:"summary-title",style:{fontSize:"1.05rem",marginBottom:"0.75rem",color:"var(--accent-purple)"},children:[o.jsx(fd,{size:18})," Code Concepts"]}),o.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.75rem"},children:(x=e.concepts)==null?void 0:x.map((f,N)=>o.jsxs("div",{style:{borderBottom:No.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.25rem",fontSize:"0.7rem",color:"var(--text-muted)"},children:[o.jsx(hd,{size:10})," ",z]},C))})]},N))})]})]})]})}const hm="".replace(/\/$/,"");function yt(e){const t=e.startsWith("/")?e:`/${e}`;return`${hm}${t}`}const mm=["How does authentication work?","What is the architecture pattern?","Which API endpoints exist?","What are the main dependencies?","Where should I start reading code?"],Ni=e=>({PlannerAgent:o.jsx(_l,{size:13,style:{color:"var(--accent-teal)"}}),ArchitectureAgent:o.jsx(xn,{size:13,style:{color:"var(--accent-purple)"}}),SecurityAgent:o.jsx(yd,{size:13,style:{color:"var(--accent-rose)"}}),ApiAgent:o.jsx(El,{size:13,style:{color:"var(--accent-indigo)"}}),DependencyAgent:o.jsx(Zl,{size:13,style:{color:"var(--accent-amber)"}}),QualityAgent:o.jsx(Gl,{size:13,style:{color:"var(--accent-green)"}}),OnboardingAgent:o.jsx(xo,{size:13,style:{color:"var(--accent-teal)"}})})[e]||o.jsx(_l,{size:13}),zi=e=>e.replace("Agent"," Agent"),gm=e=>{try{return{__html:O.parse(e||"")}}catch{return{__html:e||""}}};function ym({repo_id:e,apiKey:t}){var m,c,h,x;const[n,r]=L.useState([{id:"welcome",role:"assistant",content:"Hello! Ask me anything about this codebase — architecture, security, APIs, dependencies, or how to get started.",timeline:null,planner_decision:null,confidence:null,total_time_ms:null,references:[],retrieved_context:[]}]),[l,i]=L.useState(""),[s,a]=L.useState(!1),[u,d]=L.useState(null),[g,y]=L.useState("welcome"),p=L.useRef(null);L.useEffect(()=>{var f;(f=p.current)==null||f.scrollIntoView({behavior:"smooth"})},[n,s]);const v=async f=>{if(!f.trim()||s)return;a(!0);const N={id:`u-${Date.now()}`,role:"user",content:f.trim()};r(C=>[...C,N]),y(N.id),i("");const z={"Content-Type":"application/json"};t&&(z["x-gemini-key"]=t);try{const C=await fetch(yt("/api/chat"),{method:"POST",headers:z,body:JSON.stringify({repo_id:e,question:f.trim(),session_id:u})}),S=await C.json();if(!C.ok)throw new Error(S.detail||"Agent orchestration failed.");S.session_id&&d(S.session_id);const _={id:`a-${Date.now()}`,role:"assistant",content:S.answer,summary:S.summary,agents_used:S.agents_used,confidence:S.confidence,references:S.references||[],agent_contributions:S.agent_contributions,planner_decision:S.planner_decision,timeline:S.timeline,total_time_ms:S.total_time_ms,retrieved_context:S.retrieved_context||[],rag_latency_ms:S.rag_latency_ms,session_id:S.session_id};r(D=>[...D,_]),y(_.id)}catch(C){const S={id:`e-${Date.now()}`,role:"assistant",content:`**Error:** ${C.message}`,isError:!0};r(_=>[..._,S])}finally{a(!1)}},w=f=>{f.preventDefault(),v(l)},k=n.find(f=>f.id===g&&f.role==="assistant")||[...n].reverse().find(f=>f.role==="assistant"&&!f.isError)||n[0],R=f=>f>=.75?"high":f>=.5?"medium":"low";return o.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 340px",gap:"1.25rem",alignItems:"start"},children:[o.jsxs("div",{className:"chat-container",children:[o.jsxs("div",{className:"chat-header",children:[o.jsxs("div",{className:"chat-header-title",children:[o.jsx(ji,{size:16,style:{color:"var(--accent-teal)"}}),"AI Repository Assistant"]}),o.jsx("span",{style:{fontSize:"0.7rem",background:"var(--status-success-bg)",color:"var(--status-success-txt)",border:"1px solid #A7F3D0",borderRadius:"var(--radius-full)",padding:"0.15rem 0.6rem",fontWeight:600},children:"Multi-Agent Online"})]}),o.jsxs("div",{className:"chat-messages",children:[n.length===1&&o.jsxs("div",{className:"chat-welcome",children:[o.jsx("div",{className:"chat-welcome-icon",children:"🤖"}),o.jsx("p",{style:{fontWeight:600,color:"var(--text-primary)",marginBottom:"0.5rem"},children:n[0].content}),o.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-muted)",marginBottom:"1rem"},children:"Try one of these questions:"}),o.jsx("div",{className:"suggested-questions",children:mm.map(f=>o.jsx("button",{className:"suggested-q-btn",onClick:()=>v(f),children:f},f))})]}),n.slice(1).map(f=>{var N,z,C;return o.jsxs("div",{className:`chat-message ${f.role}`,onClick:()=>f.role==="assistant"&&y(f.id),style:{cursor:f.role==="assistant"?"pointer":"default"},children:[o.jsx("div",{className:"chat-bubble",children:f.role==="assistant"?o.jsx("div",{className:"markdown-body",dangerouslySetInnerHTML:gm(f.content),style:{fontSize:"0.9rem",lineHeight:1.65}}):o.jsx("span",{children:f.content})}),o.jsxs("div",{className:"chat-meta",style:{display:"flex",alignItems:"center",gap:"0.5rem",flexWrap:"wrap"},children:[f.role==="assistant"&&f.confidence!=null&&o.jsxs("span",{className:`confidence-badge ${R(f.confidence)}`,children:[Math.round(f.confidence*100),"% confidence"]}),f.role==="assistant"&&f.total_time_ms&&o.jsxs("span",{style:{fontSize:"0.68rem",color:"var(--text-muted)"},children:[f.total_time_ms,"ms"]}),f.role==="assistant"&&((N=f.agents_used)==null?void 0:N.length)>0&&o.jsx("div",{className:"rag-context-strip",children:f.agents_used.map(S=>o.jsxs("span",{className:"agent-tag",children:[Ni(S)," ",zi(S)]},S))}),f.role==="assistant"&&((z=f.retrieved_context)==null?void 0:z.length)>0&&o.jsxs("span",{className:"rag-badge",children:[f.retrieved_context.length," RAG chunks"]})]}),((C=f.references)==null?void 0:C.length)>0&&o.jsx("div",{style:{marginTop:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.35rem"},children:f.references.map((S,_)=>o.jsx("span",{className:"src-path-tag",children:S},_))})]},f.id)}),s&&o.jsx("div",{className:"chat-message assistant",children:o.jsxs("div",{className:"chat-bubble",style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[o.jsx(ji,{size:16,className:"spin-slow",style:{color:"var(--accent-teal)",flexShrink:0}}),o.jsx("span",{style:{color:"var(--text-muted)",fontSize:"0.88rem"},children:"Orchestrating agents..."})]})}),o.jsx("div",{ref:p})]}),o.jsx("div",{className:"chat-input-area",children:o.jsxs("form",{onSubmit:w,className:"chat-input-row",children:[o.jsx("textarea",{className:"chat-input",rows:1,value:l,onChange:f=>i(f.target.value),onKeyDown:f=>{f.key==="Enter"&&!f.shiftKey&&(f.preventDefault(),v(l))},placeholder:"Ask about architecture, security, APIs, or onboarding...",disabled:s}),o.jsx("button",{type:"submit",className:"chat-send-btn",disabled:s||!l.trim(),id:"chat-send-button",children:o.jsx(Ch,{size:16})})]})})]}),o.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.75rem"},children:o.jsxs("div",{className:"card",style:{padding:"1.25rem"},children:[o.jsxs("h4",{style:{fontFamily:"var(--font-display)",fontSize:"0.88rem",fontWeight:700,color:"var(--text-primary)",marginBottom:"0.75rem",display:"flex",alignItems:"center",gap:"0.4rem"},children:[o.jsx(Gl,{size:14,style:{color:"var(--accent-teal)"}}),"Orchestration Observability"]}),k&&k.id!=="welcome"&&!k.isError?o.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[o.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"0.5rem"},children:[o.jsxs("div",{style:{background:"var(--bg-muted)",borderRadius:"var(--radius-md)",padding:"0.65rem"},children:[o.jsx("div",{className:"msc-label",children:"Time"}),o.jsxs("div",{style:{fontWeight:700,fontSize:"1.1rem",color:"var(--accent-teal-dk)"},children:[k.total_time_ms,"ms"]})]}),o.jsxs("div",{style:{background:"var(--bg-muted)",borderRadius:"var(--radius-md)",padding:"0.65rem"},children:[o.jsx("div",{className:"msc-label",children:"Confidence"}),o.jsx("div",{style:{fontWeight:700,fontSize:"1.1rem",color:"var(--accent-green)"},children:k.confidence!=null?`${Math.round(k.confidence*100)}%`:"—"})]})]}),((m=k.retrieved_context)==null?void 0:m.length)>0&&o.jsxs("div",{style:{background:"var(--status-info-bg)",border:"1px solid #BFDBFE",borderRadius:"var(--radius-md)",padding:"0.65rem"},children:[o.jsxs("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--status-info-txt)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.4rem"},children:["RAG Context (",k.retrieved_context.length," chunks)"]}),k.retrieved_context.slice(0,3).map((f,N)=>{var z;return o.jsxs("div",{style:{fontSize:"0.72rem",color:"var(--text-secondary)",display:"flex",justifyContent:"space-between",marginBottom:"0.2rem"},children:[o.jsx("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"150px"},children:((z=f.metadata)==null?void 0:z.category)||"chunk"}),o.jsxs("span",{style:{color:"var(--accent-teal-dk)",fontWeight:600},children:[Math.round(f.similarity*100),"%"]})]},N)}),k.rag_latency_ms&&o.jsxs("div",{style:{fontSize:"0.68rem",color:"var(--text-muted)",marginTop:"0.25rem"},children:["Retrieved in ",k.rag_latency_ms,"ms"]})]}),k.planner_decision&&o.jsxs("div",{style:{background:"var(--bg-muted)",border:"1px solid var(--border-color)",borderRadius:"var(--radius-md)",padding:"0.75rem"},children:[o.jsxs("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--text-muted)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.5rem",display:"flex",alignItems:"center",gap:"0.3rem"},children:[o.jsx(_l,{size:11})," Planner Decision"]}),o.jsx("p",{style:{fontSize:"0.78rem",color:"var(--text-secondary)",lineHeight:1.5,marginBottom:"0.5rem"},children:k.planner_decision.reasoning}),o.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:(c=k.planner_decision.execution_order)==null?void 0:c.flat().map(f=>o.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"0.2rem",fontSize:"0.7rem",padding:"0.15rem 0.45rem",background:"#fff",border:"1px solid var(--border-color)",borderRadius:"var(--radius-full)",color:"var(--text-secondary)"},children:[Ni(f)," ",zi(f)]},f))})]}),((h=k.timeline)==null?void 0:h.length)>0&&o.jsxs("div",{children:[o.jsx("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--text-muted)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.5rem"},children:"Agent Timeline"}),k.timeline.map((f,N)=>o.jsxs("div",{style:{display:"flex",gap:"0.5rem",alignItems:"flex-start",marginBottom:"0.5rem"},children:[o.jsx("div",{style:{width:6,height:6,borderRadius:"50%",flexShrink:0,marginTop:6,background:f.status==="completed"||f.status==="success"?"var(--accent-green)":f.status==="error"?"var(--accent-rose)":"var(--accent-teal)"}}),o.jsxs("div",{style:{flex:1},children:[o.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[o.jsxs("span",{style:{fontSize:"0.78rem",fontWeight:600,color:"var(--text-primary)",display:"flex",alignItems:"center",gap:"0.25rem"},children:[Ni(f.agent)," ",zi(f.agent)]}),o.jsxs("span",{style:{fontSize:"0.68rem",color:"var(--text-muted)"},children:[f.execution_time_ms,"ms"]})]}),f.confidence!=null&&f.agent!=="PlannerAgent"&&o.jsx("div",{style:{marginTop:"0.2rem",height:3,background:"var(--bg-card)",borderRadius:2,overflow:"hidden"},children:o.jsx("div",{style:{width:`${f.confidence*100}%`,height:"100%",background:"var(--accent-green)",borderRadius:2}})})]})]},N))]}),((x=k.agent_contributions)==null?void 0:x.length)>0&&o.jsxs("div",{children:[o.jsxs("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--text-muted)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.4rem",display:"flex",alignItems:"center",gap:"0.3rem"},children:[o.jsx(dh,{size:11})," Contributions"]}),o.jsx("ul",{style:{listStyle:"none",display:"flex",flexDirection:"column",gap:"0.2rem"},children:k.agent_contributions.map((f,N)=>o.jsxs("li",{style:{fontSize:"0.76rem",color:"var(--text-secondary)",display:"flex",gap:"0.35rem",alignItems:"flex-start"},children:[o.jsx("span",{style:{color:"var(--accent-teal)",flexShrink:0},children:"•"}),f]},N))})]})]}):o.jsxs("div",{style:{textAlign:"center",padding:"1.5rem 0.5rem",color:"var(--text-muted)"},children:[o.jsx(ji,{size:28,style:{margin:"0 auto 0.5rem",color:"var(--border-hover)"}}),o.jsx("p",{style:{fontSize:"0.8rem",lineHeight:1.5},children:"Click any response to see agent timeline & orchestration details."})]})]})})]})}const Za=[{value:"",label:"All Categories"},{value:"report",label:"Report"},{value:"summary",label:"Summary"},{value:"architecture",label:"Architecture"},{value:"authentication",label:"Authentication"},{value:"api",label:"API Endpoints"},{value:"dependency",label:"Dependencies"},{value:"business_flow",label:"Business Flows"},{value:"concept",label:"Concepts"},{value:"file",label:"Source Files"}],vm=[{name:"repository_search",icon:o.jsx(Cl,{size:14}),color:"#2E9E9E",bg:"#E6F7F7",border:"#A8D8D8",desc:"Semantic search over indexed repo chunks"},{name:"graph_query",icon:o.jsx(xn,{size:14}),color:"#7C3AED",bg:"#F5F3FF",border:"#C4B5FD",desc:"Query architecture graph, entry points, and flows"},{name:"dependency_lookup",icon:o.jsx(Zl,{size:14}),color:"#D97706",bg:"#FFFBEB",border:"#FDE68A",desc:"Lookup packages, frameworks, and databases"},{name:"file_reader",icon:o.jsx(md,{size:14}),color:"#4338CA",bg:"#EEF2FF",border:"#C7D2FE",desc:"Retrieve specific source file content"},{name:"architecture_lookup",icon:o.jsx(Gl,{size:14}),color:"#059669",bg:"#ECFDF5",border:"#A7F3D0",desc:"Query architecture pattern and key modules"},{name:"api_lookup",icon:o.jsx(El,{size:14}),color:"#E11D48",bg:"#FFF1F2",border:"#FECDD3",desc:"Lookup HTTP routes and authentication methods"}],xm={report:{color:"#4338CA",bg:"#EEF2FF",border:"#C7D2FE"},summary:{color:"#2E9E9E",bg:"#E6F7F7",border:"#A8D8D8"},architecture:{color:"#7C3AED",bg:"#F5F3FF",border:"#C4B5FD"},authentication:{color:"#E11D48",bg:"#FFF1F2",border:"#FECDD3"},api:{color:"#059669",bg:"#ECFDF5",border:"#A7F3D0"},dependency:{color:"#D97706",bg:"#FFFBEB",border:"#FDE68A"},business_flow:{color:"#2E9E9E",bg:"#E6F7F7",border:"#A8D8D8"},concept:{color:"#7C3AED",bg:"#F5F3FF",border:"#C4B5FD"},file:{color:"#475569",bg:"#F8FAFC",border:"#CBD5E1"}},qa=e=>xm[e]||{color:"#64748B",bg:"#F8FAFC",border:"#CBD5E1"},km=[{id:"search",icon:o.jsx(Cl,{size:14}),label:"Semantic Search"},{id:"memory",icon:o.jsx(ko,{size:14}),label:"Memory Inspector"},{id:"conversations",icon:o.jsx(xs,{size:14}),label:"Conversations"},{id:"tools",icon:o.jsx(wo,{size:14}),label:"Tool Catalog"}];function wm({repo_id:e,apiKey:t}){var J;const[n,r]=L.useState("search"),[l,i]=L.useState(""),[s,a]=L.useState(""),[u,d]=L.useState(5),[g,y]=L.useState([]),[p,v]=L.useState(!1),[w,k]=L.useState(null),[R,m]=L.useState(null),[c,h]=L.useState(null),[x,f]=L.useState(!1),[N,z]=L.useState([]),[C,S]=L.useState(!1),[_,D]=L.useState(null),[ke,ne]=L.useState([]),[we,Lt]=L.useState(!1),Rt=()=>{const E={"Content-Type":"application/json"};return t&&(E["x-gemini-key"]=t),E};L.useEffect(()=>{n==="memory"&&qt(),n==="conversations"&&T()},[n]);const qt=async()=>{f(!0);try{const E=await fetch(yt(`/api/memory?repo_id=${e}`));h(await E.json())}catch{h(null)}f(!1)},T=async()=>{S(!0);try{const U=await(await fetch(yt(`/api/conversations?repo_id=${e}`))).json();z(U.sessions||[])}catch{z([])}S(!1)},M=async E=>{Lt(!0);try{const U=await fetch(yt(`/api/conversations/${E}`)),ce=await U.json();if(!U.ok)throw new Error(ce.detail);ne(ce.history||[])}catch{ne([])}finally{Lt(!1)}},F=E=>{if(_===E){D(null),ne([]);return}D(E),M(E)},G=async E=>{if(E.preventDefault(),!!l.trim()){v(!0),m(null),y([]),k(null);try{const U={repo_id:e,query:l.trim(),top_k:u};s&&(U.category=s);const ce=await fetch(yt("/api/search"),{method:"POST",headers:Rt(),body:JSON.stringify(U)}),de=await ce.json();if(!ce.ok)throw new Error(de.detail||"Search failed");y(de.results||[]),k(de.latency_ms)}catch(U){m(U.message)}v(!1)}};return o.jsxs("div",{className:"ke-container",children:[o.jsx("div",{className:"ke-tabs",children:km.map(E=>o.jsxs("button",{className:`ke-tab-btn ${n===E.id?"active":""}`,onClick:()=>r(E.id),id:`ke-tab-${E.id}`,children:[E.icon," ",E.label]},E.id))}),n==="search"&&o.jsxs("div",{className:"ke-panel",children:[o.jsxs("div",{className:"ke-panel-title",children:[o.jsx(Cl,{size:16,style:{color:"var(--accent-teal)"}}),"Semantic Knowledge Search"]}),o.jsx("p",{className:"ke-panel-desc",children:"Search across all indexed repository knowledge using natural language. Results are ranked by cosine similarity score."}),o.jsxs("form",{onSubmit:G,className:"search-form-row",children:[o.jsx("input",{type:"text",className:"ke-input",placeholder:"e.g. 'JWT authentication middleware' or 'database connection'",value:l,onChange:E=>i(E.target.value),id:"semantic-search-input"}),o.jsx("select",{className:"ke-select",value:s,onChange:E=>a(E.target.value),children:Za.map(E=>o.jsx("option",{value:E.value,children:E.label},E.value))}),o.jsx("select",{className:"ke-select ke-select-sm",value:u,onChange:E=>d(Number(E.target.value)),children:[3,5,8,10].map(E=>o.jsxs("option",{value:E,children:["Top ",E]},E))}),o.jsxs("button",{type:"submit",className:"ke-search-btn",disabled:p||!l.trim(),id:"semantic-search-btn",children:[p?o.jsx(Jt,{size:14,className:"spin-slow"}):o.jsx(Cl,{size:14}),p?"Searching…":"Search"]})]}),R&&o.jsx("div",{className:"ke-error-box",children:R}),w!=null&&!p&&o.jsxs("div",{className:"ke-latency-badge",children:[o.jsx(hh,{size:11}),g.length," results · ",w,"ms"]}),o.jsxs("div",{className:"search-results-list",children:[g.map((E,U)=>{var To,Po,Lo;const ce=qa((To=E.metadata)==null?void 0:To.category),de=E.similarity,Yt=de>.8?"var(--accent-green)":de>.6?"var(--accent-teal)":"var(--accent-amber)";return o.jsxs("div",{className:"search-result-card",children:[o.jsxs("div",{className:"src-header",children:[o.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.4rem",flexWrap:"wrap"},children:[o.jsx("span",{className:"src-category-tag",style:{color:ce.color,background:ce.bg,borderColor:ce.border},children:((Po=E.metadata)==null?void 0:Po.category)||"general"}),((Lo=E.metadata)==null?void 0:Lo.path)&&o.jsx("span",{className:"src-path-tag",children:E.metadata.path})]}),o.jsxs("div",{className:"src-score-bar-wrap",children:[o.jsxs("span",{className:"src-score-label",children:[Math.round(de*100),"%"]}),o.jsx("div",{className:"src-score-bg",children:o.jsx("div",{className:"src-score-fill",style:{width:`${de*100}%`,background:Yt}})})]})]}),o.jsx("pre",{className:"src-content",children:E.content})]},U)}),!p&&g.length===0&&w!=null&&o.jsx("div",{className:"ke-empty",children:"No results found. Try different search terms or select a different category."})]})]}),n==="memory"&&o.jsxs("div",{className:"ke-panel",children:[o.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"0.5rem"},children:[o.jsxs("div",{className:"ke-panel-title",style:{marginBottom:0},children:[o.jsx(ko,{size:16,style:{color:"var(--accent-indigo)"}}),"Vector Memory Inspector"]}),o.jsxs("button",{className:"ke-refresh-btn",onClick:qt,disabled:x,children:[o.jsx(Jt,{size:13,className:x?"spin-slow":""})," Refresh"]})]}),o.jsx("p",{className:"ke-panel-desc",children:"Inspect the repository's semantic knowledge stored in ChromaDB. Each chunk is tagged with category metadata for precise retrieval."}),x&&o.jsxs("div",{className:"ke-spinner-row",children:[o.jsx(Jt,{size:18,className:"spin-slow",style:{color:"var(--accent-teal)"}}),"Loading memory info…"]}),c&&!x&&o.jsxs("div",{className:"memory-stats-grid",children:[o.jsxs("div",{className:"memory-stat-card",children:[o.jsx("div",{className:"msc-label",children:"Indexed Chunks"}),o.jsx("div",{className:"msc-value",children:(J=c.indexed_chunks)==null?void 0:J.toLocaleString()}),o.jsx("div",{className:"msc-sub",children:"ChromaDB documents"})]}),o.jsxs("div",{className:"memory-stat-card",children:[o.jsx("div",{className:"msc-label",children:"Repository ID"}),o.jsx("div",{style:{fontFamily:"var(--font-mono)",fontSize:"0.72rem",color:"var(--text-muted)",wordBreak:"break-all",marginTop:"0.25rem"},children:c.repo_id})]}),o.jsxs("div",{className:"memory-stat-card",children:[o.jsx("div",{className:"msc-label",children:"Storage Path"}),o.jsx("div",{style:{fontFamily:"var(--font-mono)",fontSize:"0.72rem",color:"var(--text-secondary)",wordBreak:"break-all",marginTop:"0.25rem"},children:c.storage_path})]})]}),o.jsxs("div",{className:"ke-info-box",style:{marginBottom:"1rem"},children:[o.jsx(fd,{size:14,style:{flexShrink:0,marginTop:1}}),o.jsxs("span",{children:["The knowledge index contains chunked embeddings of the intelligence report, source files, architecture concepts, API endpoints, dependencies, business flows, and concepts. Embeddings are generated using Gemini ",o.jsx("code",{children:"text-embedding-004"}),"."]})]}),o.jsxs("div",{className:"ke-category-legend",children:[o.jsx("div",{className:"ke-legend-title",children:"Indexed Categories"}),o.jsx("div",{className:"ke-legend-grid",children:Za.filter(E=>E.value).map(E=>{const U=qa(E.value);return o.jsxs("div",{className:"ke-legend-item",children:[o.jsx("span",{className:"ke-legend-dot",style:{background:U.color}}),E.label]},E.value)})})]})]}),n==="conversations"&&o.jsxs("div",{className:"ke-panel",children:[o.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"0.5rem"},children:[o.jsxs("div",{className:"ke-panel-title",style:{marginBottom:0},children:[o.jsx(xs,{size:16,style:{color:"var(--accent-green)"}}),"Conversation History"]}),o.jsxs("button",{className:"ke-refresh-btn",onClick:T,disabled:C,children:[o.jsx(Jt,{size:13,className:C?"spin-slow":""})," Refresh"]})]}),o.jsx("p",{className:"ke-panel-desc",children:"All AI Assistant chat sessions for this repository, stored in conversation memory."}),C&&o.jsxs("div",{className:"ke-spinner-row",children:[o.jsx(Jt,{size:18,className:"spin-slow",style:{color:"var(--accent-teal)"}}),"Loading conversations…"]}),!C&&N.length===0&&o.jsx("div",{className:"ke-empty",children:"No conversations yet. Start chatting in the AI Assistant tab to see sessions here."}),o.jsx("div",{className:"conv-list",children:N.map((E,U)=>{var ce;return o.jsxs("div",{className:`conv-item ${_===E.session_id?"selected":""}`,onClick:()=>F(E.session_id),id:`conv-item-${U}`,children:[o.jsxs("div",{className:"conv-item-header",children:[o.jsxs("span",{className:"conv-idx",children:["#",U+1]}),o.jsx("span",{className:"conv-summary",children:E.summary||"Untitled Session"}),o.jsxs("span",{className:"conv-count",children:[E.message_count," msgs"]}),o.jsx(vo,{size:14,style:{transition:"transform 0.2s",transform:_===E.session_id?"rotate(90deg)":"none",color:"var(--text-muted)",flexShrink:0}})]}),o.jsxs("div",{className:"conv-item-meta",children:[o.jsxs("span",{className:"conv-id",children:[(ce=E.session_id)==null?void 0:ce.substring(0,8),"…"]}),o.jsx("span",{className:"conv-time",children:new Date(E.last_updated*1e3).toLocaleString()})]})]},E.session_id)})}),_&&o.jsxs("div",{style:{marginTop:"1.25rem"},children:[o.jsxs("div",{className:"ke-panel-title",style:{marginBottom:"0.75rem"},children:[o.jsx(xs,{size:14,style:{color:"var(--accent-teal)"}}),"Session Messages"]}),we&&o.jsxs("div",{className:"ke-spinner-row",children:[o.jsx(Jt,{size:16,className:"spin-slow",style:{color:"var(--accent-teal)"}}),"Loading message history…"]}),!we&&ke.length===0&&o.jsx("div",{className:"ke-empty",children:"No messages in this session."}),!we&&ke.length>0&&o.jsx("div",{className:"conv-list",style:{maxHeight:360},children:ke.map((E,U)=>o.jsxs("div",{className:"search-result-card",children:[o.jsx("div",{className:"src-header",children:o.jsx("span",{className:"src-category-tag",style:{color:E.role==="user"?"var(--accent-teal-dk)":"var(--accent-indigo)",background:E.role==="user"?"#E6F7F7":"#EEF2FF",borderColor:E.role==="user"?"#A8D8D8":"#C7D2FE"},children:E.role})}),o.jsx("div",{style:{fontSize:"0.85rem",color:"var(--text-secondary)",lineHeight:1.6,whiteSpace:"pre-wrap"},children:E.content})]},U))})]})]}),n==="tools"&&o.jsxs("div",{className:"ke-panel",children:[o.jsxs("div",{className:"ke-panel-title",children:[o.jsx(wo,{size:16,style:{color:"var(--accent-amber)"}}),"Tool Catalog (MCP-Ready)"]}),o.jsx("p",{className:"ke-panel-desc",children:"These tools are available to the Planner Agent during orchestration. Interfaces are compatible with Model Context Protocol (MCP) and Google ADK."}),o.jsx("div",{className:"tool-catalog-grid",children:vm.map(E=>o.jsxs("div",{className:"tool-card",id:`tool-${E.name}`,children:[o.jsx("div",{className:"tc-icon-wrap",style:{background:E.bg,borderColor:E.border,color:E.color},children:E.icon}),o.jsxs("div",{className:"tc-body",children:[o.jsx("div",{className:"tc-name",children:E.name}),o.jsx("div",{className:"tc-desc",children:E.desc})]}),o.jsx("span",{className:"tc-badge",children:"execute()"})]},E.name))}),o.jsxs("div",{className:"ke-info-box",style:{marginTop:"1.25rem"},children:[o.jsx(wh,{size:14,style:{flexShrink:0,marginTop:1}}),o.jsxs("span",{children:["Each tool implements a ",o.jsx("code",{children:"BaseTool"})," interface with ",o.jsx("code",{children:"name"}),",",o.jsx("code",{children:"description"}),", and ",o.jsx("code",{children:"execute(**kwargs)"}),". This design is forward-compatible with Google ADK, LangGraph, CrewAI, and MCP server registration."]})]})]})]})}const Sm=[{id:"report",icon:o.jsx(md,{size:15}),label:"Intelligence Report"},{id:"summary",icon:o.jsx(Zl,{size:15}),label:"Knowledge Summary"},{id:"profile",icon:o.jsx(xn,{size:15}),label:"Repository Profile"},{id:"graph",icon:o.jsx(_h,{size:15}),label:"Architecture Graph"},{id:"assistant",icon:o.jsx(xo,{size:15}),label:"AI Assistant"},{id:"knowledge",icon:o.jsx(ko,{size:15}),label:"Knowledge Explorer"}];function jm({analysisResult:e}){var w,k,R,m,c,h,x,f,N,z,C;const[t,n]=L.useState("report"),[r,l]=L.useState(!1),{repo_id:i,project_name:s,data:a}=e,{report:u,profile:d,summary:g,graph:y}=a,p=_s.useMemo(()=>{try{return{__html:O.parse(u||"")}}catch(S){return{__html:`

    Failed to render: ${S.message}

    `}}},[u]),v=S=>yt(`/api/download/${i}/${S}`);return o.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1.25rem"},children:[o.jsxs("div",{className:"dashboard-header",children:[o.jsxs("div",{children:[o.jsx("h2",{className:"section-title",style:{fontSize:"1.25rem",marginBottom:"0.5rem"},children:"Intelligence Dashboard"}),o.jsxs("div",{style:{display:"flex",gap:"0.5rem",flexWrap:"wrap",alignItems:"center"},children:[o.jsxs("span",{className:"repo-meta",children:[o.jsx(xn,{size:13,style:{color:"var(--accent-teal)"}}),s]}),o.jsxs("span",{className:"repo-meta",children:[o.jsx(El,{size:13,style:{color:"var(--accent-indigo)"}}),d.architecture_pattern]})]})]}),o.jsx("div",{className:"download-bar",children:[["report","Report.md"],["profile","Profile.json"],["summary","Summary.json"],["graph","Graph.json"]].map(([S,_])=>o.jsxs("a",{href:v(S),download:!0,className:"btn-secondary",children:[o.jsx(gh,{size:13})," ",_]},S))})]}),o.jsx("div",{className:"dashboard-tabs",children:Sm.map(S=>o.jsxs("button",{id:`tab-${S.id}`,className:`tab-btn ${t===S.id?"active":""}`,onClick:()=>n(S.id),children:[S.icon," ",S.label]},S.id))}),o.jsxs("div",{className:"tab-pane",children:[t==="report"&&o.jsx("div",{className:"card report-content",dangerouslySetInnerHTML:p}),t==="summary"&&o.jsxs("div",{className:"summary-grid",children:[o.jsxs("div",{className:"summary-card full-width",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(yh,{size:18,style:{color:"var(--accent-teal)"}})," Project Purpose"]}),o.jsxs("p",{style:{fontSize:"1.05rem",color:"var(--text-secondary)",fontStyle:"italic",lineHeight:1.7,borderLeft:"3px solid var(--accent-teal)",paddingLeft:"1rem"},children:['"',g.elevator_pitch,'"']})]}),o.jsxs("div",{className:"summary-card",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(Zl,{size:16,style:{color:"var(--accent-green)"}})," Core Features"]}),o.jsx("ul",{className:"list-styled",children:(w=g.core_features)==null?void 0:w.map((S,_)=>o.jsxs("li",{className:"list-item-styled",children:[o.jsx("span",{className:"list-item-icon",children:"✓"}),o.jsx("span",{children:S})]},_))})]}),o.jsxs("div",{className:"summary-card",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(Nh,{size:16,style:{color:"var(--accent-indigo)"}})," User Workflows"]}),o.jsx("ul",{className:"list-styled",children:(k=g.main_workflows)==null?void 0:k.map((S,_)=>o.jsxs("li",{className:"list-item-styled",children:[o.jsx("span",{className:"list-item-icon",children:"⚡"}),o.jsx("span",{children:S})]},_))})]}),o.jsxs("div",{className:"summary-card",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(xn,{size:16,style:{color:"var(--accent-purple)"}})," Key Components"]}),o.jsx("ul",{className:"list-styled",children:(R=g.key_components)==null?void 0:R.map((S,_)=>o.jsxs("li",{className:"list-item-styled",children:[o.jsx("span",{className:"list-item-icon",children:"⚙"}),o.jsx("span",{children:S})]},_))})]}),o.jsxs("div",{className:"summary-card",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(Th,{size:16,style:{color:"var(--accent-amber)"}})," Architectural Risks"]}),o.jsx("ul",{className:"list-styled",children:(m=g.key_risks)==null?void 0:m.map((S,_)=>o.jsxs("li",{className:"list-item-styled",children:[o.jsx("span",{className:"list-item-icon risk",children:"⚠"}),o.jsx("span",{children:S})]},_))})]}),o.jsxs("div",{className:"summary-card full-width",children:[o.jsxs("div",{className:"summary-title",children:[o.jsx(xo,{size:16,style:{color:"var(--accent-teal)"}})," Where to Start Reading Code"]}),o.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem",marginTop:"0.25rem"},children:(c=g.developer_start_points)==null?void 0:c.map((S,_)=>o.jsx("div",{className:"module-chip",style:{color:"var(--accent-teal-dk)"},children:S},_))})]})]}),t==="profile"&&o.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1.5rem"},children:[o.jsxs("div",{className:"tech-cards-grid",children:[o.jsxs("div",{className:"tech-card",children:[o.jsxs("div",{className:"tech-card-header",children:[o.jsx(El,{size:16})," Languages"]}),o.jsx("div",{className:"badge-container",children:(h=d.languages)==null?void 0:h.map((S,_)=>o.jsx("span",{className:"tech-badge language",children:S},_))})]}),o.jsxs("div",{className:"tech-card",children:[o.jsxs("div",{className:"tech-card-header",children:[o.jsx(xn,{size:16})," Frameworks"]}),o.jsx("div",{className:"badge-container",children:(x=d.frameworks)==null?void 0:x.map((S,_)=>o.jsx("span",{className:"tech-badge",children:S},_))})]}),o.jsxs("div",{className:"tech-card",children:[o.jsxs("div",{className:"tech-card-header",children:[o.jsx(Sh,{size:16})," Databases"]}),o.jsx("div",{className:"badge-container",children:(f=d.databases)==null?void 0:f.map((S,_)=>o.jsx("span",{className:"tech-badge db",children:S},_))})]}),o.jsxs("div",{className:"tech-card",children:[o.jsxs("div",{className:"tech-card-header",children:[o.jsx(yd,{size:16})," Security & Auth"]}),o.jsx("div",{className:"badge-container",children:(N=d.authentication_methods)==null?void 0:N.map((S,_)=>o.jsx("span",{className:"tech-badge",style:{color:"var(--accent-rose)",background:"#FFF1F2",borderColor:"#FECDD3"},children:S},_))})]})]}),o.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"1.25rem"},children:[o.jsxs("div",{className:"card",style:{padding:"1.5rem"},children:[o.jsx("h4",{style:{fontFamily:"var(--font-display)",marginBottom:"1rem",fontSize:"0.9rem",color:"var(--text-primary)",fontWeight:700},children:"API Endpoints"}),o.jsx("div",{style:{maxHeight:280,overflowY:"auto",display:"flex",flexDirection:"column",gap:"0.35rem"},children:(z=d.api_endpoints)==null?void 0:z.map((S,_)=>o.jsx("div",{className:"api-endpoint-item",children:S},_))})]}),o.jsxs("div",{className:"card",style:{padding:"1.5rem"},children:[o.jsx("h4",{style:{fontFamily:"var(--font-display)",marginBottom:"1rem",fontSize:"0.9rem",color:"var(--text-primary)",fontWeight:700},children:"Core Modules"}),o.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.4rem",maxHeight:280,overflowY:"auto"},children:(C=d.major_modules)==null?void 0:C.map((S,_)=>o.jsx("span",{className:"module-chip",children:S},_))})]})]}),o.jsxs("div",{className:"card",style:{padding:"1.5rem"},children:[o.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:r?"1rem":0},children:[o.jsx("h4",{style:{fontFamily:"var(--font-display)",fontSize:"0.9rem",color:"var(--text-primary)",fontWeight:700},children:"Raw repository_profile.json"}),o.jsxs("button",{className:"btn-secondary",onClick:()=>l(!r),children:[r?o.jsx(pd,{size:14}):o.jsx(vo,{size:14}),r?"Hide":"Show"," JSON"]})]}),r&&o.jsx("pre",{className:"json-block",children:JSON.stringify(d,null,2)})]})]}),t==="graph"&&o.jsxs("div",{className:"card",style:{padding:"1.5rem"},children:[o.jsx("h3",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",marginBottom:"1rem",color:"var(--text-primary)"},children:"Code Topology Graph"}),o.jsx(pm,{graphData:y})]}),t==="assistant"&&o.jsx(ym,{repo_id:i,apiKey:e.apiKey}),t==="knowledge"&&o.jsx(wm,{repo_id:i,apiKey:e.apiKey})]})]})}const Ya=[{title:"Access Verification",desc:"Checking GitHub repository accessibility..."},{title:"Workspace Isolation",desc:"Cloning repository into isolated sandbox..."},{title:"Static Profiling",desc:"Scanning files, parsing package manifests..."},{title:"Gemini Reasoning",desc:"Analyzing codebase with Gemini 2.5 Flash..."},{title:"Knowledge Indexing",desc:"Building semantic vector index (ChromaDB)..."}];function Nm(){var g;const[e,t]=L.useState("idle"),[n,r]=L.useState(null),[l,i]=L.useState(null),[s,a]=L.useState(0);L.useEffect(()=>{let y;return e==="loading"&&(a(0),y=setInterval(()=>{a(p=>pclearInterval(y)},[e]);const u=async y=>{t("loading"),i(null),r(null);const p={};y.apiKey&&(p["x-gemini-key"]=y.apiKey);try{let v;if(y.type==="url")a(0),v=await fetch(yt("/api/analyze-url"),{method:"POST",headers:{"Content-Type":"application/json",...p},body:JSON.stringify({url:y.url,token:y.token||null})});else{a(1);const k=new FormData;k.append("file",y.file),v=await fetch(yt("/api/analyze-zip"),{method:"POST",headers:p,body:k})}const w=await v.json();if(!v.ok)throw new Error(w.detail||"Analysis failed.");a(4),setTimeout(()=>{r({...w,apiKey:y.apiKey||null}),t("success")},700)}catch(v){i(v.message),t("error")}},d=()=>{t("idle"),r(null),i(null)};return o.jsxs("div",{className:"app-container",children:[o.jsxs("header",{className:"app-header",children:[o.jsxs("div",{className:"brand-section",children:[o.jsx("div",{className:"brand-icon",children:o.jsx(_l,{size:20,color:"#fff"})}),o.jsxs("div",{children:[o.jsx("div",{className:"brand-title",children:"Repository Intelligence"}),o.jsx("div",{className:"brand-subtitle",children:"AI-Powered Code Analysis Platform"})]})]}),o.jsx("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:e==="success"&&o.jsxs(o.Fragment,{children:[o.jsx("span",{style:{fontSize:"0.78rem",color:"var(--text-muted)"},children:n==null?void 0:n.project_name}),o.jsxs("button",{className:"btn-secondary",onClick:d,children:[o.jsx(ch,{size:14})," New Analysis"]})]})})]}),o.jsxs("main",{className:"main-content",children:[e==="idle"&&o.jsx(Wa,{onSubmit:u,loading:!1}),e==="error"&&o.jsxs("div",{style:{maxWidth:"680px",width:"100%",margin:"0 auto"},children:[o.jsxs("div",{className:"error-panel",children:[o.jsx(fh,{size:20,style:{flexShrink:0}}),o.jsxs("div",{children:[o.jsx("div",{className:"error-title",children:"Analysis Failed"}),o.jsx("p",{style:{fontSize:"0.88rem",marginTop:"0.25rem"},children:l})]})]}),o.jsx(Wa,{onSubmit:u,loading:!1})]}),e==="loading"&&o.jsxs("div",{className:"glass-panel progress-panel",children:[o.jsxs("div",{style:{textAlign:"center",marginBottom:"2rem"},children:[o.jsx("div",{className:"spinner"}),o.jsx("h2",{className:"section-title",style:{marginBottom:"0.5rem"},children:"Generating Intelligence Report"}),o.jsx("p",{className:"section-desc",style:{margin:0},children:"Scanning codebase, extracting semantics, and building your knowledge base. This may take up to a minute for large repositories."})]}),o.jsx("div",{className:"progress-steps",children:Ya.map((y,p)=>{const v=p + + + + + + Repository Intelligence Engine + + + + + + + + +
    + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..0e437f47e18527efcb62ce15953fdd9e2791edd6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + Repository Intelligence Engine + + + + + + +
    + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..e4f38d2492abdd5a71b7111a97c6664772fe4415 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 100M; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..ba6b8b30a208af5e18c9e751397d58b426ade2c6 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1740 @@ +{ + "name": "repository-intelligence-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "repository-intelligence-frontend", + "version": "1.0.0", + "dependencies": { + "lucide-react": "^0.395.0", + "marked": "^12.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "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.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.378", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", + "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.395.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.395.0.tgz", + "integrity": "sha512-6hzdNH5723A4FLaYZWpK50iyZH8iS2Jq5zuPRRotOFkhu6kxxJiebVdJ72tCR5XkiIeYFOU5NUawFZOac+VeYw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..95a5f269c0efa85bf41a6b527974821bf9337832 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "repository-intelligence-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "lucide-react": "^0.395.0", + "marked": "^12.0.2" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.1" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f983d02fa1b3dbaf942ba48c537d0c9afaea347b --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect } from 'react'; +import { Cpu, ArrowLeft, AlertCircle, CheckCircle } from 'lucide-react'; +import InputForm from './components/InputForm'; +import RepoTree from './components/RepoTree'; +import Dashboard from './components/Dashboard'; +import { apiUrl } from './api'; + +const LOADING_STEPS = [ + { title: 'Access Verification', desc: 'Checking GitHub repository accessibility...' }, + { title: 'Workspace Isolation', desc: 'Cloning repository into isolated sandbox...' }, + { title: 'Static Profiling', desc: 'Scanning files, parsing package manifests...' }, + { title: 'Gemini Reasoning', desc: 'Analyzing codebase with Gemini 2.5 Flash...' }, + { title: 'Knowledge Indexing', desc: 'Building semantic vector index (ChromaDB)...' }, +]; + +export default function App() { + const [appState, setAppState] = useState('idle'); // idle | loading | success | error + const [analysisResult, setAnalysisResult] = useState(null); + const [error, setError] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + + useEffect(() => { + let interval; + if (appState === 'loading') { + setCurrentStep(0); + interval = setInterval(() => { + setCurrentStep(prev => (prev < LOADING_STEPS.length - 2 ? prev + 1 : prev)); + }, 3800); + } + return () => clearInterval(interval); + }, [appState]); + + const handleStartAnalysis = async (formData) => { + setAppState('loading'); + setError(null); + setAnalysisResult(null); + + // API key is now server-side from .env — only send if user explicitly provided one + const headers = {}; + if (formData.apiKey) headers['x-gemini-key'] = formData.apiKey; + + try { + let response; + if (formData.type === 'url') { + setCurrentStep(0); + response = await fetch(apiUrl('/api/analyze-url'), { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...headers }, + body: JSON.stringify({ url: formData.url, token: formData.token || null }), + }); + } else { + setCurrentStep(1); + const fd = new FormData(); + fd.append('file', formData.file); + response = await fetch(apiUrl('/api/analyze-zip'), { method: 'POST', headers, body: fd }); + } + + const resData = await response.json(); + if (!response.ok) throw new Error(resData.detail || 'Analysis failed.'); + + setCurrentStep(4); + setTimeout(() => { + setAnalysisResult({ ...resData, apiKey: formData.apiKey || null }); + setAppState('success'); + }, 700); + } catch (e) { + setError(e.message); + setAppState('error'); + } + }; + + const handleReset = () => { + setAppState('idle'); + setAnalysisResult(null); + setError(null); + }; + + return ( +
    + + {/* ── Header ── */} +
    +
    +
    + +
    +
    +
    Repository Intelligence
    +
    AI-Powered Code Analysis Platform
    +
    +
    + +
    + {appState === 'success' && ( + <> + + {analysisResult?.project_name} + + + + )} +
    +
    + + {/* ── Main ── */} +
    + + {/* IDLE */} + {appState === 'idle' && ( + + )} + + {/* ERROR */} + {appState === 'error' && ( +
    +
    + +
    +
    Analysis Failed
    +

    {error}

    +
    +
    + +
    + )} + + {/* LOADING */} + {appState === 'loading' && ( +
    +
    +
    +

    + Generating Intelligence Report +

    +

    + Scanning codebase, extracting semantics, and building your knowledge base. + This may take up to a minute for large repositories. +

    +
    + +
    + {LOADING_STEPS.map((step, idx) => { + const state = idx < currentStep ? 'completed' : idx === currentStep ? 'active' : 'pending'; + return ( +
    +
    + {state === 'completed' ? : idx + 1} +
    +
    +
    + {step.title} +
    +
    + {step.desc} +
    +
    +
    + ); + })} +
    +
    + )} + + {/* SUCCESS */} + {appState === 'success' && analysisResult && ( +
    + +
    + +
    +
    + )} + +
    +
    + ); +} diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000000000000000000000000000000000000..dfd5a769519415cbfce7a8c875e0147f1bda14d1 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,10 @@ +const API_BASE = (import.meta.env.VITE_API_URL || '').replace(/\/$/, ''); + +export function apiUrl(path) { + const normalized = path.startsWith('/') ? path : `/${path}`; + return `${API_BASE}${normalized}`; +} + +export function apiHeaders(extra = {}) { + return { ...extra }; +} diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0a28cf6d0bface7491720307a15f43267aa67e7c --- /dev/null +++ b/frontend/src/components/Dashboard.jsx @@ -0,0 +1,302 @@ +import React, { useState } from 'react'; +import { marked } from 'marked'; +import { + FileText, Shield, Layers, HelpCircle, HardDrive, + Terminal, Download, Eye, Play, ListFilter, + AlertTriangle, Network, ChevronDown, ChevronRight, Database +} from 'lucide-react'; +import GraphViewer from './GraphViewer'; +import RepositoryAssistant from './RepositoryAssistant'; +import KnowledgeExplorer from './KnowledgeExplorer'; +import { apiUrl } from '../api'; + +const TABS = [ + { id: 'report', icon: , label: 'Intelligence Report' }, + { id: 'summary', icon: , label: 'Knowledge Summary' }, + { id: 'profile', icon: , label: 'Repository Profile' }, + { id: 'graph', icon: , label: 'Architecture Graph' }, + { id: 'assistant', icon: , label: 'AI Assistant' }, + { id: 'knowledge', icon: , label: 'Knowledge Explorer' }, +]; + +export default function Dashboard({ analysisResult }) { + const [activeTab, setActiveTab] = useState('report'); + const [showFullJson, setShowFullJson] = useState(false); + + const { repo_id, project_name, data } = analysisResult; + const { report, profile, summary, graph } = data; + + const renderedReportHtml = React.useMemo(() => { + try { return { __html: marked.parse(report || '') }; } + catch (e) { return { __html: `

    Failed to render: ${e.message}

    ` }; } + }, [report]); + + const getDownloadUrl = (type) => apiUrl(`/api/download/${repo_id}/${type}`); + + return ( +
    + + {/* ── Header Bar ── */} + + + {/* ── Tab Navigation ── */} +
    + {TABS.map(t => ( + + ))} +
    + + {/* ── Tab Content ── */} +
    + + {/* ── REPORT ── */} + {activeTab === 'report' && ( +
    + )} + + {/* ── SUMMARY ── */} + {activeTab === 'summary' && ( +
    + {/* Elevator pitch */} +
    +
    + Project Purpose +
    +

    + "{summary.elevator_pitch}" +

    +
    + + {/* Core Features */} +
    +
    + Core Features +
    +
      + {summary.core_features?.map((f, i) => ( +
    • + + {f} +
    • + ))} +
    +
    + + {/* Workflows */} +
    +
    + User Workflows +
    +
      + {summary.main_workflows?.map((w, i) => ( +
    • + + {w} +
    • + ))} +
    +
    + + {/* Key Components */} +
    +
    + Key Components +
    +
      + {summary.key_components?.map((c, i) => ( +
    • + + {c} +
    • + ))} +
    +
    + + {/* Risks */} +
    +
    + Architectural Risks +
    +
      + {summary.key_risks?.map((r, i) => ( +
    • + + {r} +
    • + ))} +
    +
    + + {/* Start Points */} +
    +
    + Where to Start Reading Code +
    +
    + {summary.developer_start_points?.map((pt, i) => ( +
    + {pt} +
    + ))} +
    +
    +
    + )} + + {/* ── PROFILE ── */} + {activeTab === 'profile' && ( +
    +
    + +
    +
    Languages
    +
    + {profile.languages?.map((l, i) => ( + {l} + ))} +
    +
    + +
    +
    Frameworks
    +
    + {profile.frameworks?.map((f, i) => ( + {f} + ))} +
    +
    + +
    +
    Databases
    +
    + {profile.databases?.map((d, i) => ( + {d} + ))} +
    +
    + +
    +
    Security & Auth
    +
    + {profile.authentication_methods?.map((a, i) => ( + {a} + ))} +
    +
    + +
    + +
    +
    +

    API Endpoints

    +
    + {profile.api_endpoints?.map((ep, i) => ( +
    {ep}
    + ))} +
    +
    + +
    +

    Core Modules

    +
    + {profile.major_modules?.map((m, i) => ( + {m} + ))} +
    +
    +
    + +
    +
    +

    + Raw repository_profile.json +

    + +
    + {showFullJson && ( +
    {JSON.stringify(profile, null, 2)}
    + )} +
    +
    + )} + + {/* ── GRAPH ── */} + {activeTab === 'graph' && ( +
    +

    + Code Topology Graph +

    + +
    + )} + + {/* ── AI ASSISTANT ── */} + {activeTab === 'assistant' && ( + + )} + + {/* ── KNOWLEDGE EXPLORER ── */} + {activeTab === 'knowledge' && ( + + )} + +
    +
    + ); +} diff --git a/frontend/src/components/GraphViewer.jsx b/frontend/src/components/GraphViewer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9d32b0d709bae05475bdc5cdb8461e3f44aa9057 --- /dev/null +++ b/frontend/src/components/GraphViewer.jsx @@ -0,0 +1,439 @@ +import React, { useState, useMemo } from 'react'; +import { Network, Activity, ArrowRight, Zap, Target, BookOpen, FileCode } from 'lucide-react'; + +export default function GraphViewer({ graphData }) { + const [selectedNode, setSelectedNode] = useState(null); + const [activeFlow, setActiveFlow] = useState(null); + const [activePath, setActivePath] = useState(null); + const [hoveredNode, setHoveredNode] = useState(null); + + const width = 800; + const height = 500; + + // 1. Process nodes and assign architectural layers (X-coordinates) + const layoutData = useMemo(() => { + if (!graphData || !graphData.nodes) return { nodes: [], edges: [] }; + + const nodes = [...graphData.nodes]; + const edges = [...graphData.edges]; + + // Group nodes by type to layer them + const layers = { + entrypoint: [], + api: [], + module: [], + file: [], + database: [], + other: [] + }; + + nodes.forEach(node => { + // Normalise type checks + const type = (node.type || 'file').toLowerCase(); + if (layers[type]) { + layers[type].push(node); + } else { + layers['other'].push(node); + } + }); + + // Map layer to an X coordinate + const layerX = { + entrypoint: 100, + api: 240, + module: 440, + file: 440, // Combine modules and files in the middle + database: 660, + other: 550 + }; + + const nodePositions = {}; + + // Position nodes evenly in Y for each layer + Object.entries(layers).forEach(([type, layerNodes]) => { + const x = layerX[type] || 380; + const count = layerNodes.length; + + layerNodes.forEach((node, index) => { + // Distribute Y values evenly + const y = count === 1 ? height / 2 : ((index + 0.5) / count) * height; + nodePositions[node.id] = { + ...node, + x, + y, + color: getNodeColor(node.type), + }; + }); + }); + + return { + nodes: Object.values(nodePositions), + edges: edges.map(edge => ({ + ...edge, + sourceNode: nodePositions[edge.source], + targetNode: nodePositions[edge.target] + })).filter(edge => edge.sourceNode && edge.targetNode) + }; + }, [graphData]); + + // Color mapping based on node type + function getNodeColor(type) { + switch (type?.toLowerCase()) { + case 'entrypoint': + return '#f97316'; // Neon Orange + case 'api': + return '#10b981'; // Neon Emerald + case 'database': + return '#a855f7'; // Purple + case 'module': + return '#00f2fe'; // Neon Cyan + case 'file': + return '#3b82f6'; // Bright Blue + default: + return '#94a3b8'; // Slate + } + } + + // Check if link or node is highlighted by selected business flow or critical path + const highlightedNodeIds = useMemo(() => { + if (activeFlow) { + const flow = graphData.business_flows.find(f => f.flow_name === activeFlow); + return new Set(flow?.steps || []); + } + if (activePath) { + const path = graphData.critical_paths.find(p => p.path_name === activePath); + return new Set(path?.nodes || []); + } + return null; + }, [activeFlow, activePath, graphData]); + + const handleNodeClick = (node) => { + setSelectedNode(node); + }; + + const clearSelection = () => { + setSelectedNode(null); + setActiveFlow(null); + setActivePath(null); + }; + + if (!graphData || !graphData.nodes || graphData.nodes.length === 0) { + return ( +
    + No relationship graph metadata available. +
    + ); + } + + return ( +
    + + {/* Graph Visualiser SVG Panel */} +
    +
    + + + {/* Arrow Head markers for directional lines */} + + + + + + + + + {/* Link lines */} + {layoutData.edges.map((edge, idx) => { + const { sourceNode, targetNode } = edge; + + // Draw a smooth quadratic Bezier curve + const dx = targetNode.x - sourceNode.x; + const dy = targetNode.y - sourceNode.y; + const cx = sourceNode.x + dx / 2; + const cy = sourceNode.y + dy / 2 - (dx > 0 ? 30 : -30); // Curve offset + + const isEdgeHighlighted = highlightedNodeIds + ? highlightedNodeIds.has(edge.source) && highlightedNodeIds.has(edge.target) + : false; + + // Dim link lines if another node/path is hovered/active + let strokeOpacity = 0.25; + if (hoveredNode) { + const isConnected = edge.source === hoveredNode || edge.target === hoveredNode; + strokeOpacity = isConnected ? 0.8 : 0.05; + } else if (highlightedNodeIds) { + strokeOpacity = isEdgeHighlighted ? 0.9 : 0.05; + } + + return ( + + + {/* Subtle label hover */} + {isEdgeHighlighted && ( + + {edge.label} + + )} + + ); + })} + + {/* Node elements */} + {layoutData.nodes.map((node) => { + const isNodeHighlighted = highlightedNodeIds ? highlightedNodeIds.has(node.id) : true; + + // Calculate focus opacity + let nodeOpacity = 1; + if (hoveredNode) { + const isSelf = node.id === hoveredNode; + const isNeighbour = layoutData.edges.some( + e => (e.source === hoveredNode && e.target === node.id) || + (e.target === hoveredNode && e.source === node.id) + ); + nodeOpacity = (isSelf || isNeighbour) ? 1 : 0.15; + } else if (highlightedNodeIds) { + nodeOpacity = isNodeHighlighted ? 1 : 0.15; + } + + const isSelected = selectedNode?.id === node.id; + + return ( + handleNodeClick(node)} + onMouseEnter={() => setHoveredNode(node.id)} + onMouseLeave={() => setHoveredNode(null)} + className="node-circle" + > + {/* Outer ring for selected node */} + {isSelected && ( + + )} + {/* Colored center node */} + + {/* Node Title text */} + + {node.label} + + + ); + })} + + + {/* Graph Legend */} +
    +
    + + Entry Points +
    +
    + + APIs +
    +
    + + Modules / Code +
    +
    + + Databases / Storage +
    +
    +
    + + {/* Selected Node Details Card */} + {selectedNode ? ( +
    +
    +

    + {selectedNode.label} +

    + +
    +

    + Type: {selectedNode.type} +

    + {selectedNode.properties?.path && ( +

    + {selectedNode.properties.path} +

    + )} + {selectedNode.properties?.db_type && ( +

    + DB Technology: {selectedNode.properties.db_type} +

    + )} +
    + ) : ( +
    + Hover over nodes to inspect dependencies. Click a node to view properties. +
    + )} +
    + + {/* Sidebar: Business Flows, Critical Paths, Concepts lists */} +
    + + {/* Business Flows Panel */} +
    +
    + Business Flows +
    +

    + Sequence steps mapping end-to-end user operations. Click to trace path in the graph. +

    +
    + {graphData.business_flows?.map((flow, idx) => ( +
    { + setActiveFlow(activeFlow === flow.flow_name ? null : flow.flow_name); + setActivePath(null); + }} + style={{ + padding: '0.75rem', + borderRadius: 'var(--radius-sm)', + background: activeFlow === flow.flow_name ? 'rgba(0, 242, 254, 0.08)' : 'rgba(255, 255, 255, 0.02)', + border: `1px solid ${activeFlow === flow.flow_name ? 'var(--accent-cyan)' : 'var(--border-color)'}`, + cursor: 'pointer', + transition: 'all 0.2s' + }} + > +
    + {flow.flow_name} +
    +
    + {flow.description} +
    + + {activeFlow === flow.flow_name && ( +
    + {flow.steps.map((step, sIdx) => ( + + + {step.split('/').pop()} + + {sIdx < flow.steps.length - 1 && } + + ))} +
    + )} +
    + ))} +
    +
    + + {/* Critical Paths Panel */} +
    +
    + Critical Paths +
    +
    + {graphData.critical_paths?.map((path, idx) => ( +
    { + setActivePath(activePath === path.path_name ? null : path.path_name); + setActiveFlow(null); + }} + style={{ + padding: '0.75rem', + borderRadius: 'var(--radius-sm)', + background: activePath === path.path_name ? 'rgba(249, 115, 22, 0.08)' : 'rgba(255, 255, 255, 0.02)', + border: `1px solid ${activePath === path.path_name ? 'var(--accent-orange)' : 'var(--border-color)'}`, + cursor: 'pointer', + transition: 'all 0.2s' + }} + > +
    + {path.path_name} +
    +
    + {path.description} +
    +
    + ))} +
    +
    + + {/* Concepts list */} +
    +
    + Code Concepts +
    +
    + {graphData.concepts?.map((concept, idx) => ( +
    +
    + {concept.name} +
    +
    + {concept.description} +
    +
    + {concept.files.map((file, fIdx) => ( +
    + {file} +
    + ))} +
    +
    + ))} +
    +
    + +
    +
    + ); +} diff --git a/frontend/src/components/InputForm.jsx b/frontend/src/components/InputForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..59aec785bd9154e994e16f2b60955b7bd91b18de --- /dev/null +++ b/frontend/src/components/InputForm.jsx @@ -0,0 +1,272 @@ +import React, { useState, useRef } from 'react'; +import { + GitBranch, UploadCloud, Link2, Lock, FileArchive, + Cpu, Sparkles, ArrowRight +} from 'lucide-react'; + +const EXAMPLE_REPOS = [ + 'https://github.com/pallets/flask', + 'https://github.com/tiangolo/fastapi', + 'https://github.com/django/django', +]; + +export default function InputForm({ onSubmit, loading }) { + const [inputType, setInputType] = useState('url'); + const [repoUrl, setRepoUrl] = useState(''); + const [token, setToken] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [zipFile, setZipFile] = useState(null); + const [dragActive, setDragActive] = useState(false); + const fileInputRef = useRef(null); + + const handleDrag = (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(e.type === 'dragenter' || e.type === 'dragover'); + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + const file = e.dataTransfer.files?.[0]; + if (file?.name.endsWith('.zip')) setZipFile(file); + else if (file) alert('Only .zip archives are supported.'); + }; + + const handleFileChange = (e) => { + const file = e.target.files?.[0]; + if (file?.name.endsWith('.zip')) setZipFile(file); + else if (file) alert('Only .zip archives are supported.'); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (inputType === 'url') { + if (!repoUrl.trim()) return; + onSubmit({ type: 'url', url: repoUrl.trim(), token, apiKey: apiKey.trim() }); + } else { + if (!zipFile) return; + onSubmit({ type: 'zip', file: zipFile, apiKey: apiKey.trim() }); + } + }; + + const canSubmit = inputType === 'url' ? !!repoUrl.trim() : !!zipFile; + + return ( +
    + {/* Hero */} +
    +
    + + Powered by Gemini 2.5 Flash + RAG +
    +

    + Understand any codebase
    in seconds +

    +

    + Analyze repositories, map architecture, extract APIs, and build a semantic knowledge + base ready for AI agents — no setup required. +

    +
    + + {/* Card */} +
    + {/* Input Type Toggle */} +
    + + +
    + +
    + {inputType === 'url' ? ( + <> +
    + +
    + + setRepoUrl(e.target.value)} + disabled={loading} + id="repo-url-input" + /> +
    +
    + +
    + +
    + + setToken(e.target.value)} + disabled={loading} + id="github-token-input" + /> +
    +
    + +
    + +
    + + setApiKey(e.target.value)} + disabled={loading} + id="gemini-api-key-input" + /> +
    +
    + + {/* Example repos */} +
    +
    Try an example
    +
    + {EXAMPLE_REPOS.map(url => ( + + ))} +
    +
    + + ) : ( +
    + +
    fileInputRef.current?.click()} + id="zip-drop-zone" + > + + + {zipFile ? ( +
    +

    {zipFile.name}

    +

    + {(zipFile.size / (1024 * 1024)).toFixed(2)} MB · Click to change +

    +
    + ) : ( +
    +

    + Drop your .zip file here +

    +

    + or click to browse files +

    +
    + )} +
    +
    + )} + + +
    +
    + + {/* Features strip */} +
    + {[ + { icon: '🏗', title: 'Architecture Map', desc: 'Auto-detect patterns, entry points, and data flows' }, + { icon: '🔍', title: 'Semantic Search', desc: 'RAG-powered search across indexed code knowledge' }, + { icon: '🤖', title: 'AI Agents', desc: 'Multi-agent Q&A with confidence scoring' }, + ].map(f => ( +
    +
    {f.icon}
    +
    {f.title}
    +
    {f.desc}
    +
    + ))} +
    +
    + ); +} diff --git a/frontend/src/components/KnowledgeExplorer.jsx b/frontend/src/components/KnowledgeExplorer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7fe748c5c6ce5939d783732d196c11eadbefdae7 --- /dev/null +++ b/frontend/src/components/KnowledgeExplorer.jsx @@ -0,0 +1,454 @@ +import React, { useState, useEffect } from 'react'; +import { + Search, Database, MessageSquare, Zap, FileText, + ChevronRight, Clock, RefreshCw, Terminal, + Activity, BookOpen, Play, Layers, Shield, Globe +} from 'lucide-react'; +import { apiUrl } from '../api'; + +const CATEGORY_OPTIONS = [ + { value: '', label: 'All Categories' }, + { value: 'report', label: 'Report' }, + { value: 'summary', label: 'Summary' }, + { value: 'architecture', label: 'Architecture' }, + { value: 'authentication', label: 'Authentication' }, + { value: 'api', label: 'API Endpoints' }, + { value: 'dependency', label: 'Dependencies' }, + { value: 'business_flow', label: 'Business Flows' }, + { value: 'concept', label: 'Concepts' }, + { value: 'file', label: 'Source Files' }, +]; + +const TOOLS = [ + { name: 'repository_search', icon: , color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8', desc: 'Semantic search over indexed repo chunks' }, + { name: 'graph_query', icon: , color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD', desc: 'Query architecture graph, entry points, and flows' }, + { name: 'dependency_lookup', icon: , color: '#D97706', bg: '#FFFBEB', border: '#FDE68A', desc: 'Lookup packages, frameworks, and databases' }, + { name: 'file_reader', icon: , color: '#4338CA', bg: '#EEF2FF', border: '#C7D2FE', desc: 'Retrieve specific source file content' }, + { name: 'architecture_lookup', icon: , color: '#059669', bg: '#ECFDF5', border: '#A7F3D0', desc: 'Query architecture pattern and key modules' }, + { name: 'api_lookup', icon: , color: '#E11D48', bg: '#FFF1F2', border: '#FECDD3', desc: 'Lookup HTTP routes and authentication methods' }, +]; + +const CATEGORY_COLORS = { + report: { color: '#4338CA', bg: '#EEF2FF', border: '#C7D2FE' }, + summary: { color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8' }, + architecture: { color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD' }, + authentication: { color: '#E11D48', bg: '#FFF1F2', border: '#FECDD3' }, + api: { color: '#059669', bg: '#ECFDF5', border: '#A7F3D0' }, + dependency: { color: '#D97706', bg: '#FFFBEB', border: '#FDE68A' }, + business_flow: { color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8' }, + concept: { color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD' }, + file: { color: '#475569', bg: '#F8FAFC', border: '#CBD5E1' }, +}; + +const getCatStyle = (cat) => CATEGORY_COLORS[cat] || { color: '#64748B', bg: '#F8FAFC', border: '#CBD5E1' }; + +const TABS = [ + { id: 'search', icon: , label: 'Semantic Search' }, + { id: 'memory', icon: , label: 'Memory Inspector' }, + { id: 'conversations', icon: , label: 'Conversations' }, + { id: 'tools', icon: , label: 'Tool Catalog' }, +]; + +export default function KnowledgeExplorer({ repo_id, apiKey }) { + const [activeTab, setActiveTab] = useState('search'); + const [searchQuery, setSearchQuery] = useState(''); + const [searchCategory, setSearchCategory] = useState(''); + const [searchTopK, setSearchTopK] = useState(5); + const [searchResults, setSearchResults] = useState([]); + const [searchLoading, setSearchLoading] = useState(false); + const [searchLatency, setSearchLatency] = useState(null); + const [searchError, setSearchError] = useState(null); + + const [memoryInfo, setMemoryInfo] = useState(null); + const [memoryLoading, setMemoryLoading] = useState(false); + + const [conversations, setConversations] = useState([]); + const [convsLoading, setConvsLoading] = useState(false); + const [selectedSession, setSelectedSession] = useState(null); + const [sessionHistory, setSessionHistory] = useState([]); + const [historyLoading, setHistoryLoading] = useState(false); + + const headers = () => { + const h = { 'Content-Type': 'application/json' }; + if (apiKey) h['x-gemini-key'] = apiKey; + return h; + }; + + useEffect(() => { + if (activeTab === 'memory') loadMemory(); + if (activeTab === 'conversations') loadConversations(); + }, [activeTab]); + + const loadMemory = async () => { + setMemoryLoading(true); + try { + const r = await fetch(apiUrl(`/api/memory?repo_id=${repo_id}`)); + setMemoryInfo(await r.json()); + } catch { setMemoryInfo(null); } + setMemoryLoading(false); + }; + + const loadConversations = async () => { + setConvsLoading(true); + try { + const r = await fetch(apiUrl(`/api/conversations?repo_id=${repo_id}`)); + const d = await r.json(); + setConversations(d.sessions || []); + } catch { setConversations([]); } + setConvsLoading(false); + }; + + const loadSessionHistory = async (sessionId) => { + setHistoryLoading(true); + try { + const r = await fetch(apiUrl(`/api/conversations/${sessionId}`)); + const data = await r.json(); + if (!r.ok) throw new Error(data.detail); + setSessionHistory(data.history || []); + } catch { + setSessionHistory([]); + } finally { + setHistoryLoading(false); + } + }; + + const handleSelectSession = (sessionId) => { + if (selectedSession === sessionId) { + setSelectedSession(null); + setSessionHistory([]); + return; + } + setSelectedSession(sessionId); + loadSessionHistory(sessionId); + }; + + const handleSearch = async (e) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + setSearchLoading(true); + setSearchError(null); + setSearchResults([]); + setSearchLatency(null); + try { + const body = { repo_id, query: searchQuery.trim(), top_k: searchTopK }; + if (searchCategory) body.category = searchCategory; + const r = await fetch(apiUrl('/api/search'), { method: 'POST', headers: headers(), body: JSON.stringify(body) }); + const d = await r.json(); + if (!r.ok) throw new Error(d.detail || 'Search failed'); + setSearchResults(d.results || []); + setSearchLatency(d.latency_ms); + } catch (e) { + setSearchError(e.message); + } + setSearchLoading(false); + }; + + return ( +
    + {/* Sub-tabs */} +
    + {TABS.map(t => ( + + ))} +
    + + {/* ── SEMANTIC SEARCH ── */} + {activeTab === 'search' && ( +
    +
    + + Semantic Knowledge Search +
    +

    + Search across all indexed repository knowledge using natural language. + Results are ranked by cosine similarity score. +

    + +
    + setSearchQuery(e.target.value)} + id="semantic-search-input" + /> + + + +
    + + {searchError &&
    {searchError}
    } + + {searchLatency != null && !searchLoading && ( +
    + + {searchResults.length} results · {searchLatency}ms +
    + )} + +
    + {searchResults.map((r, i) => { + const cs = getCatStyle(r.metadata?.category); + const sim = r.similarity; + const fillColor = sim > 0.8 ? 'var(--accent-green)' : sim > 0.6 ? 'var(--accent-teal)' : 'var(--accent-amber)'; + return ( +
    +
    +
    + + {r.metadata?.category || 'general'} + + {r.metadata?.path && {r.metadata.path}} +
    +
    + {Math.round(sim * 100)}% +
    +
    +
    +
    +
    +
    {r.content}
    +
    + ); + })} + {!searchLoading && searchResults.length === 0 && searchLatency != null && ( +
    No results found. Try different search terms or select a different category.
    + )} +
    +
    + )} + + {/* ── MEMORY INSPECTOR ── */} + {activeTab === 'memory' && ( +
    +
    +
    + + Vector Memory Inspector +
    + +
    +

    + Inspect the repository's semantic knowledge stored in ChromaDB. Each chunk is + tagged with category metadata for precise retrieval. +

    + + {memoryLoading && ( +
    + + Loading memory info… +
    + )} + + {memoryInfo && !memoryLoading && ( +
    +
    +
    Indexed Chunks
    +
    {memoryInfo.indexed_chunks?.toLocaleString()}
    +
    ChromaDB documents
    +
    +
    +
    Repository ID
    +
    + {memoryInfo.repo_id} +
    +
    +
    +
    Storage Path
    +
    + {memoryInfo.storage_path} +
    +
    +
    + )} + +
    + + + The knowledge index contains chunked embeddings of the intelligence report, source files, + architecture concepts, API endpoints, dependencies, business flows, and concepts. + Embeddings are generated using Gemini text-embedding-004. + +
    + +
    +
    Indexed Categories
    +
    + {CATEGORY_OPTIONS.filter(o => o.value).map(o => { + const cs = getCatStyle(o.value); + return ( +
    + + {o.label} +
    + ); + })} +
    +
    +
    + )} + + {/* ── CONVERSATIONS ── */} + {activeTab === 'conversations' && ( +
    +
    +
    + + Conversation History +
    + +
    +

    + All AI Assistant chat sessions for this repository, stored in conversation memory. +

    + + {convsLoading && ( +
    + + Loading conversations… +
    + )} + + {!convsLoading && conversations.length === 0 && ( +
    + No conversations yet. Start chatting in the AI Assistant tab to see sessions here. +
    + )} + +
    + {conversations.map((s, idx) => ( +
    handleSelectSession(s.session_id)} + id={`conv-item-${idx}`} + > +
    + #{idx + 1} + {s.summary || 'Untitled Session'} + {s.message_count} msgs + +
    +
    + {s.session_id?.substring(0, 8)}… + {new Date(s.last_updated * 1000).toLocaleString()} +
    +
    + ))} +
    + + {selectedSession && ( +
    +
    + + Session Messages +
    + {historyLoading && ( +
    + + Loading message history… +
    + )} + {!historyLoading && sessionHistory.length === 0 && ( +
    No messages in this session.
    + )} + {!historyLoading && sessionHistory.length > 0 && ( +
    + {sessionHistory.map((msg, i) => ( +
    +
    + + {msg.role} + +
    +
    + {msg.content} +
    +
    + ))} +
    + )} +
    + )} +
    + )} + + {/* ── TOOL CATALOG ── */} + {activeTab === 'tools' && ( +
    +
    + + Tool Catalog (MCP-Ready) +
    +

    + These tools are available to the Planner Agent during orchestration. + Interfaces are compatible with Model Context Protocol (MCP) and Google ADK. +

    + +
    + {TOOLS.map(t => ( +
    +
    + {t.icon} +
    +
    +
    {t.name}
    +
    {t.desc}
    +
    + execute() +
    + ))} +
    + +
    + + + Each tool implements a BaseTool interface with name, + description, and execute(**kwargs). This design is + forward-compatible with Google ADK, LangGraph, CrewAI, and MCP server registration. + +
    +
    + )} +
    + ); +} diff --git a/frontend/src/components/RepoTree.jsx b/frontend/src/components/RepoTree.jsx new file mode 100644 index 0000000000000000000000000000000000000000..420e21adbde1442ed0e2b09f2b98a458733af910 --- /dev/null +++ b/frontend/src/components/RepoTree.jsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import { Folder, FolderOpen, FileCode, ChevronRight, ChevronDown, GitBranch } from 'lucide-react'; + +function TreeNode({ name, node }) { + const isDir = node.type === 'directory'; + const [open, setOpen] = useState(false); + + const fmtSize = (b) => { + if (!b) return ''; + if (b < 1024) return `${b}B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)}KB`; + return `${(b / (1024 * 1024)).toFixed(1)}MB`; + }; + + return ( +
    +
    isDir && setOpen(!open)} + style={{ paddingLeft: 0 }} + > + {isDir ? ( + <> + {open + ? + : + } + {open + ? + : + } + {name} + + ) : ( + <> + + + + {name} + + {node.size !== undefined && ( + + {fmtSize(node.size)} + + )} + + )} +
    + + {isDir && open && node.children && ( +
    + {Object.entries(node.children) + .sort(([, a], [, b]) => { + if (a.type === 'directory' && b.type !== 'directory') return -1; + if (a.type !== 'directory' && b.type === 'directory') return 1; + return 0; + }) + .map(([n, c]) => ) + } +
    + )} +
    + ); +} + +export default function RepoTree({ tree, title }) { + if (!tree || Object.keys(tree).length === 0) { + return ( +
    + No file tree available. +
    + ); + } + + return ( +
    +
    + + {title || 'Repository'} +
    +
    + {Object.entries(tree) + .sort(([, a], [, b]) => { + if (a.type === 'directory' && b.type !== 'directory') return -1; + if (a.type !== 'directory' && b.type === 'directory') return 1; + return 0; + }) + .map(([n, node]) => ) + } +
    +
    + ); +} diff --git a/frontend/src/components/RepositoryAssistant.jsx b/frontend/src/components/RepositoryAssistant.jsx new file mode 100644 index 0000000000000000000000000000000000000000..51d28d3184b3cb836ef324577c0a40f9cec7828a --- /dev/null +++ b/frontend/src/components/RepositoryAssistant.jsx @@ -0,0 +1,393 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { marked } from 'marked'; +import { + Send, Bot, User, Shield, Layers, Terminal, Play, + HelpCircle, Cpu, Award, Activity, ChevronRight +} from 'lucide-react'; +import { apiUrl } from '../api'; + +const SUGGESTED = [ + 'How does authentication work?', + 'What is the architecture pattern?', + 'Which API endpoints exist?', + 'What are the main dependencies?', + 'Where should I start reading code?', +]; + +const getAgentIcon = (name) => { + const map = { + PlannerAgent: , + ArchitectureAgent: , + SecurityAgent: , + ApiAgent: , + DependencyAgent: , + QualityAgent: , + OnboardingAgent: , + }; + return map[name] || ; +}; + +const getAgentLabel = (name) => name.replace('Agent', ' Agent'); + +const renderMarkdown = (text) => { + try { return { __html: marked.parse(text || '') }; } + catch (e) { return { __html: text || '' }; } +}; + +export default function RepositoryAssistant({ repo_id, apiKey }) { + const [messages, setMessages] = useState([{ + id: 'welcome', + role: 'assistant', + content: 'Hello! Ask me anything about this codebase — architecture, security, APIs, dependencies, or how to get started.', + timeline: null, planner_decision: null, confidence: null, + total_time_ms: null, references: [], retrieved_context: [] + }]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [sessionId, setSessionId] = useState(null); + const [selectedId, setSelectedId] = useState('welcome'); + const chatEndRef = useRef(null); + + useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, loading]); + + const sendMessage = async (text) => { + if (!text.trim() || loading) return; + setLoading(true); + const userMsg = { id: `u-${Date.now()}`, role: 'user', content: text.trim() }; + setMessages(prev => [...prev, userMsg]); + setSelectedId(userMsg.id); + setInput(''); + + const headers = { 'Content-Type': 'application/json' }; + if (apiKey) headers['x-gemini-key'] = apiKey; + + try { + const res = await fetch(apiUrl('/api/chat'), { + method: 'POST', + headers, + body: JSON.stringify({ + repo_id, + question: text.trim(), + session_id: sessionId, + }), + }); + const raw = await res.text(); + let data; + try { + data = raw ? JSON.parse(raw) : {}; + } catch { + throw new Error(raw || `Server error (${res.status})`); + } + if (!res.ok) throw new Error(data.detail || 'Agent orchestration failed.'); + + if (data.session_id) setSessionId(data.session_id); + + const assistMsg = { + id: `a-${Date.now()}`, + role: 'assistant', + content: data.answer || data.summary || (data.agent_contributions || []).join('\n\n') || 'No answer returned from agent.', + summary: data.summary, + agents_used: data.agents_used, + confidence: data.confidence, + references: data.references || [], + agent_contributions: data.agent_contributions, + planner_decision: data.planner_decision, + timeline: data.timeline, + total_time_ms: data.total_time_ms, + retrieved_context: data.retrieved_context || [], + rag_latency_ms: data.rag_latency_ms, + session_id: data.session_id, + }; + setMessages(prev => [...prev, assistMsg]); + setSelectedId(assistMsg.id); + } catch (err) { + const errMsg = { id: `e-${Date.now()}`, role: 'assistant', content: `**Error:** ${err.message}`, isError: true }; + setMessages(prev => [...prev, errMsg]); + } finally { + setLoading(false); + } + }; + + const handleSubmit = (e) => { e.preventDefault(); sendMessage(input); }; + + const activeMsg = messages.find(m => m.id === selectedId && m.role === 'assistant') + || [...messages].reverse().find(m => m.role === 'assistant' && !m.isError) + || messages[0]; + + const confLevel = (c) => c >= 0.75 ? 'high' : c >= 0.5 ? 'medium' : 'low'; + + return ( +
    + + {/* ── Chat Column ── */} +
    +
    +
    + + AI Repository Assistant +
    + + Multi-Agent Online + +
    + +
    + {/* Suggested questions — shown only when just the welcome message exists */} + {messages.length === 1 && ( +
    +
    🤖
    +

    + {messages[0].content} +

    +

    + Try one of these questions: +

    +
    + {SUGGESTED.map(q => ( + + ))} +
    +
    + )} + + {messages.slice(1).map(msg => ( +
    msg.role === 'assistant' && setSelectedId(msg.id)} + style={{ cursor: msg.role === 'assistant' ? 'pointer' : 'default' }} + > +
    + {msg.role === 'assistant' ? ( +
    + ) : ( + {msg.content} + )} +
    + +
    + {msg.role === 'assistant' && msg.confidence != null && ( + + {Math.round(msg.confidence * 100)}% confidence + + )} + {msg.role === 'assistant' && msg.total_time_ms && ( + + {msg.total_time_ms}ms + + )} + {msg.role === 'assistant' && msg.agents_used?.length > 0 && ( +
    + {msg.agents_used.map(a => ( + {getAgentIcon(a)} {getAgentLabel(a)} + ))} +
    + )} + {msg.role === 'assistant' && msg.retrieved_context?.length > 0 && ( + + {msg.retrieved_context.length} RAG chunks + + )} +
    + + {/* References */} + {msg.references?.length > 0 && ( +
    + {msg.references.map((r, i) => ( + {r} + ))} +
    + )} +
    + ))} + + {loading && ( +
    +
    + + + Orchestrating agents... + +
    +
    + )} +
    +
    + +
    +
    +