[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: ahmedkhaleel2004\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\njobs:\n  frontend:\n    runs-on: ubuntu-latest\n    env:\n      POSTGRES_URL: postgresql://postgres:password@localhost:5432/gitdiagram\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.30.0\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n      - name: Lint\n        run: pnpm lint\n      - name: Typecheck\n        run: pnpm typecheck\n      - name: Frontend tests\n        run: pnpm test\n      - name: Build\n        run: pnpm build\n\n  backend:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n      - name: Install uv\n        run: pip install uv==0.5.24\n      - name: Sync backend dependencies\n        run: cd backend && uv sync --frozen --no-install-project\n      - name: Import sanity\n        run: cd backend && uv run python -m compileall app\n      - name: Backend tests\n        run: cd backend && uv run pytest -q\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy to EC2\n\non:\n  # Disabled for automatic deploys after migrating to Next.js/Vercel backend.\n  # Kept as legacy workflow for historical reference / manual fallback use.\n  workflow_dispatch:\n    inputs:\n      confirm_legacy_ec2_deploy:\n        description: \"Type true to run legacy EC2 deploy\"\n        required: false\n        default: \"false\"\n\njobs:\n  deploy:\n    if: ${{ github.event.inputs.confirm_legacy_ec2_deploy == 'true' }}\n    runs-on: ubuntu-latest\n\n    # Add concurrency to prevent multiple deployments running at once\n    concurrency:\n      group: production\n      cancel-in-progress: true\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Deploy to EC2\n        uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5\n        with:\n          host: ${{ secrets.EC2_HOST }}\n          username: ubuntu\n          key: ${{ secrets.EC2_SSH_KEY }}\n          script: |\n            cd ~/gitdiagram\n            git fetch origin main\n            git checkout main\n            git pull --ff-only origin main\n            sudo chmod +x ./backend/nginx/setup_nginx.sh\n            sudo ./backend/nginx/setup_nginx.sh\n            chmod +x ./backend/deploy.sh\n            ./backend/deploy.sh\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# database\n/prisma/db.sqlite\n/prisma/db.sqlite-journal\ndb.sqlite\n\n# next.js\n/.next/\n/out/\nnext-env.d.ts\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables\n.env\n.env*.local\n.env-e\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n\n# idea files\n.idea\n\n__pycache__/\nvenv\nbackend/.venv\n.venv\n\n# vscode\n.vscode/\n"
  },
  {
    "path": ".nvmrc",
    "content": "22\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nGitDiagram converts GitHub repositories into interactive Mermaid diagrams using a 3-stage LLM pipeline. It's a full-stack app with a Next.js frontend (Vercel) and FastAPI backend (Railway).\n\n## Commands\n\n### Frontend (pnpm, Node 22)\n```bash\npnpm install          # Install dependencies\npnpm dev              # Start Next.js dev server (Turbo)\npnpm build            # Production build\npnpm lint             # ESLint\npnpm check            # Type-check + lint\npnpm test             # Vitest (frontend unit tests)\npnpm format:write     # Prettier formatting\n```\n\n### Backend (Python 3.12, uv)\n```bash\ncd backend\nuv sync --no-install-project   # Install pinned deps into .venv\nuv run pytest -q               # Run all backend tests\nuv run pytest tests/path/test_file.py::test_name  # Run single test\nuv run python -m compileall app  # Compile check\n```\n\n### Database\n```bash\npnpm db:push       # Push schema changes to Postgres\npnpm db:generate   # Generate Drizzle migration files\npnpm db:studio     # Open Drizzle Studio\n```\n\n### Local Development\n```bash\n# Start local Postgres\n./start-database.sh\n\n# Start FastAPI backend (Docker, recommended for production parity)\ndocker-compose up --build -d\ndocker-compose logs -f api\n\n# OR start FastAPI backend directly\npnpm dev:backend   # runs uvicorn via uv\n```\n\nTo route the Next.js frontend to a local FastAPI backend, set in `.env`:\n```\nNEXT_PUBLIC_USE_LEGACY_BACKEND=true\nNEXT_PUBLIC_API_DEV_URL=http://localhost:8000\n```\n\n## Architecture\n\n### Dual-Backend Design\nThe app supports two generation backends controlled by `NEXT_PUBLIC_USE_LEGACY_BACKEND`:\n- **FastAPI** (`backend/`) on Railway — primary production path\n- **Next.js Route Handlers** (`src/app/api/generate/`) — legacy fallback\n\nBoth expose the same SSE streaming API. The frontend (`src/features/diagram/api.ts`) routes to one or the other transparently.\n\n### 3-Stage LLM Pipeline\nDiagram generation uses three sequential OpenAI streaming calls:\n1. **Explanation** — understands the repo structure\n2. **Component Mapping** — maps components to file paths (XML tags extracted)\n3. **Mermaid Diagram** — generates Mermaid syntax with click events\n\nAfter stage 3, Mermaid syntax is validated (via `backend/scripts/validate_mermaid.mjs` or `src/server/generate/mermaid.ts`) and auto-fixed for up to 3 attempts if invalid. Prompts live in `backend/app/prompts.py` and `src/server/generate/prompts.ts`.\n\n### Streaming State Machine\nSSE events flow through states: `idle → started → explanation_* → mapping_* → diagram_* → diagram_fix_* → complete`\n\nFrontend: `src/hooks/diagram/useDiagramStream.ts` manages state.\nBackend: `backend/app/routers/generate.py` emits events.\n\n### GitHub Authentication Priority\n1. User-supplied PAT (from localStorage)\n2. `GITHUB_PAT` env var\n3. GitHub App (CLIENT_ID + PRIVATE_KEY + INSTALLATION_ID)\n\n### Caching\nGenerated diagrams are cached in PostgreSQL (`gitdiagram_diagram_cache` table, schema at `src/server/db/schema.ts`) keyed by `(username, repo)`. Server action: `src/app/_actions/cache.ts`.\n\n### Path Aliases\nTypeScript uses `~/*` → `./src/*`.\n\n## Key File Locations\n\n| Concern | Frontend | Backend |\n|---|---|---|\n| Prompts | `src/server/generate/prompts.ts` | `backend/app/prompts.py` |\n| GitHub client | `src/server/generate/github.ts` | `backend/app/services/github_service.py` |\n| OpenAI streaming | `src/server/generate/openai.ts` | `backend/app/services/openai_service.py` |\n| Mermaid validation | `src/server/generate/mermaid.ts` | `backend/app/services/mermaid_service.py` |\n| Stream endpoint | `src/app/api/generate/stream/` | `backend/app/routers/generate.py` |\n| DB schema | `src/server/db/schema.ts` | — |\n| Frontend API client | `src/features/diagram/api.ts` | — |\n| Main diagram hook | `src/hooks/useDiagram.ts` | — |\n\n## Environment Variables\n\nMinimum required (see `.env.example` for full list):\n- `POSTGRES_URL` — Neon serverless Postgres\n- `OPENAI_API_KEY` — used for all generation stages\n- `GITHUB_PAT` — optional but avoids GitHub rate limits\n- `OPENAI_MODEL` — single model for all three pipeline stages\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Ahmed Khaleel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Image](./docs/readme_img.png \"GitDiagram Front Page\")](https://gitdiagram.com/)\n\n![License](https://img.shields.io/badge/license-MIT-blue.svg)\n[![Kofi](https://img.shields.io/badge/Kofi-F16061.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/ahmedkhaleel2004)\n\n# GitDiagram\n\nTurn any GitHub repository into an interactive diagram for visualization in seconds.\n\nYou can also replace `hub` with `diagram` in any Github URL to access its diagram.\n\n## 🚀 Features\n\n- 👀 **Instant Visualization**: Convert any GitHub repository structure into a system design / architecture diagram\n- 🎨 **Interactivity**: Click on components to navigate directly to source files and relevant directories\n- ⚡ **Fast Generation**: Powered by OpenAI GPT-5.4 mini (configurable) for quick and accurate diagrams\n- 🖼️ **Export Options**: Copy Mermaid code or download the generated diagram as PNG\n- 🌐 **API Access**: Public API available for integration (WIP)\n\n## ⚙️ Tech Stack\n\n- **Frontend**: Next.js, TypeScript, Tailwind CSS, ShadCN\n- **Backend**: FastAPI (Railway), with Next.js Route Handlers available as a fallback path\n- **Database**: PostgreSQL (with Drizzle ORM)\n- **AI**: OpenAI GPT-5.4 mini (via `OPENAI_MODEL`)\n- **Deployment**: Vercel (frontend) + Railway (backend)\n- **CI/CD**: GitHub Actions\n- **Analytics**: PostHog, Api-Analytics\n\n## 🔄 Backend Architecture Update\n\nGitDiagram now runs its primary generation backend on FastAPI (deployed on Railway).\n\nFrontend calls are routed to the external backend by setting:\n- `NEXT_PUBLIC_USE_LEGACY_BACKEND=true`\n- `NEXT_PUBLIC_API_DEV_URL=https://<your-railway-domain>`\n\nThe variable name contains \"LEGACY\" for backward compatibility, but it now points to the primary external backend in production.\n\n## 🤔 About\n\nI created this because I wanted to contribute to open-source projects but quickly realized their codebases are too massive for me to dig through manually, so this helps me get started - but it's definitely got many more use cases!\n\nGiven any public (or private!) GitHub repository it generates diagrams in Mermaid.js with OpenAI's GPT-5.4 mini! (Previously Claude 3.5 Sonnet)\n\nI extract information from the file tree and README for details and interactivity (you can click components to be taken to relevant files and directories).\n\nMost of what you might call the \"processing\" of this app is done with prompt engineering and a 3-step streaming pipeline in the FastAPI backend under `/backend`.\n\n## 🔒 How to diagram private repositories\n\nYou can simply click on \"Private Repos\" in the header and follow the instructions by providing a GitHub personal access token with the `repo` scope.\n\nYou can also self-host this app locally (backend separated as well!) with the steps below.\n\n## 🛠️ Self-hosting / Local Development\n\n1. Clone the repository\n\n```bash\ngit clone https://github.com/ahmedkhaleel2004/gitdiagram.git\ncd gitdiagram\n```\n\n2. Install dependencies\n\n```bash\npnpm i\n```\n\n3. Set up environment variables (create .env)\n\n```bash\ncp .env.example .env\n```\n\nThen edit the `.env` file with your OpenAI API key and optional GitHub personal access token.\n\n4. Start local database\n\n```bash\nchmod +x start-database.sh\n./start-database.sh\n```\n\nWhen prompted to generate a random password, input yes.\nThe Postgres database will start in a container at `localhost:5432`\n\n5. Initialize the database schema\n\n```bash\npnpm db:push\n```\n\nYou can view and interact with the database using `pnpm db:studio`\n\n6. Run frontend\n\n```bash\npnpm dev\n```\n\nYou can now access the website at `localhost:3000`.\n\nRun FastAPI backend (recommended if you want parity with production):\n\n```bash\ndocker-compose up --build -d\ndocker-compose logs -f api\n```\n\nTo route frontend calls to the external backend, set:\n- `NEXT_PUBLIC_USE_LEGACY_BACKEND=true`\n- `NEXT_PUBLIC_API_DEV_URL=http://localhost:8000`\n\nFor a full machine setup guide (Node/Python/uv versions + verification), see `docs/dev-setup.md`.\n\nQuick validation:\n\n```bash\npnpm check\npnpm test\npnpm build\n```\n\nRailway backend docs: `docs/railway-backend.md`.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Acknowledgements\n\nShoutout to [Romain Courtois](https://github.com/cyclotruc)'s [Gitingest](https://gitingest.com/) for inspiration and styling\n\n## 🤔 Future Steps\n\n- Implement font-awesome icons in diagram\n- Implement an embedded feature like star-history.com but for diagrams. The diagram could also be updated progressively as commits are made.\n"
  },
  {
    "path": "backend/.python-version",
    "content": "3.12\n"
  },
  {
    "path": "backend/Dockerfile",
    "content": "FROM node:22.12.0-slim AS node-runtime\nFROM python:3.12-slim\n\nWORKDIR /app\nENV ENVIRONMENT=production\nENV PORT=8000\n\nCOPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node\nCOPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules\nRUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \\\n    ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx\n\n# Install uv inside the image\nCOPY --from=ghcr.io/astral-sh/uv:0.5.24 /uv /uvx /bin/\n\n# Copy dependency manifests first for better layer caching\nCOPY pyproject.toml uv.lock ./\nCOPY package.json ./\nCOPY package-lock.json ./\n\n# Install pinned runtime dependencies from uv lockfile\nRUN uv sync --frozen --no-dev --no-install-project\nRUN npm ci --omit=dev\n\n# Copy application code\nCOPY . .\nRUN chmod +x /app/entrypoint.sh && \\\n    sed -i 's/\\r$//' /app/entrypoint.sh && \\\n    ls -la /app/entrypoint.sh\n\nEXPOSE 8000\n\nCMD [\"/bin/bash\", \"/app/entrypoint.sh\"]\n"
  },
  {
    "path": "backend/app/__init__.py",
    "content": ""
  },
  {
    "path": "backend/app/core/errors.py",
    "content": "from __future__ import annotations\n\n\ndef api_error(code: str, message: str, **extra):\n    payload = {\n        \"ok\": False,\n        \"error\": message,\n        \"error_code\": code,\n    }\n    payload.update(extra)\n    return payload\n\n\ndef api_success(**data):\n    payload = {\"ok\": True}\n    payload.update(data)\n    return payload\n"
  },
  {
    "path": "backend/app/core/observability.py",
    "content": "from __future__ import annotations\n\nimport json\nimport logging\nimport time\n\n\nlogger = logging.getLogger(\"gitdiagram.api\")\nif not logger.handlers:\n    logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n\n\ndef log_event(event: str, **fields):\n    logger.info(\n        json.dumps(\n            {\n                \"event\": event,\n                **fields,\n            },\n            default=str,\n        )\n    )\n\n\nclass Timer:\n    def __init__(self):\n        self.start = time.perf_counter()\n\n    def elapsed_ms(self) -> int:\n        return int((time.perf_counter() - self.start) * 1000)\n"
  },
  {
    "path": "backend/app/main.py",
    "content": "import os\n\nfrom api_analytics.fastapi import Analytics\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\n\nfrom app.core.errors import api_success\nfrom app.core.observability import log_event\nfrom app.routers import generate\n\napp = FastAPI()\n\ncors_origins = os.getenv(\"CORS_ORIGINS\")\nif cors_origins:\n    origins = [origin.strip() for origin in cors_origins.split(\",\") if origin.strip()]\nelse:\n    origins = [\n        \"http://localhost:3000\",\n        \"https://gitdiagram.com\",\n        \"https://www.gitdiagram.com\",\n    ]\n\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=origins,\n    allow_credentials=True,\n    allow_methods=[\"GET\", \"POST\", \"OPTIONS\"],\n    allow_headers=[\"*\"],\n)\n\napi_analytics_key = os.getenv(\"API_ANALYTICS_KEY\")\nif api_analytics_key:\n    app.add_middleware(Analytics, api_key=api_analytics_key)\n\napp.include_router(generate.router)\n\n\n@app.get(\"/\")\nasync def root():\n    return api_success(message=\"Hello from GitDiagram API!\")\n\n\n@app.get(\"/healthz\")\nasync def healthz():\n    log_event(\"healthz.ok\")\n    return api_success(status=\"ok\")\n"
  },
  {
    "path": "backend/app/prompts.py",
    "content": "# This is our processing. This is where GitDiagram makes the magic happen\n# There is a lot of DETAIL we need to extract from the repository to produce detailed and accurate diagrams\n# I will immediately put out there that I'm trying to reduce costs. Theoretically, I could, for like 5x better accuracy, include most file content as well which would make for perfect diagrams, but thats too many tokens for my wallet, and would probably greatly increase generation time. (maybe a paid feature?)\n\n# THE PROCESS:\n\n# imagine it like this:\n# def prompt1(file_tree, readme) -> explanation of diagram\n# def prompt2(explanation, file_tree) -> maps relevant directories and files to parts of diagram for interactivity\n# def prompt3(explanation, map) -> Mermaid.js code\n\n# Note: Originally prompt1 and prompt2 were combined - but I tested it, and turns out mapping relevant dirs and files in one prompt along with generating detailed and accurate diagrams was difficult for Claude 3.5 Sonnet. It lost detail in the explanation and dedicated more \"effort\" to the mappings, so this is now its own prompt.\n\n# This is my first take at prompt engineering so if you have any ideas on optimizations please make an issue on the GitHub!\n\nSYSTEM_FIRST_PROMPT = \"\"\"\nYou are tasked with explaining to a principal software engineer how to draw the best and most accurate system design diagram / architecture of a given project. This explanation should be tailored to the specific project's purpose and structure. To accomplish this, you will be provided with two key pieces of information:\n\n1. The complete and entire file tree of the project including all directory and file names, which will be enclosed in <file_tree> tags in the users message.\n\n2. The README file of the project, which will be enclosed in <readme> tags in the users message.\n\nAnalyze these components carefully, as they will provide crucial information about the project's structure and purpose. Follow these steps to create an explanation for the principal software engineer:\n\n1. Identify the project type and purpose:\n   - Examine the file structure and README to determine if the project is a full-stack application, an open-source tool, a compiler, or another type of software imaginable.\n   - Look for key indicators in the README, such as project description, features, or use cases.\n\n2. Analyze the file structure:\n   - Pay attention to top-level directories and their names (e.g., \"frontend\", \"backend\", \"src\", \"lib\", \"tests\").\n   - Identify patterns in the directory structure that might indicate architectural choices (e.g., MVC pattern, microservices).\n   - Note any configuration files, build scripts, or deployment-related files.\n\n3. Examine the README for additional insights:\n   - Look for sections describing the architecture, dependencies, or technical stack.\n   - Check for any diagrams or explanations of the system's components.\n\n4. Based on your analysis, explain how to create a system design diagram that accurately represents the project's architecture. Include the following points:\n\n   a. Identify the main components of the system (e.g., frontend, backend, database, building, external services).\n   b. Determine the relationships and interactions between these components.\n   c. Highlight any important architectural patterns or design principles used in the project.\n   d. Include relevant technologies, frameworks, or libraries that play a significant role in the system's architecture.\n\n5. Provide guidelines for tailoring the diagram to the specific project type:\n   - For a full-stack application, emphasize the separation between frontend and backend, database interactions, and any API layers.\n   - For an open-source tool, focus on the core functionality, extensibility points, and how it integrates with other systems.\n   - For a compiler or language-related project, highlight the different stages of compilation or interpretation, and any intermediate representations.\n\n6. Instruct the principal software engineer to include the following elements in the diagram:\n   - Clear labels for each component\n   - Directional arrows to show data flow or dependencies\n   - Color coding or shapes to distinguish between different types of components\n\n7. NOTE: Emphasize the importance of being very detailed and capturing the essential architectural elements. Don't overthink it too much, simply separating the project into as many components as possible is best.\n\nPresent your explanation and instructions within <explanation> tags, ensuring that you tailor your advice to the specific project based on the provided file tree and README content.\n\"\"\"\n\n# - A legend explaining any symbols or abbreviations used\n# ^ removed since it was making the diagrams very long\n\n# just adding some clear separation between the prompts\n# ************************************************************\n# ************************************************************\n\nSYSTEM_SECOND_PROMPT = \"\"\"\nYou are tasked with mapping key components of a system design to their corresponding files and directories in a project's file structure. You will be provided with a detailed explanation of the system design/architecture and a file tree of the project.\n\nFirst, carefully read the system design explanation which will be enclosed in <explanation> tags in the users message.\n\nThen, examine the file tree of the project which will be enclosed in <file_tree> tags in the users message.\n\nYour task is to analyze the system design explanation and identify key components, modules, or services mentioned. Then, try your best to map these components to what you believe could be their corresponding directories and files in the provided file tree.\n\nGuidelines:\n1. Focus on major components described in the system design.\n2. Look for directories and files that clearly correspond to these components.\n3. Include both directories and specific files when relevant.\n4. If a component doesn't have a clear corresponding file or directory, simply dont include it in the map.\n\nNow, provide your final answer in the following format:\n\n<component_mapping>\n1. [Component Name]: [File/Directory Path]\n2. [Component Name]: [File/Directory Path]\n[Continue for all identified components]\n</component_mapping>\n\nRemember to be as specific as possible in your mappings, only use what is given to you from the file tree, and to strictly follow the components mentioned in the explanation. \n\"\"\"\n\n# ❌ BELOW IS A REMOVED SECTION FROM THE ABOVE PROMPT USED FOR CLAUDE 3.5 SONNET\n# Before providing your final answer, use the <scratchpad> to think through your process:\n# 1. List the key components identified in the system design.\n# 2. For each component, brainstorm potential corresponding directories or files.\n# 3. Verify your mappings by double-checking the file tree.\n\n# <scratchpad>\n# [Your thought process here]\n# </scratchpad>\n\n# just adding some clear separation between the prompts\n# ************************************************************\n# ************************************************************\n\nSYSTEM_THIRD_PROMPT = \"\"\"\nYou are a principal software engineer tasked with creating a system design diagram using Mermaid.js based on a detailed explanation. Your goal is to accurately represent the architecture and design of the project as described in the explanation.\n\nThe detailed explanation of the design will be enclosed in <explanation> tags in the users message.\n\nAlso, sourced from the explanation, as a bonus, a few of the identified components have been mapped to their paths in the project file tree, whether it is a directory or file which will be enclosed in <component_mapping> tags in the users message.\n\nTo create the Mermaid.js diagram:\n\n1. Carefully read and analyze the provided design explanation.\n2. Identify the main components, services, and their relationships within the system.\n3. Determine the appropriate Mermaid.js diagram type to use (e.g., flowchart, sequence diagram, class diagram, architecture, etc.) based on the nature of the system described.\n4. Create the Mermaid.js code to represent the design, ensuring that:\n   a. All major components are included\n   b. Relationships between components are clearly shown\n   c. The diagram accurately reflects the architecture described in the explanation\n   d. The layout is logical and easy to understand\n\nGuidelines for diagram components and relationships:\n- Use appropriate shapes for different types of components (e.g., rectangles for services, cylinders for databases, etc.)\n- Use clear and concise labels for each component\n- Show the direction of data flow or dependencies using arrows\n- Group related components together if applicable\n- Include any important notes or annotations mentioned in the explanation\n- Just follow the explanation. It will have everything you need.\n\nIMPORTANT!!: Please orient and draw the diagram as vertically as possible. You must avoid long horizontal lists of nodes and sections!\n\nYou must include click events for components of the diagram that have been specified in the provided <component_mapping>:\n- Do not try to include the full url. This will be processed by another program afterwards. All you need to do is include the path.\n- For example:\n  - This is a correct click event: `click Example \"app/example.js\"`\n  - This is an incorrect click event: `click Example \"https://github.com/username/repo/blob/main/app/example.js\"`\n- Do this for as many components as specified in the component mapping, include directories and files.\n  - If you believe the component contains files and is a directory, include the directory path.\n  - If you believe the component references a specific file, include the file path.\n- Make sure to include the full path to the directory or file exactly as specified in the component mapping.\n- It is very important that you do this for as many files as possible. The more the better.\n\n- IMPORTANT: THESE PATHS ARE FOR CLICK EVENTS ONLY, these paths should not be included in the diagram's node's names. Only for the click events. Paths should not be seen by the user.\n\nYour output should be valid Mermaid.js code that can be rendered into a diagram.\n\nDo not include an init declaration such as `%%{init: {'key':'etc'}}%%`. This is handled externally. Just return the diagram code.\n\nYour response must strictly be just the Mermaid.js code, without any additional text or explanations.\nNo code fence or markdown ticks needed, simply return the Mermaid.js code.\n\nEnsure that your diagram adheres strictly to the given explanation, without adding or omitting any significant components or relationships. \n\nFor general direction, the provided example below is how you should structure your code:\n\n```mermaid\nflowchart TD \n    %% or graph TD, your choice\n\n    %% Global entities\n    A(\"Entity A\"):::external\n    %% more...\n\n    %% Subgraphs and modules\n    subgraph \"Layer A\"\n        A1(\"Module A\"):::example\n        %% more modules...\n        %% inner subgraphs if needed...\n    end\n\n    %% more subgraphs, modules, etc...\n\n    %% Connections\n    A -->|\"relationship\"| B\n    %% and a lot more...\n\n    %% Click Events\n    click A1 \"example/example.js\"\n    %% and a lot more...\n\n    %% Styles\n    classDef frontend %%...\n    %% and a lot more...\n```\n\nEXTREMELY Important notes on syntax!!! (PAY ATTENTION TO THIS):\n- Make sure to add colour to the diagram!!! This is extremely critical.\n- In Mermaid.js syntax, we cannot include special characters for nodes without being inside quotes! For example: `EX[/api/process (Backend)]:::api` and `API -->|calls Process()| Backend` are two examples of syntax errors. They should be `EX[\"/api/process (Backend)\"]:::api` and `API -->|\"calls Process()\"| Backend` respectively. Notice the quotes. This is extremely important. Make sure to include quotes for any string that contains special characters.\n- In Mermaid.js syntax, you cannot apply a class style directly within a subgraph declaration. For example: `subgraph \"Frontend Layer\":::frontend` is a syntax error. However, you can apply them to nodes within the subgraph. For example: `Example[\"Example Node\"]:::frontend` is valid, and `class Example1,Example2 frontend` is valid.\n- In Mermaid.js syntax, there cannot be spaces in the relationship label names. For example: `A -->| \"example relationship\" | B` is a syntax error. It should be `A -->|\"example relationship\"| B` \n- In Mermaid.js syntax, you cannot give subgraphs an alias like nodes. For example: `subgraph A \"Layer A\"` is a syntax error. It should be `subgraph \"Layer A\"` \n\"\"\"\n# ^^^ note: ive generated a few diagrams now and claude still writes incorrect mermaid code sometimes. in the future, refer to those generated diagrams and add important instructions to the prompt above to avoid those mistakes. examples are best.\n\n# e. A legend is included\n# ^ removed since it was making the diagrams very long\n\nSYSTEM_FIX_MERMAID_PROMPT = \"\"\"\nYou are a Mermaid syntax repair specialist.\n\nYou will receive:\n- <mermaid_code>...</mermaid_code>\n- <parser_error>...</parser_error>\n- <explanation>...</explanation>\n- <component_mapping>...</component_mapping>\n\nTask:\n- Fix Mermaid syntax errors while preserving the original diagram meaning.\n- Keep all click events that map to repository paths.\n- Keep diagram mostly vertical.\n- Return Mermaid code only.\n\nRules:\n- No markdown code fences.\n- No extra commentary.\n- Ensure final output is syntactically valid Mermaid.\n\"\"\"\n"
  },
  {
    "path": "backend/app/routers/generate.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport re\nfrom typing import Any\n\nfrom fastapi import APIRouter, Request\nfrom fastapi.responses import JSONResponse, StreamingResponse\nfrom pydantic import BaseModel, Field, ValidationError\n\nfrom app.core.observability import Timer, log_event\nfrom app.prompts import (\n    SYSTEM_FIRST_PROMPT,\n    SYSTEM_FIX_MERMAID_PROMPT,\n    SYSTEM_SECOND_PROMPT,\n    SYSTEM_THIRD_PROMPT,\n)\nfrom app.services.github_service import GitHubService\nfrom app.services.mermaid_service import format_validation_feedback, validate_mermaid_syntax\nfrom app.services.model_config import get_model\nfrom app.services.openai_service import OpenAIService\nfrom app.services.pricing import estimate_text_token_cost_usd\n\nrouter = APIRouter(prefix=\"/generate\", tags=[\"OpenAI\"])\n\nopenai_service = OpenAIService()\n\nMAX_MERMAID_FIX_ATTEMPTS = 3\nMULTI_STAGE_INPUT_MULTIPLIER = 2\nINPUT_OVERHEAD_TOKENS = 3000\nESTIMATED_OUTPUT_TOKENS = 8000\n\n\nclass GenerateRequest(BaseModel):\n    username: str = Field(min_length=1)\n    repo: str = Field(min_length=1)\n    api_key: str | None = Field(default=None, min_length=1)\n    github_pat: str | None = Field(default=None, min_length=1)\n\n\ndef _sse_message(payload: dict[str, Any]) -> str:\n    return f\"data: {json.dumps(payload)}\\n\\n\"\n\n\ndef _strip_mermaid_code_fences(text: str) -> str:\n    return text.replace(\"```mermaid\", \"\").replace(\"```\", \"\").strip()\n\n\ndef _extract_component_mapping(response: str) -> str:\n    start_tag = \"<component_mapping>\"\n    end_tag = \"</component_mapping>\"\n    start_index = response.find(start_tag)\n    end_index = response.find(end_tag)\n    if start_index == -1 or end_index == -1:\n        return response\n    return response[start_index:end_index]\n\n\ndef process_click_events(diagram: str, username: str, repo: str, branch: str) -> str:\n    click_pattern = r'click ([^\\s\"]+)\\s+\"([^\"]+)\"'\n\n    def replace_path(match: re.Match[str]) -> str:\n        node_id = match.group(1)\n        trimmed_path = match.group(2).strip().strip(\"\\\"'\")\n        is_file = \".\" in trimmed_path and not trimmed_path.endswith(\"/\")\n        path_type = \"blob\" if is_file else \"tree\"\n        full_url = f\"https://github.com/{username}/{repo}/{path_type}/{branch}/{trimmed_path}\"\n        return f'click {node_id} \"{full_url}\"'\n\n    return re.sub(click_pattern, replace_path, diagram)\n\n\ndef _parse_request_payload(payload: Any) -> tuple[GenerateRequest | None, str | None]:\n    try:\n        parsed = GenerateRequest.model_validate(payload)\n        return parsed, None\n    except ValidationError:\n        return None, \"Invalid request payload.\"\n\n\ndef _get_github_data(username: str, repo: str, github_pat: str | None):\n    github_service = GitHubService(pat=github_pat)\n    return github_service.get_github_data(username, repo)\n\n\nasync def _estimate_repo_input_tokens(\n    model: str,\n    file_tree: str,\n    readme: str,\n    api_key: str | None = None,\n) -> int:\n    try:\n        return await openai_service.count_input_tokens(\n            model=model,\n            system_prompt=SYSTEM_FIRST_PROMPT,\n            data={\n                \"file_tree\": file_tree,\n                \"readme\": readme,\n            },\n            api_key=api_key,\n            reasoning_effort=\"medium\",\n        )\n    except Exception:\n        return openai_service.estimate_tokens(f\"{file_tree}\\n{readme}\")\n\n\n@router.post(\"/cost\")\nasync def get_generation_cost(request: Request):\n    timer = Timer()\n    try:\n        payload = await request.json()\n        parsed, error = _parse_request_payload(payload)\n        if not parsed:\n            return JSONResponse(\n                {\n                    \"ok\": False,\n                    \"error\": error,\n                    \"error_code\": \"VALIDATION_ERROR\",\n                }\n            )\n\n        github_data = _get_github_data(parsed.username, parsed.repo, parsed.github_pat)\n        model = get_model()\n        base_input_tokens = await _estimate_repo_input_tokens(\n            model=model,\n            file_tree=github_data.file_tree,\n            readme=github_data.readme,\n            api_key=parsed.api_key,\n        )\n        estimated_input_tokens = (\n            base_input_tokens * MULTI_STAGE_INPUT_MULTIPLIER + INPUT_OVERHEAD_TOKENS\n        )\n        estimated_output_tokens = ESTIMATED_OUTPUT_TOKENS\n        cost_usd, pricing_model, pricing = estimate_text_token_cost_usd(\n            model=model,\n            input_tokens=estimated_input_tokens,\n            output_tokens=estimated_output_tokens,\n        )\n\n        response_payload = {\n            \"ok\": True,\n            \"cost\": f\"${cost_usd:.2f} USD\",\n            \"model\": model,\n            \"pricing_model\": pricing_model,\n            \"estimated_input_tokens\": estimated_input_tokens,\n            \"estimated_output_tokens\": estimated_output_tokens,\n            \"pricing\": {\n                \"input_per_million_usd\": pricing.input_per_million_usd,\n                \"output_per_million_usd\": pricing.output_per_million_usd,\n            },\n        }\n        log_event(\n            \"generate.cost.success\",\n            username=parsed.username,\n            repo=parsed.repo,\n            elapsed_ms=timer.elapsed_ms(),\n            model=model,\n        )\n        return JSONResponse(response_payload)\n    except Exception as exc:\n        log_event(\n            \"generate.cost.failed\",\n            elapsed_ms=timer.elapsed_ms(),\n            error=str(exc),\n        )\n        return JSONResponse(\n            {\n                \"ok\": False,\n                \"error\": str(exc) if isinstance(exc, Exception) else \"Failed to estimate generation cost.\",\n                \"error_code\": \"COST_ESTIMATION_FAILED\",\n            }\n        )\n\n\n@router.post(\"/stream\")\nasync def generate_stream(request: Request):\n    try:\n        payload = await request.json()\n    except Exception:\n        return JSONResponse(\n            {\n                \"ok\": False,\n                \"error\": \"Invalid request payload.\",\n                \"error_code\": \"VALIDATION_ERROR\",\n            },\n            status_code=400,\n        )\n\n    parsed, error = _parse_request_payload(payload)\n    if not parsed:\n        return JSONResponse(\n            {\n                \"ok\": False,\n                \"error\": error,\n                \"error_code\": \"VALIDATION_ERROR\",\n            },\n            status_code=400,\n        )\n\n    async def event_generator():\n        timer = Timer()\n\n        def send(payload: dict[str, Any]) -> str:\n            return _sse_message(payload)\n\n        try:\n            github_data = _get_github_data(parsed.username, parsed.repo, parsed.github_pat)\n            model = get_model()\n            token_count = await _estimate_repo_input_tokens(\n                model=model,\n                file_tree=github_data.file_tree,\n                readme=github_data.readme,\n                api_key=parsed.api_key,\n            )\n\n            yield send(\n                {\n                    \"status\": \"started\",\n                    \"message\": \"Starting generation process...\",\n                }\n            )\n\n            if token_count > 50000 and token_count < 195000 and not parsed.api_key:\n                yield send(\n                    {\n                        \"status\": \"error\",\n                        \"error\": \"File tree and README combined exceeds token limit (50,000). This repository is too large for free generation. Provide your own OpenAI API key to continue.\",\n                        \"error_code\": \"API_KEY_REQUIRED\",\n                    }\n                )\n                return\n\n            if token_count > 195000:\n                yield send(\n                    {\n                        \"status\": \"error\",\n                        \"error\": \"Repository is too large (>195k tokens) for analysis. Try a smaller repo.\",\n                        \"error_code\": \"TOKEN_LIMIT_EXCEEDED\",\n                    }\n                )\n                return\n\n            yield send(\n                {\n                    \"status\": \"explanation_sent\",\n                    \"message\": f\"Sending explanation request to {model}...\",\n                }\n            )\n            await asyncio.sleep(0.08)\n            yield send(\n                {\n                    \"status\": \"explanation\",\n                    \"message\": \"Analyzing repository structure...\",\n                }\n            )\n\n            explanation = \"\"\n            async for chunk in openai_service.stream_completion(\n                model=model,\n                system_prompt=SYSTEM_FIRST_PROMPT,\n                data={\n                    \"file_tree\": github_data.file_tree,\n                    \"readme\": github_data.readme,\n                },\n                api_key=parsed.api_key,\n                reasoning_effort=\"medium\",\n            ):\n                explanation += chunk\n                yield send({\"status\": \"explanation_chunk\", \"chunk\": chunk})\n\n            yield send(\n                {\n                    \"status\": \"mapping_sent\",\n                    \"message\": f\"Sending component mapping request to {model}...\",\n                }\n            )\n            await asyncio.sleep(0.08)\n            yield send(\n                {\n                    \"status\": \"mapping\",\n                    \"message\": \"Creating component mapping...\",\n                }\n            )\n\n            full_mapping_response = \"\"\n            async for chunk in openai_service.stream_completion(\n                model=model,\n                system_prompt=SYSTEM_SECOND_PROMPT,\n                data={\n                    \"explanation\": explanation,\n                    \"file_tree\": github_data.file_tree,\n                },\n                api_key=parsed.api_key,\n                reasoning_effort=\"low\",\n            ):\n                full_mapping_response += chunk\n                yield send({\"status\": \"mapping_chunk\", \"chunk\": chunk})\n\n            component_mapping = _extract_component_mapping(full_mapping_response)\n\n            yield send(\n                {\n                    \"status\": \"diagram_sent\",\n                    \"message\": f\"Sending diagram generation request to {model}...\",\n                }\n            )\n            await asyncio.sleep(0.08)\n            yield send(\n                {\n                    \"status\": \"diagram\",\n                    \"message\": \"Generating diagram...\",\n                }\n            )\n\n            mermaid_code = \"\"\n            async for chunk in openai_service.stream_completion(\n                model=model,\n                system_prompt=SYSTEM_THIRD_PROMPT,\n                data={\n                    \"explanation\": explanation,\n                    \"component_mapping\": component_mapping,\n                },\n                api_key=parsed.api_key,\n                reasoning_effort=\"low\",\n            ):\n                mermaid_code += chunk\n                yield send({\"status\": \"diagram_chunk\", \"chunk\": chunk})\n\n            candidate_diagram = _strip_mermaid_code_fences(mermaid_code)\n            validation_result = await asyncio.to_thread(\n                validate_mermaid_syntax,\n                candidate_diagram,\n            )\n            had_fix_loop = not validation_result.valid\n\n            if not validation_result.valid:\n                parser_feedback = format_validation_feedback(validation_result)\n                yield send(\n                    {\n                        \"status\": \"diagram_fixing\",\n                        \"message\": \"Diagram generated. Mermaid syntax validation failed, starting auto-fix loop...\",\n                        \"parser_error\": parser_feedback,\n                    }\n                )\n\n            attempt = 1\n            while (not validation_result.valid) and attempt <= MAX_MERMAID_FIX_ATTEMPTS:\n                parser_feedback = format_validation_feedback(validation_result)\n                yield send(\n                    {\n                        \"status\": \"diagram_fix_attempt\",\n                        \"message\": f\"Fixing Mermaid syntax (attempt {attempt}/{MAX_MERMAID_FIX_ATTEMPTS})...\",\n                        \"fix_attempt\": attempt,\n                        \"fix_max_attempts\": MAX_MERMAID_FIX_ATTEMPTS,\n                        \"parser_error\": parser_feedback,\n                    }\n                )\n\n                repaired_diagram = \"\"\n                async for chunk in openai_service.stream_completion(\n                    model=model,\n                    system_prompt=SYSTEM_FIX_MERMAID_PROMPT,\n                    data={\n                        \"mermaid_code\": candidate_diagram,\n                        \"parser_error\": parser_feedback,\n                        \"explanation\": explanation,\n                        \"component_mapping\": component_mapping,\n                    },\n                    api_key=parsed.api_key,\n                    reasoning_effort=\"low\",\n                ):\n                    repaired_diagram += chunk\n                    yield send(\n                        {\n                            \"status\": \"diagram_fix_chunk\",\n                            \"chunk\": chunk,\n                            \"fix_attempt\": attempt,\n                            \"fix_max_attempts\": MAX_MERMAID_FIX_ATTEMPTS,\n                        }\n                    )\n\n                candidate_diagram = _strip_mermaid_code_fences(repaired_diagram)\n                yield send(\n                    {\n                        \"status\": \"diagram_fix_validating\",\n                        \"message\": f\"Validating Mermaid syntax after attempt {attempt}/{MAX_MERMAID_FIX_ATTEMPTS}...\",\n                        \"fix_attempt\": attempt,\n                        \"fix_max_attempts\": MAX_MERMAID_FIX_ATTEMPTS,\n                    }\n                )\n                validation_result = await asyncio.to_thread(\n                    validate_mermaid_syntax,\n                    candidate_diagram,\n                )\n                attempt += 1\n\n            if not validation_result.valid:\n                yield send(\n                    {\n                        \"status\": \"error\",\n                        \"error\": \"Generated Mermaid remained syntactically invalid after auto-fix attempts. Please retry generation.\",\n                        \"error_code\": \"MERMAID_SYNTAX_UNRESOLVED\",\n                        \"parser_error\": format_validation_feedback(validation_result),\n                    }\n                )\n                return\n\n            processed_diagram = process_click_events(\n                candidate_diagram,\n                parsed.username,\n                parsed.repo,\n                github_data.default_branch,\n            )\n\n            if had_fix_loop:\n                yield send(\n                    {\n                        \"status\": \"diagram_fixing\",\n                        \"message\": \"Mermaid syntax validated. Finalizing diagram output...\",\n                    }\n                )\n\n            yield send(\n                {\n                    \"status\": \"complete\",\n                    \"diagram\": processed_diagram,\n                    \"explanation\": explanation,\n                    \"mapping\": component_mapping,\n                }\n            )\n            log_event(\n                \"generate.stream.success\",\n                username=parsed.username,\n                repo=parsed.repo,\n                elapsed_ms=timer.elapsed_ms(),\n                model=model,\n            )\n        except Exception as exc:\n            yield send(\n                {\n                    \"status\": \"error\",\n                    \"error\": str(exc) if isinstance(exc, Exception) else \"Streaming generation failed.\",\n                    \"error_code\": \"STREAM_FAILED\",\n                }\n            )\n            log_event(\n                \"generate.stream.failed\",\n                username=parsed.username,\n                repo=parsed.repo,\n                elapsed_ms=timer.elapsed_ms(),\n                error=str(exc),\n            )\n\n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Content-Type\": \"text/event-stream; charset=utf-8\",\n            \"Cache-Control\": \"no-cache, no-transform\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n"
  },
  {
    "path": "backend/app/services/github_service.py",
    "content": "from __future__ import annotations\n\nimport base64\nimport os\nfrom datetime import UTC, datetime, timedelta\nfrom dataclasses import dataclass\n\nimport jwt\nimport requests\n\nEXCLUDED_PATTERNS = [\n    \"node_modules/\",\n    \"vendor/\",\n    \"venv/\",\n    \".min.\",\n    \".pyc\",\n    \".pyo\",\n    \".pyd\",\n    \".so\",\n    \".dll\",\n    \".class\",\n    \".jpg\",\n    \".jpeg\",\n    \".png\",\n    \".gif\",\n    \".ico\",\n    \".svg\",\n    \".ttf\",\n    \".woff\",\n    \".webp\",\n    \"__pycache__/\",\n    \".cache/\",\n    \".tmp/\",\n    \"yarn.lock\",\n    \"poetry.lock\",\n    \"*.log\",\n    \".vscode/\",\n    \".idea/\",\n]\n\n\n@dataclass(frozen=True)\nclass GithubData:\n    default_branch: str\n    file_tree: str\n    readme: str\n\n\ndef _should_include_file(path: str) -> bool:\n    lower_path = path.lower()\n    return not any(pattern in lower_path for pattern in EXCLUDED_PATTERNS)\n\n\ndef _fetch_json(url: str, headers: dict[str, str], not_found_message: str) -> dict:\n    response = requests.get(url, headers=headers, timeout=30)\n    if response.status_code == 404:\n        raise ValueError(not_found_message)\n    if not response.ok:\n        raise ValueError(f\"GitHub request failed ({response.status_code}): {response.text}\")\n    return response.json()\n\n\nclass GitHubService:\n    def __init__(self, pat: str | None = None):\n        # Request-provided PAT (or env PAT) has top priority.\n        self.github_token = (pat or os.getenv(\"GITHUB_PAT\") or \"\").strip() or None\n\n        # GitHub App credentials are used when PAT is unavailable.\n        self.client_id = (os.getenv(\"GITHUB_CLIENT_ID\") or \"\").strip() or None\n        self.private_key = (os.getenv(\"GITHUB_PRIVATE_KEY\") or \"\").strip() or None\n        self.installation_id = (os.getenv(\"GITHUB_INSTALLATION_ID\") or \"\").strip() or None\n\n        self.access_token: str | None = None\n        self.token_expires_at: datetime | None = None\n\n    def _normalize_private_key(self) -> str:\n        if not self.private_key:\n            raise ValueError(\"Missing GITHUB_PRIVATE_KEY.\")\n        # Supports both literal newlines and escaped \\\\n forms.\n        return self.private_key.replace(\"\\\\n\", \"\\n\")\n\n    def _can_use_app_auth(self) -> bool:\n        return bool(self.client_id and self.private_key and self.installation_id)\n\n    def _generate_jwt(self) -> str:\n        if not self.client_id:\n            raise ValueError(\"Missing GITHUB_CLIENT_ID.\")\n        now = int(datetime.now(UTC).timestamp())\n        payload = {\n            \"iat\": now,\n            \"exp\": now + (10 * 60),\n            \"iss\": self.client_id,\n        }\n        return jwt.encode(payload, self._normalize_private_key(), algorithm=\"RS256\")\n\n    def _get_installation_token(self) -> str:\n        if self.access_token and self.token_expires_at and self.token_expires_at > datetime.now(UTC):\n            return self.access_token\n\n        if not self.installation_id:\n            raise ValueError(\"Missing GITHUB_INSTALLATION_ID.\")\n\n        jwt_token = self._generate_jwt()\n        response = requests.post(\n            f\"https://api.github.com/app/installations/{self.installation_id}/access_tokens\",\n            headers={\n                \"Authorization\": f\"Bearer {jwt_token}\",\n                \"Accept\": \"application/vnd.github+json\",\n                \"X-GitHub-Api-Version\": \"2022-11-28\",\n            },\n            timeout=30,\n        )\n        if not response.ok:\n            raise ValueError(\n                f\"GitHub app token request failed ({response.status_code}): {response.text}\"\n            )\n\n        payload = response.json()\n        token = payload.get(\"token\")\n        if not isinstance(token, str) or not token:\n            raise ValueError(\"GitHub app token response missing token.\")\n\n        expires_at_raw = payload.get(\"expires_at\")\n        if isinstance(expires_at_raw, str):\n            try:\n                expires_at = datetime.fromisoformat(expires_at_raw.replace(\"Z\", \"+00:00\"))\n            except ValueError:\n                expires_at = datetime.now(UTC) + timedelta(minutes=50)\n        else:\n            expires_at = datetime.now(UTC) + timedelta(minutes=50)\n\n        self.access_token = token\n        self.token_expires_at = expires_at\n        return token\n\n    def _get_headers(self) -> dict[str, str]:\n        if self.github_token:\n            return {\n                \"Authorization\": f\"token {self.github_token}\",\n                \"Accept\": \"application/vnd.github+json\",\n            }\n\n        if self._can_use_app_auth():\n            token = self._get_installation_token()\n            return {\n                \"Authorization\": f\"Bearer {token}\",\n                \"Accept\": \"application/vnd.github+json\",\n                \"X-GitHub-Api-Version\": \"2022-11-28\",\n            }\n\n        return {\"Accept\": \"application/vnd.github+json\"}\n\n    def get_default_branch(self, username: str, repo: str) -> str:\n        data = _fetch_json(\n            f\"https://api.github.com/repos/{username}/{repo}\",\n            self._get_headers(),\n            \"Repository not found.\",\n        )\n        return data.get(\"default_branch\") or \"main\"\n\n    def get_github_file_paths_as_list(self, username: str, repo: str, branch: str) -> str:\n        data = _fetch_json(\n            f\"https://api.github.com/repos/{username}/{repo}/git/trees/{branch}?recursive=1\",\n            self._get_headers(),\n            \"Could not fetch repository file tree.\",\n        )\n        paths = [\n            item.get(\"path\")\n            for item in (data.get(\"tree\") or [])\n            if isinstance(item.get(\"path\"), str) and _should_include_file(item[\"path\"])\n        ]\n        if not paths:\n            raise ValueError(\n                \"Could not fetch repository file tree. Repository might be empty or inaccessible.\"\n            )\n        return \"\\n\".join(paths)\n\n    def get_github_readme(self, username: str, repo: str) -> str:\n        data = _fetch_json(\n            f\"https://api.github.com/repos/{username}/{repo}/readme\",\n            self._get_headers(),\n            \"No README found for the specified repository.\",\n        )\n        content = data.get(\"content\")\n        if not isinstance(content, str) or not content:\n            raise ValueError(\"No README found for the specified repository.\")\n\n        encoding = data.get(\"encoding\")\n        if encoding == \"base64\":\n            return base64.b64decode(content).decode(\"utf-8\")\n        return content\n\n    def get_github_data(self, username: str, repo: str) -> GithubData:\n        default_branch = self.get_default_branch(username, repo)\n        file_tree = self.get_github_file_paths_as_list(username, repo, default_branch)\n        readme = self.get_github_readme(username, repo)\n        return GithubData(\n            default_branch=default_branch,\n            file_tree=file_tree,\n            readme=readme,\n        )\n"
  },
  {
    "path": "backend/app/services/mermaid_service.py",
    "content": "from __future__ import annotations\n\nimport json\nimport subprocess\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True)\nclass MermaidValidationResult:\n    valid: bool\n    message: str | None = None\n    line: int | None = None\n    token: str | None = None\n    expected: list[str] | None = None\n\n\ndef normalize_parser_message(message: str | None) -> str:\n    if not message:\n        return \"Mermaid syntax is invalid and could not be parsed.\"\n\n    if \"sanitize is not a function\" in message or \"__TURBOPACK__imported__module\" in message:\n        return \"Mermaid parser runtime failed in server context (sanitizer issue).\"\n\n    return message\n\n\ndef validate_mermaid_syntax(diagram: str) -> MermaidValidationResult:\n    try:\n        proc = subprocess.run(\n            [\"node\", \"scripts/validate_mermaid.mjs\"],\n            input=diagram,\n            text=True,\n            capture_output=True,\n            check=False,\n        )\n    except Exception as exc:\n        return MermaidValidationResult(\n            valid=False,\n            message=normalize_parser_message(str(exc)),\n        )\n\n    if proc.returncode != 0:\n        message = proc.stderr.strip() or proc.stdout.strip() or \"Mermaid validation failed.\"\n        return MermaidValidationResult(valid=False, message=normalize_parser_message(message))\n\n    try:\n        payload = json.loads(proc.stdout)\n    except json.JSONDecodeError:\n        return MermaidValidationResult(\n            valid=False,\n            message=normalize_parser_message(\"Mermaid validator returned invalid JSON.\"),\n        )\n\n    valid = bool(payload.get(\"valid\"))\n    message = payload.get(\"message\")\n    normalized_message = (\n        normalize_parser_message(message)\n        if not valid\n        else (message if isinstance(message, str) else None)\n    )\n\n    return MermaidValidationResult(\n        valid=valid,\n        message=normalized_message,\n        line=payload.get(\"line\"),\n        token=payload.get(\"token\"),\n        expected=payload.get(\"expected\"),\n    )\n\n\ndef format_validation_feedback(result: MermaidValidationResult) -> str:\n    if result.valid:\n        return \"No syntax errors found.\"\n\n    details = [f\"message: {result.message or 'unknown parse error'}\"]\n    if isinstance(result.line, int):\n        details.append(f\"line: {result.line}\")\n    if result.token:\n        details.append(f\"token: {result.token}\")\n    if result.expected:\n        details.append(f\"expected: {', '.join(result.expected)}\")\n\n    return \"\\n\".join(details)\n"
  },
  {
    "path": "backend/app/services/model_config.py",
    "content": "from __future__ import annotations\n\nimport os\n\nDEFAULT_MODEL = \"gpt-5.4-mini\"\n\n\ndef get_model() -> str:\n    model = os.getenv(\"OPENAI_MODEL\", \"\").strip()\n    return model or DEFAULT_MODEL\n"
  },
  {
    "path": "backend/app/services/openai_service.py",
    "content": "from __future__ import annotations\n\nfrom typing import AsyncGenerator, Literal\nimport math\nimport os\n\nfrom dotenv import load_dotenv\nfrom openai import AsyncOpenAI\n\nfrom app.utils.format_message import format_user_message\n\nload_dotenv()\n\nReasoningEffort = Literal[\"low\", \"medium\", \"high\"]\n\n\nclass OpenAIService:\n    def __init__(self):\n        self.default_api_key = os.getenv(\"OPENAI_API_KEY\")\n\n    def _resolve_api_key(self, override_api_key: str | None = None) -> str:\n        api_key = (override_api_key or self.default_api_key or \"\").strip()\n        if not api_key:\n            raise ValueError(\n                \"Missing OpenAI API key. Set OPENAI_API_KEY or provide api_key in request.\"\n            )\n        return api_key\n\n    @staticmethod\n    def estimate_tokens(text: str) -> int:\n        # Mirrors Next.js fallback heuristic.\n        return math.ceil(len(text) / 4)\n\n    @staticmethod\n    def _build_input(system_prompt: str, user_prompt: str) -> list[dict]:\n        return [\n            {\"role\": \"system\", \"content\": system_prompt},\n            {\"role\": \"user\", \"content\": user_prompt},\n        ]\n\n    @staticmethod\n    def _create_client(api_key: str) -> AsyncOpenAI:\n        # Keep explicit config local to this service.\n        return AsyncOpenAI(\n            api_key=api_key,\n            max_retries=2,\n            timeout=600,\n        )\n\n    async def stream_completion(\n        self,\n        *,\n        model: str,\n        system_prompt: str,\n        data: dict[str, str | None],\n        api_key: str | None = None,\n        reasoning_effort: ReasoningEffort | None = None,\n        max_output_tokens: int | None = None,\n    ) -> AsyncGenerator[str, None]:\n        user_prompt = format_user_message(data)\n        resolved_api_key = self._resolve_api_key(api_key)\n        payload: dict = {\n            \"model\": model,\n            \"stream\": True,\n            \"input\": self._build_input(system_prompt, user_prompt),\n        }\n        if reasoning_effort:\n            payload[\"reasoning\"] = {\"effort\": reasoning_effort}\n        if max_output_tokens:\n            payload[\"max_output_tokens\"] = max_output_tokens\n\n        client = self._create_client(resolved_api_key)\n        stream = await client.responses.create(**payload)\n        try:\n            async for event in stream:\n                if event.type == \"response.output_text.delta\":\n                    delta = getattr(event, \"delta\", None)\n                    if isinstance(delta, str) and delta:\n                        yield delta\n                    continue\n\n                if event.type == \"error\":\n                    message = getattr(event, \"message\", None) or \"OpenAI stream failed.\"\n                    raise ValueError(str(message))\n        finally:\n            await stream.close()\n            await client.close()\n\n    async def count_input_tokens(\n        self,\n        *,\n        model: str,\n        system_prompt: str,\n        data: dict[str, str | None],\n        api_key: str | None = None,\n        reasoning_effort: ReasoningEffort | None = None,\n    ) -> int:\n        user_prompt = format_user_message(data)\n        resolved_api_key = self._resolve_api_key(api_key)\n        payload: dict = {\n            \"model\": model,\n            \"input\": self._build_input(system_prompt, user_prompt),\n        }\n        if reasoning_effort:\n            payload[\"reasoning\"] = {\"effort\": reasoning_effort}\n\n        client = self._create_client(resolved_api_key)\n        try:\n            response = await client.responses.input_tokens.count(**payload)\n            input_tokens = getattr(response, \"input_tokens\", None)\n            if not isinstance(input_tokens, int):\n                raise ValueError(\"OpenAI input token count returned invalid payload.\")\n            return input_tokens\n        finally:\n            await client.close()\n"
  },
  {
    "path": "backend/app/services/pricing.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\n\nDEFAULT_PRICING_MODEL = \"gpt-5.4-mini\"\n\n\n@dataclass(frozen=True)\nclass ModelPricing:\n    input_per_million_usd: float\n    output_per_million_usd: float\n\n\nMODEL_PRICING: dict[str, ModelPricing] = {\n    \"gpt-5.4\": ModelPricing(input_per_million_usd=2.5, output_per_million_usd=15.0),\n    \"gpt-5.4-pro\": ModelPricing(input_per_million_usd=30.0, output_per_million_usd=180.0),\n    \"gpt-5.4-mini\": ModelPricing(input_per_million_usd=0.75, output_per_million_usd=4.5),\n    \"gpt-5.4-nano\": ModelPricing(input_per_million_usd=0.2, output_per_million_usd=1.25),\n    \"gpt-5.2\": ModelPricing(input_per_million_usd=1.75, output_per_million_usd=14.0),\n    \"gpt-5.2-chat-latest\": ModelPricing(\n        input_per_million_usd=1.75,\n        output_per_million_usd=14.0,\n    ),\n    \"gpt-5.2-codex\": ModelPricing(input_per_million_usd=1.75, output_per_million_usd=14.0),\n    \"gpt-5.2-pro\": ModelPricing(input_per_million_usd=21.0, output_per_million_usd=168.0),\n    \"gpt-5.1\": ModelPricing(input_per_million_usd=1.25, output_per_million_usd=10.0),\n    \"gpt-5\": ModelPricing(input_per_million_usd=1.25, output_per_million_usd=10.0),\n    \"gpt-5-mini\": ModelPricing(input_per_million_usd=0.25, output_per_million_usd=2.0),\n    \"gpt-5-nano\": ModelPricing(input_per_million_usd=0.05, output_per_million_usd=0.4),\n    \"o4-mini\": ModelPricing(input_per_million_usd=1.1, output_per_million_usd=4.4),\n}\n\nDEFAULT_PRICING = MODEL_PRICING[DEFAULT_PRICING_MODEL]\n\n\ndef _strip_date_snapshot_suffix(model: str) -> str:\n    import re\n\n    return re.sub(r\"-\\d{4}-\\d{2}-\\d{2}$\", \"\", model, flags=re.IGNORECASE)\n\n\ndef resolve_pricing_model(model: str) -> str:\n    normalized = model.strip().lower()\n    if normalized in MODEL_PRICING:\n        return normalized\n\n    without_date = _strip_date_snapshot_suffix(normalized)\n    if without_date in MODEL_PRICING:\n        return without_date\n\n    if without_date.startswith(\"gpt-5.4-pro\"):\n        return \"gpt-5.4-pro\"\n    if without_date.startswith(\"gpt-5.4-mini\"):\n        return \"gpt-5.4-mini\"\n    if without_date.startswith(\"gpt-5.4-nano\"):\n        return \"gpt-5.4-nano\"\n    if without_date.startswith(\"gpt-5.4\"):\n        return \"gpt-5.4\"\n    if without_date.startswith(\"gpt-5.2-pro\"):\n        return \"gpt-5.2-pro\"\n    if without_date.startswith(\"gpt-5.2-codex\"):\n        return \"gpt-5.2-codex\"\n    if without_date.startswith(\"gpt-5.2-chat\"):\n        return \"gpt-5.2-chat-latest\"\n    if without_date.startswith(\"gpt-5.2\"):\n        return \"gpt-5.2\"\n    if without_date.startswith(\"gpt-5.1\"):\n        return \"gpt-5.1\"\n    if without_date.startswith(\"gpt-5-mini\"):\n        return \"gpt-5-mini\"\n    if without_date.startswith(\"gpt-5-nano\"):\n        return \"gpt-5-nano\"\n    if without_date.startswith(\"gpt-5\"):\n        return \"gpt-5\"\n    if without_date.startswith(\"o4-mini\"):\n        return \"o4-mini\"\n\n    return DEFAULT_PRICING_MODEL\n\n\ndef estimate_text_token_cost_usd(\n    model: str,\n    input_tokens: int,\n    output_tokens: int,\n) -> tuple[float, str, ModelPricing]:\n    pricing_model = resolve_pricing_model(model)\n    pricing = MODEL_PRICING.get(pricing_model, DEFAULT_PRICING)\n    input_cost = (max(input_tokens, 0) / 1_000_000) * pricing.input_per_million_usd\n    output_cost = (max(output_tokens, 0) / 1_000_000) * pricing.output_per_million_usd\n    return (input_cost + output_cost, pricing_model, pricing)\n"
  },
  {
    "path": "backend/app/utils/format_message.py",
    "content": "def format_user_message(data: dict[str, str | None]) -> str:\n    parts: list[str] = []\n    for key, value in data.items():\n        if isinstance(value, str):\n            parts.append(f\"<{key}>\\n{value}\\n</{key}>\")\n    return \"\\n\".join(parts)\n"
  },
  {
    "path": "backend/deploy.sh",
    "content": "#!/bin/bash\n\n# Exit on any error\nset -e\n\n# Navigate to project directory\ncd ~/gitdiagram\n\n# Pull latest changes\ngit pull --ff-only origin main\n\n# Build and restart containers with production environment\ndocker-compose down\nENVIRONMENT=production docker-compose up --build -d\n\n# Remove unused images\ndocker image prune -f\n\n# Show logs only if --logs flag is passed\nif [ \"$1\" == \"--logs\" ]; then\n    docker-compose logs -f\nelse\n    echo \"Deployment complete! Run 'docker-compose logs -f' to view logs\"\nfi\n"
  },
  {
    "path": "backend/entrypoint.sh",
    "content": "#!/bin/bash\n\nset -euo pipefail\n\nENVIRONMENT=\"${ENVIRONMENT:-production}\"\nHOST=\"${HOST:-0.0.0.0}\"\nPORT=\"${PORT:-8000}\"\nWEB_CONCURRENCY=\"${WEB_CONCURRENCY:-2}\"\n\necho \"Current ENVIRONMENT: ${ENVIRONMENT}\"\necho \"Binding to ${HOST}:${PORT}\"\n\nif [ \"${ENVIRONMENT}\" = \"development\" ]; then\n    echo \"Starting in development mode with hot reload...\"\n    exec uv run --no-dev uvicorn app.main:app --host \"${HOST}\" --port \"${PORT}\" --reload\nfi\n\necho \"Starting in production mode...\"\nexec uv run --no-dev uvicorn app.main:app \\\n    --host \"${HOST}\" \\\n    --port \"${PORT}\" \\\n    --timeout-keep-alive 300 \\\n    --workers \"${WEB_CONCURRENCY}\" \\\n    --loop uvloop \\\n    --http httptools\n"
  },
  {
    "path": "backend/nginx/api.conf",
    "content": "server {\n    server_name api.gitdiagram.com;\n\n    # Block requests with no valid Host header\n    if ($host !~ ^(api.gitdiagram.com)$) {\n        return 444;\n    }\n\n    # Strictly allow only GET, POST, and OPTIONS requests for the specified paths (defined in my fastapi app)\n    location ~ ^/(generate(/cost|/stream)?|healthz|)?$ {\n        if ($request_method !~ ^(GET|POST|OPTIONS)$) {\n            return 444;\n        }\n\n        proxy_pass http://127.0.0.1:8000;\n        include proxy_params;\n        proxy_redirect off;\n\n        # Disable buffering for SSE\n        proxy_buffering off;\n        proxy_cache off;\n        \n        # Required headers for SSE\n        proxy_set_header Connection '';\n        proxy_http_version 1.1;\n    }\n\n    # Return 444 for everything else (no response, just close connection)\n    location / {\n        return 444;\n        # keep access log on\n    }\n\n    # Add timeout settings\n    proxy_connect_timeout 300;\n    proxy_send_timeout 300;\n    proxy_read_timeout 300;\n    send_timeout 300;\n\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate /etc/letsencrypt/live/api.gitdiagram.com/fullchain.pem; # managed by Certbot\n    ssl_certificate_key /etc/letsencrypt/live/api.gitdiagram.com/privkey.pem; # managed by Certbot\n    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot\n\n}\nserver {\n    if ($host = api.gitdiagram.com) {\n        return 301 https://$host$request_uri;\n    } # managed by Certbot\n\n\n    listen 80;\n    server_name api.gitdiagram.com;\n    return 404; # managed by Certbot\n}\n"
  },
  {
    "path": "backend/nginx/setup_nginx.sh",
    "content": "#!/bin/bash\n\n# Exit on any error\nset -e\n\n# Check if running as root\nif [ \"$EUID\" -ne 0 ]; then \n    echo \"Please run as root or with sudo\"\n    exit 1\nfi\n\n# Copy Nginx configuration\necho \"Copying Nginx configuration...\"\ncp \"$(dirname \"$0\")/api.conf\" /etc/nginx/sites-available/api\nln -sf /etc/nginx/sites-available/api /etc/nginx/sites-enabled/\n\n# Test Nginx configuration\necho \"Testing Nginx configuration...\"\nnginx -t\n\n# Reload Nginx\necho \"Reloading Nginx...\"\nsystemctl reload nginx\n\necho \"Nginx configuration updated successfully!\" "
  },
  {
    "path": "backend/package.json",
    "content": "{\n  \"name\": \"gitdiagram-backend-mermaid-validator\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"dompurify\": \"3.3.1\",\n    \"jsdom\": \"28.1.0\",\n    \"mermaid\": \"11.12.3\"\n  }\n}\n"
  },
  {
    "path": "backend/pyproject.toml",
    "content": "[project]\nname = \"gitdiagram-backend\"\nversion = \"0.1.0\"\ndescription = \"FastAPI backend for GitDiagram\"\nrequires-python = \">=3.12,<3.13\"\ndependencies = [\n  \"aiohttp==3.13.3\",\n  \"api-analytics==1.2.7\",\n  \"fastapi==0.128.8\",\n  \"openai==2.21.0\",\n  \"PyJWT[crypto]==2.11.0\",\n  \"python-dotenv==1.2.1\",\n  \"requests==2.32.5\",\n  \"tiktoken==0.12.0\",\n  \"uvicorn[standard]==0.40.0\",\n]\n\n[dependency-groups]\ndev = [\n  \"httpx==0.28.1\",\n  \"pytest==8.3.4\",\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.uv]\npackage = false\n"
  },
  {
    "path": "backend/scripts/validate_mermaid.mjs",
    "content": "import { createRequire } from \"node:module\";\nimport { stdin, stdout, stderr } from \"node:process\";\n\nimport DOMPurify from \"dompurify\";\n\nconst require = createRequire(import.meta.url);\nlet mermaidInstance = null;\nlet initialized = false;\nlet domPurifyPatched = false;\n\nfunction ensureDomPurifyPatched() {\n  if (domPurifyPatched) return;\n\n  try {\n    const domPurify = DOMPurify;\n    if (typeof domPurify === \"function\" && typeof domPurify.sanitize !== \"function\") {\n      const { JSDOM } = require(\"jsdom\");\n      const domWindow = new JSDOM(\"<!doctype html><html><body></body></html>\").window;\n      const domPurifyInstance = domPurify(domWindow);\n      Object.assign(domPurify, domPurifyInstance);\n    }\n  } catch {\n    // Best effort patch.\n  } finally {\n    domPurifyPatched = true;\n  }\n}\n\nasync function getMermaid() {\n  if (mermaidInstance) return mermaidInstance;\n  ensureDomPurifyPatched();\n  const mermaidModule = await import(\"mermaid\");\n  mermaidInstance = mermaidModule.default;\n  return mermaidInstance;\n}\n\nasync function ensureMermaidInitialized() {\n  const mermaid = await getMermaid();\n  if (initialized) return mermaid;\n  mermaid.initialize({\n    startOnLoad: false,\n    securityLevel: \"loose\",\n  });\n  initialized = true;\n  return mermaid;\n}\n\nasync function readStdin() {\n  let data = \"\";\n  for await (const chunk of stdin) {\n    data += chunk;\n  }\n  return data;\n}\n\nfunction normalizeError(error) {\n  return {\n    valid: false,\n    message: error?.message || \"Mermaid syntax is invalid and could not be parsed.\",\n    line: error?.hash?.line,\n    token: error?.hash?.token,\n    expected: error?.hash?.expected,\n  };\n}\n\nasync function main() {\n  try {\n    const diagram = (await readStdin()).toString();\n    const mermaid = await ensureMermaidInitialized();\n    await mermaid.parse(diagram);\n    stdout.write(JSON.stringify({ valid: true }));\n  } catch (error) {\n    stdout.write(JSON.stringify(normalizeError(error)));\n  }\n}\n\nmain().catch((error) => {\n  stderr.write(String(error?.message || error));\n  process.exit(1);\n});\n"
  },
  {
    "path": "backend/tests/conftest.py",
    "content": "from pathlib import Path\nimport sys\n\nBACKEND_ROOT = Path(__file__).resolve().parents[1]\nif str(BACKEND_ROOT) not in sys.path:\n    sys.path.insert(0, str(BACKEND_ROOT))\n"
  },
  {
    "path": "backend/tests/test_generate_router.py",
    "content": "import json\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom app.main import app\nfrom app.routers import generate\nfrom app.services.mermaid_service import MermaidValidationResult\n\nclient = TestClient(app)\n\n\ndef test_healthz_ok():\n    response = client.get(\"/healthz\")\n    assert response.status_code == 200\n    assert response.json() == {\"ok\": True, \"status\": \"ok\"}\n\n\ndef test_generate_cost_success(monkeypatch):\n    monkeypatch.setattr(\n        generate,\n        \"_get_github_data\",\n        lambda username, repo, github_pat=None: SimpleNamespace(\n            default_branch=\"main\",\n            file_tree=\"src/main.py\",\n            readme=\"# readme\",\n        ),\n    )\n    monkeypatch.setattr(generate, \"get_model\", lambda: \"gpt-5.4-mini\")\n\n    async def fake_count_input_tokens(*, model, system_prompt, data, api_key=None, reasoning_effort=None):\n        return 100\n\n    monkeypatch.setattr(generate.openai_service, \"count_input_tokens\", fake_count_input_tokens)\n\n    response = client.post(\n        \"/generate/cost\",\n        json={\"username\": \"acme\", \"repo\": \"demo\"},\n    )\n\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"ok\"] is True\n    assert data[\"cost\"].endswith(\"USD\")\n    assert data[\"model\"] == \"gpt-5.4-mini\"\n    assert data[\"pricing_model\"] == \"gpt-5.4-mini\"\n    assert \"estimated_input_tokens\" in data\n    assert \"estimated_output_tokens\" in data\n\n\ndef test_generate_cost_error(monkeypatch):\n    def fail_github_data(username, repo, github_pat=None):\n        raise ValueError(\"repo not found\")\n\n    monkeypatch.setattr(generate, \"_get_github_data\", fail_github_data)\n\n    response = client.post(\n        \"/generate/cost\",\n        json={\"username\": \"acme\", \"repo\": \"missing\"},\n    )\n\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"ok\"] is False\n    assert data[\"error_code\"] == \"COST_ESTIMATION_FAILED\"\n\n\ndef test_generate_stream_event_order_with_fix_loop(monkeypatch):\n    monkeypatch.setattr(\n        generate,\n        \"_get_github_data\",\n        lambda username, repo, github_pat=None: SimpleNamespace(\n            default_branch=\"main\",\n            file_tree=\"src/main.py\",\n            readme=\"# readme\",\n        ),\n    )\n    monkeypatch.setattr(generate, \"get_model\", lambda: \"gpt-5.4-mini\")\n\n    async def fake_estimate_repo_input_tokens(model, file_tree, readme, api_key=None):\n        return 1000\n\n    async def fake_stream_completion(*, model, system_prompt, data, api_key=None, reasoning_effort=None, max_output_tokens=None):\n        if \"explaining to a principal\" in system_prompt:\n            yield \"<explanation>Repo explanation</explanation>\"\n            return\n        if \"mapping key components\" in system_prompt:\n            yield \"<component_mapping>\"\n            yield \"1. API: src/main.py\"\n            yield \"</component_mapping>\"\n            return\n        if \"syntax repair specialist\" in system_prompt:\n            yield 'flowchart TD\\nA[\"API\"] --> B[\"Worker\"]\\nclick A \"src/main.py\"'\n            return\n        yield 'flowchart TD\\nA[\"API\"] --> B[\"Worker\"]\\nclick A \"src/main.py\"'\n\n    validation_results = iter(\n        [\n            MermaidValidationResult(valid=False, message=\"bad syntax\"),\n            MermaidValidationResult(valid=True),\n        ]\n    )\n\n    monkeypatch.setattr(generate, \"_estimate_repo_input_tokens\", fake_estimate_repo_input_tokens)\n    monkeypatch.setattr(generate.openai_service, \"stream_completion\", fake_stream_completion)\n    monkeypatch.setattr(generate, \"validate_mermaid_syntax\", lambda diagram: next(validation_results))\n\n    response = client.post(\n        \"/generate/stream\",\n        json={\"username\": \"acme\", \"repo\": \"demo\"},\n    )\n\n    assert response.status_code == 200\n    events = []\n    payloads = []\n    for block in response.text.split(\"\\n\\n\"):\n        if not block.startswith(\"data: \"):\n            continue\n        payload = json.loads(block[6:])\n        payloads.append(payload)\n        if \"status\" in payload:\n            events.append(payload[\"status\"])\n\n    assert \"started\" in events\n    assert \"explanation_sent\" in events\n    assert \"mapping_sent\" in events\n    assert \"diagram_sent\" in events\n    assert \"diagram_fixing\" in events\n    assert \"diagram_fix_attempt\" in events\n    assert \"diagram_fix_validating\" in events\n    assert events[-1] == \"complete\"\n    complete_payload = payloads[-1]\n    assert complete_payload[\"status\"] == \"complete\"\n    assert \"https://github.com/acme/demo/blob/main/src/main.py\" in complete_payload[\"diagram\"]\n\n\ndef test_modify_route_removed():\n    response = client.post(\"/modify\", json={})\n    assert response.status_code == 404\n"
  },
  {
    "path": "backend/tests/test_generate_utils.py",
    "content": "from app.routers.generate import process_click_events\n\n\ndef test_process_click_events_builds_blob_and_tree_links():\n    diagram = 'flowchart TD\\nclick Api \"src/api.ts\"\\nclick Core \"src/core\"'\n    output = process_click_events(diagram, \"u\", \"r\", \"main\")\n\n    assert 'click Api \"https://github.com/u/r/blob/main/src/api.ts\"' in output\n    assert 'click Core \"https://github.com/u/r/tree/main/src/core\"' in output\n"
  },
  {
    "path": "backend/tests/test_pricing.py",
    "content": "from app.services.pricing import estimate_text_token_cost_usd, resolve_pricing_model\n\n\ndef test_resolve_pricing_model_keeps_gpt_5_4_mini_on_its_own_tier():\n    assert resolve_pricing_model(\"gpt-5.4-mini\") == \"gpt-5.4-mini\"\n    assert resolve_pricing_model(\"gpt-5.4-mini-2026-03-17\") == \"gpt-5.4-mini\"\n\n\ndef test_estimate_text_token_cost_uses_gpt_5_4_mini_pricing():\n    cost_usd, pricing_model, pricing = estimate_text_token_cost_usd(\n        model=\"gpt-5.4-mini\",\n        input_tokens=1_000_000,\n        output_tokens=1_000_000,\n    )\n\n    assert pricing_model == \"gpt-5.4-mini\"\n    assert pricing.input_per_million_usd == 0.75\n    assert pricing.output_per_million_usd == 4.5\n    assert cost_usd == 5.25\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"src/styles/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"~/components\",\n    \"utils\": \"~/lib/utils\",\n    \"ui\": \"~/components/ui\",\n    \"lib\": \"~/lib\",\n    \"hooks\": \"~/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  api:\n    build: \n      context: ./backend\n      dockerfile: Dockerfile\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - ./backend:/app\n    env_file:\n      - .env\n    environment:\n      - ENVIRONMENT=${ENVIRONMENT:-development} # Default to development if not set\n    restart: unless-stopped\n"
  },
  {
    "path": "docs/dev-setup.md",
    "content": "# Local Development Setup\n\nThis project runs generation primarily through the FastAPI backend in `backend/` (Railway in production).\n\nNext.js Route Handlers under `/api/generate/*` remain available as an optional fallback path.\n\n## 1) Install tool versions\n\nRecommended versions:\n- Node.js: `22.x` (see `.nvmrc`)\n- pnpm: `9.13.0`\n- Python: `3.12.x` (required for FastAPI backend work)\n- uv: `0.5.24+` (required for FastAPI backend work)\n- Docker: latest stable\n\nInstall/check:\n\n```bash\nnode -v\npnpm -v\npython3 --version\nuv --version\ndocker --version\n```\n\nExpected:\n- Node starts with `v22`\n- pnpm prints `9.13.0` (or compatible in the same series)\n- Python starts with `3.12`\n\n## 2) Install frontend dependencies\n\n```bash\npnpm install\n```\n\n## 3) Sync backend dependencies with uv\n\n```bash\ncd backend\nuv sync --no-install-project\ncd ..\n```\n\nThis creates `backend/.venv` and installs pinned Python dependencies from `backend/uv.lock`.\n\n## 4) Configure environment variables\n\n```bash\ncp .env.example .env\n```\n\nThen set at least:\n- `POSTGRES_URL`\n- `OPENAI_API_KEY`\n\nOptional:\n- `OPENAI_MODEL` (single model used for all generation stages, defaults to `gpt-5.4-mini`)\n- `GITHUB_PAT`\n- `NEXT_PUBLIC_POSTHOG_KEY`\n- `NEXT_PUBLIC_USE_LEGACY_BACKEND=true` and `NEXT_PUBLIC_API_DEV_URL` (to route frontend calls to an external backend such as Railway/local FastAPI)\n\n## 5) Start local services\n\nStart local Postgres (if using local DB URL):\n\n```bash\nchmod +x start-database.sh\n./start-database.sh\n```\n\nPush schema:\n\n```bash\npnpm db:push\n```\n\nStart frontend:\n\n```bash\npnpm dev\n```\n\nStart FastAPI backend (recommended for production parity):\n\n```bash\ndocker-compose up --build -d\ndocker-compose logs -f api\n```\n\nor\n\n```bash\npnpm dev:backend\n```\n\nIf the FastAPI backend is running locally at `http://localhost:8000`, set:\n- `NEXT_PUBLIC_USE_LEGACY_BACKEND=true`\n- `NEXT_PUBLIC_API_DEV_URL=http://localhost:8000`\n\n## 6) Verification commands\n\nRun all baseline checks:\n\n```bash\npnpm check\npnpm test\npnpm build\n```\n\nFastAPI backend checks:\n\n```bash\ncd backend\nuv run pytest -q\nuv run python -m compileall app\ncd ..\n```\n\nIf all pass, your local environment is ready.\n"
  },
  {
    "path": "docs/railway-backend.md",
    "content": "# Railway Backend Deploy Guide\n\nThis guide deploys the production FastAPI backend from this monorepo.\n\n## 1) Prerequisites\n\n- Railway account + project access\n- Railway CLI installed\n- Logged in locally:\n\n```bash\nrailway login\n```\n\n## 2) Create/link the Railway service\n\nYou can use dashboard or CLI. CLI flow:\n\n```bash\ncd /path/to/gitdiagram\nrailway init -n gitdiagram\nrailway add --service gitdiagram-api\nrailway link --service gitdiagram-api\n```\n\n## 3) Set backend environment variables\n\nRequired:\n- `OPENAI_API_KEY`\n\nRecommended:\n- `OPENAI_MODEL=gpt-5.4-mini`\n- `ENVIRONMENT=production`\n- `WEB_CONCURRENCY=2`\n- `CORS_ORIGINS=https://gitdiagram.com,https://www.gitdiagram.com,https://<your-vercel-domain>`\n\nOptional:\n- `GITHUB_PAT` (higher GitHub API rate limits for repository fetches)\n- `GITHUB_CLIENT_ID`\n- `GITHUB_PRIVATE_KEY`\n- `GITHUB_INSTALLATION_ID`\n- `API_ANALYTICS_KEY`\n\nSet variables via CLI:\n\n```bash\nrailway variables --service gitdiagram-api --set \"OPENAI_API_KEY=...\"\nrailway variables --service gitdiagram-api --set \"OPENAI_MODEL=gpt-5.4-mini\"\nrailway variables --service gitdiagram-api --set \"ENVIRONMENT=production\"\nrailway variables --service gitdiagram-api --set \"WEB_CONCURRENCY=2\"\nrailway variables --service gitdiagram-api --set \"CORS_ORIGINS=https://gitdiagram.com,https://www.gitdiagram.com,https://<your-vercel-domain>\"\n```\n\nDo not set `PORT` manually unless needed. Railway injects it automatically.\n\n## 4) Deploy backend from `backend/`\n\n```bash\ncd /path/to/gitdiagram\nrailway up --service gitdiagram-api --path-as-root backend\n```\n\n## 5) Create a public Railway domain\n\n```bash\nrailway domain --service gitdiagram-api\n```\n\nCopy the generated URL, for example:\n`https://gitdiagram-api-production-xxxx.up.railway.app`\n\n## 6) Point Vercel frontend to Railway backend\n\nIn your Vercel project environment variables, set:\n\n- `NEXT_PUBLIC_USE_LEGACY_BACKEND=true`\n- `NEXT_PUBLIC_API_DEV_URL=https://<your-railway-domain>`\n\nThen redeploy Vercel.\n\nNote: the variable name includes \"LEGACY\" for backward compatibility, but this is now the primary external backend path.\n\n## 7) Verify\n\n1. Health endpoint:\n   - `GET https://<your-railway-domain>/healthz`\n   - expected JSON: `{\"ok\": true, \"status\": \"ok\"}`\n2. Open your frontend and generate a diagram.\n3. Check Railway logs:\n\n```bash\nrailway logs --service gitdiagram-api\n```\n"
  },
  {
    "path": "drizzle.config.ts",
    "content": "import { type Config } from \"drizzle-kit\";\n\nimport { env } from \"~/env\";\n\nexport default {\n  schema: \"./src/server/db/schema.ts\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: env.POSTGRES_URL,\n  },\n  tablesFilter: [\"gitdiagram_*\"],\n} satisfies Config;\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import nextCoreVitals from \"eslint-config-next/core-web-vitals\";\nimport nextTypescript from \"eslint-config-next/typescript\";\nimport drizzle from \"eslint-plugin-drizzle\";\nimport tseslint from \"@typescript-eslint/eslint-plugin\";\n\nconst config = [\n  ...nextCoreVitals,\n  ...nextTypescript,\n  {\n    ignores: [\n      \".next/**\",\n      \"node_modules/**\",\n      \"backend/**\",\n      \"dist/**\",\n      \"coverage/**\",\n      \"next-env.d.ts\",\n    ],\n  },\n  {\n    files: [\"**/*.{ts,tsx}\"],\n    plugins: {\n      drizzle,\n      \"@typescript-eslint\": tseslint,\n    },\n    rules: {\n      \"@typescript-eslint/array-type\": \"off\",\n      \"@typescript-eslint/consistent-type-definitions\": \"off\",\n      \"@typescript-eslint/consistent-type-imports\": [\n        \"warn\",\n        {\n          prefer: \"type-imports\",\n          fixStyle: \"inline-type-imports\",\n        },\n      ],\n      \"@typescript-eslint/no-require-imports\": \"off\",\n      \"@typescript-eslint/no-unused-vars\": [\n        \"warn\",\n        {\n          argsIgnorePattern: \"^_\",\n        },\n      ],\n      \"@typescript-eslint/require-await\": \"off\",\n      \"react-hooks/set-state-in-effect\": \"off\",\n      \"drizzle/enforce-delete-with-where\": [\n        \"error\",\n        {\n          drizzleObjectName: [\"db\", \"ctx.db\"],\n        },\n      ],\n      \"drizzle/enforce-update-with-where\": [\n        \"error\",\n        {\n          drizzleObjectName: [\"db\", \"ctx.db\"],\n        },\n      ],\n    },\n  },\n];\n\nexport default config;\n"
  },
  {
    "path": "next.config.js",
    "content": "/**\n * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful\n * for Docker builds.\n */\nimport \"./src/env.js\";\n\n/** @type {import(\"next\").NextConfig} */\nconst config = {\n  reactStrictMode: false,\n  async rewrites() {\n    return [\n      {\n        source: \"/phx9a/static/:path*\",\n        destination: \"https://us-assets.i.posthog.com/static/:path*\",\n      },\n      {\n        source: \"/phx9a/:path*\",\n        destination: \"https://us.i.posthog.com/:path*\",\n      },\n    ];\n  },\n  // This is required to support PostHog trailing slash API requests\n  skipTrailingSlashRedirect: true,\n};\n\nexport default config;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"gitdiagram\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"check\": \"pnpm lint && tsc --noEmit\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:push\": \"drizzle-kit push\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"dev\": \"next dev --turbo\",\n    \"dev:backend\": \"cd backend && ENVIRONMENT=development uv run --no-dev uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx\",\n    \"lint:fix\": \"eslint . --ext .js,.jsx,.ts,.tsx --fix\",\n    \"preview\": \"next build && next start\",\n    \"start\": \"next start\",\n    \"start:backend\": \"cd backend && ENVIRONMENT=production uv run --no-dev uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}\",\n    \"test\": \"vitest run\",\n    \"test:backend\": \"cd backend && uv run pytest -q\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"format:write\": \"prettier --write \\\"**/*.{ts,tsx,js,jsx,mdx}\\\" --cache\",\n    \"format:check\": \"prettier --check \\\"**/*.{ts,tsx,js,jsx,mdx}\\\" --cache\"\n  },\n  \"dependencies\": {\n    \"@mermaid-js/layout-elk\": \"^0.2.1\",\n    \"@neondatabase/serverless\": \"^1.0.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-progress\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.2.8\",\n    \"@t3-oss/env-nextjs\": \"^0.13.10\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"dompurify\": \"^3.3.1\",\n    \"dotenv\": \"^17.3.1\",\n    \"drizzle-orm\": \"^0.45.1\",\n    \"geist\": \"^1.7.0\",\n    \"ldrs\": \"^1.1.9\",\n    \"lucide-react\": \"^0.574.0\",\n    \"mermaid\": \"^11.12.3\",\n    \"next\": \"^16.1.6\",\n    \"next-themes\": \"^0.4.6\",\n    \"openai\": \"^6.22.0\",\n    \"postgres\": \"^3.4.8\",\n    \"posthog-js\": \"^1.351.3\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-icons\": \"^5.5.0\",\n    \"sonner\": \"^2.0.7\",\n    \"svg-pan-zoom\": \"^3.6.2\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"4.2.0\",\n    \"@testing-library/jest-dom\": \"6.9.1\",\n    \"@testing-library/react\": \"16.3.2\",\n    \"@types/eslint\": \"^9.6.1\",\n    \"@types/node\": \"^25.3.0\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.56.0\",\n    \"@typescript-eslint/parser\": \"^8.56.0\",\n    \"drizzle-kit\": \"^0.31.9\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-next\": \"^16.1.6\",\n    \"eslint-plugin-drizzle\": \"^0.2.3\",\n    \"jsdom\": \"26.1.0\",\n    \"postcss\": \"^8.5.6\",\n    \"prettier\": \"^3.8.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.7.2\",\n    \"tailwind-scrollbar\": \"^4.0.2\",\n    \"tailwindcss\": \"^4.2.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"4.0.18\"\n  },\n  \"ct3aMetadata\": {\n    \"initVersion\": \"7.38.1\"\n  },\n  \"engines\": {\n    \"node\": \">=22 <24\",\n    \"pnpm\": \">=9 <11\"\n  },\n  \"packageManager\": \"pnpm@10.30.0\"\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */\nconst config = {\n  plugins: [\"prettier-plugin-tailwindcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "src/app/[username]/[repo]/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport RepoPageClient from \"./repo-page-client\";\n\ntype RepoPageProps = {\n  params: Promise<{ username: string; repo: string }>;\n};\n\nexport async function generateMetadata({\n  params,\n}: RepoPageProps): Promise<Metadata> {\n  const { username, repo } = await params;\n  return {\n    title: `${username}/${repo} Diagram | GitDiagram`,\n    description: `Interactive architecture diagram for ${username}/${repo}.`,\n  };\n}\n\nexport default async function Repo({ params }: RepoPageProps) {\n  const { username, repo } = await params;\n  return <RepoPageClient username={username} repo={repo} />;\n}\n"
  },
  {
    "path": "src/app/[username]/[repo]/repo-page-client.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport MainCard from \"~/components/main-card\";\nimport Loading from \"~/components/loading\";\nimport MermaidChart from \"~/components/mermaid-diagram\";\nimport { useDiagram } from \"~/hooks/useDiagram\";\nimport { ApiKeyDialog } from \"~/components/api-key-dialog\";\nimport { ApiKeyButton } from \"~/components/api-key-button\";\nimport { useStarReminder } from \"~/hooks/useStarReminder\";\n\ntype RepoPageClientProps = {\n  username: string;\n  repo: string;\n};\n\nexport default function RepoPageClient({ username, repo }: RepoPageClientProps) {\n  const [zoomingEnabled, setZoomingEnabled] = useState(false);\n\n  useStarReminder();\n\n  const normalizedUsername = username.toLowerCase();\n  const normalizedRepo = repo.toLowerCase();\n\n  const {\n    diagram,\n    error,\n    loading,\n    lastGenerated,\n    cost,\n    showApiKeyDialog,\n    handleCopy,\n    handleApiKeySubmit,\n    handleCloseApiKeyDialog,\n    handleOpenApiKeyDialog,\n    handleExportImage,\n    handleRegenerate,\n    state,\n  } = useDiagram(normalizedUsername, normalizedRepo);\n\n  return (\n    <div className=\"flex flex-col items-center p-4\">\n      <div className=\"flex w-full justify-center pt-8\">\n        <MainCard\n          isHome={false}\n          username={normalizedUsername}\n          repo={normalizedRepo}\n          onCopy={handleCopy}\n          lastGenerated={lastGenerated}\n          onExportImage={handleExportImage}\n          onRegenerate={handleRegenerate}\n          zoomingEnabled={zoomingEnabled}\n          onZoomToggle={() => setZoomingEnabled((prev) => !prev)}\n          loading={loading}\n        />\n      </div>\n      <div className=\"mt-8 flex w-full flex-col items-center gap-8\">\n        {loading ? (\n          <Loading\n            cost={cost}\n            status={state.status}\n            message={state.message}\n            parserError={state.parserError}\n            fixAttempt={state.fixAttempt}\n            fixMaxAttempts={state.fixMaxAttempts}\n            fixDiagramDraft={state.fixDiagramDraft}\n            explanation={state.explanation}\n            mapping={state.mapping}\n            diagram={state.diagram}\n          />\n        ) : error || state.error ? (\n          <div className=\"mt-12 text-center\">\n            <p className=\"max-w-4xl text-lg font-medium text-red-700 dark:text-red-300\">\n              {error || state.error}\n            </p>\n            {state.parserError && (\n              <pre className=\"mx-auto mt-4 max-w-4xl overflow-x-auto whitespace-pre-wrap rounded-md border border-neutral-300 bg-neutral-100 p-4 text-left text-xs text-neutral-800 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200\">\n                {state.parserError}\n              </pre>\n            )}\n            {(error?.includes(\"API key\") ||\n              state.error?.includes(\"API key\")) && (\n              <div className=\"mt-8 flex flex-col items-center gap-2\">\n                <ApiKeyButton onClick={handleOpenApiKeyDialog} />\n              </div>\n            )}\n          </div>\n        ) : (\n          <div className=\"flex w-full justify-center px-4\">\n            <MermaidChart chart={diagram} zoomingEnabled={zoomingEnabled} />\n          </div>\n        )}\n      </div>\n\n      <ApiKeyDialog\n        isOpen={showApiKeyDialog}\n        onClose={handleCloseApiKeyDialog}\n        onSubmit={handleApiKeySubmit}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/_actions/cache.ts",
    "content": "\"use server\";\n\nimport { db } from \"~/server/db\";\nimport { eq, and } from \"drizzle-orm\";\nimport { diagramCache } from \"~/server/db/schema\";\nimport { sql } from \"drizzle-orm\";\n\nexport async function getCachedDiagram(username: string, repo: string) {\n  try {\n    const cached = await db\n      .select()\n      .from(diagramCache)\n      .where(\n        and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)),\n      )\n      .limit(1);\n\n    return cached[0]?.diagram ?? null;\n  } catch (error) {\n    console.error(\"Error fetching cached diagram:\", error);\n    return null;\n  }\n}\n\nexport async function getCachedExplanation(username: string, repo: string) {\n  try {\n    const cached = await db\n      .select()\n      .from(diagramCache)\n      .where(\n        and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)),\n      )\n      .limit(1);\n\n    return cached[0]?.explanation ?? null;\n  } catch (error) {\n    console.error(\"Error fetching cached explanation:\", error);\n    return null;\n  }\n}\n\nexport async function cacheDiagramAndExplanation(\n  username: string,\n  repo: string,\n  diagram: string,\n  explanation: string,\n  usedOwnKey = false,\n) {\n  try {\n    await db\n      .insert(diagramCache)\n      .values({\n        username,\n        repo,\n        diagram,\n        explanation,\n        usedOwnKey,\n      })\n      .onConflictDoUpdate({\n        target: [diagramCache.username, diagramCache.repo],\n        set: {\n          diagram,\n          explanation,\n          usedOwnKey,\n          updatedAt: new Date(),\n        },\n      });\n  } catch (error) {\n    console.error(\"Error caching diagram:\", error);\n  }\n}\n\nexport async function getDiagramStats() {\n  try {\n    const stats = await db\n      .select({\n        totalDiagrams: sql`COUNT(*)`,\n        ownKeyUsers: sql`COUNT(CASE WHEN ${diagramCache.usedOwnKey} = true THEN 1 END)`,\n        freeUsers: sql`COUNT(CASE WHEN ${diagramCache.usedOwnKey} = false THEN 1 END)`,\n      })\n      .from(diagramCache);\n\n    return stats[0];\n  } catch (error) {\n    console.error(\"Error getting diagram stats:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/app/_actions/repo.ts",
    "content": "\"use server\";\n\nimport { db } from \"~/server/db\";\nimport { eq, and } from \"drizzle-orm\";\nimport { diagramCache } from \"~/server/db/schema\";\n\nexport async function getLastGeneratedDate(username: string, repo: string) {\n  const result = await db\n    .select()\n    .from(diagramCache)\n    .where(\n      and(eq(diagramCache.username, username), eq(diagramCache.repo, repo)),\n    );\n\n  return result[0]?.updatedAt;\n}\n"
  },
  {
    "path": "src/app/api/generate/cost/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { toTaggedMessage } from \"~/server/generate/format\";\nimport { getGithubData } from \"~/server/generate/github\";\nimport { getModel } from \"~/server/generate/model-config\";\nimport { countInputTokens, estimateTokens } from \"~/server/generate/openai\";\nimport { SYSTEM_FIRST_PROMPT } from \"~/server/generate/prompts\";\nimport { estimateTextTokenCostUsd } from \"~/server/generate/pricing\";\nimport { generateRequestSchema } from \"~/server/generate/types\";\n\nexport const runtime = \"nodejs\";\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nconst MULTI_STAGE_INPUT_MULTIPLIER = 2;\nconst INPUT_OVERHEAD_TOKENS = 3000;\nconst ESTIMATED_OUTPUT_TOKENS = 8000;\n\nasync function estimateRepoInputTokens(\n  model: string,\n  fileTree: string,\n  readme: string,\n  apiKey?: string,\n) {\n  try {\n    return await countInputTokens({\n      model,\n      systemPrompt: SYSTEM_FIRST_PROMPT,\n      userPrompt: toTaggedMessage({\n        file_tree: fileTree,\n        readme,\n      }),\n      apiKey,\n      reasoningEffort: \"medium\",\n    });\n  } catch {\n    return estimateTokens(`${fileTree}\\n${readme}`);\n  }\n}\n\nexport async function POST(request: Request) {\n  try {\n    const parsed = generateRequestSchema.safeParse(await request.json());\n    if (!parsed.success) {\n      return NextResponse.json({\n        ok: false,\n        error: \"Invalid request payload.\",\n        error_code: \"VALIDATION_ERROR\",\n      });\n    }\n\n    const {\n      username,\n      repo,\n      api_key: apiKey,\n      github_pat: githubPat,\n    } = parsed.data;\n    const githubData = await getGithubData(username, repo, githubPat);\n    const model = getModel();\n\n    const baseInputTokens = await estimateRepoInputTokens(\n      model,\n      githubData.fileTree,\n      githubData.readme,\n      apiKey,\n    );\n    const estimatedInputTokens =\n      baseInputTokens * MULTI_STAGE_INPUT_MULTIPLIER + INPUT_OVERHEAD_TOKENS;\n    const estimatedOutputTokens = ESTIMATED_OUTPUT_TOKENS;\n\n    const { costUsd, pricingModel, pricing } = estimateTextTokenCostUsd(\n      model,\n      estimatedInputTokens,\n      estimatedOutputTokens,\n    );\n\n    return NextResponse.json({\n      ok: true,\n      cost: `$${costUsd.toFixed(2)} USD`,\n      model,\n      pricing_model: pricingModel,\n      estimated_input_tokens: estimatedInputTokens,\n      estimated_output_tokens: estimatedOutputTokens,\n      pricing: {\n        input_per_million_usd: pricing.inputPerMillionUsd,\n        output_per_million_usd: pricing.outputPerMillionUsd,\n      },\n    });\n  } catch (error) {\n    return NextResponse.json({\n      ok: false,\n      error:\n        error instanceof Error\n          ? error.message\n          : \"Failed to estimate generation cost.\",\n      error_code: \"COST_ESTIMATION_FAILED\",\n    });\n  }\n}\n"
  },
  {
    "path": "src/app/api/generate/stream/route.ts",
    "content": "import { getModel } from \"~/server/generate/model-config\";\nimport {\n  extractComponentMapping,\n  processClickEvents,\n  stripMermaidCodeFences,\n  toTaggedMessage,\n} from \"~/server/generate/format\";\nimport { getGithubData } from \"~/server/generate/github\";\nimport {\n  formatValidationFeedback,\n  validateMermaidSyntax,\n} from \"~/server/generate/mermaid\";\nimport {\n  countInputTokens,\n  estimateTokens,\n  streamCompletion,\n} from \"~/server/generate/openai\";\nimport {\n  SYSTEM_FIRST_PROMPT,\n  SYSTEM_FIX_MERMAID_PROMPT,\n  SYSTEM_SECOND_PROMPT,\n  SYSTEM_THIRD_PROMPT,\n} from \"~/server/generate/prompts\";\nimport { generateRequestSchema, sseMessage } from \"~/server/generate/types\";\n\nexport const runtime = \"nodejs\";\nexport const dynamic = \"force-dynamic\";\nexport const maxDuration = 300;\nconst MAX_MERMAID_FIX_ATTEMPTS = 3;\n\nfunction sleep(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function estimateRepoTokenCount(\n  model: string,\n  fileTree: string,\n  readme: string,\n  apiKey?: string,\n) {\n  try {\n    return await countInputTokens({\n      model,\n      systemPrompt: SYSTEM_FIRST_PROMPT,\n      userPrompt: toTaggedMessage({\n        file_tree: fileTree,\n        readme,\n      }),\n      apiKey,\n      reasoningEffort: \"medium\",\n    });\n  } catch {\n    return estimateTokens(`${fileTree}\\n${readme}`);\n  }\n}\n\nexport async function POST(request: Request) {\n  const parsed = generateRequestSchema.safeParse(await request.json());\n\n  if (!parsed.success) {\n    return new Response(\n      JSON.stringify({\n        ok: false,\n        error: \"Invalid request payload.\",\n        error_code: \"VALIDATION_ERROR\",\n      }),\n      { status: 400, headers: { \"Content-Type\": \"application/json\" } },\n    );\n  }\n\n  const { username, repo, api_key: apiKey, github_pat: githubPat } = parsed.data;\n\n  const encoder = new TextEncoder();\n\n  const stream = new ReadableStream<Uint8Array>({\n    start(controller) {\n      const send = (payload: Record<string, unknown>) => {\n        controller.enqueue(encoder.encode(sseMessage(payload)));\n      };\n\n      const run = async () => {\n        try {\n          const githubData = await getGithubData(username, repo, githubPat);\n          const model = getModel();\n          const tokenCount = await estimateRepoTokenCount(\n            model,\n            githubData.fileTree,\n            githubData.readme,\n            apiKey,\n          );\n\n          send({\n            status: \"started\",\n            message: \"Starting generation process...\",\n          });\n\n          if (tokenCount > 50000 && tokenCount < 195000 && !apiKey) {\n            send({\n              status: \"error\",\n              error:\n                \"File tree and README combined exceeds token limit (50,000). This repository is too large for free generation. Provide your own OpenAI API key to continue.\",\n              error_code: \"API_KEY_REQUIRED\",\n            });\n            controller.close();\n            return;\n          }\n\n          if (tokenCount > 195000) {\n            send({\n              status: \"error\",\n              error:\n                \"Repository is too large (>195k tokens) for analysis. Try a smaller repo.\",\n              error_code: \"TOKEN_LIMIT_EXCEEDED\",\n            });\n            controller.close();\n            return;\n          }\n\n          send({\n            status: \"explanation_sent\",\n            message: `Sending explanation request to ${model}...`,\n          });\n          await sleep(80);\n          send({\n            status: \"explanation\",\n            message: \"Analyzing repository structure...\",\n          });\n\n          let explanation = \"\";\n          for await (const chunk of streamCompletion({\n            model,\n            systemPrompt: SYSTEM_FIRST_PROMPT,\n            userPrompt: toTaggedMessage({\n              file_tree: githubData.fileTree,\n              readme: githubData.readme,\n            }),\n            apiKey,\n            reasoningEffort: \"medium\",\n          })) {\n            explanation += chunk;\n            send({ status: \"explanation_chunk\", chunk });\n          }\n\n          send({\n            status: \"mapping_sent\",\n            message: `Sending component mapping request to ${model}...`,\n          });\n          await sleep(80);\n          send({\n            status: \"mapping\",\n            message: \"Creating component mapping...\",\n          });\n\n          let fullMappingResponse = \"\";\n          for await (const chunk of streamCompletion({\n            model,\n            systemPrompt: SYSTEM_SECOND_PROMPT,\n            userPrompt: toTaggedMessage({\n              explanation,\n              file_tree: githubData.fileTree,\n            }),\n            apiKey,\n            reasoningEffort: \"low\",\n          })) {\n            fullMappingResponse += chunk;\n            send({ status: \"mapping_chunk\", chunk });\n          }\n\n          const componentMapping = extractComponentMapping(fullMappingResponse);\n\n          send({\n            status: \"diagram_sent\",\n            message: `Sending diagram generation request to ${model}...`,\n          });\n          await sleep(80);\n          send({\n            status: \"diagram\",\n            message: \"Generating diagram...\",\n          });\n\n          let mermaidCode = \"\";\n          for await (const chunk of streamCompletion({\n            model,\n            systemPrompt: SYSTEM_THIRD_PROMPT,\n            userPrompt: toTaggedMessage({\n              explanation,\n              component_mapping: componentMapping,\n            }),\n            apiKey,\n            reasoningEffort: \"low\",\n          })) {\n            mermaidCode += chunk;\n            send({ status: \"diagram_chunk\", chunk });\n          }\n\n          let candidateDiagram = stripMermaidCodeFences(mermaidCode);\n          let validationResult = await validateMermaidSyntax(candidateDiagram);\n          const hadFixLoop = !validationResult.valid;\n\n          if (!validationResult.valid) {\n            const parserFeedback = formatValidationFeedback(validationResult);\n            send({\n              status: \"diagram_fixing\",\n              message:\n                \"Diagram generated. Mermaid syntax validation failed, starting auto-fix loop...\",\n              parser_error: parserFeedback,\n            });\n          }\n\n          for (\n            let attempt = 1;\n            !validationResult.valid && attempt <= MAX_MERMAID_FIX_ATTEMPTS;\n            attempt++\n          ) {\n            const parserFeedback = formatValidationFeedback(validationResult);\n            send({\n              status: \"diagram_fix_attempt\",\n              message: `Fixing Mermaid syntax (attempt ${attempt}/${MAX_MERMAID_FIX_ATTEMPTS})...`,\n              fix_attempt: attempt,\n              fix_max_attempts: MAX_MERMAID_FIX_ATTEMPTS,\n              parser_error: parserFeedback,\n            });\n\n            let repairedDiagram = \"\";\n            for await (const chunk of streamCompletion({\n              model,\n              systemPrompt: SYSTEM_FIX_MERMAID_PROMPT,\n              userPrompt: toTaggedMessage({\n                mermaid_code: candidateDiagram,\n                parser_error: parserFeedback,\n                explanation,\n                component_mapping: componentMapping,\n              }),\n              apiKey,\n              reasoningEffort: \"low\",\n            })) {\n              repairedDiagram += chunk;\n              send({\n                status: \"diagram_fix_chunk\",\n                chunk,\n                fix_attempt: attempt,\n                fix_max_attempts: MAX_MERMAID_FIX_ATTEMPTS,\n              });\n            }\n\n            candidateDiagram = stripMermaidCodeFences(repairedDiagram);\n            send({\n              status: \"diagram_fix_validating\",\n              message: `Validating Mermaid syntax after attempt ${attempt}/${MAX_MERMAID_FIX_ATTEMPTS}...`,\n              fix_attempt: attempt,\n              fix_max_attempts: MAX_MERMAID_FIX_ATTEMPTS,\n            });\n            validationResult = await validateMermaidSyntax(candidateDiagram);\n          }\n\n          if (!validationResult.valid) {\n            send({\n              status: \"error\",\n              error:\n                \"Generated Mermaid remained syntactically invalid after auto-fix attempts. Please retry generation.\",\n              error_code: \"MERMAID_SYNTAX_UNRESOLVED\",\n              parser_error: formatValidationFeedback(validationResult),\n            });\n            return;\n          }\n\n          const processedDiagram = processClickEvents(\n            candidateDiagram,\n            username,\n            repo,\n            githubData.defaultBranch,\n          );\n\n          if (hadFixLoop) {\n            send({\n              status: \"diagram_fixing\",\n              message: \"Mermaid syntax validated. Finalizing diagram output...\",\n            });\n          }\n\n          send({\n            status: \"complete\",\n            diagram: processedDiagram,\n            explanation,\n            mapping: componentMapping,\n          });\n        } catch (error) {\n          send({\n            status: \"error\",\n            error:\n              error instanceof Error\n                ? error.message\n                : \"Streaming generation failed.\",\n            error_code: \"STREAM_FAILED\",\n          });\n        } finally {\n          controller.close();\n        }\n      };\n\n      void run();\n    },\n  });\n\n  return new Response(stream, {\n    headers: {\n      \"Content-Type\": \"text/event-stream; charset=utf-8\",\n      \"Cache-Control\": \"no-cache, no-transform\",\n      Connection: \"keep-alive\",\n      \"X-Accel-Buffering\": \"no\",\n    },\n  });\n}\n"
  },
  {
    "path": "src/app/api/healthz/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nexport const runtime = \"nodejs\";\nexport const dynamic = \"force-dynamic\";\n\nexport async function GET() {\n  return NextResponse.json({ ok: true, status: \"ok\" });\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import \"~/styles/globals.css\";\n\nimport { GeistSans } from \"geist/font/sans\";\nimport { type Metadata } from \"next\";\nimport { Header } from \"~/components/header\";\nimport { Footer } from \"~/components/footer\";\nimport { CSPostHogProvider } from \"./providers\";\nimport { Toaster } from \"~/components/ui/sonner\";\n\nexport const metadata: Metadata = {\n  title: \"GitDiagram\",\n  description:\n    \"Turn any GitHub repository into an interactive diagram for visualization in seconds.\",\n  metadataBase: new URL(\"https://gitdiagram.com\"),\n  keywords: [\n    \"github\",\n    \"git diagram\",\n    \"git diagram generator\",\n    \"git diagram tool\",\n    \"git diagram maker\",\n    \"git diagram creator\",\n    \"git diagram\",\n    \"diagram\",\n    \"repository\",\n    \"visualization\",\n    \"code structure\",\n    \"system design\",\n    \"software architecture\",\n    \"software design\",\n    \"software engineering\",\n    \"software development\",\n    \"software architecture\",\n    \"software design\",\n    \"software engineering\",\n    \"software development\",\n    \"open source\",\n    \"open source software\",\n    \"ahmedkhaleel2004\",\n    \"ahmed khaleel\",\n    \"gitdiagram\",\n    \"gitdiagram.com\",\n  ],\n  authors: [\n    { name: \"Ahmed Khaleel\", url: \"https://github.com/ahmedkhaleel2004\" },\n  ],\n  creator: \"Ahmed Khaleel\",\n  openGraph: {\n    type: \"website\",\n    locale: \"en_US\",\n    url: \"https://gitdiagram.com\",\n    title: \"GitDiagram - Repository to Diagram in Seconds\",\n    description:\n      \"Turn any GitHub repository into an interactive diagram for visualization.\",\n    siteName: \"GitDiagram\",\n    images: [\n      {\n        url: \"/og-image.png\", // You'll need to create this image\n        width: 1200,\n        height: 630,\n        alt: \"GitDiagram - Repository Visualization Tool\",\n      },\n    ],\n  },\n  robots: {\n    index: true,\n    follow: true,\n    googleBot: {\n      index: true,\n      follow: true,\n      \"max-snippet\": -1,\n    },\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{ children: React.ReactNode }>) {\n  return (\n    <html\n      lang=\"en\"\n      suppressHydrationWarning\n      className={`${GeistSans.variable}`}\n    >\n      <body className=\"flex min-h-screen flex-col\">\n        <CSPostHogProvider>\n          <Header />\n          <main className=\"flex-grow\">{children}</main>\n          <Footer />\n          <Toaster />\n        </CSPostHogProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "import MainCard from \"~/components/main-card\";\nimport Hero from \"~/components/hero\";\nimport type { Metadata } from \"next\";\n\nexport const metadata: Metadata = {\n  title: \"GitDiagram - Visualize Any GitHub Repository\",\n  description:\n    \"Turn any GitHub repository into an interactive architecture diagram for quick codebase understanding.\",\n};\n\nexport default function HomePage() {\n  return (\n    <main className=\"flex-grow px-8 pb-8 md:p-8\">\n      <div className=\"mx-auto mb-4 max-w-4xl lg:my-8\">\n        <Hero />\n        <div className=\"mt-12\"></div>\n        <p className=\"mx-auto mt-8 max-w-2xl text-center text-lg\">\n          Turn any GitHub repository into an interactive diagram for\n          visualization.\n        </p>\n        <p className=\"mx-auto mt-0 max-w-2xl text-center text-lg\">\n          This is useful for quickly visualizing projects.\n        </p>\n        <p className=\"mx-auto mt-2 max-w-2xl text-center text-lg\">\n          You can also replace &apos;hub&apos; with &apos;diagram&apos; in any\n          Github URL\n        </p>\n      </div>\n      <div className=\"mb-16 flex justify-center lg:mb-0\">\n        <MainCard />\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/app/providers.tsx",
    "content": "// app/providers.js\n\"use client\";\nimport posthog from \"posthog-js\";\nimport { PostHogProvider } from \"posthog-js/react\";\nimport { ThemeProvider } from \"next-themes\";\n\nif (typeof window !== \"undefined\") {\n  // Only initialize PostHog if the environment variables are available\n  const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;\n\n  if (posthogKey) {\n    posthog.init(posthogKey, {\n      // Use a non-default first-party path to reduce adblock filter hits.\n      api_host: \"/phx9a\",\n      ui_host: \"https://us.posthog.com\",\n      person_profiles: \"always\",\n    });\n  } else {\n    console.log(\n      \"PostHog environment variables are not set. Analytics will be disabled. Skipping PostHog initialization.\",\n    );\n  }\n}\n\nexport function CSPostHogProvider({ children }: { children: React.ReactNode }) {\n  return (\n    <ThemeProvider\n      attribute=\"class\"\n      defaultTheme=\"light\"\n      enableSystem={false}\n      storageKey=\"gitdiagram-theme\"\n    >\n      <PostHogProvider client={posthog}>{children}</PostHogProvider>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/action-button.tsx",
    "content": "import { Button } from \"~/components/ui/button\";\nimport type { LucideIcon } from \"lucide-react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"~/components/ui/tooltip\";\n\ninterface ActionButtonProps {\n  onClick: () => void;\n  icon: LucideIcon;\n  tooltipText: string;\n  disabled?: boolean;\n  text?: string;\n}\n\nexport function ActionButton({\n  onClick,\n  icon: Icon,\n  tooltipText,\n  disabled,\n  text,\n}: ActionButtonProps) {\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            onClick={(e) => {\n              e.preventDefault();\n              onClick();\n            }}\n            disabled={disabled}\n            className=\"neo-button p-4 px-4 text-base sm:p-6 sm:px-6 sm:text-lg\"\n          >\n            <Icon className=\"h-6 w-6\" />\n            {text && <span className=\"text-sm\">{text}</span>}\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{tooltipText}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/api-key-button.tsx",
    "content": "import { Key } from \"lucide-react\";\nimport { Button } from \"./ui/button\";\n\ninterface ApiKeyButtonProps {\n  onClick: () => void;\n}\n\nexport function ApiKeyButton({ onClick }: ApiKeyButtonProps) {\n  return (\n    <Button\n      onClick={onClick}\n      className=\"neo-button px-4 py-2\"\n    >\n      <Key className=\"mr-2 h-5 w-5\" />\n      Use Your API Key\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/api-key-dialog.tsx",
    "content": "\"use client\";\n\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"./ui/dialog\";\nimport { Input } from \"./ui/input\";\nimport { Button } from \"./ui/button\";\nimport { useState, useEffect } from \"react\";\nimport Link from \"next/link\";\n\ninterface ApiKeyDialogProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onSubmit: (apiKey: string) => void;\n}\n\nexport function ApiKeyDialog({ isOpen, onClose, onSubmit }: ApiKeyDialogProps) {\n  const [apiKey, setApiKey] = useState<string>(\"\");\n\n  useEffect(() => {\n    const storedKey = localStorage.getItem(\"openai_key\");\n    if (storedKey) {\n      setApiKey(storedKey);\n    }\n  }, []);\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    onSubmit(apiKey);\n    setApiKey(\"\");\n  };\n\n  const handleClear = () => {\n    localStorage.removeItem(\"openai_key\");\n    setApiKey(\"\");\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"neo-panel p-6 sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle className=\"text-xl font-bold text-black dark:text-neutral-100\">\n            Enter OpenAI API Key\n          </DialogTitle>\n        </DialogHeader>\n        <form\n          onSubmit={handleSubmit}\n          className=\"space-y-4 text-black dark:text-neutral-200\"\n        >\n          <div className=\"text-sm\">\n            GitDiagram offers infinite free diagram generations! You can also\n            provide an OpenAI API key to generate diagrams at your own cost. The\n            key will be stored locally in your browser.\n            {/* GitDiagram offers one free diagram generation. For additional\n            diagrams, you&apos;ll need to provide an OpenAI API key. The key\n            will be stored locally in your browser. */}\n            <br />\n            <br />\n            <span className=\"font-medium\">Get your OpenAI API key </span>\n            <Link\n              href=\"https://platform.openai.com/api-keys\"\n              className=\"neo-link font-medium\"\n            >\n              here\n            </Link>\n            .\n          </div>\n          <details className=\"group text-sm [&>summary:focus-visible]:outline-none\">\n            <summary className=\"neo-link cursor-pointer font-medium\">\n              Data storage disclaimer\n            </summary>\n            <div className=\"animate-accordion-down mt-2 space-y-2 overflow-hidden pl-2\">\n              <p>\n                Your API key will be stored locally in your browser and used\n                only for generating diagrams. You can also self-host this app by\n                following the instructions in the{\" \"}\n                <Link\n                  href=\"https://github.com/ahmedkhaleel2004/gitdiagram\"\n                  className=\"neo-link\"\n                >\n                  README\n                </Link>\n                .\n              </p>\n            </div>\n          </details>\n          <Input\n            type=\"password\"\n            placeholder=\"sk-...\"\n            value={apiKey}\n            onChange={(e) => setApiKey(e.target.value)}\n            className=\"neo-input flex-1 rounded-md px-3 py-2 text-base font-bold placeholder:text-base placeholder:font-normal placeholder:text-gray-700 dark:placeholder:text-neutral-400\"\n            required\n          />\n          <div className=\"flex items-center justify-between\">\n            <button\n              type=\"button\"\n              onClick={handleClear}\n              className=\"neo-link text-sm\"\n            >\n              Clear\n            </button>\n            <div className=\"flex gap-3\">\n              <Button\n                type=\"button\"\n                onClick={onClose}\n                className=\"neo-button-muted px-4 py-2\"\n              >\n                Cancel\n              </Button>\n              <Button\n                type=\"submit\"\n                disabled={!apiKey.startsWith(\"sk-\")}\n                className=\"neo-button px-4 py-2 disabled:opacity-50\"\n              >\n                Save Key\n              </Button>\n            </div>\n          </div>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/components/copy-button.tsx",
    "content": "import React, { useState } from \"react\";\nimport { Button } from \"~/components/ui/button\";\nimport { FileText, Check } from \"lucide-react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"~/components/ui/tooltip\";\n\ninterface CopyButtonProps {\n  onClick: () => void;\n}\n\nexport function CopyButton({ onClick }: CopyButtonProps) {\n  const [copied, setCopied] = useState(false);\n\n  const handleClick = () => {\n    onClick();\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds\n  };\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            onClick={handleClick}\n            className=\"neo-button p-4 px-4 text-base sm:p-6 sm:px-6 sm:text-lg\"\n          >\n            {copied ? (\n              <>\n                <Check className=\"h-6 w-6\" />\n                <span className=\"text-sm\">Copied!</span>\n              </>\n            ) : (\n              <>\n                <FileText className=\"h-6 w-6\" />\n                <span className=\"text-sm\">Copy Mermaid.js Code</span>\n              </>\n            )}\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>\n            {copied\n              ? \"Copied!\"\n              : \"Copy the internal Mermaid.js code needed to generate the diagram\"}\n          </p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/export-dropdown.tsx",
    "content": "import { CopyButton } from \"./copy-button\";\nimport { Image } from \"lucide-react\";\nimport { ActionButton } from \"./action-button\";\n\ninterface ExportDropdownProps {\n  onCopy: () => void;\n  lastGenerated: Date;\n  onExportImage: () => void;\n  isOpen: boolean;\n}\n\nexport function ExportDropdown({\n  onCopy,\n  lastGenerated,\n  onExportImage,\n}: ExportDropdownProps) {\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:gap-4\">\n        <ActionButton\n          onClick={onExportImage}\n          icon={Image}\n          tooltipText=\"Download diagram as high-quality PNG\"\n          text=\"Download PNG\"\n        />\n        <CopyButton onClick={onCopy} />\n      </div>\n\n      <div className=\"flex items-center\">\n        <span className=\"text-sm text-gray-700 dark:text-neutral-300\">\n          Last generated: {lastGenerated.toLocaleString()}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/footer.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\n\nexport function Footer() {\n  return (\n    <footer className=\"mt-auto border-t-[3px] border-black py-4 lg:px-8 dark:border-black\">\n      <div className=\"container mx-auto flex h-8 max-w-4xl items-center justify-center\">\n        <span className=\"text-sm font-medium text-black dark:text-neutral-100\">\n          Made by{\" \"}\n          <Link\n            href=\"https://ahmedkhaleel.com\"\n            className=\"neo-link hover:underline\"\n          >\n            Ahmed Khaleel\n          </Link>\n        </span>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "src/components/header-client.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { FaGithub } from \"react-icons/fa\";\nimport { ApiKeyDialog } from \"./api-key-dialog\";\nimport { PrivateReposDialog } from \"./private-repos-dialog\";\nimport { ThemeToggle } from \"./theme-toggle\";\n\ninterface HeaderClientProps {\n  starCount: number | null;\n}\n\nconst compactNumberFormatter = new Intl.NumberFormat(\"en\", {\n  notation: \"compact\",\n  maximumFractionDigits: 1,\n});\n\nfunction formatStarCount(count: number) {\n  return compactNumberFormatter.format(count).toLowerCase();\n}\n\nexport function HeaderClient({ starCount }: HeaderClientProps) {\n  const [isPrivateReposDialogOpen, setIsPrivateReposDialogOpen] =\n    useState(false);\n  const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false);\n\n  const handlePrivateReposSubmit = (pat: string) => {\n    localStorage.setItem(\"github_pat\", pat);\n    setIsPrivateReposDialogOpen(false);\n  };\n\n  const handleApiKeySubmit = (apiKey: string) => {\n    localStorage.setItem(\"openai_key\", apiKey);\n    setIsApiKeyDialogOpen(false);\n  };\n\n  return (\n    <header className=\"border-b-[3px] border-black dark:border-black\">\n      <div className=\"mx-auto flex h-16 max-w-4xl items-center justify-between px-4 sm:px-8\">\n        <Link href=\"/\" className=\"flex items-center\">\n          <span className=\"text-lg font-semibold sm:text-xl\">\n            <span className=\"text-black transition-colors duration-200 hover:text-gray-600 dark:text-white dark:hover:text-[hsl(var(--neo-button-hover))]\">\n              Git\n            </span>\n            <span className=\"text-purple-600 transition-colors duration-200 hover:text-purple-500 dark:text-[hsl(var(--neo-button))] dark:hover:text-[hsl(var(--neo-button-hover))]\">\n              Diagram\n            </span>\n          </span>\n        </Link>\n        <nav className=\"flex items-center gap-3 sm:gap-6\">\n          <button\n            type=\"button\"\n            onClick={() => setIsApiKeyDialogOpen(true)}\n            className=\"text-sm font-medium text-black transition-transform hover:translate-y-[-2px] hover:text-purple-600 dark:text-neutral-200 dark:hover:text-[hsl(var(--neo-link-hover))]\"\n          >\n            <span className=\"flex items-center sm:hidden\">\n              <span>API Key</span>\n            </span>\n            <span className=\"hidden items-center gap-1 sm:flex\">\n              <span>API Key</span>\n            </span>\n          </button>\n          <button\n            type=\"button\"\n            onClick={() => setIsPrivateReposDialogOpen(true)}\n            className=\"text-sm font-medium text-black transition-transform hover:translate-y-[-2px] hover:text-purple-600 dark:text-neutral-200 dark:hover:text-[hsl(var(--neo-link-hover))]\"\n          >\n            <span className=\"sm:hidden\">Private Repos</span>\n            <span className=\"hidden sm:inline\">Private Repos</span>\n          </button>\n          <ThemeToggle />\n          <Link\n            href=\"https://github.com/ahmedkhaleel2004/gitdiagram\"\n            className=\"flex items-center gap-1 text-sm font-medium text-black transition-transform hover:translate-y-[-2px] hover:text-purple-600 dark:text-neutral-200 dark:hover:text-[hsl(var(--neo-link-hover))] sm:gap-2\"\n          >\n            <FaGithub className=\"h-5 w-5\" />\n            <span className=\"hidden sm:inline\">GitHub</span>\n          </Link>\n          {starCount !== null ? (\n            <span className=\"flex items-center gap-1 text-sm font-medium text-black dark:text-neutral-200\">\n              <span className=\"text-amber-400 dark:text-[hsl(var(--neo-link))]\">\n                ★\n              </span>\n              {formatStarCount(starCount)}\n            </span>\n          ) : null}\n        </nav>\n\n        <PrivateReposDialog\n          isOpen={isPrivateReposDialogOpen}\n          onClose={() => setIsPrivateReposDialogOpen(false)}\n          onSubmit={handlePrivateReposSubmit}\n        />\n        <ApiKeyDialog\n          isOpen={isApiKeyDialogOpen}\n          onClose={() => setIsApiKeyDialogOpen(false)}\n          onSubmit={handleApiKeySubmit}\n        />\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/components/header.tsx",
    "content": "import { getStarCount } from \"~/server/github-stars\";\nimport { HeaderClient } from \"./header-client\";\n\nexport async function Header() {\n  const starCount = await getStarCount();\n\n  return <HeaderClient starCount={starCount} />;\n}\n"
  },
  {
    "path": "src/components/hero.tsx",
    "content": "import React from \"react\";\n\nconst Hero = () => {\n  return (\n    <div className=\"relative mx-auto flex w-full flex-col items-start justify-center sm:flex-row sm:items-center\">\n      <svg\n        className=\"left-0 h-auto w-16 flex-shrink-0 -translate-x-2 translate-y-4 p-2 sm:absolute sm:w-20 sm:-translate-y-16 md:relative md:w-24 md:-translate-y-0 md:translate-x-10 lg:absolute lg:ml-32 lg:-translate-x-full lg:-translate-y-10\"\n        viewBox=\"0 0 91 98\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"m35.878 14.162 1.333-5.369 1.933 5.183c4.47 11.982 14.036 21.085 25.828 24.467l5.42 1.555-5.209 2.16c-11.332 4.697-19.806 14.826-22.888 27.237l-1.333 5.369-1.933-5.183C34.56 57.599 24.993 48.496 13.201 45.114l-5.42-1.555 5.21-2.16c11.331-4.697 19.805-14.826 22.887-27.237Z\"\n          className=\"fill-violet-500 stroke-black dark:fill-[hsl(var(--neo-button))] dark:stroke-black\"\n          strokeWidth=\"3.445\"\n        />\n        <path\n          d=\"M79.653 5.729c-2.436 5.323-9.515 15.25-18.341 12.374m9.197 16.336c2.6-5.851 10.008-16.834 18.842-13.956m-9.738-15.07c-.374 3.787 1.076 12.078 9.869 14.943M70.61 34.6c.503-4.21-.69-13.346-9.49-16.214M14.922 65.967c1.338 5.677 6.372 16.756 15.808 15.659M18.21 95.832c-1.392-6.226-6.54-18.404-15.984-17.305m12.85-12.892c-.41 3.771-3.576 11.588-12.968 12.681M18.025 96c.367-4.21 3.453-12.905 12.854-14\"\n          className=\"stroke-black dark:stroke-[hsl(var(--foreground))]\"\n          strokeWidth=\"2.548\"\n          strokeLinecap=\"round\"\n        />\n      </svg>\n      <h1 className=\"relative inline-block w-full text-center text-5xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:pt-5 lg:text-7xl\">\n        Repository to <br />\n        diagram&nbsp;\n      </h1>\n      <svg\n        className=\"bottom-0 right-0 hidden h-auto w-16 flex-shrink-0 -translate-x-10 translate-y-10 md:block md:translate-y-20 lg:absolute lg:w-20 lg:-translate-x-12 lg:translate-y-4\"\n        viewBox=\"0 0 92 80\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"m35.213 16.953.595-5.261 2.644 4.587a35.056 35.056 0 0 0 26.432 17.33l5.261.594-4.587 2.644A35.056 35.056 0 0 0 48.23 63.28l-.595 5.26-2.644-4.587a35.056 35.056 0 0 0-26.432-17.328l-5.261-.595 4.587-2.644a35.056 35.056 0 0 0 17.329-26.433Z\"\n          className=\"fill-sky-400 stroke-black dark:fill-[hsl(var(--neo-button-hover))] dark:stroke-black\"\n          strokeWidth=\"2.868\"\n        />\n        <path\n          d=\"M75.062 40.108c1.07 5.255 1.072 16.52-7.472 19.54m7.422-19.682c1.836 2.965 7.643 8.14 16.187 5.121-8.544 3.02-8.207 15.23-6.971 20.957-1.97-3.343-8.044-9.274-16.588-6.254M12.054 28.012c1.34-5.22 6.126-15.4 14.554-14.369M12.035 28.162c-.274-3.487-2.93-10.719-11.358-11.75C9.104 17.443 14.013 6.262 15.414.542c.226 3.888 2.784 11.92 11.212 12.95\"\n          className=\"stroke-black dark:stroke-[hsl(var(--foreground))]\"\n          strokeWidth=\"2.319\"\n          strokeLinecap=\"round\"\n        />\n      </svg>\n    </div>\n  );\n};\n\nexport default Hero;\n"
  },
  {
    "path": "src/components/loading-animation.tsx",
    "content": "\"use client\";\n\nimport { trio } from \"ldrs\";\nimport { useTheme } from \"next-themes\";\n\ntrio.register();\n\nconst LoadingAnimation = () => {\n  const { resolvedTheme } = useTheme();\n  const color = resolvedTheme === \"dark\" ? \"#f5f5f5\" : \"#171717\";\n\n  return <l-trio size=\"40\" speed=\"2.0\" color={color} />;\n};\n\nexport default LoadingAnimation;\n"
  },
  {
    "path": "src/components/loading.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\nimport type { DiagramStreamStatus } from \"~/features/diagram/types\";\n\nconst messages = [\n  \"Checking if its cached...\",\n  \"Generating diagram...\",\n  \"Analyzing repository...\",\n  \"Prompting GPT-5.2...\",\n  \"Inspecting file paths...\",\n  \"Finding component relationships...\",\n  \"Linking components to code...\",\n  \"Extracting relevant directories...\",\n  \"Reasoning about the diagram...\",\n  \"Prompt engineers needed -> Check out the GitHub\",\n  \"Shoutout to GitIngest for inspiration\",\n  \"I need to find a way to make this faster...\",\n  \"Finding the meaning of life...\",\n  \"I'm tired...\",\n  \"Please just give me the diagram...\",\n  \"...NOW!\",\n  \"guess not...\",\n];\n\ninterface LoadingProps {\n  cost?: string;\n  status: DiagramStreamStatus;\n  message?: string;\n  parserError?: string;\n  fixAttempt?: number;\n  fixMaxAttempts?: number;\n  fixDiagramDraft?: string;\n  explanation?: string;\n  mapping?: string;\n  diagram?: string;\n}\n\nconst getStepNumber = (status: string): number => {\n  if (status.startsWith(\"diagram\")) return 3;\n  if (status.startsWith(\"mapping\")) return 2;\n  if (status.startsWith(\"explanation\")) return 1;\n  return 0;\n};\n\nconst SequentialDots = () => {\n  return (\n    <span className=\"inline-flex w-8 justify-start\">\n      <span className=\"flex gap-0.5\">\n        <span className=\"h-1 w-1 animate-[dot1_1.5s_steps(1)_infinite] rounded-full bg-[hsl(var(--neo-dot-active))]\" />\n        <span className=\"h-1 w-1 animate-[dot2_1.5s_steps(1)_infinite] rounded-full bg-[hsl(var(--neo-dot-active))]\" />\n        <span className=\"h-1 w-1 animate-[dot3_1.5s_steps(1)_infinite] rounded-full bg-[hsl(var(--neo-dot-active))]\" />\n      </span>\n    </span>\n  );\n};\n\nconst StepDots = ({ currentStep }: { currentStep: number }) => {\n  return (\n    <div className=\"flex gap-1\">\n      {[1, 2, 3].map((step) => (\n        <div\n          key={step}\n          className={`h-1.5 w-1.5 rounded-full transition-colors duration-300 ${\n            step <= currentStep\n              ? \"bg-[hsl(var(--neo-dot-active))]\"\n              : \"bg-[hsl(var(--neo-dot-inactive))]\"\n          }`}\n        />\n      ))}\n    </div>\n  );\n};\n\nexport default function Loading({\n  status = \"idle\",\n  message,\n  parserError,\n  fixAttempt,\n  fixMaxAttempts,\n  fixDiagramDraft,\n  explanation,\n  mapping,\n  diagram,\n  cost,\n}: LoadingProps) {\n  const [currentMessageIndex, setCurrentMessageIndex] = useState(0);\n  const scrollRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % messages.length);\n    }, 3000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  // Auto-scroll effect\n  useEffect(() => {\n    if (scrollRef.current) {\n      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n    }\n  }, [\n    status,\n    message,\n    parserError,\n    fixAttempt,\n    fixMaxAttempts,\n    fixDiagramDraft,\n    explanation,\n    mapping,\n    diagram,\n  ]);\n\n  const shouldShowReasoning = (currentStatus: string) => {\n    if (\n      currentStatus === \"diagram_fixing\" ||\n      currentStatus === \"diagram_fix_attempt\" ||\n      currentStatus === \"diagram_fix_chunk\" ||\n      currentStatus === \"diagram_fix_validating\"\n    ) {\n      return null;\n    }\n    if (\n      currentStatus === \"explanation_sent\" ||\n      (currentStatus.startsWith(\"explanation\") && !explanation)\n    ) {\n      return \"explanation\";\n    }\n    if (\n      currentStatus === \"mapping_sent\" ||\n      (currentStatus.startsWith(\"mapping\") && !mapping)\n    ) {\n      return \"mapping\";\n    }\n    if (\n      currentStatus === \"diagram_sent\" ||\n      (currentStatus.startsWith(\"diagram\") && !diagram)\n    ) {\n      return \"diagram\";\n    }\n    return null;\n  };\n\n  const renderReasoningMessage = () => {\n    const reasoningType = shouldShowReasoning(status);\n    switch (reasoningType) {\n      case \"explanation\":\n        return \"Model is analyzing the repository structure and codebase...\";\n      case \"mapping\":\n        return \"Model is identifying component relationships and dependencies...\";\n      case \"diagram\":\n        return \"Model is planning the diagram layout and connections...\";\n      default:\n        return null;\n    }\n  };\n\n  const getStatusDisplay = () => {\n    const reasoningType = shouldShowReasoning(status);\n    switch (status) {\n      case \"explanation_sent\":\n      case \"explanation\":\n      case \"explanation_chunk\":\n        return {\n          text: reasoningType\n            ? \"Model is reasoning about repository structure\"\n            : \"Explaining repository structure...\",\n          isReasoning: !!reasoningType,\n        };\n      case \"mapping_sent\":\n      case \"mapping\":\n      case \"mapping_chunk\":\n        return {\n          text: reasoningType\n            ? \"Model is reasoning about component relationships\"\n            : \"Creating component mapping...\",\n          isReasoning: !!reasoningType,\n        };\n      case \"diagram_sent\":\n      case \"diagram\":\n      case \"diagram_chunk\":\n        return {\n          text: reasoningType\n            ? \"Model is reasoning about diagram structure\"\n            : \"Generating diagram...\",\n          isReasoning: !!reasoningType,\n        };\n      case \"diagram_fixing\":\n      case \"diagram_fix_attempt\":\n      case \"diagram_fix_chunk\":\n      case \"diagram_fix_validating\":\n        return {\n          text: message ?? \"Fixing Mermaid syntax...\",\n          isReasoning: false,\n        };\n      default:\n        return {\n          text: messages[currentMessageIndex],\n          isReasoning: false,\n        };\n    }\n  };\n\n  const statusDisplay = getStatusDisplay();\n  const reasoningMessage = renderReasoningMessage();\n  const hasFixTelemetry =\n    status === \"diagram_fixing\" ||\n    status === \"diagram_fix_attempt\" ||\n    status === \"diagram_fix_chunk\" ||\n    status === \"diagram_fix_validating\" ||\n    typeof fixAttempt === \"number\" ||\n    !!parserError ||\n    !!fixDiagramDraft;\n\n  return (\n    <div className=\"mx-auto w-full max-w-4xl p-4\">\n      <div className=\"overflow-hidden rounded-xl border-2 border-purple-200 bg-purple-50/30 backdrop-blur-sm dark:border-[#2d1d4e] dark:bg-[linear-gradient(160deg,#1a1228,#150f22)]\">\n        <div className=\"border-b border-purple-100 bg-purple-100/50 px-6 py-3 dark:border-[#2d1d4e] dark:bg-[#1e1832]/90\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-2\">\n              <span className=\"text-sm font-medium text-purple-500 dark:text-[hsl(var(--neo-button-hover))]\">\n                {statusDisplay.text}\n              </span>\n              {statusDisplay.isReasoning && <SequentialDots />}\n            </div>\n            <div className=\"flex items-center gap-3 text-xs font-medium text-purple-500 dark:text-[hsl(var(--foreground))]\">\n              {cost && <span>Estimated cost: {cost}</span>}\n              <div className=\"flex items-center gap-2\">\n                <span className=\"rounded-full bg-purple-100 px-2 py-0.5 dark:bg-[#251b3a]\">\n                  Step {getStepNumber(status)}/3\n                </span>\n                <StepDots currentStep={getStepNumber(status)} />\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Scrollable content */}\n        <div ref={scrollRef} className=\"max-h-[400px] overflow-y-auto p-6\">\n          <div className=\"flex flex-col gap-6\">\n            {/* Only show reasoning message if we have some content */}\n            {reasoningMessage &&\n              statusDisplay.isReasoning &&\n              (explanation ?? mapping ?? diagram) && (\n                <div className=\"rounded-lg bg-purple-100/50 p-4 text-sm text-purple-500 dark:bg-[#1d1530] dark:text-[hsl(var(--foreground))]\">\n                  <div className=\"flex items-center gap-2\">\n                    <p className=\"font-medium\">Reasoning</p>\n                    <SequentialDots />\n                  </div>\n                  <p className=\"mt-2 leading-relaxed\">{reasoningMessage}</p>\n                </div>\n            )}\n            {explanation && (\n              <div className=\"rounded-lg bg-white/50 p-4 text-sm text-gray-600 dark:bg-[#1a1228]/80 dark:text-[hsl(var(--foreground))]\">\n                <p className=\"font-medium text-purple-500 dark:text-[hsl(var(--neo-link-hover))]\">\n                  Explanation:\n                </p>\n                <p className=\"mt-2 leading-relaxed\">{explanation}</p>\n              </div>\n            )}\n            {mapping && (\n              <div className=\"rounded-lg bg-white/50 p-4 text-sm text-gray-600 dark:bg-[#1a1228]/80 dark:text-[hsl(var(--foreground))]\">\n                <p className=\"font-medium text-purple-500 dark:text-[hsl(var(--neo-link-hover))]\">\n                  Mapping:\n                </p>\n                <pre className=\"mt-2 overflow-x-auto whitespace-pre-wrap leading-relaxed\">\n                  {mapping}\n                </pre>\n              </div>\n            )}\n            {diagram && (\n              <div className=\"rounded-lg bg-white/50 p-4 text-sm text-gray-600 dark:bg-[#1a1228]/80 dark:text-[hsl(var(--foreground))]\">\n                <p className=\"font-medium text-purple-500 dark:text-[hsl(var(--neo-link-hover))]\">\n                  Mermaid.js diagram:\n                </p>\n                <pre className=\"mt-2 overflow-x-auto whitespace-pre-wrap leading-relaxed\">\n                  {diagram}\n                </pre>\n              </div>\n            )}\n            {hasFixTelemetry && (\n              <div className=\"rounded-lg border border-purple-200 bg-white/70 p-4 text-sm text-gray-600 dark:border-[#2d1d4e] dark:bg-[#1a1228]/85 dark:text-[hsl(var(--foreground))]\">\n                <p className=\"font-medium text-purple-500 dark:text-[hsl(var(--neo-link-hover))]\">\n                  Syntax Repair Loop\n                </p>\n                {typeof fixAttempt === \"number\" &&\n                  typeof fixMaxAttempts === \"number\" && (\n                    <p className=\"mt-1 text-xs text-purple-500 dark:text-[hsl(var(--foreground))]\">\n                      Attempt {fixAttempt}/{fixMaxAttempts}\n                    </p>\n                  )}\n                {message && <p className=\"mt-2 leading-relaxed\">{message}</p>}\n                {parserError && (\n                  <pre className=\"mt-3 overflow-x-auto whitespace-pre-wrap rounded-md bg-purple-50 p-3 text-xs text-gray-700 dark:bg-[#130f22] dark:text-[hsl(var(--foreground))]\">\n                    {parserError}\n                  </pre>\n                )}\n                {fixDiagramDraft && (\n                  <div className=\"mt-3\">\n                    <p className=\"mb-2 text-xs font-medium text-purple-500 dark:text-[hsl(var(--neo-link-hover))]\">\n                      Candidate Mermaid fix (streaming)\n                    </p>\n                    <pre className=\"overflow-x-auto whitespace-pre-wrap rounded-md bg-purple-50 p-3 text-xs text-gray-700 dark:bg-[#130f22] dark:text-[hsl(var(--foreground))]\">\n                      {fixDiagramDraft}\n                    </pre>\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/main-card.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Card } from \"~/components/ui/card\";\nimport { Input } from \"~/components/ui/input\";\nimport { Button } from \"~/components/ui/button\";\nimport { Sparkles } from \"lucide-react\";\nimport React from \"react\";\nimport { exampleRepos, isExampleRepo } from \"~/lib/exampleRepos\";\nimport { ExportDropdown } from \"./export-dropdown\";\nimport { ChevronUp, ChevronDown } from \"lucide-react\";\nimport { Switch } from \"~/components/ui/switch\";\nimport { parseGitHubRepoUrl } from \"~/features/diagram/github-url\";\n\ninterface MainCardProps {\n  isHome?: boolean;\n  username?: string;\n  repo?: string;\n  onCopy?: () => void;\n  lastGenerated?: Date;\n  onExportImage?: () => void;\n  onRegenerate?: () => void;\n  zoomingEnabled?: boolean;\n  onZoomToggle?: () => void;\n  loading?: boolean;\n}\n\nexport default function MainCard({\n  isHome = true,\n  username,\n  repo,\n  onCopy,\n  lastGenerated,\n  onExportImage,\n  onRegenerate,\n  zoomingEnabled,\n  onZoomToggle,\n  loading,\n}: MainCardProps) {\n  const [repoUrl, setRepoUrl] = useState(\"\");\n  const [error, setError] = useState(\"\");\n  const [activeDropdown, setActiveDropdown] = useState<\"export\" | null>(null);\n  const router = useRouter();\n  const isExampleRepoSelected =\n    !isHome && !!username && !!repo && isExampleRepo(username, repo);\n\n  useEffect(() => {\n    if (username && repo) {\n      setRepoUrl(`https://github.com/${username}/${repo}`);\n    }\n  }, [username, repo]);\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    setError(\"\");\n\n    const parsed = parseGitHubRepoUrl(repoUrl);\n    if (!parsed) {\n      setError(\"Please enter a valid GitHub repository URL\");\n      return;\n    }\n\n    const { username, repo } = parsed;\n    const sanitizedUsername = encodeURIComponent(username);\n    const sanitizedRepo = encodeURIComponent(repo);\n    router.push(`/${sanitizedUsername}/${sanitizedRepo}`);\n  };\n\n  const handleExampleClick = (repoPath: string, e: React.MouseEvent) => {\n    e.preventDefault();\n    router.push(repoPath);\n  };\n\n  const handleDropdownToggle = (dropdown: \"export\") => {\n    setActiveDropdown(activeDropdown === dropdown ? null : dropdown);\n  };\n\n  return (\n    <Card className=\"neo-panel relative w-full max-w-3xl !bg-[hsl(var(--neo-panel))] p-4 sm:p-8\">\n      <form onSubmit={handleSubmit} className=\"space-y-4 sm:space-y-6\">\n        <div className=\"flex flex-col gap-3 sm:flex-row sm:gap-4\">\n          <Input\n            placeholder=\"https://github.com/username/repo\"\n            className=\"neo-input flex-1 rounded-md px-3 py-4 text-base font-bold placeholder:text-base placeholder:font-normal placeholder:text-gray-700 sm:px-4 sm:py-6 sm:text-lg sm:placeholder:text-lg dark:placeholder:text-neutral-400\"\n            value={repoUrl}\n            onChange={(e) => setRepoUrl(e.target.value)}\n            required\n          />\n          <Button\n            type=\"submit\"\n            className=\"neo-button p-4 px-4 text-base sm:p-6 sm:px-6 sm:text-lg\"\n          >\n            Diagram\n          </Button>\n        </div>\n\n        {error && <p className=\"text-sm text-red-600\">{error}</p>}\n\n        {/* Dropdowns Container */}\n        {!isHome && (\n          <div className=\"space-y-4\">\n            {/* Only show buttons and dropdowns when not loading */}\n            {!loading && (\n              <>\n                {/* Buttons Container */}\n                <div className=\"flex flex-col items-center gap-4 sm:flex-row sm:gap-4\">\n                  {onRegenerate && (\n                    <button\n                      type=\"button\"\n                      disabled={isExampleRepoSelected}\n                      title={\n                        isExampleRepoSelected\n                          ? \"Regeneration is disabled for example repositories.\"\n                          : undefined\n                      }\n                      className={`flex items-center justify-between gap-2 rounded-md border-[3px] border-black px-4 py-2 font-medium text-black transition-colors sm:max-w-[250px] dark:text-black ${\n                        isExampleRepoSelected\n                          ? \"cursor-not-allowed bg-purple-200 opacity-70 dark:bg-[#251b3a] dark:text-[hsl(var(--foreground))]\"\n                          : \"bg-purple-300 hover:bg-purple-400 dark:border-[#2d1d4e] dark:bg-[hsl(var(--neo-subtle-muted))] dark:hover:bg-[hsl(var(--neo-subtle))]\"\n                      }`}\n                      onClick={(e) => {\n                        e.preventDefault();\n                        setActiveDropdown(null);\n                        if (isExampleRepoSelected) return;\n                        onRegenerate();\n                      }}\n                    >\n                      Regenerate Diagram\n                    </button>\n                  )}\n                  {onCopy && lastGenerated && onExportImage && (\n                    <div className=\"flex flex-col items-center justify-center gap-2\">\n                      <button\n                        onClick={(e) => {\n                          e.preventDefault();\n                          handleDropdownToggle(\"export\");\n                        }}\n                        className={`flex cursor-pointer items-center justify-between gap-2 rounded-md border-[3px] border-black px-4 py-2 font-medium text-black transition-colors sm:max-w-[250px] dark:text-black ${\n                          activeDropdown === \"export\"\n                            ? \"bg-purple-400 dark:border-[#2d1d4e] dark:bg-[hsl(var(--neo-button))]\"\n                            : \"bg-purple-300 hover:bg-purple-400 dark:border-[#2d1d4e] dark:bg-[hsl(var(--neo-subtle-muted))] dark:hover:bg-[hsl(var(--neo-button-hover))]\"\n                        }`}\n                      >\n                        <span>Export Diagram</span>\n                        {activeDropdown === \"export\" ? (\n                          <ChevronUp size={20} />\n                        ) : (\n                          <ChevronDown size={20} />\n                        )}\n                      </button>\n                    </div>\n                  )}\n                  {lastGenerated && (\n                    <>\n                      <label\n                        htmlFor=\"zoom-toggle\"\n                        className=\"font-medium text-black dark:text-neutral-100\"\n                      >\n                        Enable Zoom\n                      </label>\n                      <Switch\n                        id=\"zoom-toggle\"\n                        checked={zoomingEnabled}\n                        onCheckedChange={onZoomToggle}\n                      />\n                    </>\n                  )}\n                </div>\n\n                {/* Dropdown Content */}\n                <div\n                  className={`transition-all duration-200 ${\n                    activeDropdown\n                      ? \"pointer-events-auto max-h-[500px] opacity-100\"\n                      : \"pointer-events-none max-h-0 opacity-0\"\n                  }`}\n                >\n                  {activeDropdown === \"export\" && (\n                    <ExportDropdown\n                      onCopy={onCopy!}\n                      lastGenerated={lastGenerated!}\n                      onExportImage={onExportImage!}\n                      isOpen={true}\n                    />\n                  )}\n                </div>\n              </>\n            )}\n          </div>\n        )}\n\n        {/* Example Repositories */}\n        {isHome && (\n          <div className=\"space-y-2\">\n            <div className=\"text-sm text-gray-700 dark:text-neutral-300 sm:text-base\">\n              Try these example repositories:\n            </div>\n            <div className=\"flex flex-wrap gap-2\">\n              {Object.entries(exampleRepos).map(([name, path]) => (\n                <Button\n                  key={name}\n                  variant=\"outline\"\n                  className=\"border-2 border-black bg-purple-400 text-sm text-black transition-transform hover:-translate-y-0.5 hover:transform hover:bg-purple-300 dark:border-black dark:bg-[hsl(var(--neo-panel-muted))] dark:text-[hsl(var(--foreground))] dark:hover:bg-[hsl(var(--neo-button))] dark:hover:text-[#0d0a19] sm:text-base\"\n                  onClick={(e) => handleExampleClick(path, e)}\n                >\n                  {name}\n                </Button>\n              ))}\n            </div>\n          </div>\n        )}\n      </form>\n\n      {/* Decorative Sparkle */}\n      <div className=\"absolute -bottom-8 -left-12 hidden sm:block\">\n        <Sparkles\n          className=\"h-20 w-20 fill-sky-400 text-black dark:fill-[hsl(var(--neo-button))] dark:text-[hsl(var(--background))]\"\n          strokeWidth={0.6}\n          style={{ transform: \"rotate(-15deg)\" }}\n        />\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/components/mermaid-diagram.test.tsx",
    "content": "import { render, screen } from \"@testing-library/react\";\nimport React from \"react\";\nimport { describe, expect, it, vi } from \"vitest\";\n\nimport MermaidChart from \"~/components/mermaid-diagram\";\n\nvi.mock(\"mermaid\", () => ({\n  default: {\n    initialize: vi.fn(),\n    registerLayoutLoaders: vi.fn(),\n    render: vi.fn().mockResolvedValue({ svg: \"<svg></svg>\" }),\n  },\n}));\n\ndescribe(\"MermaidChart\", () => {\n  it(\"renders chart container\", () => {\n    const { container } = render(\n      <MermaidChart chart=\"flowchart TD\\nA-->B\" zoomingEnabled={false} />,\n    );\n\n    expect(container.querySelector(\".mermaid\")).toBeInTheDocument();\n    expect(screen.queryByText(/Mermaid render failed:/)).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "src/components/mermaid-diagram.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport mermaid from \"mermaid\";\nimport elkLayouts from \"@mermaid-js/layout-elk\";\nimport { useTheme } from \"next-themes\";\n\ninterface MermaidChartProps {\n  chart: string;\n  zoomingEnabled?: boolean;\n}\n\ntype SvgPanZoomInstance = {\n  destroy: () => void;\n};\n\nlet elkLayoutRegistered = false;\nlet domToJsonPatched = false;\n\nfunction ensureDomNodesSerializeSafely() {\n  if (domToJsonPatched || typeof window === \"undefined\") return;\n\n  const elementProto = window.Element?.prototype;\n  if (!elementProto || \"toJSON\" in elementProto) {\n    domToJsonPatched = true;\n    return;\n  }\n\n  Object.defineProperty(elementProto, \"toJSON\", {\n    configurable: true,\n    value: function toJSON(this: Element) {\n      return {\n        tagName: this.tagName,\n        id: this.id || undefined,\n        className:\n          typeof this.className === \"string\" ? this.className : undefined,\n      };\n    },\n  });\n\n  domToJsonPatched = true;\n}\n\nconst MermaidChart = ({ chart, zoomingEnabled = true }: MermaidChartProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const panZoomRef = useRef<SvgPanZoomInstance | null>(null);\n  const [renderMessage, setRenderMessage] = useState<string | null>(null);\n  const { resolvedTheme } = useTheme();\n  const isDark = resolvedTheme === \"dark\";\n\n  useEffect(() => {\n    ensureDomNodesSerializeSafely();\n\n    if (!elkLayoutRegistered) {\n      mermaid.registerLayoutLoaders(elkLayouts);\n      elkLayoutRegistered = true;\n    }\n\n    const baseConfig = {\n      startOnLoad: false,\n      suppressErrorRendering: true,\n      securityLevel: \"loose\" as const,\n      theme: \"base\" as const,\n      htmlLabels: true,\n      flowchart: {\n        defaultRenderer: \"elk\" as const,\n        curve: \"linear\" as const,\n        nodeSpacing: 50,\n        rankSpacing: 50,\n        padding: 15,\n      },\n      themeVariables: isDark\n        ? {\n            background: \"#1f2631\",\n            primaryColor: \"#2c3544\",\n            primaryBorderColor: \"#6dd4e9\",\n            primaryTextColor: \"#e8edf5\",\n            lineColor: \"#ffd486\",\n            secondaryColor: \"#26303f\",\n            tertiaryColor: \"#323d4d\",\n          }\n        : {\n            background: \"#ffffff\",\n            primaryColor: \"#f7f7f7\",\n            primaryBorderColor: \"#000000\",\n            primaryTextColor: \"#171717\",\n            lineColor: \"#000000\",\n            secondaryColor: \"#f0f0f0\",\n            tertiaryColor: \"#f7f7f7\",\n          },\n      themeCSS: `\n        .clickable {\n          transition: transform 0.2s ease;\n        }\n        .clickable:hover {\n          transform: scale(1.05);\n          cursor: pointer;\n        }\n        .clickable:hover > * {\n          filter: brightness(0.85);\n        }\n      `,\n      };\n\n    const initializeMermaid = () => {\n      mermaid.initialize({\n        ...baseConfig,\n      });\n    };\n\n    const renderDiagram = async () => {\n      const mermaidElement = containerRef.current?.querySelector(\".mermaid\");\n      if (!(mermaidElement instanceof HTMLDivElement)) return;\n\n      setRenderMessage(null);\n      panZoomRef.current?.destroy();\n      panZoomRef.current = null;\n\n      const applyPanZoom = async () => {\n        const svgElement = containerRef.current?.querySelector(\"svg\");\n        if (!(svgElement instanceof SVGSVGElement) || !zoomingEnabled) return;\n\n        svgElement.style.maxWidth = \"none\";\n        svgElement.style.width = \"100%\";\n        svgElement.style.height = \"100%\";\n\n        try {\n          const svgPanZoom = (await import(\"svg-pan-zoom\")).default;\n          panZoomRef.current = svgPanZoom(svgElement, {\n            zoomEnabled: true,\n            controlIconsEnabled: true,\n            fit: true,\n            center: true,\n            minZoom: 0.1,\n            maxZoom: 10,\n            zoomScaleSensitivity: 0.3,\n          }) as SvgPanZoomInstance;\n        } catch (error) {\n          console.error(\"Failed to load svg-pan-zoom:\", error);\n        }\n      };\n\n      initializeMermaid();\n      mermaidElement.removeAttribute(\"data-processed\");\n      mermaidElement.textContent = \"\";\n\n      try {\n        const renderId = `gitdiagram-${Math.random().toString(36).slice(2)}`;\n        const { svg, bindFunctions } = await mermaid.render(\n          renderId,\n          chart,\n          mermaidElement,\n        );\n        mermaidElement.innerHTML = svg;\n        bindFunctions?.(mermaidElement);\n        await applyPanZoom();\n        return;\n      } catch (error) {\n        console.error(\"Mermaid render failed:\", error);\n        const message =\n          error instanceof Error ? error.message : \"Unknown Mermaid render error.\";\n        setRenderMessage(`Mermaid render failed: ${message}`);\n      }\n    };\n\n    void renderDiagram();\n\n    return () => {\n      panZoomRef.current?.destroy();\n      panZoomRef.current = null;\n    };\n  }, [chart, zoomingEnabled, isDark]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={`w-full max-w-full p-4 ${zoomingEnabled ? \"h-[600px]\" : \"\"}`}\n    >\n      {renderMessage && (\n        <div className=\"mb-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200\">\n          {renderMessage}\n        </div>\n      )}\n      <div\n        key={`${chart}-${zoomingEnabled}-${resolvedTheme ?? \"light\"}`}\n        className={`mermaid h-full text-foreground ${\n          zoomingEnabled\n            ? \"rounded-lg border-2 border-black bg-white dark:border-[#3b4656] dark:bg-[#1f2631]\"\n            : \"\"\n        }`}\n      >\n        {chart}\n      </div>\n    </div>\n  );\n};\n\nexport default MermaidChart;\n"
  },
  {
    "path": "src/components/private-repos-dialog.tsx",
    "content": "\"use client\";\n\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"./ui/dialog\";\nimport { Input } from \"./ui/input\";\nimport { Button } from \"./ui/button\";\nimport { useState, useEffect } from \"react\";\nimport Link from \"next/link\";\n\ninterface PrivateReposDialogProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onSubmit: (pat: string) => void;\n}\n\nexport function PrivateReposDialog({\n  isOpen,\n  onClose,\n  onSubmit,\n}: PrivateReposDialogProps) {\n  const [pat, setPat] = useState<string>(\"\");\n\n  useEffect(() => {\n    const storedPat = localStorage.getItem(\"github_pat\");\n    if (storedPat) {\n      setPat(storedPat);\n    }\n  }, []);\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    onSubmit(pat);\n    setPat(\"\");\n  };\n\n  const handleClear = () => {\n    localStorage.removeItem(\"github_pat\");\n    setPat(\"\");\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"neo-panel p-6 sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle className=\"text-xl font-bold text-black dark:text-neutral-100\">\n            Enter GitHub Personal Access Token\n          </DialogTitle>\n        </DialogHeader>\n        <form\n          onSubmit={handleSubmit}\n          className=\"space-y-4 text-black dark:text-neutral-200\"\n        >\n          <div className=\"text-sm\">\n            To enable private repositories, you&apos;ll need to provide a GitHub\n            Personal Access Token with repo scope. The token will be stored\n            locally in your browser. Find out how{\" \"}\n            <Link\n              href=\"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\"\n              className=\"neo-link\"\n            >\n              here\n            </Link>\n            .\n          </div>\n          <details className=\"group text-sm [&>summary:focus-visible]:outline-none\">\n            <summary className=\"neo-link cursor-pointer font-medium\">\n              Data storage disclaimer\n            </summary>\n            <div className=\"animate-accordion-down mt-2 space-y-2 overflow-hidden pl-2\">\n              <p>\n                Take note that the diagram data will be stored in my database\n                (not that I would use it for anything anyways). You can also\n                self-host this app by following the instructions in the{\" \"}\n                <Link\n                  href=\"https://github.com/ahmedkhaleel2004/gitdiagram\"\n                  className=\"neo-link\"\n                >\n                  README\n                </Link>\n                .\n              </p>\n            </div>\n          </details>\n          <Input\n            type=\"password\"\n            placeholder=\"ghp_...\"\n            value={pat}\n            onChange={(e) => setPat(e.target.value)}\n            className=\"neo-input flex-1 rounded-md px-3 py-2 text-base font-bold placeholder:text-base placeholder:font-normal placeholder:text-gray-700 dark:placeholder:text-neutral-400\"\n            required\n          />\n          <div className=\"flex items-center justify-between\">\n            <button\n              type=\"button\"\n              onClick={handleClear}\n              className=\"neo-link text-sm\"\n            >\n              Clear\n            </button>\n            <div className=\"flex gap-3\">\n              <Button\n                type=\"button\"\n                onClick={onClose}\n                className=\"neo-button-muted px-4 py-2\"\n              >\n                Cancel\n              </Button>\n              <Button\n                type=\"submit\"\n                disabled={!pat.startsWith(\"ghp_\")}\n                className=\"neo-button px-4 py-2 disabled:opacity-50\"\n              >\n                Save Token\n              </Button>\n            </div>\n          </div>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\n\nexport function ThemeToggle() {\n  const { resolvedTheme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <button\n        type=\"button\"\n        className=\"text-sm font-medium text-black transition-transform hover:translate-y-[-2px] hover:text-purple-600\"\n      >\n        Dark\n      </button>\n    );\n  }\n\n  const isDark = resolvedTheme === \"dark\";\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setTheme(isDark ? \"light\" : \"dark\")}\n      aria-label={isDark ? \"Switch to light mode\" : \"Switch to dark mode\"}\n      className=\"text-sm font-medium text-black transition-transform hover:translate-y-[-2px] hover:text-purple-600 dark:text-neutral-200 dark:hover:text-[hsl(var(--neo-link-hover))]\"\n    >\n      {isDark ? \"Light\" : \"Dark\"}\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "src/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className,\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "src/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "src/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative h-4 w-full overflow-hidden rounded-full bg-black\",\n      className,\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-green-500 transition-all\"\n      style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n"
  },
  {
    "path": "src/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner } from \"sonner\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"toast !bg-purple-100 dark:!bg-[#1a1228] !text-black dark:!text-[hsl(var(--foreground))] !shadow-[3px_3px_0_0_#000000] dark:!shadow-[3px_3px_0_0_#0d0a19] !border-[2px] !border-black dark:!border-[#2d1d4e] !rounded-md !p-3 !flex !items-center !justify-between !gap-4\",\n          title: \"font-bold text-base m-0\",\n          description: \"text-muted-foreground dark:!text-[hsl(var(--muted-foreground))]\",\n          actionButton:\n            \"!bg-purple-200 dark:!bg-[hsl(var(--neo-button))] !border-[2px] !border-solid !border-black dark:!border-[#2d1d4e] !py-[14px] !px-6 !text-lg !text-black hover:!bg-purple-300 dark:hover:!bg-[hsl(var(--neo-button-hover))] !transition-colors !cursor-pointer\",\n          cancelButton:\n            \"text-neutral-500 underline hover:text-neutral-700 dark:text-[hsl(var(--muted-foreground))] dark:hover:text-[hsl(var(--foreground))]\",\n        },\n        duration: 5000,\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "src/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n    <SwitchPrimitives.Root\n      className={cn(\n        \"peer inline-flex h-8 w-16 shrink-0 cursor-pointer items-center rounded-full border-2 border-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-purple-500 data-[state=unchecked]:bg-purple-300 dark:data-[state=checked]:bg-[hsl(var(--neo-button))] dark:data-[state=unchecked]:bg-[#251b3a]\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n      <SwitchPrimitives.Thumb\n        className={cn(\n          \"pointer-events-none block h-6 w-6 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-[34px] data-[state=unchecked]:translate-x-[2px] dark:bg-neutral-950\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst Textarea = React.forwardRef<\n  HTMLTextAreaElement,\n  React.ComponentProps<\"textarea\">\n>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  );\n});\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "src/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"~/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md\",\n      \"duration-300 animate-in fade-in-0 zoom-in-95\",\n      \"duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n      \"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "src/env.js",
    "content": "import { createEnv } from \"@t3-oss/env-nextjs\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  /**\n   * Specify your server-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars.\n   */\n  server: {\n    POSTGRES_URL: z.string().url(),\n    NODE_ENV: z\n      .enum([\"development\", \"test\", \"production\"])\n      .default(\"development\"),\n  },\n\n  /**\n   * Specify your client-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars. To expose them to the client, prefix them with\n   * `NEXT_PUBLIC_`.\n   */\n  client: {\n    // NEXT_PUBLIC_CLIENTVAR: z.string(),\n  },\n\n  /**\n   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.\n   * middlewares) or client-side so we need to destruct manually.\n   */\n  runtimeEnv: {\n    POSTGRES_URL: process.env.POSTGRES_URL,\n    NODE_ENV: process.env.NODE_ENV,\n    // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,\n  },\n  /**\n   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially\n   * useful for Docker builds.\n   */\n  skipValidation: !!process.env.SKIP_ENV_VALIDATION,\n  /**\n   * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and\n   * `SOME_VAR=''` will throw an error.\n   */\n  emptyStringAsUndefined: true,\n});\n"
  },
  {
    "path": "src/features/diagram/api.ts",
    "content": "import { parseSSEStreamBuffer } from \"~/features/diagram/sse\";\nimport type {\n  DiagramCostResponse,\n  DiagramStreamMessage,\n  StreamGenerationParams,\n} from \"~/features/diagram/types\";\n\ninterface StreamHandlers {\n  onMessage: (\n    message: DiagramStreamMessage,\n  ) => boolean | void | Promise<boolean | void>;\n}\n\nconst getGenerateBasePath = () => {\n  const useLegacyBackend =\n    process.env.NEXT_PUBLIC_USE_LEGACY_BACKEND?.trim() === \"true\";\n  if (!useLegacyBackend) {\n    return \"/api/generate\";\n  }\n\n  const legacyApiBase = process.env.NEXT_PUBLIC_API_DEV_URL?.trim();\n  if (legacyApiBase) {\n    return `${legacyApiBase.replace(/\\/$/, \"\")}/generate`;\n  }\n  return \"/api/generate\";\n};\n\nexport async function getGenerationCost(\n  username: string,\n  repo: string,\n  githubPat?: string,\n  apiKey?: string,\n): Promise<DiagramCostResponse> {\n  try {\n    const response = await fetch(`${getGenerateBasePath()}/cost`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        username,\n        repo,\n        api_key: apiKey,\n        github_pat: githubPat,\n      }),\n    });\n\n    if (response.status === 429) {\n      return { error: \"Rate limit exceeded. Please try again later.\" };\n    }\n\n    const data = (await response.json()) as DiagramCostResponse;\n    return {\n      cost: data.cost,\n      error: data.error,\n      error_code: data.error_code,\n      ok: data.ok,\n    };\n  } catch {\n    return { error: \"Failed to get cost estimate.\" };\n  }\n}\n\nexport async function streamDiagramGeneration(\n  params: StreamGenerationParams,\n  handlers: StreamHandlers,\n): Promise<void> {\n  const response = await fetch(`${getGenerateBasePath()}/stream`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      username: params.username,\n      repo: params.repo,\n      api_key: params.apiKey,\n      github_pat: params.githubPat,\n    }),\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to start streaming\");\n  }\n\n  const reader = response.body?.getReader();\n  if (!reader) {\n    throw new Error(\"No reader available\");\n  }\n\n  try {\n    let streamBuffer = \"\";\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      streamBuffer += new TextDecoder().decode(value);\n      const { messages, remainder } = parseSSEStreamBuffer(streamBuffer);\n      streamBuffer = remainder;\n      for (const message of messages) {\n        const shouldContinue = await handlers.onMessage(message);\n        if (shouldContinue === false) {\n          return;\n        }\n      }\n    }\n\n    const { messages } = parseSSEStreamBuffer(`${streamBuffer}\\n\\n`);\n    for (const message of messages) {\n      const shouldContinue = await handlers.onMessage(message);\n      if (shouldContinue === false) {\n        return;\n      }\n    }\n  } finally {\n    reader.releaseLock();\n  }\n}\n"
  },
  {
    "path": "src/features/diagram/export.ts",
    "content": "export function exportMermaidSvgAsPng(svgElement: SVGSVGElement): void {\n  const canvas = document.createElement(\"canvas\");\n  const scale = 4;\n\n  const bbox = svgElement.getBBox();\n  const transform = svgElement.getScreenCTM();\n  if (!transform) return;\n\n  const width = Math.ceil(bbox.width * transform.a);\n  const height = Math.ceil(bbox.height * transform.d);\n  canvas.width = width * scale;\n  canvas.height = height * scale;\n\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) return;\n\n  const svgData = new XMLSerializer().serializeToString(svgElement);\n  const img = new Image();\n\n  img.onload = () => {\n    ctx.fillStyle = \"white\";\n    ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n    ctx.scale(scale, scale);\n    ctx.drawImage(img, 0, 0, width, height);\n\n    const anchor = document.createElement(\"a\");\n    anchor.download = \"diagram.png\";\n    anchor.href = canvas.toDataURL(\"image/png\", 1.0);\n    document.body.appendChild(anchor);\n    anchor.click();\n    document.body.removeChild(anchor);\n  };\n\n  img.src =\n    \"data:image/svg+xml;base64,\" +\n    btoa(unescape(encodeURIComponent(svgData)));\n}\n"
  },
  {
    "path": "src/features/diagram/github-url.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { parseGitHubRepoUrl } from \"~/features/diagram/github-url\";\n\ndescribe(\"parseGitHubRepoUrl\", () => {\n  it(\"parses valid repository urls\", () => {\n    expect(parseGitHubRepoUrl(\"https://github.com/vercel/next.js\")).toEqual({\n      username: \"vercel\",\n      repo: \"next.js\",\n    });\n  });\n\n  it(\"returns null for invalid urls\", () => {\n    expect(parseGitHubRepoUrl(\"https://gitlab.com/vercel/next.js\")).toBeNull();\n    expect(parseGitHubRepoUrl(\"not-a-url\")).toBeNull();\n  });\n});\n"
  },
  {
    "path": "src/features/diagram/github-url.ts",
    "content": "export interface ParsedGitHubRepo {\n  username: string;\n  repo: string;\n}\n\nconst GITHUB_URL_PATTERN =\n  /^https?:\\/\\/github\\.com\\/([a-zA-Z0-9-_]+)\\/([a-zA-Z0-9-_.]+)\\/?$/;\n\nexport function parseGitHubRepoUrl(url: string): ParsedGitHubRepo | null {\n  const match = GITHUB_URL_PATTERN.exec(url.trim());\n  if (!match) return null;\n\n  const [, username, repo] = match;\n  if (!username || !repo) return null;\n\n  return { username, repo };\n}\n"
  },
  {
    "path": "src/features/diagram/sse.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { parseSSEChunk, parseSSEStreamBuffer } from \"~/features/diagram/sse\";\n\ndescribe(\"parseSSEChunk\", () => {\n  it(\"parses valid SSE data lines\", () => {\n    const chunk =\n      'data: {\"status\":\"started\",\"message\":\"Starting\"}\\n\\n' +\n      'data: {\"status\":\"diagram_chunk\",\"chunk\":\"flowchart TD\"}\\n\\n';\n\n    const messages = parseSSEChunk(chunk);\n\n    expect(messages).toHaveLength(2);\n    expect(messages[0]?.status).toBe(\"started\");\n    expect(messages[1]?.chunk).toBe(\"flowchart TD\");\n  });\n\n  it(\"ignores malformed lines\", () => {\n    const chunk =\n      \"event: custom\\n\" +\n      \"data: {not-json}\\n\" +\n      'data: {\"status\":\"complete\"}\\n';\n\n    const messages = parseSSEChunk(chunk);\n\n    expect(messages).toHaveLength(1);\n    expect(messages[0]?.status).toBe(\"complete\");\n  });\n\n  it(\"handles events split across network boundaries\", () => {\n    const firstHalf = 'data: {\"status\":\"diagram_fix_attempt\",\"message\":\"Attempt 1';\n    const secondHalf = '/3\"}\\n\\n';\n\n    const firstPass = parseSSEStreamBuffer(firstHalf);\n    expect(firstPass.messages).toHaveLength(0);\n\n    const secondPass = parseSSEStreamBuffer(firstPass.remainder + secondHalf);\n    expect(secondPass.messages).toHaveLength(1);\n    expect(secondPass.messages[0]?.status).toBe(\"diagram_fix_attempt\");\n    expect(secondPass.messages[0]?.message).toBe(\"Attempt 1/3\");\n  });\n});\n"
  },
  {
    "path": "src/features/diagram/sse.ts",
    "content": "import type { DiagramStreamMessage } from \"~/features/diagram/types\";\n\nexport function parseSSEChunk(chunk: string): DiagramStreamMessage[] {\n  const messages: DiagramStreamMessage[] = [];\n  const lines = chunk.split(/\\r?\\n/);\n\n  for (const line of lines) {\n    if (!line.startsWith(\"data:\")) continue;\n    const payload = line.slice(5).trim();\n    if (!payload) continue;\n    try {\n      const parsed = JSON.parse(payload) as DiagramStreamMessage;\n      messages.push(parsed);\n    } catch {\n      // Ignore malformed chunks.\n    }\n  }\n\n  return messages;\n}\n\nexport function parseSSEStreamBuffer(buffer: string): {\n  messages: DiagramStreamMessage[];\n  remainder: string;\n} {\n  const messages: DiagramStreamMessage[] = [];\n  const normalized = buffer.replace(/\\r\\n/g, \"\\n\");\n  const rawEvents = normalized.split(\"\\n\\n\");\n  const remainder = rawEvents.pop() ?? \"\";\n\n  for (const rawEvent of rawEvents) {\n    if (!rawEvent.trim()) continue;\n    messages.push(...parseSSEChunk(rawEvent));\n  }\n\n  return { messages, remainder };\n}\n"
  },
  {
    "path": "src/features/diagram/types.ts",
    "content": "export type DiagramStreamStatus =\n  | \"idle\"\n  | \"started\"\n  | \"explanation_sent\"\n  | \"explanation\"\n  | \"explanation_chunk\"\n  | \"mapping_sent\"\n  | \"mapping\"\n  | \"mapping_chunk\"\n  | \"diagram_sent\"\n  | \"diagram\"\n  | \"diagram_chunk\"\n  | \"diagram_fixing\"\n  | \"diagram_fix_attempt\"\n  | \"diagram_fix_chunk\"\n  | \"diagram_fix_validating\"\n  | \"complete\"\n  | \"error\";\n\nexport interface DiagramStreamState {\n  status: DiagramStreamStatus;\n  message?: string;\n  explanation?: string;\n  mapping?: string;\n  diagram?: string;\n  error?: string;\n  errorCode?: string;\n  parserError?: string;\n  fixAttempt?: number;\n  fixMaxAttempts?: number;\n  fixDiagramDraft?: string;\n}\n\nexport interface DiagramStreamMessage {\n  status: DiagramStreamStatus;\n  message?: string;\n  chunk?: string;\n  explanation?: string;\n  mapping?: string;\n  diagram?: string;\n  error?: string;\n  error_code?: string;\n  parser_error?: string;\n  fix_attempt?: number;\n  fix_max_attempts?: number;\n}\n\nexport interface DiagramCostResponse {\n  cost?: string;\n  error?: string;\n  error_code?: string;\n  ok?: boolean;\n}\n\nexport interface StreamGenerationParams {\n  username: string;\n  repo: string;\n  apiKey?: string;\n  githubPat?: string;\n}\n"
  },
  {
    "path": "src/hooks/diagram/useDiagramExport.ts",
    "content": "import { useCallback } from \"react\";\n\nimport { exportMermaidSvgAsPng } from \"~/features/diagram/export\";\n\nexport function useDiagramExport(diagram: string) {\n  const handleCopy = useCallback(async () => {\n    await navigator.clipboard.writeText(diagram);\n  }, [diagram]);\n\n  const handleExportImage = useCallback(() => {\n    const svgElement = document.querySelector(\".mermaid svg\");\n    if (!(svgElement instanceof SVGSVGElement)) return;\n\n    exportMermaidSvgAsPng(svgElement);\n  }, []);\n\n  return {\n    handleCopy,\n    handleExportImage,\n  };\n}\n"
  },
  {
    "path": "src/hooks/diagram/useDiagramStream.test.ts",
    "content": "import { act, renderHook } from \"@testing-library/react\";\nimport { describe, expect, it, vi } from \"vitest\";\n\nimport { useDiagramStream } from \"~/hooks/diagram/useDiagramStream\";\n\nvi.mock(\"~/features/diagram/api\", () => ({\n  streamDiagramGeneration: vi.fn(async (_params, handlers) => {\n    await handlers.onMessage({ status: \"started\", message: \"starting\" });\n    await handlers.onMessage({ status: \"explanation_chunk\", chunk: \"Repo details\" });\n    await handlers.onMessage({\n      status: \"complete\",\n      diagram: \"flowchart TD\\nA-->B\",\n      explanation: \"done\",\n    });\n  }),\n}));\n\ndescribe(\"useDiagramStream\", () => {\n  it(\"updates state through stream lifecycle\", async () => {\n    const onComplete = vi.fn(async () => undefined);\n    const onError = vi.fn();\n\n    const { result } = renderHook(() =>\n      useDiagramStream({\n        username: \"acme\",\n        repo: \"demo\",\n        onComplete,\n        onError,\n      }),\n    );\n\n    await act(async () => {\n      await result.current.runGeneration();\n    });\n\n    expect(result.current.state.status).toBe(\"complete\");\n    expect(result.current.state.diagram).toContain(\"flowchart TD\");\n    expect(onComplete).toHaveBeenCalledTimes(1);\n    expect(onError).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/hooks/diagram/useDiagramStream.ts",
    "content": "import { useCallback, useState } from \"react\";\n\nimport { streamDiagramGeneration } from \"~/features/diagram/api\";\nimport type {\n  DiagramStreamMessage,\n  DiagramStreamState,\n} from \"~/features/diagram/types\";\n\ninterface UseDiagramStreamOptions {\n  username: string;\n  repo: string;\n  onComplete: (result: { diagram: string; explanation: string }) => Promise<void>;\n  onError: (message: string) => void;\n}\n\nexport function useDiagramStream({\n  username,\n  repo,\n  onComplete,\n  onError,\n}: UseDiagramStreamOptions) {\n  const [state, setState] = useState<DiagramStreamState>({ status: \"idle\" });\n\n  const handleStreamMessage = useCallback(\n    async (\n      data: DiagramStreamMessage,\n      buffers: {\n        explanation: string;\n        mapping: string;\n        diagram: string;\n        fixDiagramDraft: string;\n      },\n    ) => {\n      if (data.error) {\n        setState({\n          status: \"error\",\n          error: data.error,\n          errorCode: data.error_code,\n          parserError: data.parser_error,\n        });\n        onError(data.error);\n        return false;\n      }\n\n      switch (data.status) {\n        case \"started\":\n        case \"explanation_sent\":\n        case \"explanation\":\n        case \"mapping_sent\":\n        case \"mapping\":\n        case \"diagram_sent\":\n        case \"diagram\":\n        case \"diagram_fixing\":\n        case \"diagram_fix_attempt\":\n        case \"diagram_fix_validating\":\n          setState((prev) => ({\n            ...prev,\n            status: data.status,\n            message: data.message,\n            parserError: data.parser_error,\n            fixAttempt: data.fix_attempt,\n            fixMaxAttempts: data.fix_max_attempts,\n            ...(data.status === \"diagram_fix_attempt\"\n              ? { fixDiagramDraft: \"\" }\n              : {}),\n          }));\n          break;\n        case \"diagram_fix_chunk\":\n          if (data.chunk) {\n            buffers.fixDiagramDraft += data.chunk;\n            setState((prev) => ({\n              ...prev,\n              status: \"diagram_fix_chunk\",\n              fixDiagramDraft: buffers.fixDiagramDraft,\n              fixAttempt: data.fix_attempt ?? prev.fixAttempt,\n              fixMaxAttempts: data.fix_max_attempts ?? prev.fixMaxAttempts,\n            }));\n          }\n          break;\n        case \"explanation_chunk\":\n          if (data.chunk) {\n            buffers.explanation += data.chunk;\n            setState((prev) => ({\n              ...prev,\n              status: \"explanation_chunk\",\n              explanation: buffers.explanation,\n            }));\n          }\n          break;\n        case \"mapping_chunk\":\n          if (data.chunk) {\n            buffers.mapping += data.chunk;\n            setState((prev) => ({\n              ...prev,\n              status: \"mapping_chunk\",\n              mapping: buffers.mapping,\n            }));\n          }\n          break;\n        case \"diagram_chunk\":\n          if (data.chunk) {\n            buffers.diagram += data.chunk;\n            setState((prev) => ({\n              ...prev,\n              status: \"diagram_chunk\",\n              diagram: buffers.diagram,\n            }));\n          }\n          break;\n        case \"complete\": {\n          const explanation = data.explanation ?? buffers.explanation;\n          const diagram = data.diagram ?? buffers.diagram;\n          setState({\n            status: \"complete\",\n            explanation,\n            diagram,\n            mapping: data.mapping ?? buffers.mapping,\n          });\n          await onComplete({ explanation, diagram });\n          return false;\n        }\n        case \"error\":\n          setState({\n            status: \"error\",\n            error: data.error,\n            parserError: data.parser_error,\n          });\n          if (data.error) onError(data.error);\n          return false;\n      }\n\n      return true;\n    },\n    [onComplete, onError],\n  );\n\n  const runGeneration = useCallback(\n    async (githubPat?: string) => {\n      setState({ status: \"started\", message: \"Starting generation process...\" });\n      const buffers = {\n        explanation: \"\",\n        mapping: \"\",\n        diagram: \"\",\n        fixDiagramDraft: \"\",\n      };\n\n      await streamDiagramGeneration(\n        {\n          username,\n          repo,\n          apiKey: localStorage.getItem(\"openai_key\") ?? undefined,\n          githubPat,\n        },\n        {\n          onMessage: (message) => handleStreamMessage(message, buffers),\n        },\n      );\n    },\n    [handleStreamMessage, repo, username],\n  );\n\n  return {\n    state,\n    runGeneration,\n    setState,\n  };\n}\n"
  },
  {
    "path": "src/hooks/useDiagram.ts",
    "content": "import { useState, useEffect, useCallback } from \"react\";\n\nimport {\n  cacheDiagramAndExplanation,\n  getCachedDiagram,\n} from \"~/app/_actions/cache\";\nimport { getLastGeneratedDate } from \"~/app/_actions/repo\";\nimport { getGenerationCost } from \"~/features/diagram/api\";\nimport { type DiagramStreamState } from \"~/features/diagram/types\";\nimport { useDiagramStream } from \"~/hooks/diagram/useDiagramStream\";\nimport { useDiagramExport } from \"~/hooks/diagram/useDiagramExport\";\nimport { isExampleRepo } from \"~/lib/exampleRepos\";\n\nexport function useDiagram(username: string, repo: string) {\n  const [diagram, setDiagram] = useState<string>(\"\");\n  const [error, setError] = useState<string>(\"\");\n  const [loading, setLoading] = useState<boolean>(true);\n  const [lastGenerated, setLastGenerated] = useState<Date | undefined>();\n  const [cost, setCost] = useState<string>(\"\");\n  const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);\n  const [hasUsedFreeGeneration, setHasUsedFreeGeneration] = useState<boolean>(\n    () => {\n      if (typeof window === \"undefined\") return false;\n      return localStorage.getItem(\"has_used_free_generation\") === \"true\";\n    },\n  );\n\n  const onStreamComplete = useCallback(\n    async ({\n      diagram: nextDiagram,\n      explanation,\n    }: {\n      diagram: string;\n      explanation: string;\n    }) => {\n      const hasApiKey = !!localStorage.getItem(\"openai_key\");\n      await cacheDiagramAndExplanation(\n        username,\n        repo,\n        nextDiagram,\n        explanation || \"No explanation provided\",\n        hasApiKey,\n      );\n\n      setDiagram(nextDiagram);\n      const date = await getLastGeneratedDate(username, repo);\n      setLastGenerated(date ?? undefined);\n      if (!hasUsedFreeGeneration) {\n        localStorage.setItem(\"has_used_free_generation\", \"true\");\n        setHasUsedFreeGeneration(true);\n      }\n      setLoading(false);\n    },\n    [hasUsedFreeGeneration, repo, username],\n  );\n\n  const onStreamError = useCallback((message: string) => {\n    setError(message);\n    setLoading(false);\n  }, []);\n\n  const { state, runGeneration } = useDiagramStream({\n    username,\n    repo,\n    onComplete: onStreamComplete,\n    onError: onStreamError,\n  });\n\n  useEffect(() => {\n    if (state.status === \"error\") {\n      setLoading(false);\n    }\n  }, [state.status]);\n\n  const getDiagram = useCallback(async () => {\n    setLoading(true);\n    setError(\"\");\n    setCost(\"\");\n\n    try {\n      const cached = await getCachedDiagram(username, repo);\n      const githubPat = localStorage.getItem(\"github_pat\");\n      const apiKey = localStorage.getItem(\"openai_key\");\n\n      if (cached) {\n        setDiagram(cached);\n        const date = await getLastGeneratedDate(username, repo);\n        setLastGenerated(date ?? undefined);\n        setLoading(false);\n        return;\n      }\n\n      const costEstimate = await getGenerationCost(\n        username,\n        repo,\n        githubPat ?? undefined,\n        apiKey ?? undefined,\n      );\n\n      if (costEstimate.error) {\n        setError(costEstimate.error);\n        setLoading(false);\n        return;\n      }\n\n      setCost(costEstimate.cost ?? \"\");\n      await runGeneration(githubPat ?? undefined);\n    } catch {\n      setError(\"Something went wrong. Please try again later.\");\n      setLoading(false);\n    }\n  }, [repo, runGeneration, username]);\n\n  const handleRegenerate = useCallback(async () => {\n    if (isExampleRepo(username, repo)) {\n      return;\n    }\n\n    setLoading(true);\n    setError(\"\");\n    setCost(\"\");\n\n    const githubPat = localStorage.getItem(\"github_pat\");\n    const apiKey = localStorage.getItem(\"openai_key\");\n\n    try {\n      const costEstimate = await getGenerationCost(\n        username,\n        repo,\n        githubPat ?? undefined,\n        apiKey ?? undefined,\n      );\n\n      if (costEstimate.error) {\n        setError(costEstimate.error);\n        setLoading(false);\n        return;\n      }\n\n      setCost(costEstimate.cost ?? \"\");\n      await runGeneration(githubPat ?? undefined);\n    } catch {\n      setError(\"Something went wrong. Please try again later.\");\n      setLoading(false);\n    }\n  }, [repo, runGeneration, username]);\n\n  useEffect(() => {\n    void getDiagram();\n  }, [getDiagram]);\n\n  const { handleCopy, handleExportImage } = useDiagramExport(diagram);\n\n  const handleApiKeySubmit = async (apiKey: string) => {\n    setShowApiKeyDialog(false);\n    setLoading(true);\n    setError(\"\");\n\n    localStorage.setItem(\"openai_key\", apiKey);\n\n    const githubPat = localStorage.getItem(\"github_pat\");\n    try {\n      await runGeneration(githubPat ?? undefined);\n    } catch {\n      setError(\"Failed to generate diagram with provided API key.\");\n      setLoading(false);\n    }\n  };\n\n  const handleCloseApiKeyDialog = () => {\n    setShowApiKeyDialog(false);\n  };\n\n  const handleOpenApiKeyDialog = () => {\n    setShowApiKeyDialog(true);\n  };\n\n  return {\n    diagram,\n    error,\n    loading,\n    lastGenerated,\n    cost,\n    handleCopy,\n    showApiKeyDialog,\n    handleApiKeySubmit,\n    handleCloseApiKeyDialog,\n    handleOpenApiKeyDialog,\n    handleExportImage,\n    handleRegenerate,\n    state: state as DiagramStreamState,\n  };\n}\n"
  },
  {
    "path": "src/hooks/useStarReminder.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { toast } from \"sonner\";\n\nexport function useStarReminder() {\n  useEffect(() => {\n    // Check if we've already shown the toast\n    const hasShownStarReminder = localStorage.getItem(\"hasShownStarReminder\");\n\n    if (!hasShownStarReminder) {\n      // Set a timeout to show the toast after 3 seconds\n      const timeoutId = setTimeout(() => {\n        toast(\"Enjoying GitDiagram?\", {\n          className: \"star-reminder-toast\",\n          action: {\n            label: \"Star ★\",\n            onClick: () =>\n              window.open(\n                \"https://github.com/ahmedkhaleel2004/gitdiagram\",\n                \"_blank\",\n              ),\n          },\n          duration: 5000,\n          dismissible: true,\n        });\n\n        // Set flag in localStorage to prevent showing again\n        localStorage.setItem(\"hasShownStarReminder\", \"true\");\n      }, 5000);\n\n      // Clean up the timeout if the component unmounts\n      return () => clearTimeout(timeoutId);\n    }\n  }, []);\n}\n"
  },
  {
    "path": "src/lib/exampleRepos.ts",
    "content": "export const exampleRepos = {\n  FastAPI: \"/fastapi/fastapi\",\n  Streamlit: \"/streamlit/streamlit\",\n  Flask: \"/pallets/flask\",\n  \"api-analytics\": \"/tom-draper/api-analytics\",\n  Monkeytype: \"/monkeytypegame/monkeytype\",\n};\n\nfunction normalizePathSegment(value: string) {\n  try {\n    return decodeURIComponent(value).toLowerCase();\n  } catch {\n    return value.toLowerCase();\n  }\n}\n\nexport function isExampleRepo(username: string, repo: string) {\n  const currentPath = `/${normalizePathSegment(username)}/${normalizePathSegment(repo)}`;\n  return Object.values(exampleRepos).some(\n    (path) => normalizePathSegment(path) === currentPath,\n  );\n}\n"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "src/server/db/index.ts",
    "content": "import * as schema from \"./schema\";\nimport { drizzle as drizzleNeon } from \"drizzle-orm/neon-http\";\nimport { drizzle as drizzlePostgres } from \"drizzle-orm/postgres-js\";\nimport { neon } from \"@neondatabase/serverless\";\nimport postgres from \"postgres\";\nimport { config } from \"dotenv\";\nimport type { PostgresJsDatabase } from \"drizzle-orm/postgres-js\";\nimport type { NeonHttpDatabase } from \"drizzle-orm/neon-http\";\n\nconfig({ path: \".env\" });\n\n// Define a type that can be either Neon or Postgres database\ntype DrizzleDatabase =\n  | NeonHttpDatabase<typeof schema>\n  | PostgresJsDatabase<typeof schema>;\n\n// Check if we're using Neon/Vercel (production) or local Postgres\nconst isNeonConnection = process.env.POSTGRES_URL?.includes(\"neon.tech\");\n\nlet db: DrizzleDatabase;\nif (isNeonConnection) {\n  // Production: Use Neon HTTP connection\n  const sql = neon(process.env.POSTGRES_URL!);\n  db = drizzleNeon(sql, { schema });\n} else {\n  // Local development: Use standard Postgres connection\n  const client = postgres(process.env.POSTGRES_URL!);\n  db = drizzlePostgres(client, { schema });\n}\n\nexport { db };\n"
  },
  {
    "path": "src/server/db/schema.ts",
    "content": "// Example model schema from the Drizzle docs\n// https://orm.drizzle.team/docs/sql-schema-declaration\n\nimport { sql } from \"drizzle-orm\";\nimport {\n  pgTableCreator,\n  timestamp,\n  varchar,\n  primaryKey,\n  boolean,\n} from \"drizzle-orm/pg-core\";\n\n/**\n * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same\n * database instance for multiple projects.\n *\n * @see https://orm.drizzle.team/docs/goodies#multi-project-schema\n */\nexport const createTable = pgTableCreator((name) => `gitdiagram_${name}`);\n\nexport const diagramCache = createTable(\n  \"diagram_cache\",\n  {\n    username: varchar(\"username\", { length: 256 }).notNull(),\n    repo: varchar(\"repo\", { length: 256 }).notNull(),\n    diagram: varchar(\"diagram\", { length: 10000 }).notNull(), // Adjust length as needed\n    explanation: varchar(\"explanation\", { length: 10000 })\n      .notNull()\n      .default(\"No explanation provided\"), // Default explanation to avoid data loss of existing rows\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .default(sql`CURRENT_TIMESTAMP`)\n      .notNull(),\n    updatedAt: timestamp(\"updated_at\", { withTimezone: true }).$onUpdate(\n      () => new Date(),\n    ),\n    usedOwnKey: boolean(\"used_own_key\").default(false),\n  },\n  (table) => ({\n    pk: primaryKey({ columns: [table.username, table.repo] }),\n  }),\n);\n"
  },
  {
    "path": "src/server/generate/format.ts",
    "content": "type TaggedValues = Record<string, string | undefined>;\n\nexport function toTaggedMessage(values: TaggedValues): string {\n  return Object.entries(values)\n    .filter(([, value]) => typeof value === \"string\")\n    .map(([key, value]) => `<${key}>\\n${value}\\n</${key}>`)\n    .join(\"\\n\");\n}\n\nexport function processClickEvents(\n  diagram: string,\n  username: string,\n  repo: string,\n  branch: string,\n): string {\n  const clickPattern = /click ([^\\s\"]+)\\s+\"([^\"]+)\"/g;\n\n  return diagram.replace(clickPattern, (_, nodeId: string, path: string) => {\n    const trimmedPath = path.trim().replace(/^['\"]|['\"]$/g, \"\");\n    const isFile = trimmedPath.includes(\".\") && !trimmedPath.endsWith(\"/\");\n    const pathType = isFile ? \"blob\" : \"tree\";\n    const fullUrl = `https://github.com/${username}/${repo}/${pathType}/${branch}/${trimmedPath}`;\n\n    return `click ${nodeId} \"${fullUrl}\"`;\n  });\n}\n\nexport function extractComponentMapping(response: string): string {\n  const startTag = \"<component_mapping>\";\n  const endTag = \"</component_mapping>\";\n  const startIndex = response.indexOf(startTag);\n  const endIndex = response.indexOf(endTag);\n\n  if (startIndex === -1 || endIndex === -1) {\n    return response;\n  }\n\n  return response.slice(startIndex, endIndex);\n}\n\nexport function stripMermaidCodeFences(text: string): string {\n  return text.replace(/```mermaid/g, \"\").replace(/```/g, \"\").trim();\n}\n"
  },
  {
    "path": "src/server/generate/github.ts",
    "content": "interface GitHubRepoResponse {\n  default_branch?: string;\n}\n\ninterface GitHubTreeItem {\n  path: string;\n}\n\ninterface GitHubTreeResponse {\n  tree?: GitHubTreeItem[];\n}\n\ninterface GitHubReadmeResponse {\n  content?: string;\n  encoding?: string;\n}\n\nexport interface GithubData {\n  defaultBranch: string;\n  fileTree: string;\n  readme: string;\n}\n\nconst EXCLUDED_PATTERNS = [\n  \"node_modules/\",\n  \"vendor/\",\n  \"venv/\",\n  \".min.\",\n  \".pyc\",\n  \".pyo\",\n  \".pyd\",\n  \".so\",\n  \".dll\",\n  \".class\",\n  \".jpg\",\n  \".jpeg\",\n  \".png\",\n  \".gif\",\n  \".ico\",\n  \".svg\",\n  \".ttf\",\n  \".woff\",\n  \".webp\",\n  \"__pycache__/\",\n  \".cache/\",\n  \".tmp/\",\n  \"yarn.lock\",\n  \"poetry.lock\",\n  \"*.log\",\n  \".vscode/\",\n  \".idea/\",\n];\n\nfunction shouldIncludeFile(path: string): boolean {\n  const lowerPath = path.toLowerCase();\n  return !EXCLUDED_PATTERNS.some((pattern) => lowerPath.includes(pattern));\n}\n\nfunction createHeaders(githubPat?: string): HeadersInit {\n  const token = githubPat?.trim();\n\n  if (!token) {\n    return {\n      Accept: \"application/vnd.github+json\",\n    };\n  }\n\n  return {\n    Authorization: `token ${token}`,\n    Accept: \"application/vnd.github+json\",\n  };\n}\n\nasync function fetchJson<T>(\n  url: string,\n  headers: HeadersInit,\n  notFoundMessage: string,\n): Promise<T> {\n  const response = await fetch(url, {\n    headers,\n    cache: \"no-store\",\n  });\n\n  if (response.status === 404) {\n    throw new Error(notFoundMessage);\n  }\n\n  if (!response.ok) {\n    throw new Error(\n      `GitHub request failed (${response.status}): ${await response.text()}`,\n    );\n  }\n\n  return (await response.json()) as T;\n}\n\nasync function getDefaultBranch(\n  username: string,\n  repo: string,\n  headers: HeadersInit,\n): Promise<string> {\n  const data = await fetchJson<GitHubRepoResponse>(\n    `https://api.github.com/repos/${username}/${repo}`,\n    headers,\n    \"Repository not found.\",\n  );\n\n  return data.default_branch || \"main\";\n}\n\nasync function getFileTree(\n  username: string,\n  repo: string,\n  branch: string,\n  headers: HeadersInit,\n): Promise<string> {\n  const data = await fetchJson<GitHubTreeResponse>(\n    `https://api.github.com/repos/${username}/${repo}/git/trees/${branch}?recursive=1`,\n    headers,\n    \"Could not fetch repository file tree.\",\n  );\n\n  const paths = (data.tree ?? [])\n    .map((item) => item.path)\n    .filter((path): path is string => Boolean(path))\n    .filter(shouldIncludeFile);\n\n  if (!paths.length) {\n    throw new Error(\n      \"Could not fetch repository file tree. Repository might be empty or inaccessible.\",\n    );\n  }\n\n  return paths.join(\"\\n\");\n}\n\nasync function getReadme(\n  username: string,\n  repo: string,\n  headers: HeadersInit,\n): Promise<string> {\n  const data = await fetchJson<GitHubReadmeResponse>(\n    `https://api.github.com/repos/${username}/${repo}/readme`,\n    headers,\n    \"No README found for the specified repository.\",\n  );\n\n  if (!data.content) {\n    throw new Error(\"No README found for the specified repository.\");\n  }\n\n  if (data.encoding === \"base64\") {\n    return Buffer.from(data.content, \"base64\").toString(\"utf-8\");\n  }\n\n  return data.content;\n}\n\nexport async function getGithubData(\n  username: string,\n  repo: string,\n  githubPat?: string,\n): Promise<GithubData> {\n  const headers = createHeaders(githubPat);\n  const defaultBranch = await getDefaultBranch(username, repo, headers);\n  const [fileTree, readme] = await Promise.all([\n    getFileTree(username, repo, defaultBranch, headers),\n    getReadme(username, repo, headers),\n  ]);\n\n  return {\n    defaultBranch,\n    fileTree,\n    readme,\n  };\n}\n"
  },
  {
    "path": "src/server/generate/mermaid.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { validateMermaidSyntax } from \"~/server/generate/mermaid\";\n\ndescribe(\"validateMermaidSyntax\", () => {\n  it(\"accepts valid Mermaid flowchart syntax\", async () => {\n    const result = await validateMermaidSyntax(\"flowchart TD\\nA-->B\");\n    expect(result.valid).toBe(true);\n  });\n\n  it(\"rejects invalid Mermaid flowchart syntax\", async () => {\n    const result = await validateMermaidSyntax(\"flowchart TD\\nA-=>B\");\n    expect(result.valid).toBe(false);\n    expect(result.message).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "src/server/generate/mermaid.ts",
    "content": "import { createRequire } from \"node:module\";\n\nimport DOMPurify from \"dompurify\";\nimport type { Mermaid as MermaidClient } from \"mermaid\";\n\nconst require = createRequire(import.meta.url);\nlet mermaidInstance: MermaidClient | null = null;\nlet initialized = false;\nlet domPurifyPatched = false;\n\ninterface DomPurifyLike {\n  (window?: Window): unknown;\n  sanitize?: (value: unknown, config?: unknown) => unknown;\n  addHook?: (...args: unknown[]) => unknown;\n}\n\nfunction ensureDomPurifyPatched() {\n  if (domPurifyPatched) return;\n\n  try {\n    const domPurify = DOMPurify as unknown as DomPurifyLike;\n    if (typeof domPurify === \"function\" && typeof domPurify.sanitize !== \"function\") {\n      const { JSDOM } = require(\"jsdom\") as { JSDOM: new (html?: string) => { window: Window } };\n      const domWindow = new JSDOM(\"<!doctype html><html><body></body></html>\").window;\n      const domPurifyInstance = domPurify(domWindow as unknown as Window) as Partial<DomPurifyLike>;\n      Object.assign(domPurify, domPurifyInstance);\n    }\n  } catch {\n    // Best effort patch.\n  } finally {\n    domPurifyPatched = true;\n  }\n}\n\nasync function getMermaid() {\n  if (mermaidInstance) return mermaidInstance;\n\n  ensureDomPurifyPatched();\n  const mermaidModule = (await import(\"mermaid\")) as { default: MermaidClient };\n  mermaidInstance = mermaidModule.default;\n  return mermaidInstance;\n}\n\nasync function ensureMermaidInitialized() {\n  const mermaid = await getMermaid();\n  if (initialized) return mermaid;\n\n  mermaid.initialize({\n    startOnLoad: false,\n    securityLevel: \"loose\",\n  });\n  initialized = true;\n  return mermaid;\n}\n\nfunction normalizeParserMessage(message?: string): string {\n  if (!message) {\n    return \"Mermaid syntax is invalid and could not be parsed.\";\n  }\n\n  if (\n    message.includes(\"sanitize is not a function\") ||\n    message.includes(\"__TURBOPACK__imported__module\")\n  ) {\n    return \"Mermaid parser runtime failed in server context (sanitizer issue).\";\n  }\n\n  return message;\n}\n\ninterface MermaidErrorHash {\n  line?: number;\n  token?: string;\n  expected?: string[];\n}\n\ninterface MermaidParserError extends Error {\n  hash?: MermaidErrorHash;\n}\n\nexport interface MermaidValidationResult {\n  valid: boolean;\n  message?: string;\n  line?: number;\n  token?: string;\n  expected?: string[];\n}\n\nexport async function validateMermaidSyntax(\n  diagram: string,\n): Promise<MermaidValidationResult> {\n  const mermaid = await ensureMermaidInitialized();\n  try {\n    await mermaid.parse(diagram);\n    return { valid: true };\n  } catch (error) {\n    const parserError = error as MermaidParserError;\n    return {\n      valid: false,\n      message: normalizeParserMessage(parserError?.message),\n      line: parserError?.hash?.line,\n      token: parserError?.hash?.token,\n      expected: parserError?.hash?.expected,\n    };\n  }\n}\n\nexport function formatValidationFeedback(result: MermaidValidationResult): string {\n  if (result.valid) {\n    return \"No syntax errors found.\";\n  }\n\n  const details = [\n    `message: ${result.message ?? \"unknown parse error\"}`,\n    typeof result.line === \"number\" ? `line: ${result.line}` : undefined,\n    result.token ? `token: ${result.token}` : undefined,\n    result.expected?.length\n      ? `expected: ${result.expected.join(\", \")}`\n      : undefined,\n  ].filter(Boolean);\n\n  return details.join(\"\\n\");\n}\n"
  },
  {
    "path": "src/server/generate/model-config.ts",
    "content": "const DEFAULT_MODEL = \"gpt-5.4-mini\";\n\nfunction readEnvValue(name: string): string | undefined {\n  const value = process.env[name]?.trim();\n  return value ? value : undefined;\n}\n\nexport function getModel(): string {\n  return readEnvValue(\"OPENAI_MODEL\") ?? DEFAULT_MODEL;\n}\n"
  },
  {
    "path": "src/server/generate/openai.ts",
    "content": "import OpenAI from \"openai\";\n\nexport type ReasoningEffort = \"low\" | \"medium\" | \"high\";\n\nfunction resolveApiKey(overrideApiKey?: string): string {\n  const apiKey = overrideApiKey?.trim() || process.env.OPENAI_API_KEY?.trim();\n  if (!apiKey) {\n    throw new Error(\n      \"Missing OpenAI API key. Set OPENAI_API_KEY or provide api_key in request.\",\n    );\n  }\n  return apiKey;\n}\n\nexport function estimateTokens(text: string): number {\n  // Rough heuristic used for fast gating/cost estimates in serverless.\n  return Math.ceil(text.length / 4);\n}\n\ninterface StreamCompletionParams {\n  model: string;\n  systemPrompt: string;\n  userPrompt: string;\n  apiKey?: string;\n  reasoningEffort?: ReasoningEffort;\n  maxOutputTokens?: number;\n}\n\nexport async function* streamCompletion({\n  model,\n  systemPrompt,\n  userPrompt,\n  apiKey,\n  reasoningEffort,\n  maxOutputTokens,\n}: StreamCompletionParams): AsyncGenerator<string, void, void> {\n  const client = new OpenAI({ apiKey: resolveApiKey(apiKey) });\n\n  const stream = await client.responses.create({\n    model,\n    stream: true,\n    input: [\n      { role: \"system\", content: systemPrompt },\n      { role: \"user\", content: userPrompt },\n    ],\n    ...(reasoningEffort ? { reasoning: { effort: reasoningEffort } } : {}),\n    ...(maxOutputTokens ? { max_output_tokens: maxOutputTokens } : {}),\n  });\n\n  for await (const event of stream) {\n    if (event.type === \"response.output_text.delta\") {\n      if (event.delta) {\n        yield event.delta;\n      }\n      continue;\n    }\n\n    if (event.type === \"error\") {\n      const message = event.message ?? \"OpenAI stream failed.\";\n      throw new Error(message);\n    }\n  }\n}\n\ninterface CountInputTokensParams {\n  model: string;\n  systemPrompt: string;\n  userPrompt: string;\n  apiKey?: string;\n  reasoningEffort?: ReasoningEffort;\n}\n\nexport async function countInputTokens({\n  model,\n  systemPrompt,\n  userPrompt,\n  apiKey,\n  reasoningEffort,\n}: CountInputTokensParams): Promise<number> {\n  const client = new OpenAI({ apiKey: resolveApiKey(apiKey) });\n\n  const response = await client.responses.inputTokens.count({\n    model,\n    input: [\n      { role: \"system\", content: systemPrompt },\n      { role: \"user\", content: userPrompt },\n    ],\n    ...(reasoningEffort ? { reasoning: { effort: reasoningEffort } } : {}),\n  });\n\n  return response.input_tokens;\n}\n"
  },
  {
    "path": "src/server/generate/pricing.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { estimateTextTokenCostUsd, resolvePricingModel } from \"~/server/generate/pricing\";\n\ndescribe(\"resolvePricingModel\", () => {\n  it(\"keeps gpt-5.4-mini on its own pricing tier\", () => {\n    expect(resolvePricingModel(\"gpt-5.4-mini\")).toBe(\"gpt-5.4-mini\");\n    expect(resolvePricingModel(\"gpt-5.4-mini-2026-03-17\")).toBe(\"gpt-5.4-mini\");\n  });\n});\n\ndescribe(\"estimateTextTokenCostUsd\", () => {\n  it(\"uses gpt-5.4-mini pricing for cost estimates\", () => {\n    const result = estimateTextTokenCostUsd(\"gpt-5.4-mini\", 1_000_000, 1_000_000);\n\n    expect(result.pricingModel).toBe(\"gpt-5.4-mini\");\n    expect(result.pricing.inputPerMillionUsd).toBe(0.75);\n    expect(result.pricing.outputPerMillionUsd).toBe(4.5);\n    expect(result.costUsd).toBe(5.25);\n  });\n});\n"
  },
  {
    "path": "src/server/generate/pricing.ts",
    "content": "export interface ModelPricing {\n  inputPerMillionUsd: number;\n  outputPerMillionUsd: number;\n}\n\nconst DEFAULT_PRICING_MODEL = \"gpt-5.4-mini\";\n\nconst MODEL_PRICING: Record<string, ModelPricing> = {\n  \"gpt-5.4\": { inputPerMillionUsd: 2.5, outputPerMillionUsd: 15.0 },\n  \"gpt-5.4-pro\": { inputPerMillionUsd: 30.0, outputPerMillionUsd: 180.0 },\n  \"gpt-5.4-mini\": { inputPerMillionUsd: 0.75, outputPerMillionUsd: 4.5 },\n  \"gpt-5.4-nano\": { inputPerMillionUsd: 0.2, outputPerMillionUsd: 1.25 },\n\n  // Legacy fallbacks kept for older env values.\n  \"gpt-5.2\": { inputPerMillionUsd: 1.75, outputPerMillionUsd: 14.0 },\n  \"gpt-5.2-chat-latest\": { inputPerMillionUsd: 1.75, outputPerMillionUsd: 14.0 },\n  \"gpt-5.2-codex\": { inputPerMillionUsd: 1.75, outputPerMillionUsd: 14.0 },\n  \"gpt-5.2-pro\": { inputPerMillionUsd: 21.0, outputPerMillionUsd: 168.0 },\n\n  \"gpt-5.1\": { inputPerMillionUsd: 1.25, outputPerMillionUsd: 10.0 },\n  \"gpt-5\": { inputPerMillionUsd: 1.25, outputPerMillionUsd: 10.0 },\n  \"gpt-5-mini\": { inputPerMillionUsd: 0.25, outputPerMillionUsd: 2.0 },\n  \"gpt-5-nano\": { inputPerMillionUsd: 0.05, outputPerMillionUsd: 0.4 },\n  \"o4-mini\": { inputPerMillionUsd: 1.1, outputPerMillionUsd: 4.4 },\n};\nconst DEFAULT_PRICING = MODEL_PRICING[DEFAULT_PRICING_MODEL] as ModelPricing;\n\nfunction normalizeModelId(model: string): string {\n  return model.trim().toLowerCase();\n}\n\nfunction stripDateSnapshotSuffix(model: string): string {\n  return model.replace(/-\\d{4}-\\d{2}-\\d{2}$/i, \"\");\n}\n\nexport function resolvePricingModel(model: string): string {\n  const normalized = normalizeModelId(model);\n  if (MODEL_PRICING[normalized]) return normalized;\n\n  const withoutDate = stripDateSnapshotSuffix(normalized);\n  if (MODEL_PRICING[withoutDate]) return withoutDate;\n\n  if (withoutDate.startsWith(\"gpt-5.4-pro\")) return \"gpt-5.4-pro\";\n  if (withoutDate.startsWith(\"gpt-5.4-mini\")) return \"gpt-5.4-mini\";\n  if (withoutDate.startsWith(\"gpt-5.4-nano\")) return \"gpt-5.4-nano\";\n  if (withoutDate.startsWith(\"gpt-5.4\")) return \"gpt-5.4\";\n  if (withoutDate.startsWith(\"gpt-5.2-pro\")) return \"gpt-5.2-pro\";\n  if (withoutDate.startsWith(\"gpt-5.2-codex\")) return \"gpt-5.2-codex\";\n  if (withoutDate.startsWith(\"gpt-5.2-chat\")) return \"gpt-5.2-chat-latest\";\n  if (withoutDate.startsWith(\"gpt-5.2\")) return \"gpt-5.2\";\n  if (withoutDate.startsWith(\"gpt-5.1\")) return \"gpt-5.1\";\n  if (withoutDate.startsWith(\"gpt-5-mini\")) return \"gpt-5-mini\";\n  if (withoutDate.startsWith(\"gpt-5-nano\")) return \"gpt-5-nano\";\n  if (withoutDate.startsWith(\"gpt-5\")) return \"gpt-5\";\n  if (withoutDate.startsWith(\"o4-mini\")) return \"o4-mini\";\n\n  return DEFAULT_PRICING_MODEL;\n}\n\nexport function estimateTextTokenCostUsd(\n  model: string,\n  inputTokens: number,\n  outputTokens: number,\n): { costUsd: number; pricingModel: string; pricing: ModelPricing } {\n  const pricingModel = resolvePricingModel(model);\n  const pricing = MODEL_PRICING[pricingModel] ?? DEFAULT_PRICING;\n  const inputCost = (Math.max(inputTokens, 0) / 1_000_000) * pricing.inputPerMillionUsd;\n  const outputCost =\n    (Math.max(outputTokens, 0) / 1_000_000) * pricing.outputPerMillionUsd;\n\n  return {\n    costUsd: inputCost + outputCost,\n    pricingModel,\n    pricing,\n  };\n}\n"
  },
  {
    "path": "src/server/generate/prompts.ts",
    "content": "export const SYSTEM_FIRST_PROMPT = `\nYou are tasked with explaining to a principal software engineer how to draw the best and most accurate system design diagram / architecture of a given project. This explanation should be tailored to the specific project's purpose and structure. To accomplish this, you will be provided with two key pieces of information:\n\n1. The complete and entire file tree of the project including all directory and file names, which will be enclosed in <file_tree> tags in the users message.\n\n2. The README file of the project, which will be enclosed in <readme> tags in the users message.\n\nAnalyze these components carefully, as they will provide crucial information about the project's structure and purpose. Follow these steps to create an explanation for the principal software engineer:\n\n1. Identify the project type and purpose:\n   - Examine the file structure and README to determine if the project is a full-stack application, an open-source tool, a compiler, or another type of software imaginable.\n   - Look for key indicators in the README, such as project description, features, or use cases.\n\n2. Analyze the file structure:\n   - Pay attention to top-level directories and their names (e.g., \"frontend\", \"backend\", \"src\", \"lib\", \"tests\").\n   - Identify patterns in the directory structure that might indicate architectural choices (e.g., MVC pattern, microservices).\n   - Note any configuration files, build scripts, or deployment-related files.\n\n3. Examine the README for additional insights:\n   - Look for sections describing the architecture, dependencies, or technical stack.\n   - Check for any diagrams or explanations of the system's components.\n\n4. Based on your analysis, explain how to create a system design diagram that accurately represents the project's architecture. Include the following points:\n\n   a. Identify the main components of the system (e.g., frontend, backend, database, building, external services).\n   b. Determine the relationships and interactions between these components.\n   c. Highlight any important architectural patterns or design principles used in the project.\n   d. Include relevant technologies, frameworks, or libraries that play a significant role in the system's architecture.\n\n5. Provide guidelines for tailoring the diagram to the specific project type:\n   - For a full-stack application, emphasize the separation between frontend and backend, database interactions, and any API layers.\n   - For an open-source tool, focus on the core functionality, extensibility points, and how it integrates with other systems.\n   - For a compiler or language-related project, highlight the different stages of compilation or interpretation, and any intermediate representations.\n\n6. Instruct the principal software engineer to include the following elements in the diagram:\n   - Clear labels for each component\n   - Directional arrows to show data flow or dependencies\n   - Color coding or shapes to distinguish between different types of components\n\n7. NOTE: Emphasize the importance of being very detailed and capturing the essential architectural elements. Don't overthink it too much, simply separating the project into as many components as possible is best.\n\nPresent your explanation and instructions within <explanation> tags, ensuring that you tailor your advice to the specific project based on the provided file tree and README content.\n`;\n\nexport const SYSTEM_SECOND_PROMPT = `\nYou are tasked with mapping key components of a system design to their corresponding files and directories in a project's file structure. You will be provided with a detailed explanation of the system design/architecture and a file tree of the project.\n\nFirst, carefully read the system design explanation which will be enclosed in <explanation> tags in the users message.\n\nThen, examine the file tree of the project which will be enclosed in <file_tree> tags in the users message.\n\nYour task is to analyze the system design explanation and identify key components, modules, or services mentioned. Then, try your best to map these components to what you believe could be their corresponding directories and files in the provided file tree.\n\nGuidelines:\n1. Focus on major components described in the system design.\n2. Look for directories and files that clearly correspond to these components.\n3. Include both directories and specific files when relevant.\n4. If a component doesn't have a clear corresponding file or directory, simply dont include it in the map.\n\nNow, provide your final answer in the following format:\n\n<component_mapping>\n1. [Component Name]: [File/Directory Path]\n2. [Component Name]: [File/Directory Path]\n[Continue for all identified components]\n</component_mapping>\n\nRemember to be as specific as possible in your mappings, only use what is given to you from the file tree, and to strictly follow the components mentioned in the explanation. \n`;\n\nexport const SYSTEM_THIRD_PROMPT = `\nYou are a principal software engineer tasked with creating a system design diagram using Mermaid.js based on a detailed explanation. Your goal is to accurately represent the architecture and design of the project as described in the explanation.\n\nThe detailed explanation of the design will be enclosed in <explanation> tags in the users message.\n\nAlso, sourced from the explanation, as a bonus, a few of the identified components have been mapped to their paths in the project file tree, whether it is a directory or file which will be enclosed in <component_mapping> tags in the users message.\n\nTo create the Mermaid.js diagram:\n\n1. Carefully read and analyze the provided design explanation.\n2. Identify the main components, services, and their relationships within the system.\n3. Determine the appropriate Mermaid.js diagram type to use (e.g., flowchart, sequence diagram, class diagram, architecture, etc.) based on the nature of the system described.\n4. Create the Mermaid.js code to represent the design, ensuring that:\n   a. All major components are included\n   b. Relationships between components are clearly shown\n   c. The diagram accurately reflects the architecture described in the explanation\n   d. The layout is logical and easy to understand\n\nGuidelines for diagram components and relationships:\n- Use appropriate shapes for different types of components (e.g., rectangles for services, cylinders for databases, etc.)\n- Use clear and concise labels for each component\n- Show the direction of data flow or dependencies using arrows\n- Group related components together if applicable\n- Include any important notes or annotations mentioned in the explanation\n- Just follow the explanation. It will have everything you need.\n\nIMPORTANT!!: Please orient and draw the diagram as vertically as possible. You must avoid long horizontal lists of nodes and sections!\n\nYou must include click events for components of the diagram that have been specified in the provided <component_mapping>:\n- Do not try to include the full url. This will be processed by another program afterwards. All you need to do is include the path.\n- For example:\n  - This is a correct click event: \\`click Example \"app/example.js\"\\`\n  - This is an incorrect click event: \\`click Example \"https://github.com/username/repo/blob/main/app/example.js\"\\`\n- Do this for as many components as specified in the component mapping, include directories and files.\n  - If you believe the component contains files and is a directory, include the directory path.\n  - If you believe the component references a specific file, include the file path.\n- Make sure to include the full path to the directory or file exactly as specified in the component mapping.\n- It is very important that you do this for as many files as possible. The more the better.\n\n- IMPORTANT: THESE PATHS ARE FOR CLICK EVENTS ONLY, these paths should not be included in the diagram's node's names. Only for the click events. Paths should not be seen by the user.\n\nYour output should be valid Mermaid.js code that can be rendered into a diagram.\n\nDo not include an init declaration such as \\`%%{init: {'key':'etc'}}%%\\`. This is handled externally. Just return the diagram code.\n\nYour response must strictly be just the Mermaid.js code, without any additional text or explanations.\nNo code fence or markdown ticks needed, simply return the Mermaid.js code.\n\nEnsure that your diagram adheres strictly to the given explanation, without adding or omitting any significant components or relationships. \n\nFor general direction, the provided example below is how you should structure your code:\n\n\\`\\`\\`mermaid\nflowchart TD \n    %% or graph TD, your choice\n\n    %% Global entities\n    A(\"Entity A\"):::external\n    %% more...\n\n    %% Subgraphs and modules\n    subgraph \"Layer A\"\n        A1(\"Module A\"):::example\n        %% more modules...\n        %% inner subgraphs if needed...\n    end\n\n    %% more subgraphs, modules, etc...\n\n    %% Connections\n    A -->|\"relationship\"| B\n    %% and a lot more...\n\n    %% Click Events\n    click A1 \"example/example.js\"\n    %% and a lot more...\n\n    %% Styles\n    classDef frontend %%...\n    %% and a lot more...\n\\`\\`\\`\n\nEXTREMELY Important notes on syntax!!! (PAY ATTENTION TO THIS):\n- Make sure to add colour to the diagram!!! This is extremely critical.\n- In Mermaid.js syntax, we cannot include special characters for nodes without being inside quotes! For example: \\`EX[/api/process (Backend)]:::api\\` and \\`API -->|calls Process()| Backend\\` are two examples of syntax errors. They should be \\`EX[\"/api/process (Backend)\"]:::api\\` and \\`API -->|\"calls Process()\"| Backend\\` respectively. Notice the quotes. This is extremely important. Make sure to include quotes for any string that contains special characters.\n- In Mermaid.js syntax, you cannot apply a class style directly within a subgraph declaration. For example: \\`subgraph \"Frontend Layer\":::frontend\\` is a syntax error. However, you can apply them to nodes within the subgraph. For example: \\`Example[\"Example Node\"]:::frontend\\` is valid, and \\`class Example1,Example2 frontend\\` is valid.\n- In Mermaid.js syntax, there cannot be spaces in the relationship label names. For example: \\`A -->| \"example relationship\" | B\\` is a syntax error. It should be \\`A -->|\"example relationship\"| B\\` \n- In Mermaid.js syntax, you cannot give subgraphs an alias like nodes. For example: \\`subgraph A \"Layer A\"\\` is a syntax error. It should be \\`subgraph \"Layer A\"\\` \n`;\n\nexport const SYSTEM_FIX_MERMAID_PROMPT = `\nYou are a Mermaid syntax repair specialist.\n\nYou will receive:\n- <mermaid_code>...</mermaid_code>\n- <parser_error>...</parser_error>\n- <explanation>...</explanation>\n- <component_mapping>...</component_mapping>\n\nTask:\n- Fix Mermaid syntax errors while preserving the original diagram meaning.\n- Keep all click events that map to repository paths.\n- Keep diagram mostly vertical.\n- Return Mermaid code only.\n\nRules:\n- No markdown code fences.\n- No extra commentary.\n- Ensure final output is syntactically valid Mermaid.\n`;\n"
  },
  {
    "path": "src/server/generate/types.ts",
    "content": "import { z } from \"zod\";\n\nexport const generateRequestSchema = z.object({\n  username: z.string().min(1),\n  repo: z.string().min(1),\n  api_key: z.string().min(1).optional(),\n  github_pat: z.string().min(1).optional(),\n});\n\nexport type GenerateRequest = z.infer<typeof generateRequestSchema>;\n\nexport function sseMessage(payload: Record<string, unknown>): string {\n  return `data: ${JSON.stringify(payload)}\\n\\n`;\n}\n"
  },
  {
    "path": "src/server/github-stars.ts",
    "content": "import \"server-only\";\n\ninterface GitHubRepoResponse {\n  stargazers_count: number;\n}\n\nconst GITHUB_REPO_URL =\n  \"https://api.github.com/repos/ahmedkhaleel2004/gitdiagram\";\nconst GITHUB_API_VERSION = \"2022-11-28\";\nconst STAR_COUNT_REVALIDATE_SECONDS = 60 * 30;\n\nfunction createHeaders(): HeadersInit {\n  const githubPat = process.env.GITHUB_PAT?.trim();\n\n  if (!githubPat) {\n    return {\n      Accept: \"application/vnd.github+json\",\n      \"X-GitHub-Api-Version\": GITHUB_API_VERSION,\n    };\n  }\n\n  return {\n    Authorization: `Bearer ${githubPat}`,\n    Accept: \"application/vnd.github+json\",\n    \"X-GitHub-Api-Version\": GITHUB_API_VERSION,\n  };\n}\n\nexport async function getStarCount() {\n  try {\n    const response = await fetch(GITHUB_REPO_URL, {\n      cache: \"force-cache\",\n      headers: createHeaders(),\n      next: {\n        revalidate: STAR_COUNT_REVALIDATE_SECONDS,\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to fetch star count (${response.status})`);\n    }\n\n    const data = (await response.json()) as GitHubRepoResponse;\n    return data.stargazers_count;\n  } catch (error) {\n    console.error(\"Error fetching GitHub star count:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/styles/globals.css",
    "content": "@import \"tailwindcss\";\n@config \"../../tailwind.config.ts\";\n@layer base {\n  :root {\n    --background: 269 100% 95%;\n    --foreground: 0 0% 3.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 0 0% 3.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 0 0% 3.9%;\n    --primary: 0 0% 9%;\n    --primary-foreground: 0 0% 98%;\n    --secondary: 0 0% 96.1%;\n    --secondary-foreground: 0 0% 9%;\n    --muted: 0 0% 96.1%;\n    --muted-foreground: 0 0% 45.1%;\n    --accent: 0 0% 96.1%;\n    --accent-foreground: 0 0% 9%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 0 0% 98%;\n    --border: 0 0% 89.8%;\n    --input: 0 0% 89.8%;\n    --ring: 0 0% 3.9%;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n    --radius: 0.5rem;\n    --neo-panel: 270 100% 92%;\n    --neo-panel-muted: 270 95% 88%;\n    --neo-button: 270 97% 75%;\n    --neo-button-hover: 271 90% 80%;\n    --neo-subtle: 0 0% 98%;\n    --neo-subtle-muted: 0 0% 93%;\n    --neo-link: 271 81% 55%;\n    --neo-link-hover: 271 70% 48%;\n    --neo-soft-text: 220 13% 30%;\n    --neo-dot-active: 271 81% 55%;\n    --neo-dot-inactive: 271 46% 78%;\n    --neo-input-bg: 270 92% 89%;\n  }\n  .dark {\n    --background: 264 22% 9%;\n    --foreground: 270 25% 93%;\n    --card: 264 20% 13%;\n    --card-foreground: 270 25% 93%;\n    --popover: 264 20% 13%;\n    --popover-foreground: 270 25% 93%;\n    --primary: 270 25% 93%;\n    --primary-foreground: 264 22% 9%;\n    --secondary: 264 18% 18%;\n    --secondary-foreground: 270 25% 93%;\n    --muted: 264 18% 18%;\n    --muted-foreground: 270 15% 65%;\n    --accent: 264 20% 22%;\n    --accent-foreground: 270 25% 93%;\n    --destructive: 0 62.8% 40%;\n    --destructive-foreground: 0 0% 98%;\n    --border: 264 28% 22%;\n    --input: 264 22% 17%;\n    --ring: 270 85% 68%;\n    --chart-1: 270 85% 68%;\n    --chart-2: 280 75% 72%;\n    --chart-3: 260 70% 60%;\n    --chart-4: 290 65% 65%;\n    --chart-5: 250 80% 70%;\n    --neo-panel: 264 20% 13%;\n    --neo-panel-muted: 264 18% 17%;\n    --neo-button: 270 85% 68%;\n    --neo-button-hover: 270 90% 75%;\n    --neo-subtle: 270 35% 72%;\n    --neo-subtle-muted: 270 28% 62%;\n    --neo-link: 280 80% 72%;\n    --neo-link-hover: 275 90% 80%;\n    --neo-soft-text: 270 18% 72%;\n    --neo-dot-active: 270 85% 68%;\n    --neo-dot-inactive: 264 20% 32%;\n    --neo-input-bg: 264 24% 11%;\n  }\n}\n@layer base {\n  * {\n    border-color: hsl(var(--border));\n  }\n  button:not(:disabled) {\n    cursor: pointer;\n  }\n  body {\n    background-color: hsl(var(--background));\n    color: hsl(var(--foreground));\n  }\n\n  .dark body {\n    background-image:\n      radial-gradient(\n        ellipse at 18% 5%,\n        hsl(270 60% 20% / 0.4) 0%,\n        transparent 52%\n      ),\n      radial-gradient(\n        ellipse at 85% 90%,\n        hsl(280 50% 16% / 0.25) 0%,\n        transparent 46%\n      ),\n      linear-gradient(135deg, hsl(265 25% 10%) 0%, hsl(258 20% 8%) 100%);\n    background-attachment: fixed;\n  }\n}\n\n@layer components {\n  .neo-panel {\n    border: 3px solid #000 !important;\n    background: linear-gradient(\n      165deg,\n      hsl(var(--neo-panel)) 0%,\n      hsl(var(--neo-panel-muted)) 100%\n    ) !important;\n    box-shadow: 8px 8px 0 0 #000 !important;\n    color: hsl(var(--foreground));\n  }\n\n  .neo-button {\n    border: 3px solid #000 !important;\n    background: hsl(var(--neo-button)) !important;\n    color: #000 !important;\n    box-shadow: 4px 4px 0 0 #000 !important;\n    transition:\n      transform 0.2s ease,\n      background-color 0.2s ease;\n  }\n\n  .neo-button:hover:not(:disabled) {\n    background: hsl(var(--neo-button-hover)) !important;\n    transform: translate(-2px, -2px);\n  }\n\n  .neo-button-muted {\n    border: 3px solid #000 !important;\n    background: hsl(var(--neo-subtle-muted)) !important;\n    color: #000 !important;\n    box-shadow: 4px 4px 0 0 #000 !important;\n    transition:\n      transform 0.2s ease,\n      background-color 0.2s ease;\n  }\n\n  .neo-button-muted:hover:not(:disabled) {\n    background: hsl(var(--neo-subtle)) !important;\n    transform: translate(-2px, -2px);\n  }\n\n  .neo-input {\n    border: 3px solid #000 !important;\n    box-shadow: 4px 4px 0 0 #000 !important;\n    color: hsl(var(--foreground));\n  }\n\n  .dark .neo-input {\n    background: hsl(var(--neo-input-bg)) !important;\n  }\n\n  .neo-link {\n    color: hsl(var(--neo-link));\n    transition: color 0.2s ease;\n  }\n\n  .neo-link:hover {\n    color: hsl(var(--neo-link-hover));\n  }\n\n  .dark .neo-panel,\n  .dark .neo-input {\n    border-color: black !important;\n    box-shadow: 8px 8px 0 0 #0d0a19 !important;\n  }\n\n  .dark .neo-button,\n  .dark .neo-button-muted {\n    border-color: #1a0d30 !important;\n    box-shadow: 4px 4px 0 0 #0d0a19 !important;\n    color: #0d0a19 !important;\n  }\n}\n\n.star-reminder-toast [data-button],\n.star-reminder-toast button {\n  background: hsl(var(--neo-button)) !important;\n  color: #000 !important;\n  border: 2px solid #000 !important;\n  cursor: pointer !important;\n}\n\n.star-reminder-toast [data-button]:hover,\n.star-reminder-toast button:hover {\n  background: hsl(var(--neo-button-hover)) !important;\n}\n"
  },
  {
    "path": "start-database.sh",
    "content": "#!/usr/bin/env bash\n# Use this script to start a docker container for a local development database\n\n# TO RUN ON WINDOWS:\n# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install\n# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/\n# 3. Open WSL - `wsl`\n# 4. Run this script - `./start-database.sh`\n\n# On Linux and macOS you can run this script directly - `./start-database.sh`\n\nDB_CONTAINER_NAME=\"gitdiagram-postgres\"\n\nif ! [ -x \"$(command -v docker)\" ]; then\n  echo -e \"Docker is not installed. Please install docker and try again.\\nDocker install guide: https://docs.docker.com/engine/install/\"\n  exit 1\nfi\n\nif ! docker info > /dev/null 2>&1; then\n  echo \"Docker daemon is not running. Please start Docker and try again.\"\n  exit 1\nfi\n\nif [ \"$(docker ps -q -f name=$DB_CONTAINER_NAME)\" ]; then\n  echo \"Database container '$DB_CONTAINER_NAME' already running\"\n  exit 0\nfi\n\nif [ \"$(docker ps -q -a -f name=$DB_CONTAINER_NAME)\" ]; then\n  docker start \"$DB_CONTAINER_NAME\"\n  echo \"Existing database container '$DB_CONTAINER_NAME' started\"\n  exit 0\nfi\n\n# import env variables from .env\nset -a\nsource .env\n\nDB_PASSWORD=$(echo \"$POSTGRES_URL\" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')\nDB_PORT=$(echo \"$POSTGRES_URL\" | awk -F':' '{print $4}' | awk -F'\\/' '{print $1}')\n\nif [ \"$DB_PASSWORD\" = \"password\" ]; then\n  echo \"You are using the default database password\"\n  read -p \"Should we generate a random password for you? [y/N]: \" -r REPLY\n  if ! [[ $REPLY =~ ^[Yy]$ ]]; then\n    echo \"Please change the default password in the .env file and try again\"\n    exit 1\n  fi\n  # Generate a random URL-safe password\n  DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')\n  sed -i -e \"s#:password@#:$DB_PASSWORD@#\" .env\nfi\n\ndocker run -d \\\n  --name $DB_CONTAINER_NAME \\\n  -e POSTGRES_USER=\"postgres\" \\\n  -e POSTGRES_PASSWORD=\"$DB_PASSWORD\" \\\n  -e POSTGRES_DB=gitdiagram \\\n  -p \"$DB_PORT\":5432 \\\n  docker.io/postgres && echo \"Database container '$DB_CONTAINER_NAME' was successfully created\"\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import { type Config } from \"tailwindcss\";\nimport defaultTheme from \"tailwindcss/defaultTheme\";\n\nexport default {\n  darkMode: \"class\",\n  content: [\"./src/**/*.tsx\"],\n  theme: {\n    extend: {\n      keyframes: {\n        fadeInUp: {\n          \"0%\": { opacity: \"0\", transform: \"translateY(10px)\" },\n          \"50%\": { opacity: \"1\", transform: \"translateY(0)\" },\n          \"100%\": { opacity: \"0\", transform: \"translateY(-10px)\" },\n        },\n        fadeIn: {\n          \"0%\": { opacity: \"0\" },\n          \"100%\": { opacity: \"1\" },\n        },\n        dot1: {\n          \"0%, 100%\": { opacity: \"1\" },\n          \"33%, 66%\": { opacity: \"1\" },\n          \"67%, 99%\": { opacity: \"1\" },\n        },\n        dot2: {\n          \"0%, 32%\": { opacity: \"0\" },\n          \"33%, 100%\": { opacity: \"1\" },\n        },\n        dot3: {\n          \"0%, 65%\": { opacity: \"0\" },\n          \"66%, 100%\": { opacity: \"1\" },\n        },\n      },\n      animation: {\n        \"fade-in-up\": \"fadeInUp 3s ease-in-out infinite\",\n        \"fade-in\": \"fadeIn 1s ease-out forwards\",\n      },\n      fontFamily: {\n        sans: [\"var(--font-geist-sans)\", ...defaultTheme.fontFamily.sans],\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      colors: {\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        chart: {\n          \"1\": \"hsl(var(--chart-1))\",\n          \"2\": \"hsl(var(--chart-2))\",\n          \"3\": \"hsl(var(--chart-3))\",\n          \"4\": \"hsl(var(--chart-4))\",\n          \"5\": \"hsl(var(--chart-5))\",\n        },\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\"), require(\"tailwind-scrollbar\")],\n} satisfies Config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Base Options: */\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"target\": \"es2022\",\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"moduleDetection\": \"force\",\n    \"isolatedModules\": true,\n    /* Strictness */\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"checkJs\": true,\n    /* Bundled projects */\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"ES2022\"\n    ],\n    \"noEmit\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"jsx\": \"react-jsx\",\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"incremental\": true,\n    /* Path Aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\n        \"./src/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/*.cjs\",\n    \"**/*.js\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"backend/venv\",\n    \"backend/.venv\",\n    \"backend/__pycache__\",\n    \"**/__pycache__\"\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport { fileURLToPath } from \"node:url\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"~\": fileURLToPath(new URL(\"./src\", import.meta.url)),\n    },\n  },\n  test: {\n    environment: \"jsdom\",\n    setupFiles: [\"./vitest.setup.ts\"],\n    include: [\"src/**/*.test.ts\", \"src/**/*.test.tsx\"],\n  },\n});\n"
  },
  {
    "path": "vitest.setup.ts",
    "content": "import \"@testing-library/jest-dom/vitest\";\n"
  }
]