[
  {
    "path": ".dockerignore",
    "content": "# Git\n.git\n.gitignore\n.github\n\n# Node.js\nnode_modules\nnpm-debug.log\nyarn-debug.log\nyarn-error.log\n\n# Next.js\n.next\nout\n\n# Python cache files (but keep api/ directory)\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n# Keep api/ directory but exclude cache\napi/__pycache__/\napi/*.pyc\n\n# Environment variables\n# .env is now allowed to be included in the build\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# Docker\nDockerfile\ndocker-compose.yml\n.dockerignore\n\n# Misc\n.DS_Store\n*.pem\nREADME.md\nLICENSE\nscreenshots/\n*.md\n!api/README.md\n"
  },
  {
    "path": ".github/workflows/docker-build-push.yml",
    "content": "name: Build and Push Docker Image\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n  # Allow manual trigger\n  workflow_dispatch:\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n  \nconcurrency:\n  # This concurrency group ensures that only one job in the group runs at a time.\n  # If a new job is triggered, the previous one will be canceled.\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n  \njobs:\n  build-and-push:\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            platform: linux/amd64\n          - os: ubuntu-24.04-arm\n            platform: linux/arm64\n    runs-on: ${{ matrix.os }}\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Prepare environment for current platform\n        id: prepare\n        run: |\n          platform=${{ matrix.platform }}\n          echo \"PLATFORM_PAIR=${platform//\\//-}\" >> $GITHUB_ENV\n          echo \"GHCR_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}\" >> $GITHUB_ENV\n      \n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to the Container registry\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.GHCR_IMAGE }}\n\n      - name: Create empty .env file for build\n        run: touch .env\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        id: build\n        with:\n          context: .\n          platforms: ${{ matrix.platform }}\n          push: ${{ github.event_name != 'pull_request' }}\n          annotations: ${{ steps.meta.outputs.annotations }}\n          labels: ${{ steps.meta.outputs.labels }}\n          outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }},oci-mediatypes=true\n          cache-from: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}\n          cache-to: type=gha,mode=max,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}\n          \n      - name: Export digest\n        run: |\n          mkdir -p /tmp/digests\n          digest=\"${{ steps.build.outputs.digest }}\"\n          touch \"/tmp/digests/${digest#sha256:}\"\n\n      - name: Upload digest \n        uses: actions/upload-artifact@v4\n        with:\n          name: digests-${{ env.PLATFORM_PAIR }}\n          path: /tmp/digests/*\n          if-no-files-found: error\n          retention-days: 1\n  \n  merge:\n    name: merge Docker manifests\n    runs-on: ubuntu-latest\n    if: github.event_name != 'pull_request'\n    permissions:\n      contents: read\n      packages: write\n\n    needs:\n      - build-and-push\n    steps:\n      - name: Prepare environment\n        id: prepare\n        run: |\n          echo \"GHCR_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}\" >> $GITHUB_ENV\n      \n      - name: Download digests\n        uses: actions/download-artifact@v4\n        with:\n          path: /tmp/digests\n          pattern: digests-*\n          merge-multiple: true\n\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.GHCR_IMAGE }}\n          annotations: |\n            type=org.opencontainers.image.description,value=${{ github.event.repository.description || 'No description provided' }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha,format=short\n            type=ref,event=branch\n            type=ref,event=pr\n            latest\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          driver-opts: |\n            network=host\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Get execution timestamp with RFC3339 format\n        id: timestamp\n        run: |\n          echo \"timestamp=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\" >> $GITHUB_OUTPUT\n\n      - name: Create manifest list and pushs\n        working-directory: /tmp/digests\n        id: manifest-annotate\n        continue-on-error: true\n        run: |\n              docker buildx imagetools create \\\n                $(jq -cr '.tags | map(\"-t \" + .) | join(\" \")' <<< \"$DOCKER_METADATA_OUTPUT_JSON\") \\\n                --annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \\\n                --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \\\n                --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \\\n                --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \\\n                $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)\n\n      - name: Create manifest list and push without annotations\n        if: steps.manifest-annotate.outcome == 'failure'\n        working-directory: /tmp/digests\n        run: |\n              docker buildx imagetools create  $(jq -cr '.tags | map(\"-t \" + .) | join(\" \")' <<< \"$DOCKER_METADATA_OUTPUT_JSON\") \\\n                $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)\n\n      - name: Inspect image\n        id: inspect\n        run: |\n          docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'\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.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\napi/logs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n*.venv\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\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# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n.idea/\n\n# ignore adding self-signed certs\ncerts/\n"
  },
  {
    "path": ".python-version",
    "content": "3.12\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Deepwiki-Open\",\n            \"type\": \"python\",\n            \"request\": \"launch\",\n            \"module\": \"uvicorn\",\n            \"args\": [\n                \"api.api:app\",\n                \"--reload\",\n                \"--port\",\n                \"8001\"\n            ],\n            \"jinja\": true,\n            \"justMyCode\": true\n        }\n    ]\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1-labs\n\n# Build argument for custom certificates directory\nARG CUSTOM_CERT_DIR=\"certs\"\n\nFROM node:20-alpine3.22 AS node_base\n\nFROM node_base AS node_deps\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci --legacy-peer-deps\n\nFROM node_base AS node_builder\nWORKDIR /app\nCOPY --from=node_deps /app/node_modules ./node_modules\n# Copy only necessary files for Next.js build\nCOPY package.json package-lock.json next.config.ts tsconfig.json tailwind.config.js postcss.config.mjs ./\nCOPY src/ ./src/\nCOPY public/ ./public/\n# Increase Node.js memory limit for build and disable telemetry\nENV NODE_OPTIONS=\"--max-old-space-size=4096\"\nENV NEXT_TELEMETRY_DISABLED=1\nRUN NODE_ENV=production npm run build\n\nFROM python:3.11-slim AS py_deps\nWORKDIR /api\nCOPY api/pyproject.toml .\nCOPY api/poetry.lock .\nRUN python -m pip install poetry==2.0.1 --no-cache-dir && \\\n    poetry config virtualenvs.create true --local && \\\n    poetry config virtualenvs.in-project true --local && \\\n    poetry config virtualenvs.options.always-copy --local true && \\\n    POETRY_MAX_WORKERS=10 poetry install --no-interaction --no-ansi --only main && \\\n    poetry cache clear --all .\n\n# Use Python 3.11 as final image\nFROM python:3.11-slim\n\n# Set working directory\nWORKDIR /app\n\n# Install Node.js and npm\nRUN apt-get update && apt-get install -y \\\n    curl \\\n    gnupg \\\n    git \\\n    ca-certificates \\\n    && mkdir -p /etc/apt/keyrings \\\n    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \\\n    && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list \\\n    && apt-get update \\\n    && apt-get install -y nodejs \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Update certificates if custom ones were provided and copied successfully\nRUN if [ -n \"${CUSTOM_CERT_DIR}\" ]; then \\\n        mkdir -p /usr/local/share/ca-certificates && \\\n        if [ -d \"${CUSTOM_CERT_DIR}\" ]; then \\\n            cp -r ${CUSTOM_CERT_DIR}/* /usr/local/share/ca-certificates/ 2>/dev/null || true; \\\n            update-ca-certificates; \\\n            echo \"Custom certificates installed successfully.\"; \\\n        else \\\n            echo \"Warning: ${CUSTOM_CERT_DIR} not found. Skipping certificate installation.\"; \\\n        fi \\\n    fi\n\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Copy Python dependencies\nCOPY --from=py_deps /api/.venv /opt/venv\nCOPY api/ ./api/\n\n# Copy Node app\nCOPY --from=node_builder /app/public ./public\nCOPY --from=node_builder /app/.next/standalone ./\nCOPY --from=node_builder /app/.next/static ./.next/static\n\n# Expose the port the app runs on\nEXPOSE ${PORT:-8001} 3000\n\n# Create a script to run both backend and frontend\nRUN echo '#!/bin/bash\\n\\\n# Load environment variables from .env file if it exists\\n\\\nif [ -f .env ]; then\\n\\\n  export $(grep -v \"^#\" .env | xargs -r)\\n\\\nfi\\n\\\n\\n\\\n# Check for required environment variables\\n\\\nif [ -z \"$OPENAI_API_KEY\" ] || [ -z \"$GOOGLE_API_KEY\" ]; then\\n\\\n  echo \"Warning: OPENAI_API_KEY and/or GOOGLE_API_KEY environment variables are not set.\"\\n\\\n  echo \"These are required for DeepWiki to function properly.\"\\n\\\n  echo \"You can provide them via a mounted .env file or as environment variables when running the container.\"\\n\\\nfi\\n\\\n\\n\\\n# Start the API server in the background with the configured port\\n\\\npython -m api.main --port ${PORT:-8001} &\\n\\\nPORT=3000 HOSTNAME=0.0.0.0 node server.js &\\n\\\nwait -n\\n\\\nexit $?' > /app/start.sh && chmod +x /app/start.sh\n\n# Set environment variables\nENV PORT=8001\nENV NODE_ENV=production\nENV SERVER_BASE_URL=http://localhost:${PORT:-8001}\n\n# Create empty .env file (will be overridden if one exists at runtime)\nRUN touch .env\n\n# Command to run the application\nCMD [\"/app/start.sh\"]\n"
  },
  {
    "path": "Dockerfile-ollama-local",
    "content": "# syntax=docker/dockerfile:1-labs\n\nFROM node:20-alpine AS node_base\n\nFROM node_base AS node_deps\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci --legacy-peer-deps\n\nFROM node_base AS node_builder\nWORKDIR /app\nCOPY --from=node_deps /app/node_modules ./node_modules\nCOPY --exclude=./api . .\nRUN NODE_ENV=production npm run build\n\nFROM python:3.11-slim AS py_deps\nWORKDIR /api\nCOPY api/pyproject.toml .\nCOPY api/poetry.lock .\nRUN python -m pip install poetry==2.0.1 --no-cache-dir && \\\n    poetry config virtualenvs.create true --local && \\\n    poetry config virtualenvs.in-project true --local && \\\n    poetry config virtualenvs.options.always-copy --local true && \\\n    POETRY_MAX_WORKERS=10 poetry install --no-interaction --no-ansi --only main && \\\n    poetry cache clear --all .\n\nFROM python:3.11-slim AS ollama_base\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    curl zstd && rm -rf /var/lib/apt/lists/*\n\n# Detect architecture and download appropriate Ollama version\n# ARG TARGETARCH can be set at build time with --build-arg TARGETARCH=arm64 or TARGETARCH=amd64\nARG TARGETARCH=arm64\nRUN OLLAMA_ARCH=\"\" && \\\n    if [ \"$TARGETARCH\" = \"arm64\" ]; then \\\n        echo \"Building for ARM64 architecture.\" && \\\n        OLLAMA_ARCH=\"arm64\"; \\\n    elif [ \"$TARGETARCH\" = \"amd64\" ]; then \\\n        echo \"Building for AMD64 architecture.\" && \\\n        OLLAMA_ARCH=\"amd64\"; \\\n    else \\\n        echo \"Error: Unsupported architecture '$TARGETARCH'. Supported architectures are 'arm64' and 'amd64'.\" >&2 && \\\n        exit 1; \\\n    fi && \\\n    (set -o pipefail; \\\n     curl -fL \"https://ollama.com/download/ollama-linux-${OLLAMA_ARCH}.tar.zst\" \\\n      | zstd -d | tar -x -C /usr)\n\nRUN ollama serve > /dev/null 2>&1 & \\\n    sleep 20 && \\\n    ollama pull nomic-embed-text && \\\n    ollama pull qwen3:1.7b\n\n# Use Python 3.11 as final image\nFROM python:3.11-slim\n\n# Set working directory\nWORKDIR /app\n\n# Install Node.js and npm\nRUN apt-get update && apt-get install -y \\\n    curl \\\n    gnupg \\\n    git \\\n    && mkdir -p /etc/apt/keyrings \\\n    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \\\n    && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list \\\n    && apt-get update \\\n    && apt-get install -y nodejs \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Copy Python dependencies\nCOPY --from=py_deps /api/.venv /opt/venv\nCOPY api/ ./api/\n\n# Copy Node app\nCOPY --from=node_builder /app/public ./public\nCOPY --from=node_builder /app/.next/standalone ./\nCOPY --from=node_builder /app/.next/static ./.next/static\nCOPY --from=ollama_base /usr/bin/ollama /usr/local/bin/\nCOPY --from=ollama_base /root/.ollama /root/.ollama\n\n# Expose the port the app runs on\nEXPOSE ${PORT:-8001} 3000\n\n# Create a script to run both backend and frontend\nRUN echo '#!/bin/bash\\n\\\n# Start ollama serve in background\\n\\\nollama serve > /dev/null 2>&1 &\\n\\\n\\n\\\n# Load environment variables from .env file if it exists\\n\\\nif [ -f .env ]; then\\n\\\n  export $(grep -v \"^#\" .env | xargs -r)\\n\\\nfi\\n\\\n\\n\\\n# Check for required environment variables\\n\\\nif [ -z \"$OPENAI_API_KEY\" ] || [ -z \"$GOOGLE_API_KEY\" ]; then\\n\\\n  echo \"Warning: OPENAI_API_KEY and/or GOOGLE_API_KEY environment variables are not set.\"\\n\\\n  echo \"These are required for DeepWiki to function properly.\"\\n\\\n  echo \"You can provide them via a mounted .env file or as environment variables when running the container.\"\\n\\\nfi\\n\\\n\\n\\\n# Start the API server in the background with the configured port\\n\\\npython -m api.main --port ${PORT:-8001} &\\n\\\nPORT=3000 HOSTNAME=0.0.0.0 node server.js &\\n\\\nwait -n\\n\\\nexit $?' > /app/start.sh && chmod +x /app/start.sh\n\n# Set environment variables\nENV PORT=8001\nENV NODE_ENV=production\nENV SERVER_BASE_URL=http://localhost:${PORT:-8001}\n\n# Create empty .env file (will be overridden if one exists at runtime)\nRUN touch .env\n\n# Command to run the application\nCMD [\"/app/start.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Sheing Ng\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": "Ollama-instruction.md",
    "content": "# Using DeepWiki with Ollama: Beginner's Guide\n\nDeepWiki supports local AI models through Ollama, which is perfect if you want to:\n\n- Run everything locally without relying on cloud APIs\n- Avoid API costs from OpenAI or Google\n- Have more privacy with your code analysis\n\n## Step 1: Install Ollama\n\n### For Windows\n- Download Ollama from the [official website](https://ollama.com/download)\n- Run the installer and follow the on-screen instructions\n- After installation, Ollama will run in the background (check your system tray)\n\n### For macOS\n- Download Ollama from the [official website](https://ollama.com/download)\n- Open the downloaded file and drag Ollama to your Applications folder\n- Launch Ollama from your Applications folder\n\n### For Linux\n- Run the following command:\n  ```bash\n  curl -fsSL https://ollama.com/install.sh | sh\n  ```\n\n## Step 2: Download Required Models\n\nOpen a terminal (Command Prompt or PowerShell on Windows) and run:\n\n```bash\nollama pull nomic-embed-text\nollama pull qwen3:1.7b\n```\n\nThe first command downloads the embedding model that DeepWiki uses to understand your code. The second downloads a small but capable language model for generating documentation.\n\n## Step 3: Set Up DeepWiki\n\nClone the DeepWiki repository:\n```bash\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n```\n\nCreate a `.env` file in the project root:\n```\n# No need for API keys when using Ollama locally\nPORT=8001\n# Optionally, provide OLLAMA_HOST if Ollama is not local\nOLLAMA_HOST=your_ollama_host # (default: http://localhost:11434)\n```\n\nConfigure the Local Embedder for Ollama:\n```\ncp api/config/embedder.ollama.json.bak api/config/embedder.json\n# overwrite api/config/embedder.json? (y/n [n]) y\n```\n\nStart the backend:\n```bash\npython -m pip install poetry==2.0.1 && poetry install\npython -m api.main\n```\n\nStart the frontend:\n```bash\nnpm install\nnpm run dev\n```\n\n## Step 4: Use DeepWiki with Ollama\n\n1. Open http://localhost:3000 in your browser\n2. Enter a GitHub, GitLab, or Bitbucket repository URL\n3. Check the use \"Local Ollama Model\" option\n4. Click \"Generate Wiki\"\n\n![Ollama Option](screenshots/Ollama.png)\n\n## Alternative using Dockerfile\n\n1. Configure the Local Embedder for Ollama:\n```\ncp api/config/embedder.ollama.json.bak api/config/embedder.json\n# overwrite api/config/embedder.json? (y/n [n]) y\n```\n\n2. Build the docker image `docker build -f Dockerfile-ollama-local -t deepwiki:ollama-local .`\n3. Run the container:\n   ```bash\n   # For regular use\n   docker run -p 3000:3000 -p 8001:8001 --name deepwiki \\\n     -v ~/.adalflow:/root/.adalflow \\\n     -e OLLAMA_HOST=your_ollama_host \\\n     deepwiki:ollama-local\n   \n   # For local repository analysis\n   docker run -p 3000:3000 -p 8001:8001 --name deepwiki \\\n     -v ~/.adalflow:/root/.adalflow \\\n     -e OLLAMA_HOST=your_ollama_host \\\n     -v /path/to/your/repo:/app/local-repos/repo-name \\\n     deepwiki:ollama-local\n   ```\n\n4. When using local repositories in the interface: use `/app/local-repos/repo-name` as the local repository path.\n\n5. Open http://localhost:3000 in your browser\n\nNote: For Apple Silicon Macs, the Dockerfile automatically uses ARM64 binaries for better performance.\n\n## How It Works\n\nWhen you select \"Use Local Ollama\", DeepWiki will:\n\n1. Use the `nomic-embed-text` model for creating embeddings of your code\n2. Use the `qwen3:1.7b` model for generating documentation\n3. Process everything locally on your machine\n\n## Troubleshooting\n\n### \"Cannot connect to Ollama server\"\n- Make sure Ollama is running in the background. You can check by running `ollama list` in your terminal.\n- Verify that Ollama is running on the default port (11434)\n- Try restarting Ollama\n\n### Slow generation\n- Local models are typically slower than cloud APIs. Consider using a smaller repository or a more powerful computer.\n- The `qwen3:1.7b` model is optimized for speed and quality balance. Larger models will be slower but may produce better results.\n\n### Out of memory errors\n- If you encounter memory issues, try using a smaller model like `phi3:mini` instead of larger models.\n- Close other memory-intensive applications while running Ollama\n\n## Advanced: Using Different Models\n\nIf you want to try different models, you can modify the `api/config/generator.json` file:\n\n```python\n\"generator_ollama\": {\n    \"model_client\": OllamaClient,\n    \"model_kwargs\": {\n        \"model\": \"qwen3:1.7b\",  # Change this to another model\n        \"options\": {\n            \"temperature\": 0.7,\n            \"top_p\": 0.8,\n        }\n    },\n},\n```\n\nYou can replace `\"model\": \"qwen3:1.7b\"` with any model you've pulled with Ollama. For a list of available models, visit [Ollama's model library](https://ollama.com/library) or run `ollama list` in your terminal.\n\nSimilarly, you can change the embedding model:\n\n```python\n\"embedder_ollama\": {\n    \"model_client\": OllamaClient,\n    \"model_kwargs\": {\n        \"model\": \"nomic-embed-text\"  # Change this to another embedding model\n    },\n},\n```\n\n## Performance Considerations\n\n### Hardware Requirements\n\nFor optimal performance with Ollama:\n- **CPU**: 4+ cores recommended\n- **RAM**: 8GB minimum, 16GB+ recommended\n- **Storage**: 10GB+ free space for models\n- **GPU**: Optional but highly recommended for faster processing\n\n### Model Selection Guide\n\n| Model | Size | Speed | Quality | Use Case |\n|-------|------|-------|---------|----------|\n| phi3:mini | 1.3GB | Fast | Good | Small projects, quick testing |\n| qwen3:1.7b | 3.8GB | Medium | Better | Default, good balance |\n| llama3:8b | 8GB | Slow | Best | Complex projects, detailed analysis |\n\n## Limitations\n\nWhen using Ollama with DeepWiki:\n\n1. **No Internet Access**: The models run completely offline and cannot access external information\n2. **Limited Context Window**: Local models typically have smaller context windows than cloud APIs\n3. **Less Powerful**: Local models may not match the quality of the latest cloud models\n\n## Conclusion\n\nUsing DeepWiki with Ollama gives you a completely local, private solution for code documentation. While it may not match the speed or quality of cloud-based solutions, it provides a free and privacy-focused alternative that works well for most projects.\n\nEnjoy using DeepWiki with your local Ollama models!\n"
  },
  {
    "path": "README.es.md",
    "content": "# DeepWiki-Open\n\n![Banner de DeepWiki](screenshots/Deepwiki.png)\n\n**DeepWiki** crea automáticamente wikis hermosas e interactivas para cualquier repositorio de GitHub, GitLab o BitBucket. ¡Solo ingresa el nombre de un repositorio y DeepWiki:\n\n1. Analizará la estructura del código\n2. Generará documentación completa\n3. Creará diagramas visuales para explicar cómo funciona todo\n4. Organizará todo en una wiki fácil de navegar\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Características\n\n- **Documentación Instantánea**: Convierte cualquier repositorio de GitHub, GitLab o BitBucket en una wiki en segundos\n- **Soporte para Repositorios Privados**: Accede de forma segura a repositorios privados con tokens de acceso personal\n- **Análisis Inteligente**: Comprensión de la estructura y relaciones del código impulsada por IA\n- **Diagramas Hermosos**: Diagramas Mermaid automáticos para visualizar la arquitectura y el flujo de datos\n- **Navegación Sencilla**: Interfaz simple e intuitiva para explorar la wiki\n- **Función de Preguntas**: Chatea con tu repositorio usando IA potenciada por RAG para obtener respuestas precisas\n- **Investigación Profunda**: Proceso de investigación de múltiples turnos que examina a fondo temas complejos\n- **Múltiples Proveedores de Modelos**: Soporte para Google Gemini, OpenAI, OpenRouter y modelos locales de Ollama\n\n## 🚀 Inicio Rápido (¡Súper Fácil!)\n\n### Opción 1: Usando Docker\n\n```bash\n# Clonar el repositorio\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Crear un archivo .env con tus claves API\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# Opcional: Añadir clave API de OpenRouter si quieres usar modelos de OpenRouter\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# Ejecutar con Docker Compose\ndocker-compose up\n```\n\n(Los comandos de Docker anteriores, así como la configuración de `docker-compose.yml`, montan el directorio `~/.adalflow` de tu host en `/root/.adalflow` dentro del contenedor. Esta ruta se utiliza para almacenar:\n- Repositorios clonados (`~/.adalflow/repos/`)\n- Sus embeddings e índices (`~/.adalflow/databases/`)\n- Contenido de wiki generado y cacheado (`~/.adalflow/wikicache/`)\n\nEsto asegura que tus datos persistan incluso si el contenedor se detiene o se elimina.)\n\n> 💡 **Dónde obtener estas claves:**\n> - Obtén una clave API de Google en [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Obtén una clave API de OpenAI en [OpenAI Platform](https://platform.openai.com/api-keys)\n\n### Opción 2: Configuración Manual (Recomendada)\n\n#### Paso 1: Configurar tus Claves API\n\nCrea un archivo `.env` en la raíz del proyecto con estas claves:\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# Opcional: Añade esto si quieres usar modelos de OpenRouter\nOPENROUTER_API_KEY=your_openrouter_api_key\n```\n\n#### Paso 2: Iniciar el Backend\n\n```bash\n# Instalar dependencias de Python\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Iniciar el servidor API\npython -m api.main\n```\n\n#### Paso 3: Iniciar el Frontend\n\n```bash\n# Instalar dependencias de JavaScript\nnpm install\n# o\nyarn install\n\n# Iniciar la aplicación web\nnpm run dev\n# o\nyarn dev\n```\n\n#### Paso 4: ¡Usar DeepWiki!\n\n1. Abre [http://localhost:3000](http://localhost:3000) en tu navegador\n2. Ingresa un repositorio de GitHub, GitLab o Bitbucket (como `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, o `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. Para repositorios privados, haz clic en \"+ Agregar tokens de acceso\" e ingresa tu token de acceso personal de GitHub o GitLab\n4. ¡Haz clic en \"Generar Wiki\" y observa la magia suceder!\n\n## 🔍 Cómo Funciona\n\nDeepWiki usa IA para:\n\n1. Clonar y analizar el repositorio de GitHub, GitLab o Bitbucket (incluyendo repos privados con autenticación por token)\n2. Crear embeddings del código para recuperación inteligente\n3. Generar documentación con IA consciente del contexto (usando modelos de Google Gemini, OpenAI, OpenRouter o Ollama local)\n4. Crear diagramas visuales para explicar las relaciones del código\n5. Organizar todo en una wiki estructurada\n6. Habilitar preguntas y respuestas inteligentes con el repositorio a través de la función de Preguntas\n7. Proporcionar capacidades de investigación en profundidad con Investigación Profunda\n\n```mermaid\ngraph TD\n    A[Usuario ingresa repo GitHub/GitLab/Bitbucket] --> AA{¿Repo privado?}\n    AA -->|Sí| AB[Agregar token de acceso]\n    AA -->|No| B[Clonar Repositorio]\n    AB --> B\n    B --> C[Analizar Estructura del Código]\n    C --> D[Crear Embeddings del Código]\n\n    D --> M{Seleccionar Proveedor de Modelo}\n    M -->|Google Gemini| E1[Generar con Gemini]\n    M -->|OpenAI| E2[Generar con OpenAI]\n    M -->|OpenRouter| E3[Generar con OpenRouter]\n    M -->|Ollama Local| E4[Generar con Ollama]\n\n    E1 --> E[Generar Documentación]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[Crear Diagramas Visuales]\n    E --> G[Organizar como Wiki]\n    F --> G\n    G --> H[DeepWiki Interactiva]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ Estructura del Proyecto\n\n```\ndeepwiki/\n├── api/                  # Servidor API backend\n│   ├── main.py           # Punto de entrada de la API\n│   ├── api.py            # Implementación FastAPI\n│   ├── rag.py            # Generación Aumentada por Recuperación\n│   ├── data_pipeline.py  # Utilidades de procesamiento de datos\n│   └── requirements.txt  # Dependencias Python\n│\n├── src/                  # App frontend Next.js\n│   ├── app/              # Directorio app de Next.js\n│   │   └── page.tsx      # Página principal de la aplicación\n│   └── components/       # Componentes React\n│       └── Mermaid.tsx   # Renderizador de diagramas Mermaid\n│\n├── public/               # Activos estáticos\n├── package.json          # Dependencias JavaScript\n└── .env                  # Variables de entorno (crear este archivo)\n```\n\n## 🤖 Sistema de Selección de Modelos Basado en Proveedores\n\nDeepWiki ahora implementa un sistema flexible de selección de modelos basado en proveedores que soporta múltiples proveedores de LLM:\n\n### Proveedores y Modelos Soportados\n\n- **Google**: Predeterminado `gemini-2.5-flash`, también soporta `gemini-2.5-flash-lite`, `gemini-2.5-pro`, etc.\n- **OpenAI**: Predeterminado `gpt-5-nano`, también soporta `gpt-5`, `4o`, etc.\n- **OpenRouter**: Acceso a múltiples modelos a través de una API unificada, incluyendo Claude, Llama, Mistral, etc.\n- **Ollama**: Soporte para modelos de código abierto ejecutados localmente como `llama3`\n\n### Variables de Entorno\n\nCada proveedor requiere sus correspondientes variables de entorno para las claves API:\n\n```\n# Claves API\nGOOGLE_API_KEY=tu_clave_api_google        # Requerida para modelos Google Gemini\nOPENAI_API_KEY=tu_clave_api_openai        # Requerida para modelos OpenAI\nOPENROUTER_API_KEY=tu_clave_api_openrouter # Requerida para modelos OpenRouter\n\n# Configuración de URL Base de OpenAI API\nOPENAI_BASE_URL=https://punto-final-personalizado.com/v1  # Opcional, para endpoints personalizados de OpenAI API\n\n# Directorio de Configuración\nDEEPWIKI_CONFIG_DIR=/ruta/a/directorio/config/personalizado  # Opcional, para ubicación personalizada de archivos de configuración\n```\n\n### Archivos de Configuración\n\nDeepWiki utiliza archivos de configuración JSON para gestionar varios aspectos del sistema:\n\n1. **`generator.json`**: Configuración para modelos de generación de texto\n   - Define los proveedores de modelos disponibles (Google, OpenAI, OpenRouter, Ollama)\n   - Especifica los modelos predeterminados y disponibles para cada proveedor\n   - Contiene parámetros específicos de los modelos como temperatura y top_p\n\n2. **`embedder.json`**: Configuración para modelos de embeddings y procesamiento de texto\n   - Define modelos de embeddings para almacenamiento vectorial\n   - Contiene configuración del recuperador para RAG\n   - Especifica ajustes del divisor de texto para fragmentación de documentos\n\n3. **`repo.json`**: Configuración para manejo de repositorios\n   - Contiene filtros de archivos para excluir ciertos archivos y directorios\n   - Define límites de tamaño de repositorio y reglas de procesamiento\n\nPor defecto, estos archivos se encuentran en el directorio `api/config/`. Puedes personalizar su ubicación usando la variable de entorno `DEEPWIKI_CONFIG_DIR`.\n\n### Selección de Modelos Personalizados para Proveedores de Servicios\n\nLa función de selección de modelos personalizados está diseñada específicamente para proveedores de servicios que necesitan:\n\n- Puede ofrecer a los usuarios dentro de su organización una selección de diferentes modelos de IA\n- Puede adaptarse rápidamente al panorama de LLM en rápida evolución sin cambios de código\n- Puede soportar modelos especializados o ajustados que no están en la lista predefinida\n\nUsted puede implementar sus ofertas de modelos seleccionando entre las opciones predefinidas o ingresando identificadores de modelos personalizados en la interfaz frontend.\n\n### Configuración de URL Base para Canales Privados Empresariales\n\nLa configuración de base_url del Cliente OpenAI está diseñada principalmente para usuarios empresariales con canales API privados. Esta función:\n\n- Permite la conexión a endpoints API privados o específicos de la empresa\n- Permite a las organizaciones usar sus propios servicios LLM auto-alojados o desplegados a medida\n- Soporta integración con servicios de terceros compatibles con la API de OpenAI\n\n**Próximamente**: En futuras actualizaciones, DeepWiki soportará un modo donde los usuarios deberán proporcionar sus propias claves API en las solicitudes. Esto permitirá a los clientes empresariales con canales privados utilizar sus disposiciones API existentes sin compartir credenciales con el despliegue de DeepWiki.\n\n## 🧩 Uso de modelos de embedding compatibles con OpenAI (por ejemplo, Alibaba Qwen)\n\nSi deseas usar modelos de embedding compatibles con la API de OpenAI (como Alibaba Qwen), sigue estos pasos:\n\n1. Sustituye el contenido de `api/config/embedder.json` por el de `api/config/embedder_openai_compatible.json`.\n2. En el archivo `.env` de la raíz del proyecto, configura las variables de entorno necesarias, por ejemplo:\n   ```\n   OPENAI_API_KEY=tu_api_key\n   OPENAI_BASE_URL=tu_endpoint_compatible_openai\n   ```\n3. El programa sustituirá automáticamente los placeholders de embedder.json por los valores de tus variables de entorno.\n\nAsí puedes cambiar fácilmente a cualquier servicio de embedding compatible con OpenAI sin modificar el código.\n\n## 🤖 Funciones de Preguntas e Investigación Profunda\n\n### Función de Preguntas\n\nLa función de Preguntas te permite chatear con tu repositorio usando Generación Aumentada por Recuperación (RAG):\n\n- **Respuestas Conscientes del Contexto**: Obtén respuestas precisas basadas en el código real de tu repositorio\n- **Potenciada por RAG**: El sistema recupera fragmentos de código relevantes para proporcionar respuestas fundamentadas\n- **Transmisión en Tiempo Real**: Ve las respuestas mientras se generan para una experiencia más interactiva\n- **Historial de Conversación**: El sistema mantiene el contexto entre preguntas para interacciones más coherentes\n\n### Función de Investigación Profunda\n\nInvestigación Profunda lleva el análisis de repositorios al siguiente nivel con un proceso de investigación de múltiples turnos:\n\n- **Investigación en Profundidad**: Explora a fondo temas complejos a través de múltiples iteraciones de investigación\n- **Proceso Estructurado**: Sigue un plan de investigación claro con actualizaciones y una conclusión completa\n- **Continuación Automática**: La IA continúa automáticamente la investigación hasta llegar a una conclusión (hasta 5 iteraciones)\n- **Etapas de Investigación**:\n  1. **Plan de Investigación**: Describe el enfoque y los hallazgos iniciales\n  2. **Actualizaciones de Investigación**: Desarrolla las iteraciones anteriores con nuevas perspectivas\n  3. **Conclusión Final**: Proporciona una respuesta completa basada en todas las iteraciones\n\nPara usar Investigación Profunda, simplemente activa el interruptor \"Investigación Profunda\" en la interfaz de Preguntas antes de enviar tu pregunta.\n\n## 📱 Capturas de Pantalla\n\n![Interfaz Principal de DeepWiki](screenshots/Interface.png)\n*La interfaz principal de DeepWiki*\n\n![Soporte para Repositorios Privados](screenshots/privaterepo.png)\n*Acceso a repositorios privados con tokens de acceso personal*\n\n![Función de Investigación Profunda](screenshots/DeepResearch.png)\n*Investigación Profunda realiza investigaciones de múltiples turnos para temas complejos*\n\n### Video de Demostración\n\n[![Video de Demostración de DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*¡Mira DeepWiki en acción!*\n\n## ❓ Solución de Problemas\n\n### Problemas con Claves API\n- **\"Faltan variables de entorno\"**: Asegúrate de que tu archivo `.env` esté en la raíz del proyecto y contenga las claves API requeridas\n- **\"Clave API no válida\"**: Verifica que hayas copiado la clave completa correctamente sin espacios adicionales\n- **\"Error de API OpenRouter\"**: Verifica que tu clave API de OpenRouter sea válida y tenga créditos suficientes\n\n### Problemas de Conexión\n- **\"No se puede conectar al servidor API\"**: Asegúrate de que el servidor API esté ejecutándose en el puerto 8001\n- **\"Error CORS\"**: La API está configurada para permitir todos los orígenes, pero si tienes problemas, intenta ejecutar tanto el frontend como el backend en la misma máquina\n\n### Problemas de Generación\n- **\"Error al generar wiki\"**: Para repositorios muy grandes, prueba primero con uno más pequeño\n- **\"Formato de repositorio no válido\"**: Asegúrate de usar un formato de URL válido para GitHub, GitLab o Bitbucket\n- **\"No se pudo obtener la estructura del repositorio\"**: Para repositorios privados, asegúrate de haber ingresado un token de acceso personal válido con los permisos apropiados\n- **\"Error de renderizado de diagrama\"**: La aplicación intentará automáticamente arreglar los diagramas rotos\n\n### Soluciones Comunes\n1. **Reiniciar ambos servidores**: A veces un simple reinicio soluciona la mayoría de los problemas\n2. **Revisar los registros de la consola**: Abre las herramientas de desarrollo del navegador para ver cualquier error de JavaScript\n3. **Revisar los registros de la API**: Mira la terminal donde se ejecuta la API para ver errores de Python\n\n## 🤝 Contribuir\n\n¡Las contribuciones son bienvenidas! Siéntete libre de:\n- Abrir issues para bugs o solicitudes de funciones\n- Enviar pull requests para mejorar el código\n- Compartir tus comentarios e ideas\n\n## 📄 Licencia\n\nEste proyecto está licenciado bajo la Licencia MIT - consulta el archivo [LICENSE](LICENSE) para más detalles.\n\n## ⭐ Historial de Estrellas\n\n[![Gráfico de Historial de Estrellas](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n"
  },
  {
    "path": "README.fr.md",
    "content": "\n# DeepWiki-Open\n\n![Bannière DeepWiki](screenshots/Deepwiki.png)\n\n**DeepWiki** est ma propre tentative d’implémentation de DeepWiki, un outil qui crée automatiquement des wikis magnifiques et interactifs pour n’importe quel dépôt GitHub, GitLab ou Bitbucket ! Il suffit d’entrer un nom de dépôt, et DeepWiki :\n\n1. Analyse la structure du code  \n2. Génère une documentation complète  \n3. Crée des diagrammes visuels pour expliquer le fonctionnement  \n4. Organise le tout dans un wiki facile à naviguer\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n[![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc)\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Fonctionnalités\n\n- **Documentation instantanée** : Transforme un dépôt GitHub, GitLab ou Bitbucket en wiki en quelques secondes\n- **Support des dépôts privés** : Accès sécurisé avec jetons d’accès personnels\n- **Analyse intelligente** : Compréhension de la structure et des relations du code via l’IA\n- **Diagrammes élégants** : Diagrammes Mermaid automatiques pour visualiser l’architecture et les flux de données\n- **Navigation facile** : Interface simple et intuitive\n- **Fonction “Ask”** : Posez des questions à votre dépôt avec une IA alimentée par RAG\n- **DeepResearch** : Processus de recherche multi-étapes pour explorer des sujets complexes\n- **Multiples fournisseurs de modèles IA** : Prise en charge de Google Gemini, OpenAI, OpenRouter, et Ollama local\n\n## 🚀 Démarrage rapide (super facile !)\n\n### Option 1 : Avec Docker\n\n```bash\n# Cloner le dépôt\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Créer un fichier .env avec vos clés API\necho \"GOOGLE_API_KEY=votre_clé_google\" > .env\necho \"OPENAI_API_KEY=votre_clé_openai\" >> .env\n# Facultatif : clé OpenRouter\necho \"OPENROUTER_API_KEY=votre_clé_openrouter\" >> .env\n# Facultatif : hôte personnalisé Ollama\necho \"OLLAMA_HOST=votre_hote_ollama\" >> .env\n# Facultatif : Azure OpenAI\necho \"AZURE_OPENAI_API_KEY=votre_clé_azure\" >> .env\necho \"AZURE_OPENAI_ENDPOINT=votre_endpoint\" >> .env\necho \"AZURE_OPENAI_VERSION=version_api\" >> .env\n\n# Lancer avec Docker Compose\ndocker-compose up\n```\n\nPour des instructions détaillées sur l’utilisation de DeepWiki avec Ollama et Docker, consultez [Ollama Instructions](Ollama-instruction.md).\n\n> 💡 **Où obtenir ces clés :**\n> - Obtenez une clé API Google depuis [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Obtenez une clé API OpenAI depuis [OpenAI Platform](https://platform.openai.com/api-keys)\n> - Obtenez les identifiants Azure OpenAI depuis [Azure Portal](https://portal.azure.com/) – créez une ressource Azure OpenAI et récupérez la clé API, l’endpoint et la version de l’API\n\n### Option 2 : Installation manuelle (Recommandée)\n\n#### Étape 1 : Configurez vos clés API\n\nCréez un fichier `.env` à la racine du projet avec ces clés :\n```\nGOOGLE_API_KEY=votre_clé_google\nOPENAI_API_KEY=votre_clé_openai\n# Optionnel : Ajoutez ceci pour utiliser des modèles OpenRouter\nOPENROUTER_API_KEY=votre_clé_openrouter\n# Optionnel : Ajoutez ceci pour utiliser des modèles Azure OpenAI\nAZURE_OPENAI_API_KEY=votre_clé_azure_openai\nAZURE_OPENAI_ENDPOINT=votre_endpoint_azure_openai\nAZURE_OPENAI_VERSION=votre_version_azure_openai\n# Optionnel :Ajouter un hôte distant Ollama si il n'est pas local. défaut : http://localhost:11434\nOLLAMA_HOST=votre_hote_ollama\n```\n\n#### Étape 2 : Démarrer le Backend\n\n```bash\n# Installer dépendances Python\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Démarrer le serveur API\npython -m api.main\n```\n\n#### Étape 3 : Démarrer le Frontend\n\n```bash\n# Installer les dépendances JavaScript\nnpm install\n# ou\nyarn install\n\n# Démarrer le serveur web\nnpm run dev\n# ou\nyarn dev\n```\n\n#### Étape 4 : Utiliser DeepWiki!\n\n1. Ouvrir [http://localhost:3000](http://localhost:3000) dans votre navigateur\n2. Entrer l'adresse d'un dépôt GitHub, GitLab ou Bitbucket (comme `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, or `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. Pour les dépôts privés, cliquez sur \"+ Ajouter un jeton d'accès\" et entrez votre jeton d’accès personnel GitHub ou GitLab.\n4. Cliquez sur \"Générer le Wiki\" et regardez la magie opérer !\n\n## 🔍 Comment ça marche\n\nDeepWiki utilise l'IA pour :\n\n1. Cloner et analyser le dépôt GitHub, GitLab ou Bitbucket (y compris les dépôts privés avec authentification par jeton d'accès)\n2. Créer des embeddings du code pour une récupération intelligente\n3. Générer de la documentation avec une IA sensible au contexte (en utilisant les modèles Google Gemini, OpenAI, OpenRouter, Azure OpenAI ou Ollama local)\n4. Créer des diagrammes visuels pour expliquer les relations du code\n5. Organiser le tout dans un wiki structuré\n6. Permettre des questions-réponses intelligentes avec le dépôt grâce à la fonctionnalité Ask\n7. Fournir des capacités de recherche approfondie avec DeepResearch\n\n```mermaid\ngraph TD\n    A[Utilisateur entre un dépôt GitHub/GitLab/Bitbucket] --> AA{Dépôt privé?}\n    AA -->|Oui| AB[Ajouter un jeton d'accès]\n    AA -->|Non| B[Cloner le dépôt]\n    AB --> B\n    B --> C[Analyser la structure du code]\n    C --> D[Créer des Embeddings]\n\n    D --> M{Sélectionner le modèle}\n    M -->|Google Gemini| E1[Générer avec Gemini]\n    M -->|OpenAI| E2[Générer avec OpenAI]\n    M -->|OpenRouter| E3[Générer avec OpenRouter]\n    M -->|Local Ollama| E4[Générer avec Ollama]\n    M -->|Azure| E5[Générer avec Azure]\n\n    E1 --> E[Générer la documentation]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n    E5 --> E\n\n    D --> F[Créer des diagrammes]\n    E --> G[Organiser en Wiki]\n    F --> G\n    G --> H[DeepWiki interactif]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4,E5 process;\n    class H result;\n```\n\n## 🛠️ Structure du Projet\n\n```\ndeepwiki/\n├── api/                  # Serveur API Backend\n│   ├── main.py           # Point d'entrée de l'API\n│   ├── api.py            # Implémentation FastAPI\n│   ├── rag.py            # Génération Augmentée par Récupération (RAG)\n│   ├── data_pipeline.py  # Utilitaires de traitement des données\n│   └── requirements.txt  # Dépendances Python\n│\n├── src/                  # Application Frontend Next.js\n│   ├── app/              # Répertoire de l'application Next.js\n│   │   └── page.tsx      # Page principale de l'application\n│   └── components/       # Composants React\n│       └── Mermaid.tsx   # Rendu des diagrammes Mermaid\n│\n├── public/               # Ressources statiques\n├── package.json          # Dépendances JavaScript\n└── .env                  # Variables d'environnement (à créer)\n```\n\n## 🤖 Système de sélection de modèles\n\nDeepWiki implémente désormais un système de sélection de modèles flexible, qui prend en charge plusieurs fournisseurs de LLM :\n\n### Fournisseurs et modèles pris en charge\n\n- **Google** : Par défaut `gemini-2.5-flash`, prend également en charge `gemini-2.5-flash-lite`, `gemini-2.5-pro`, etc.\n- **OpenAI** : Par défaut `gpt-5-nano`, prend également en charge `gpt-5`, `4o`, etc.\n- **OpenRouter** : Accès à plusieurs modèles via une API unifiée, notamment Claude, Llama, Mistral, etc.\n- **Azure OpenAI** : Par défaut `gpt-4o`, prend également en charge `o4-mini`, etc.\n- **Ollama** : Prise en charge des modèles open source exécutés localement, tels que `llama3`.\n\n### Variables d'environnement\n\nChaque fournisseur requiert les variables d'environnement de clé API correspondantes :\n\n```\n# API Keys\nGOOGLE_API_KEY=votre_clé_google        # Requis pour les modèles Google Gemini\nOPENAI_API_KEY=votre_clé_openai        # Requis pour les modèles OpenAI\nOPENROUTER_API_KEY=votre_clé_openrouter # Requis pour les modèles OpenRouter\nAZURE_OPENAI_API_KEY=votre_clé_azure_openai  #Requis pour les modèles Azure OpenAI\nAZURE_OPENAI_ENDPOINT=votre_endpoint_azure_openai  #Requis pour les modèles Azure OpenAI\nAZURE_OPENAI_VERSION=votre_version_azure_openai  #Requis pour les modèles Azure OpenAI\n\n# Configuration d'un endpoint OpenAI API personnalisé\nOPENAI_BASE_URL=https://custom-api-endpoint.com/v1  # Optionnel, pour les endpoints API OpenAI personnalisés\n\n# Hôte Ollama personnalisé\nOLLAMA_HOST=votre_hôte_ollama # Optionnel, si Ollama n'est pas local. défaut: http://localhost:11434\n\n# Répertoire de configuration\nDEEPWIKI_CONFIG_DIR=/chemin/vers/dossier/de/configuration  # Optionnel, pour personaliser le répertoire de stockage de la configuration\n```\n\n### Fichiers de Configuration\n\nDeepWiki utilise des fichiers de configuration JSON pour gérer différents aspects du système :\n\n1. **`generator.json`** : Configuration des modèles de génération de texte\n   - Définit les fournisseurs de modèles disponibles (Google, OpenAI, OpenRouter, Azure, Ollama)\n   - Spécifie les modèles par défaut et disponibles pour chaque fournisseur\n   - Contient des paramètres spécifiques aux modèles tels que la température et top_p\n\n2. **`embedder.json`** : Configuration des modèles d'embedding et du traitement de texte\n   - Définit les modèles d'embedding pour le stockage vectoriel\n   - Contient la configuration du retriever pour RAG\n   - Spécifie les paramètres du séparateur de texte pour le chunking de documents\n\n3. **`repo.json`** : Configuration de la gestion des dépôts\n   - Contient des filtres de fichiers pour exclure certains fichiers et répertoires\n   - Définit les limites de taille des dépôts et les règles de traitement\n\nPar défaut, ces fichiers sont situés dans le répertoire `api/config/`. Vous pouvez personnaliser leur emplacement à l'aide de la variable d'environnement `DEEPWIKI_CONFIG_DIR`.\n\n### Sélection de Modèles Personnalisés pour les Fournisseurs de Services\n\nLa fonctionnalité de sélection de modèles personnalisés est spécialement conçue pour les fournisseurs de services qui ont besoin de :\n\n- Offrir plusieurs choix de modèles d'IA aux utilisateurs au sein de leur organisation\n- S'adapter rapidement à l'évolution rapide du paysage des LLM sans modifications de code\n- Prendre en charge des modèles spécialisés ou affinés qui ne figurent pas dans la liste prédéfinie\n\nLes fournisseurs de services peuvent implémenter leurs offres de modèles en sélectionnant parmi les options prédéfinies ou en entrant des identifiants de modèles personnalisés dans l'interface utilisateur.\n\n### Configuration de l'URL de base pour les canaux privés d'entreprise\n\nLa configuration `base_url` du client OpenAI est principalement conçue pour les utilisateurs d'entreprise disposant de canaux API privés. Cette fonctionnalité :\n\n- Permet la connexion à des points de terminaison API privés ou spécifiques à l'entreprise.\n- Permet aux organisations d'utiliser leurs propres services LLM auto-hébergés ou déployés sur mesure.\n- Prend en charge l'intégration avec des services tiers compatibles avec l'API OpenAI.\n\n**Bientôt disponible** : Dans les prochaines mises à jour, DeepWiki prendra en charge un mode où les utilisateurs devront fournir leurs propres clés API dans les requêtes. Cela permettra aux entreprises clientes disposant de canaux privés d'utiliser leurs accords API existants sans partager leurs informations d'identification avec le déploiement DeepWiki.\n\n## 🧩 Utilisation de modèles d'embedding compatibles avec OpenAI (par exemple, Alibaba Qwen)\n\nSi vous souhaitez utiliser des modèles d'embedding compatibles avec l'API OpenAI (comme Alibaba Qwen), suivez ces étapes :\n\n1. Remplacez le contenu de `api/config/embedder.json` par celui de `api/config/embedder_openai_compatible.json`.\n2. Dans votre fichier `.env` à la racine du projet, définissez les variables d'environnement appropriées, par exemple :\n   ```\n   OPENAI_API_KEY=votre_clé_api\n   OPENAI_BASE_URL=votre_endpoint_compatible_openai\n   ```\n3. Le programme substituera automatiquement les espaces réservés dans `embedder.json` avec les valeurs de vos variables d'environnement.\n\nCela vous permet de passer facilement à n'importe quel service d'embedding compatible avec OpenAI sans modifications de code.\n\n### Journalisation (Logging)\n\nDeepWiki utilise le module `logging` intégré de Python pour la sortie de diagnostics. Vous pouvez configurer la verbosité et la destination du fichier journal via des variables d'environnement :\n\n| Variable        | Description                                                               | Valeur par défaut             |\n|-----------------|---------------------------------------------------------------------------|------------------------------|\n| `LOG_LEVEL`     | Niveau de journalisation (DEBUG, INFO, WARNING, ERROR, CRITICAL).         | INFO                         |\n| `LOG_FILE_PATH` | Chemin vers le fichier journal. Si défini, les journaux y seront écrits.  | `api/logs/application.log`   |\n\nPour activer la journalisation de débogage et diriger les journaux vers un fichier personnalisé :\n```bash\nexport LOG_LEVEL=DEBUG\nexport LOG_FILE_PATH=./debug.log\npython -m api.main\n```\nOu avec Docker Compose:\n```bash\nLOG_LEVEL=DEBUG LOG_FILE_PATH=./debug.log docker-compose up\n```\n\nLors de l'exécution avec Docker Compose, le répertoire `api/logs` du conteneur est lié à `./api/logs` sur votre hôte (voir la section `volumes` dans `docker-compose.yml`), ce qui garantit que les fichiers journaux persistent lors des redémarrages.\n\nVous pouvez également stocker ces paramètres dans votre fichier `.env` :\n\n```bash\nLOG_LEVEL=DEBUG\nLOG_FILE_PATH=./debug.log\n```\nPuis exécutez simplement :\n\n```bash\ndocker-compose up\n```\n\n**Considérations de sécurité concernant le chemin des journaux :** Dans les environnements de production, assurez-vous que le répertoire `api/logs` et tout chemin de fichier journal personnalisé sont sécurisés avec des permissions de système de fichiers et des contrôles d'accès appropriés. L'application s'assure que `LOG_FILE_PATH` se trouve dans le répertoire `api/logs` du projet afin d'empêcher le parcours de chemin ou les écritures non autorisées.\n\n## 🛠️ Configuration Avancée\n\n### Variables d'environnement\n\n| Variable                | Description                                                     | Requis     | Note                                                                                                     |\n|-------------------------|-----------------------------------------------------------------|------------|----------------------------------------------------------------------------------------------------------|\n| `GOOGLE_API_KEY`        | Clé API Google Gemini pour la génération                        | Non        | Requis uniquement si vous souhaitez utiliser les modèles Google Gemini                                   |\n| `OPENAI_API_KEY`        | Clé API OpenAI pour les embeddings et la génération              | Oui        | Remarque : Ceci est requis même si vous n'utilisez pas les modèles OpenAI, car elle est utilisée pour les embeddings. |\n| `OPENROUTER_API_KEY`    | Clé API OpenRouter pour les modèles alternatifs                 | Non        | Requis uniquement si vous souhaitez utiliser les modèles OpenRouter                                      |\n| `AZURE_OPENAI_API_KEY`  | Clé API Azure OpenAI                                            | Non        | Requis uniquement si vous souhaitez utiliser les modèles Azure OpenAI                                    |\n| `AZURE_OPENAI_ENDPOINT` | Point de terminaison Azure OpenAI                               | Non        | Requis uniquement si vous souhaitez utiliser les modèles Azure OpenAI                                    |\n| `AZURE_OPENAI_VERSION`  | Version Azure OpenAI                                            | Non        | Requis uniquement si vous souhaitez utiliser les modèles Azure OpenAI                                    |\n| `OLLAMA_HOST`           | Hôte Ollama (par défaut : http://localhost:11434)               | Non        | Requis uniquement si vous souhaitez utiliser un serveur Ollama externe                                   |\n| `PORT`                  | Port du serveur API (par défaut : 8001)                         | Non        | Si vous hébergez l'API et le frontend sur la même machine, assurez-vous de modifier le port de `SERVER_BASE_URL` en conséquence |\n| `SERVER_BASE_URL`       | URL de base du serveur API (par défaut : http://localhost:8001) | Non        |                                                                                                           |\n| `DEEPWIKI_AUTH_MODE`    | Définir sur `true` ou `1` pour activer le mode verrouillé        | Non        | La valeur par défaut est `false`. Si activé, `DEEPWIKI_AUTH_CODE` est requis.                             |\n| `DEEPWIKI_AUTH_CODE`    | Le code requis pour la génération de wiki lorsque `DEEPWIKI_AUTH_MODE` est activé. | Non        | Utilisé uniquement si `DEEPWIKI_AUTH_MODE` est `true` ou `1`.                          |\n\nSi vous n'utilisez pas le mode Ollama, vous devez configurer une clé API OpenAI pour les embeddings. Les autres clés API ne sont requises que si vous configurez et utilisez des modèles des fournisseurs correspondants.\n\n## Mode vérouillé\n\nDeepWiki peut être configuré pour fonctionner en mode vérouillé, où la génération de wiki nécessite un code d'autorisation valide. Ceci est utile si vous souhaitez contrôler qui peut utiliser la fonctionnalité de génération.\nRestreint l'initialisation du frontend et protège la suppression du cache, mais n'empêche pas complètement la génération backend si les points de terminaison de l'API sont atteints directement.\n\nPour activer le mode vérouillé, définissez les variables d'environnement suivantes :\n\n- `DEEPWIKI_AUTH_MODE` : définissez cette variable sur `true` ou `1`. Une fois activée, l'interface affichera un champ de saisie pour le code d'autorisation.\n- `DEEPWIKI_AUTH_CODE` : définissez cette variable sur le code secret souhaité. Restreint l'initialisation du frontend et protège la suppression du cache, mais n'empêche pas complètement la génération backend si les points de terminaison de l'API sont atteints directement.\n\nSi `DEEPWIKI_AUTH_MODE` n'est pas défini ou est défini sur `false` (ou toute autre valeur que `true`/`1`), la fonctionnalité d'autorisation sera désactivée et aucun code ne sera requis.\n\n### Configuration Docker\n\nVous pouvez utiliser Docker pour exécuter DeepWiki :\n\n#### Exécution du conteneur\n\n```bash\n# Récupérer l'image depuis GitHub Container Registry\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# Exécuter le conteneur avec les variables d'environnement\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=votre_clé_google \\\n  -e OPENAI_API_KEY=votre_clé_openai \\\n  -e OPENROUTER_API_KEY=votre_clé_openrouter \\\n  -e OLLAMA_HOST=votre_hôte_ollama \\\n  -e AZURE_OPENAI_API_KEY=votre_clé_azure_openai \\\n  -e AZURE_OPENAI_ENDPOINT=votre_endpoint_azure_openai \\\n  -e AZURE_OPENAI_VERSION=votre_version_azure_openai \\\n\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nCette commande monte également `~/.adalflow` de votre hôte vers `/root/.adalflow` dans le conteneur. Ce chemin est utilisé pour stocker :\n- Les dépôts clonés (`~/.adalflow/repos/`)\n- Leurs embeddings et index (`~/.adalflow/databases/`)\n- Le contenu wiki généré mis en cache (`~/.adalflow/wikicache/`)\n\nCela garantit que vos données persistent même si le conteneur est arrêté ou supprimé.\n\nVous pouvez également utiliser le fichier `docker-compose.yml` fourni :\n\n```bash\n# Modifiez d'abord le fichier .env avec vos clés API\ndocker-compose up\n```\n\n(Le fichier `docker-compose.yml` est préconfiguré pour monter `~/.adalflow` pour la persistance des données, de manière similaire à la commande `docker run` ci-dessus.)\n\n#### Utilisation d'un fichier .env avec Docker\n\nVous pouvez également monter un fichier `.env` dans le conteneur :\n\n```bash\n# Créer un fichier .env avec vos clés API\necho \"GOOGLE_API_KEY=votre_clé_google\" > .env\necho \"OPENAI_API_KEY=votre_clé_openai\" >> .env\necho \"OPENROUTER_API_KEY=votre_clé_openrouter\" >> .env\necho \"AZURE_OPENAI_API_KEY=votre_clé_azure_openai\" >> .env\necho \"AZURE_OPENAI_ENDPOINT=votre_endpoint_azure_openai\" >> .env\necho \"AZURE_OPENAI_VERSION=votre_version_azure_openai\"  >> .env\necho \"OLLAMA_HOST=votre_hôte_ollama\" >> .env\n\n# Run the container with the .env file mounted\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nCette commande monte également `~/.adalflow` de votre hôte vers `/root/.adalflow` dans le conteneur. Ce chemin est utilisé pour stocker :\n- Les dépôts clonés (`~/.adalflow/repos/`)\n- Leurs embeddings et index (`~/.adalflow/databases/`)\n- Le contenu wiki généré mis en cache (`~/.adalflow/wikicache/`)\n\nCela garantit que vos données persistent même si le conteneur est arrêté ou supprimé.\n\n#### Construction de l'image Docker localement\n\nIf you want to build the Docker image locally:\n\n```bash\n# Clone the repository\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Build the Docker image\ndocker build -t deepwiki-open .\n\n# Run the container\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=votre_clé_google \\\n  -e OPENAI_API_KEY=votre_clé_openai \\\n  -e OPENROUTER_API_KEY=votre_clé_openrouter \\\n  -e AZURE_OPENAI_API_KEY=votre_clé_azure_openai \\\n  -e AZURE_OPENAI_ENDPOINT=votre_endpoint_azure_openai \\\n  -e AZURE_OPENAI_VERSION=votre_version_azure_openai \\\n  -e OLLAMA_HOST=votre_hôte_ollama \\\n  deepwiki-open\n```\n\n#### Utilisation de certificats auto-signés dans Docker\n\nSi vous êtes dans un environnement qui utilise des certificats auto-signés, vous pouvez les inclure dans la construction de l'image Docker :\n\n1. Créez un répertoire pour vos certificats (le répertoire par défaut est `certs` à la racine de votre projet)\n2. Copiez vos fichiers de certificats `.crt` ou `.pem` dans ce répertoire\n3. Construisez l'image Docker :\n\n```bash\n# Construire avec le répertoire de certificats par défaut (certs)\ndocker build .\n\n# Ou construire avec un répertoire de certificats personnalisé\ndocker build --build-arg CUSTOM_CERT_DIR=my-custom-certs .\n```\n\n### Détails du serveur API\n\nLe serveur API fournit :\n- Clonage et indexation des dépôts\n- RAG (Retrieval Augmented Generation - Génération augmentée par récupération)\n- Complétion de chat en streaming\n\nPour plus de détails, consultez le [README de l’API](./api/README.md).\n\n## 🔌 Intégration OpenRouter\n\nDeepWiki prend désormais en charge [OpenRouter](https://openrouter.ai/) en tant que fournisseur de modèles, vous donnant accès à des centaines de modèles d'IA via une seule API :\n\n- **Options de modèles multiples** : accédez aux modèles d'OpenAI, Anthropic, Google, Meta, Mistral, et plus encore\n- **Configuration simple** : ajoutez simplement votre clé API OpenRouter et sélectionnez le modèle que vous souhaitez utiliser\n- **Rentabilité** : choisissez des modèles qui correspondent à votre budget et à vos besoins en termes de performances\n- **Commutation facile** : basculez entre différents modèles sans modifier votre code\n\n### Comment utiliser OpenRouter avec DeepWiki\n\n1. **Obtenez une clé API** : inscrivez-vous sur [OpenRouter](https://openrouter.ai/) et obtenez votre clé API\n2. **Ajouter à l'environnement** : ajoutez `OPENROUTER_API_KEY=votre_clé` à votre fichier `.env`\n3. **Activer dans l'interface utilisateur** : cochez l'option \"Utiliser l'API OpenRouter\" sur la page d'accueil\n4. **Sélectionnez le modèle** : choisissez parmi les modèles populaires tels que GPT-4o, Claude 3.5 Sonnet, Gemini 2.0, et plus encore\n\nOpenRouter est particulièrement utile si vous souhaitez :\n\n- Essayer différents modèles sans vous inscrire à plusieurs services\n- Accéder à des modèles qui pourraient être restreints dans votre région\n- Comparer les performances entre différents fournisseurs de modèles\n- Optimiser le rapport coût/performance en fonction de vos besoins\n\n## 🤖 Fonctionnalités Ask & DeepResearch\n\n### Fonctionnalité Ask\n\nLa fonctionnalité Ask vous permet de discuter avec votre dépôt en utilisant la génération augmentée par récupération (RAG) :\n\n- **Réponses sensibles au contexte** : obtenez des réponses précises basées sur le code réel de votre dépôt\n- **Alimenté par RAG** : le système récupère des extraits de code pertinents pour fournir des réponses fondées\n- **Streaming en temps réel** : visualisez les réponses au fur et à mesure de leur génération pour une expérience plus interactive\n- **Historique des conversations** : le système conserve le contexte entre les questions pour des interactions plus cohérentes\n\n### Fonctionnalité DeepResearch\n\nDeepResearch fait passer l'analyse de référentiel au niveau supérieur avec un processus de recherche en plusieurs étapes :\n\n- **Enquête approfondie** : explore en profondeur des sujets complexes grâce à de multiples itérations de recherche\n- **Processus structuré** : suit un plan de recherche clair avec des mises à jour et une conclusion complète\n- **Continuation automatique** : l'IA poursuit automatiquement la recherche jusqu'à ce qu'elle atteigne une conclusion (jusqu'à 5 itérations)\n- **Étapes de la recherche** :\n  1. **Plan de recherche** : décrit l'approche et les premières conclusions\n  2. **Mises à jour de la recherche** : s'appuie sur les itérations précédentes avec de nouvelles informations\n  3. **Conclusion finale** : fournit une réponse complète basée sur toutes les itérations\n\nPour utiliser DeepResearch, activez simplement le commutateur \"Deep Research\" dans l'interface Ask avant de soumettre votre question.\n\n## 📱 Captures d'écran\n\n![Interface principale de DeepWiki](screenshots/Interface.png)\n*L'interface principale de DeepWiki*\n\n![Prise en charge des dépôts privés](screenshots/privaterepo.png)\n*Accédez aux dépôts privés avec des jetons d'accès personnels*\n\n![Fonctionnalité DeepResearch](screenshots/DeepResearch.png)\n*DeepResearch effectue des recherches en plusieurs étapes pour des sujets complexes*\n\n### Vidéo de démonstration\n\n[![Vidéo de démo DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*Regardez DeepWiki en action !*\n## ❓ Dépannage\n\n### Problèmes de clé API\n\n- **\"Variables d'environnement manquantes\"** : assurez-vous que votre fichier `.env` se trouve à la racine du projet et qu'il contient les clés API requises.\n- **\"Clé API non valide\"** : vérifiez que vous avez correctement copié la clé complète, sans espaces supplémentaires.\n- **\"Erreur d'API OpenRouter\"** : vérifiez que votre clé API OpenRouter est valide et qu'elle dispose de crédits suffisants.\n- **\"Erreur d'API Azure OpenAI\"** : vérifiez que vos informations d'identification Azure OpenAI (clé API, point de terminaison et version) sont correctes et que le service est correctement déployé.\n\n### Problèmes de connexion\n\n- **\"Impossible de se connecter au serveur API\"** : assurez-vous que le serveur API est en cours d'exécution sur le port 8001.\n- **\"Erreur CORS\"** : l'API est configurée pour autoriser toutes les origines, mais si vous rencontrez des problèmes, essayez d'exécuter le frontend et le backend sur la même machine.\n\n### Problèmes de génération\n\n- **\"Erreur lors de la génération du wiki\"** : pour les très grands référentiels, essayez d'abord un référentiel plus petit.\n- **\"Format de référentiel non valide\"** : assurez-vous que vous utilisez un format d'URL GitHub, GitLab ou Bitbucket valide.\n- **\"Impossible de récupérer la structure du référentiel\"** : pour les référentiels privés, assurez-vous d'avoir saisi un jeton d'accès personnel valide avec les autorisations appropriées.\n- **\"Erreur de rendu du diagramme\"** : l'application essaiera automatiquement de corriger les diagrammes cassés.\n\n### Solutions courantes\n\n1. **Redémarrez les deux serveurs** : parfois, un simple redémarrage résout la plupart des problèmes.\n2. **Vérifiez les journaux de la console** : ouvrez les outils de développement du navigateur pour voir les erreurs JavaScript.\n3. **Vérifiez les journaux de l'API** : consultez le terminal où l'API est en cours d'exécution pour les erreurs Python.\n\n## 🤝 Contribution\n\nLes contributions sont les bienvenues ! N'hésitez pas à :\n- Ouvrir des issues pour les bugs ou les demandes de fonctionnalités\n- Soumettre des pull requests pour améliorer le code\n- Partager vos commentaires et vos idées\n\n## 📄 Licence\n\nProjet sous licence MIT – Voir le fichier [LICENSE](LICENSE).\n\n## ⭐ Historique des stars\n\n[![Historique des stars](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n\n"
  },
  {
    "path": "README.ja.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki バナー](screenshots/Deepwiki.png)\n\n**DeepWiki**は、GitHub、GitLab、または Bitbucket リポジトリのための美しくインタラクティブな Wiki を自動的に作成します！リポジトリ名を入力するだけで、DeepWiki は以下を行います：\n\n1. コード構造を分析\n2. 包括的なドキュメントを生成\n3. すべての仕組みを説明する視覚的な図を作成\n4. すべてを簡単に閲覧できる Wiki に整理\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ 特徴\n\n- **即時ドキュメント生成**: あらゆる GitHub、GitLab、または Bitbucket リポジトリを数秒で Wiki に変換\n- **プライベートリポジトリ対応**: 個人アクセストークンを使用してプライベートリポジトリに安全にアクセス\n- **スマート分析**: AI を活用したコード構造と関係の理解\n- **美しい図表**: アーキテクチャとデータフローを視覚化する自動 Mermaid 図\n- **簡単なナビゲーション**: Wiki を探索するためのシンプルで直感的なインターフェース\n- **質問機能**: RAG 搭載 AI を使用してリポジトリとチャットし、正確な回答を得る\n- **詳細調査**: 複雑なトピックを徹底的に調査する多段階研究プロセス\n- **複数のモデルプロバイダー**: Google Gemini、OpenAI、OpenRouter、およびローカル Ollama モデルのサポート\n\n## 🚀 クイックスタート（超簡単！）\n\n### オプション 1: Docker を使用\n\n```bash\n# リポジトリをクローン\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# APIキーを含む.envファイルを作成\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# オプション: OpenRouterモデルを使用する場合はOpenRouter APIキーを追加\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# Docker Composeで実行\ndocker-compose up\n```\n\n(上記の Docker コマンドおよび`docker-compose.yml`の設定では、ホスト上の`~/.adalflow`ディレクトリをコンテナ内の`/root/.adalflow`にマウントします。このパスは以下のものを保存するために使用されます：\n\n- クローンされたリポジトリ (`~/.adalflow/repos/`)\n- それらのエンベディングとインデックス (`~/.adalflow/databases/`)\n- 生成された Wiki のキャッシュ (`~/.adalflow/wikicache/`)\n\nこれにより、コンテナが停止または削除されてもデータが永続化されます。)\n\n> 💡 **これらのキーの入手先:**\n>\n> - Google API キーは[Google AI Studio](https://makersuite.google.com/app/apikey)から取得\n> - OpenAI API キーは[OpenAI Platform](https://platform.openai.com/api-keys)から取得\n\n### オプション 2: 手動セットアップ（推奨）\n\n#### ステップ 1: API キーの設定\n\nプロジェクトのルートに`.env`ファイルを作成し、以下のキーを追加します：\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# オプション: OpenRouterモデルを使用する場合は追加\nOPENROUTER_API_KEY=your_openrouter_api_key\n```\n\n#### ステップ 2: バックエンドの起動\n\n```bash\n# Pythonの依存関係をインストール\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# APIサーバーを起動\npython -m api.main\n```\n\n#### ステップ 3: フロントエンドの起動\n\n```bash\n# JavaScript依存関係をインストール\nnpm install\n# または\nyarn install\n\n# Webアプリを起動\nnpm run dev\n# または\nyarn dev\n```\n\n#### ステップ 4: DeepWiki を使用！\n\n1. ブラウザで[http://localhost:3000](http://localhost:3000)を開く\n2. GitHub、GitLab、または Bitbucket リポジトリを入力（例：`https://github.com/openai/codex`、`https://github.com/microsoft/autogen`、`https://gitlab.com/gitlab-org/gitlab`、または`https://bitbucket.org/redradish/atlassian_app_versions`）\n3. プライベートリポジトリの場合は、「+ アクセストークンを追加」をクリックして GitHub または GitLab の個人アクセストークンを入力\n4. 「Wiki を生成」をクリックして、魔法が起こるのを見守りましょう！\n\n## 🔍 仕組み\n\nDeepWiki は AI を使用して：\n\n1. GitHub、GitLab、または Bitbucket リポジトリをクローンして分析（トークン認証によるプライベートリポジトリを含む）\n2. スマート検索のためのコードの埋め込みを作成\n3. コンテキスト対応 AI でドキュメントを生成（Google Gemini、OpenAI、OpenRouter、またはローカル Ollama モデルを使用）\n4. コードの関係を説明する視覚的な図を作成\n5. すべてを構造化された Wiki に整理\n6. 質問機能を通じてリポジトリとのインテリジェントな Q&A を可能に\n7. 詳細調査機能で深い研究能力を提供\n\n```mermaid\ngraph TD\n    A[ユーザーがGitHub/GitLab/Bitbucketリポジトリを入力] --> AA{プライベートリポジトリ?}\n    AA -->|はい| AB[アクセストークンを追加]\n    AA -->|いいえ| B[リポジトリをクローン]\n    AB --> B\n    B --> C[コード構造を分析]\n    C --> D[コード埋め込みを作成]\n\n    D --> M{モデルプロバイダーを選択}\n    M -->|Google Gemini| E1[Geminiで生成]\n    M -->|OpenAI| E2[OpenAIで生成]\n    M -->|OpenRouter| E3[OpenRouterで生成]\n    M -->|ローカルOllama| E4[Ollamaで生成]\n\n    E1 --> E[ドキュメントを生成]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[視覚的な図を作成]\n    E --> G[Wikiとして整理]\n    F --> G\n    G --> H[インタラクティブなDeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ プロジェクト構造\n\n```\ndeepwiki/\n├── api/                  # バックエンドAPIサーバー\n│   ├── main.py           # APIエントリーポイント\n│   ├── api.py            # FastAPI実装\n│   ├── rag.py            # 検索拡張生成\n│   ├── data_pipeline.py  # データ処理ユーティリティ\n│   └── requirements.txt  # Python依存関係\n│\n├── src/                  # フロントエンドNext.jsアプリ\n│   ├── app/              # Next.jsアプリディレクトリ\n│   │   └── page.tsx      # メインアプリケーションページ\n│   └── components/       # Reactコンポーネント\n│       └── Mermaid.tsx   # Mermaid図レンダラー\n│\n├── public/               # 静的アセット\n├── package.json          # JavaScript依存関係\n└── .env                  # 環境変数（作成する必要あり）\n```\n\n## 🛠️ 高度な設定\n\n### 環境変数\n\n| 変数                          | 説明                                                            | 必須 | 注意                                                                                                          |\n| ----------------------------- | --------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- |\n| `GOOGLE_API_KEY`              | AI 生成のための Google Gemini API キー                          | ◯    |                                                                                                               |\n| `OPENAI_API_KEY`              | 埋め込みのための OpenAI API キー                                | ◯    |                                                                                                               |\n| `OPENROUTER_API_KEY`          | 代替モデルのための OpenRouter API キー                          | ✗    | OpenRouter モデルを使用する場合にのみ必須です                                                                 |\n| `PORT`                        | API サーバーのポート（デフォルト：8001）                        | ✗    | API とフロントエンドを同じマシンでホストする場合、`NEXT_PUBLIC_SERVER_BASE_URL`のポートを適宜変更してください |\n| `SERVER_BASE_URL`             | API サーバーのベース URL（デフォルト：`http://localhost:8001`） | ✗    |                                                                                                               |\n\n### 設定ファイル\n\nDeepWikiはシステムの様々な側面を管理するためにJSON設定ファイルを使用しています：\n\n1. **`generator.json`**: テキスト生成モデルの設定\n   - 利用可能なモデルプロバイダー（Google、OpenAI、OpenRouter、Ollama）を定義\n   - 各プロバイダーのデフォルトおよび利用可能なモデルを指定\n   - temperatureやtop_pなどのモデル固有のパラメータを含む\n\n2. **`embedder.json`**: 埋め込みモデルとテキスト処理の設定\n   - ベクトルストレージ用の埋め込みモデルを定義\n   - RAG用の検索設定を含む\n   - ドキュメントチャンク分割のためのテキスト分割設定を指定\n\n3. **`repo.json`**: リポジトリ処理の設定\n   - 特定のファイルやディレクトリを除外するファイルフィルターを含む\n   - リポジトリサイズ制限と処理ルールを定義\n\nデフォルトでは、これらのファイルは`api/config/`ディレクトリにあります。`DEEPWIKI_CONFIG_DIR`環境変数を使用して、その場所をカスタマイズできます。\n\n### Docker セットアップ\n\nDocker を使用して DeepWiki を実行できます：\n\n```bash\n# GitHub Container Registryからイメージをプル\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# 環境変数を設定してコンテナを実行\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nこのコマンドは、ホスト上の ⁠~/.adalflow をコンテナ内の ⁠/root/.adalflow にマウントします。このパスは以下のものを保存するために使用されます：\n\n- クローンされたリポジトリ (⁠~/.adalflow/repos/)\n- それらのエンベディングとインデックス (⁠~/.adalflow/databases/)\n- 生成された Wiki のキャッシュ (⁠~/.adalflow/wikicache/)\n\nこれにより、コンテナが停止または削除されてもデータが永続化されます。\nまたは、提供されている ⁠docker-compose.yml ファイルを使用します。\n\n```bash\n# まず.envファイルをAPIキーで編集\ndocker-compose up\n```\n\n（⁠docker-compose.yml ファイルは、上記の ⁠docker run コマンドと同様に、データ永続化のために ⁠~/.adalflow をマウントするように事前設定されています。）\n\n#### Docker で.env ファイルを使用する\n\n.env ファイルをコンテナにマウントすることもできます：\n\n```bash\n# APIキーを含む.envファイルを作成\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# .envファイルをマウントしてコンテナを実行\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nこのコマンドは、ホスト上の ⁠~/.adalflow をコンテナ内の ⁠/root/.adalflow にマウントします。このパスは以下のものを保存するために使用されます：\n\n- クローンされたリポジトリ (⁠~/.adalflow/repos/)\n- それらのエンベディングとインデックス (⁠~/.adalflow/databases/)\n- 生成された Wiki のキャッシュ (⁠~/.adalflow/wikicache/)\n\nこれにより、コンテナが停止または削除されてもデータが永続化されます。\n\n#### Docker イメージをローカルでビルドする\n\nDocker イメージをローカルでビルドしたい場合：\n\n```bash\n# リポジトリをクローン\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Dockerイメージをビルド\ndocker build -t deepwiki-open .\n\n# コンテナを実行\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  deepwiki-open\n```\n\n# API サーバー詳細\n\nAPI サーバーは以下を提供します：\n\n- リポジトリのクローンとインデックス作成\n- RAG（Retrieval Augmented Generation：検索拡張生成）\n- ストリーミングチャット補完\n\n詳細については、API README を参照してください。\n\n## 🤖 プロバイダーベースのモデル選択システム\n\nDeepWikiでは、複数のLLMプロバイダーをサポートする柔軟なプロバイダーベースのモデル選択システムを実装しています：\n\n### サポートされているプロバイダーとモデル\n\n- **Google**: デフォルトは `gemini-2.5-flash`、また `gemini-2.5-flash-lite`、`gemini-2.5-pro` などもサポート\n- **OpenAI**: デフォルトは `gpt-5-nano`、また `gpt-5`、 `4o` などもサポート\n- **OpenRouter**: Claude、Llama、Mistralなど、統一APIを通じて複数のモデルにアクセス\n- **Ollama**: `llama3` などのローカルで実行するオープンソースモデルをサポート\n\n### 環境変数\n\n各プロバイダーには、対応するAPI鍵の環境変数が必要です：\n\n```\n# API鍵\nGOOGLE_API_KEY=あなたのGoogle API鍵        # Google Geminiモデルに必要\nOPENAI_API_KEY=あなたのOpenAI鍵            # OpenAIモデルに必要\nOPENROUTER_API_KEY=あなたのOpenRouter鍵    # OpenRouterモデルに必要\n\n# OpenAI APIベースURL設定\nOPENAI_BASE_URL=https://カスタムAPIエンドポイント.com/v1  # オプション、カスタムOpenAI APIエンドポイント用\n```\n\n### サービスプロバイダー向けのカスタムモデル選択\n\nカスタムモデル選択機能は、あなたの組織のユーザーに様々なAIモデルの選択肢を提供するために特別に設計されています：\n\n- あなたは組織内のユーザーに様々なAIモデルの選択肢を提供できます\n- あなたはコード変更なしで急速に進化するLLM環境に迅速に適応できます\n- あなたは事前定義リストにない専門的またはファインチューニングされたモデルをサポートできます\n\nサービスプロバイダーは、事前定義されたオプションから選択するか、フロントエンドインターフェースでカスタムモデル識別子を入力することで、モデル提供を実装できます。\n\n### エンタープライズプライベートチャネル向けのベースURL設定\n\nOpenAIクライアントのbase_url設定は、主にプライベートAPIチャネルを持つエンタープライズユーザー向けに設計されています。この機能は：\n\n- プライベートまたは企業固有のAPIエンドポイントへの接続を可能に\n- 組織が自己ホスト型または独自にデプロイされたLLMサービスを使用可能に\n- サードパーティのOpenAI API互換サービスとの統合をサポート\n\n**近日公開**: 将来のアップデートでは、ユーザーがリクエストで自分のAPI鍵を提供する必要があるモードをDeepWikiがサポートする予定です。これにより、プライベートチャネルを持つエンタープライズ顧客は、DeepWikiデプロイメントと認証情報を共有することなく、既存のAPI設定を使用できるようになります。\n\n## 🔌 OpenRouter 連携\n\nDeepWiki は、モデルプロバイダーとして OpenRouter をサポートするようになり、単一の API を通じて数百の AI モデルにアクセスできるようになりました。\n\n- 複数のモデルオプション: OpenAI、Anthropic、Google、Meta、Mistralなど、統一APIを通じて複数のモデルにアクセス\n- 簡単な設定: OpenRouter API キーを追加し、使用したいモデルを選択するだけ\n- コスト効率: 予算とパフォーマンスのニーズに合ったモデルを選択\n- 簡単な切り替え: コードを変更することなく、異なるモデル間を切り替え可能\n\n### DeepWiki で OpenRouter を使用する方法\n\n1. API キーを取得: OpenRouter でサインアップし、API キーを取得します\n2. 環境に追加: ⁠.env ファイルに ⁠OPENROUTER_API_KEY=your_key を追加します\n3. UI で有効化: ホームページの「OpenRouter API を使用」オプションをチェックします\n4. モデルを選択: GPT-4o、Claude 3.5 Sonnet、Gemini 2.0 などの人気モデルから選択します\n\nOpenRouter は特に以下のような場合に便利です：\n\n- 複数のサービスにサインアップせずに異なるモデルを試したい\n- お住まいの地域で制限されている可能性のあるモデルにアクセスしたい\n- 異なるモデルプロバイダー間でパフォーマンスを比較したい\n- ニーズに基づいてコストとパフォーマンスを最適化したい\n\n## 🤖 質問と詳細調査機能\n\n### 質問機能\n\n質問機能を使用すると、検索拡張生成（RAG）を使用してリポジトリとチャットできます：\n\n- **コンテキスト対応の回答**: リポジトリの実際のコードに基づいた正確な回答を取得\n- **RAG 搭載**: システムは関連するコードスニペットを取得して根拠のある回答を提供\n- **リアルタイムストリーミング**: よりインタラクティブな体験のために、生成されるレスポンスをリアルタイムで確認\n- **会話履歴**: システムは質問間のコンテキストを維持し、より一貫性のあるインタラクションを実現\n\n### 詳細調査機能\n\n詳細調査は、複数ターンの研究プロセスでリポジトリ分析を次のレベルに引き上げます：\n\n- **詳細な調査**: 複数の研究反復を通じて複雑なトピックを徹底的に探索\n- **構造化されたプロセス**: 明確な研究計画、更新、包括的な結論を含む\n- **自動継続**: AI は結論に達するまで自動的に研究を継続（最大 5 回の反復）\n- **研究段階**:\n  1. **研究計画**: アプローチと初期調査結果の概要\n  2. **研究更新**: 新しい洞察を加えて前の反復を発展\n  3. **最終結論**: すべての反復に基づく包括的な回答を提供\n\n詳細調査を使用するには、質問を送信する前に質問インターフェースの「詳細調査」スイッチをオンにするだけです。\n\n## 📱 スクリーンショット\n\n![DeepWikiメインインターフェース](screenshots/Interface.png)\n_DeepWiki のメインインターフェース_\n\n![プライベートリポジトリサポート](screenshots/privaterepo.png)\n_個人アクセストークンを使用したプライベートリポジトリへのアクセス_\n\n![詳細調査機能](screenshots/DeepResearch.png)\n_詳細調査は複雑なトピックに対して多段階の調査を実施_\n\n### デモビデオ\n\n[![DeepWikiデモビデオ](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n_DeepWiki の動作を見る！_\n\n## ❓ トラブルシューティング\n\n### API キーの問題\n\n- **「環境変数が見つかりません」**: `.env`ファイルがプロジェクトのルートにあり、必要な API キーが含まれていることを確認\n- **「API キーが無効です」**: キー全体が余分なスペースなしで正しくコピーされていることを確認\n- **「OpenRouter API エラー」**: OpenRouter API キーが有効で、十分なクレジットがあることを確認\n\n### 接続の問題\n\n- **「API サーバーに接続できません」**: API サーバーがポート 8001 で実行されていることを確認\n- **「CORS エラー」**: API はすべてのオリジンを許可するように設定されていますが、問題がある場合は、フロントエンドとバックエンドを同じマシンで実行してみてください\n\n### 生成の問題\n\n- **「Wiki の生成中にエラーが発生しました」**: 非常に大きなリポジトリの場合は、まず小さいものから試してみてください\n- **「無効なリポジトリ形式」**: 有効な GitHub、GitLab、または Bitbucket URL の形式を使用していることを確認\n- **「リポジトリ構造を取得できませんでした」**: プライベートリポジトリの場合、適切な権限を持つ有効な個人アクセストークンを入力したことを確認\n- **「図のレンダリングエラー」**: アプリは自動的に壊れた図を修正しようとします\n\n### 一般的な解決策\n\n1. **両方のサーバーを再起動**: 単純な再起動でほとんどの問題が解決することがあります\n2. **コンソールログを確認**: ブラウザの開発者ツールを開いて JavaScript エラーを確認\n3. **API ログを確認**: API が実行されているターミナルで Python エラーを確認\n\n## 🤝 貢献\n\n貢献は歓迎します！以下のことを自由に行ってください：\n\n- バグや機能リクエストの問題を開く\n- コードを改善するためのプルリクエストを提出\n- フィードバックやアイデアを共有\n\n## 📄 ライセンス\n\nこのプロジェクトは MIT ライセンスの下でライセンスされています - 詳細は[LICENSE](LICENSE)ファイルを参照してください。\n\n## ⭐ スター履歴\n\n[![スター履歴チャート](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n"
  },
  {
    "path": "README.kr.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki Banner](screenshots/Deepwiki.png)\n\n**DeepWiki**는 제가 직접 구현한 프로젝트로, GitHub, GitLab 또는 BitBucket 저장소에 대해 아름답고 대화형 위키를 자동 생성합니다! 저장소 이름만 입력하면 DeepWiki가 다음을 수행합니다:\n\n1. 코드 구조 분석\n2. 포괄적인 문서 생성\n3. 모든 작동 방식을 설명하는 시각적 다이어그램 생성\n4. 이를 쉽게 탐색할 수 있는 위키로 정리\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ 주요 기능\n\n- **즉시 문서화**: 어떤 GitHub, GitLab 또는 BitBucket 저장소든 몇 초 만에 위키로 변환\n- **비공개 저장소 지원**: 개인 액세스 토큰으로 비공개 저장소 안전하게 접근\n- **스마트 분석**: AI 기반 코드 구조 및 관계 이해\n- **아름다운 다이어그램**: 아키텍처와 데이터 흐름을 시각화하는 자동 Mermaid 다이어그램\n- **쉬운 탐색**: 간단하고 직관적인 인터페이스로 위키 탐색 가능\n- **Ask 기능**: RAG 기반 AI와 저장소에 대해 대화하며 정확한 답변 얻기\n- **DeepResearch**: 복잡한 주제를 철저히 조사하는 다중 턴 연구 프로세스\n- **다양한 모델 제공자 지원**: Google Gemini, OpenAI, OpenRouter, 로컬 Ollama 모델 지원\n\n## 🚀 빠른 시작 (초간단!)\n\n### 옵션 1: Docker 사용\n\n```bash\n# 저장소 클론\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# API 키를 포함한 .env 파일 생성\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# 선택 사항: OpenRouter 모델 사용 시 API 키 추가\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# Docker Compose로 실행\ndocker-compose up\n```\n\n> 💡 **API 키는 어디서 얻나요:**\n> - [Google AI Studio](https://makersuite.google.com/app/apikey)에서 Google API 키 받기\n> - [OpenAI 플랫폼](https://platform.openai.com/api-keys)에서 OpenAI API 키 받기\n\n### 옵션 2: 수동 설정 (권장)\n\n#### 1단계: API 키 설정\n\n프로젝트 루트에 `.env` 파일을 만들고 다음 키들을 추가하세요:\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# 선택 사항: OpenRouter 모델 사용 시 추가\nOPENROUTER_API_KEY=your_openrouter_api_key\n```\n\n#### 2단계: 백엔드 시작\n\n```bash\n# Python 의존성 설치\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# API 서버 실행\npython -m api.main\n```\n\n#### 3단계: 프론트엔드 시작\n\n```bash\n# JavaScript 의존성 설치\nnpm install\n# 또는\nyarn install\n\n# 웹 앱 실행\nnpm run dev\n# 또는\nyarn dev\n```\n\n#### 4단계: DeepWiki 사용하기!\n\n1. 브라우저에서 [http://localhost:3000](http://localhost:3000) 열기\n2. GitHub, GitLab 또는 Bitbucket 저장소 입력 (예: `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. 비공개 저장소인 경우 \"+ 액세스 토큰 추가\" 클릭 후 GitHub 또는 GitLab 개인 액세스 토큰 입력\n4. \"Generate Wiki\" 클릭 후 마법을 지켜보기!\n\n## 🔍 작동 방식\n\nDeepWiki는 AI를 사용하여 다음을 수행합니다:\n\n1. GitHub, GitLab 또는 Bitbucket 저장소 복제 및 분석 (토큰 인증이 필요한 비공개 저장소 포함)\n2. 스마트 검색을 위한 코드 임베딩 생성\n3. 문맥 인지 AI로 문서 생성 (Google Gemini, OpenAI, OpenRouter 또는 로컬 Ollama 모델 사용)\n4. 코드 관계를 설명하는 시각적 다이어그램 생성\n5. 모든 것을 구조화된 위키로 정리\n6. Ask 기능을 통한 저장소와의 지능형 Q&A 지원\n7. DeepResearch로 심층 연구 기능 제공\n\n```mermaid\ngraph TD\n    A[사용자가 GitHub/GitLab/Bitbucket 저장소 입력] --> AA{비공개 저장소인가?}\n    AA -->|예| AB[액세스 토큰 추가]\n    AA -->|아니오| B[저장소 복제]\n    AB --> B\n    B --> C[코드 구조 분석]\n    C --> D[코드 임베딩 생성]\n\n    D --> M{모델 제공자 선택}\n    M -->|Google Gemini| E1[Gemini로 생성]\n    M -->|OpenAI| E2[OpenAI로 생성]\n    M -->|OpenRouter| E3[OpenRouter로 생성]\n    M -->|로컬 Ollama| E4[Ollama로 생성]\n\n    E1 --> E[문서 생성]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[시각적 다이어그램 생성]\n    E --> G[위키로 정리]\n    F --> G\n    G --> H[대화형 DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ 프로젝트 구조\n\n```\ndeepwiki/\n├── api/                  # 백엔드 API 서버\n│   ├── main.py           # API 진입점\n│   ├── api.py            # FastAPI 구현\n│   ├── rag.py            # Retrieval Augmented Generation\n│   ├── data_pipeline.py  # 데이터 처리 유틸리티\n│   └── requirements.txt  # Python 의존성\n│\n├── src/                  # 프론트엔드 Next.js 앱\n│   ├── app/              # Next.js 앱 디렉토리\n│   │   └── page.tsx      # 메인 애플리케이션 페이지\n│   └── components/       # React 컴포넌트\n│       └── Mermaid.tsx   # Mermaid 다이어그램 렌더러\n│\n├── public/               # 정적 자산\n├── package.json          # JavaScript 의존성\n└── .env                  # 환경 변수 (직접 생성)\n```\n\n## 🛠️ 고급 설정\n\n### 환경 변수\n\n| 변수명 | 설명 | 필수 | 비고 |\n|----------|-------------|----------|------|\n| `GOOGLE_API_KEY` | AI 생성용 Google Gemini API 키 | 예 |\n| `OPENAI_API_KEY` | 임베딩용 OpenAI API 키 | 예 |\n| `OPENROUTER_API_KEY` | 대체 모델용 OpenRouter API 키 | 아니오 | OpenRouter 모델 사용 시 필요 |\n| `PORT` | API 서버 포트 (기본값: 8001) | 아니오 | API와 프론트엔드를 같은 머신에서 호스팅 시 `SERVER_BASE_URL`의 포트도 변경 필요 |\n| `SERVER_BASE_URL` | API 서버 기본 URL (기본값: http://localhost:8001) | 아니오 |\n\n### 설정 파일\n\nDeepWiki는 시스템의 다양한 측면을 관리하기 위해 JSON 설정 파일을 사용합니다:\n\n1. **`generator.json`**: 텍스트 생성 모델 설정\n   - 사용 가능한 모델 제공자(Google, OpenAI, OpenRouter, Ollama) 정의\n   - 각 제공자의 기본 및 사용 가능한 모델 지정\n   - temperature와 top_p 같은 모델별 매개변수 포함\n\n2. **`embedder.json`**: 임베딩 모델 및 텍스트 처리 설정\n   - 벡터 저장소용 임베딩 모델 정의\n   - RAG를 위한 검색기 설정 포함\n   - 문서 청킹을 위한 텍스트 분할기 설정 지정\n\n3. **`repo.json`**: 저장소 처리 설정\n   - 특정 파일 및 디렉토리를 제외하는 파일 필터 포함\n   - 저장소 크기 제한 및 처리 규칙 정의\n\n기본적으로 이러한 파일은 `api/config/` 디렉토리에 위치합니다. `DEEPWIKI_CONFIG_DIR` 환경 변수를 사용하여 위치를 사용자 정의할 수 있습니다.\n\n### Docker 설정\n\nDocker를 사용하여 DeepWiki를 실행할 수 있습니다:\n\n```bash\n# GitHub 컨테이너 레지스트리에서 이미지 가져오기\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# 환경 변수와 함께 컨테이너 실행\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n이 명령어는 또한 호스트의 `~/.adalflow`를 컨테이너의 `/root/.adalflow`에 마운트합니다. 이 경로는 다음을 저장하는 데 사용됩니다:\n- 복제된 저장소 (`~/.adalflow/repos/`)\n- 해당 저장소의 임베딩 및 인덱스 (`~/.adalflow/databases/`)\n- 생성된 위키의 캐시 (`~/.adalflow/wikicache/`)\n\n이를 통해 컨테이너가 중지되거나 제거되어도 데이터가 유지됩니다.\n\n또는 제공된 `docker-compose.yml` 파일을 사용하세요:\n\n```bash\n# API 키가 포함된 .env 파일을 먼저 편집\ndocker-compose up\n```\n\n(`docker-compose.yml` 파일은 위의 `docker run` 명령어와 유사하게 데이터 지속성을 위해 `~/.adalflow`를 마운트하도록 미리 구성되어 있습니다.)\n\n#### Docker에서 .env 파일 사용하기\n\n.env 파일을 컨테이너에 마운트할 수도 있습니다:\n\n```bash\n# API 키가 포함된 .env 파일 생성\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# .env 파일을 마운트하여 컨테이너 실행\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n이 명령어는 또한 호스트의 `~/.adalflow`를 컨테이너의 `/root/.adalflow`에 마운트합니다. 이 경로는 다음을 저장하는 데 사용됩니다:\n- 복제된 저장소 (`~/.adalflow/repos/`)\n- 해당 저장소의 임베딩 및 인덱스 (`~/.adalflow/databases/`)\n- 생성된 위키의 캐시 (`~/.adalflow/wikicache/`)\n\n이를 통해 컨테이너가 중지되거나 제거되어도 데이터가 유지됩니다.\n\n#### 로컬에서 Docker 이미지 빌드하기\n\n로컬에서 Docker 이미지를 빌드하려면:\n\n```bash\n# 저장소 클론\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Docker 이미지 빌드\ndocker build -t deepwiki-open .\n\n# 컨테이너 실행\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  deepwiki-open\n```\n\n### API 서버 상세 정보\n\nAPI 서버는 다음을 제공합니다:\n- 저장소 복제 및 인덱싱\n- RAG (Retrieval Augmented Generation)\n- 스트리밍 채팅 완성\n\n자세한 내용은 [API README](./api/README.md)를 참조하세요.\n\n## 🤖 제공자 기반 모델 선택 시스템\n\nDeepWiki는 이제 여러 LLM 제공자를 지원하는 유연한 제공자 기반 모델 선택 시스템을 구현했습니다:\n\n### 지원되는 제공자 및 모델\n\n- **Google**: 기본값 `gemini-2.5-flash`, 또한 `gemini-2.5-flash-lite`, `gemini-2.5-pro` 등도 지원\n- **OpenAI**: 기본값 `gpt-5-nano`, 또한 `gpt-5`, `4o` 등도 지원\n- **OpenRouter**: Claude, Llama, Mistral 등 통합 API를 통해 다양한 모델 접근 가능\n- **Ollama**: `llama3`와 같은 로컬에서 실행되는 오픈소스 모델 지원\n\n### 환경 변수\n\n각 제공자는 해당 API 키 환경 변수가 필요합니다:\n\n```\n# API 키\nGOOGLE_API_KEY=귀하의_구글_API_키        # Google Gemini 모델에 필요\nOPENAI_API_KEY=귀하의_OpenAI_키         # OpenAI 모델에 필요\nOPENROUTER_API_KEY=귀하의_OpenRouter_키 # OpenRouter 모델에 필요\n\n# OpenAI API 기본 URL 구성\nOPENAI_BASE_URL=https://사용자정의_API_엔드포인트.com/v1  # 선택 사항, 사용자 정의 OpenAI API 엔드포인트용\n```\n\n### 서비스 제공자를 위한 사용자 정의 모델 선택\n\n사용자 정의 모델 선택 기능은 다음이 필요한 서비스 제공자를 위해 특별히 설계되었습니다:\n\n- 귀하는 조직 내 사용자에게 다양한 AI 모델 선택 옵션을 제공할 수 있습니다\n- 귀하는 코드 변경 없이 빠르게 진화하는 LLM 환경에 신속하게 적응할 수 있습니다\n- 귀하는 사전 정의된 목록에 없는 특수하거나 미세 조정된 모델을 지원할 수 있습니다\n\n서비스 제공자는 사전 정의된 옵션에서 선택하거나 프론트엔드 인터페이스에서 사용자 정의 모델 식별자를 입력하여 모델 제공을 구현할 수 있습니다.\n\n### 기업 전용 채널을 위한 기본 URL 구성\n\nOpenAI 클라이언트의 base_url 구성은 주로 비공개 API 채널이 있는 기업 사용자를 위해 설계되었습니다. 이 기능은:\n\n- 비공개 또는 기업 전용 API 엔드포인트 연결 가능\n- 조직이 자체 호스팅되거나 사용자 정의 배포된 LLM 서비스 사용 가능\n- 서드파티 OpenAI API 호환 서비스와의 통합 지원\n\n**출시 예정**: 향후 업데이트에서 DeepWiki는 사용자가 요청에서 자신의 API 키를 제공해야 하는 모드를 지원할 예정입니다. 이를 통해 비공개 채널이 있는 기업 고객은 DeepWiki 배포와 자격 증명을 공유하지 않고도 기존 API 구성을 사용할 수 있습니다.\n\n## 🔌 OpenRouter 통합\n\nDeepWiki는 이제 [OpenRouter](https://openrouter.ai/)를 모델 제공자로 지원하여, 단일 API를 통해 수백 개의 AI 모델에 접근할 수 있습니다:\n\n- **다양한 모델 옵션**: OpenAI, Anthropic, Google, Meta, Mistral 등 다양한 모델 이용 가능\n- **간편한 설정**: OpenRouter API 키만 추가하고 원하는 모델 선택\n- **비용 효율성**: 예산과 성능에 맞는 모델 선택 가능\n- **손쉬운 전환**: 코드 변경 없이 다양한 모델 간 전환 가능\n\n### DeepWiki에서 OpenRouter 사용법\n\n1. **API 키 받기**: [OpenRouter](https://openrouter.ai/) 가입 후 API 키 획득\n2. **환경 변수 추가**: `.env` 파일에 `OPENROUTER_API_KEY=your_key` 추가\n3. **UI에서 활성화**: 홈페이지에서 \"Use OpenRouter API\" 옵션 체크\n4. **모델 선택**: GPT-4o, Claude 3.5 Sonnet, Gemini 2.0 등 인기 모델 선택\n\nOpenRouter는 특히 다음과 같은 경우 유용합니다:\n- 여러 서비스에 가입하지 않고 다양한 모델 시도\n- 지역 제한이 있는 모델 접근\n- 모델 제공자별 성능 비교\n- 비용과 성능 최적화\n\n## 🤖 Ask 및 DeepResearch 기능\n\n### Ask 기능\n\nAsk 기능은 Retrieval Augmented Generation (RAG)을 사용해 저장소와 대화할 수 있습니다:\n\n- **문맥 인지 답변**: 저장소 내 실제 코드 기반으로 정확한 답변 제공\n- **RAG 기반**: 관련 코드 조각을 검색해 근거 있는 답변 생성\n- **실시간 스트리밍**: 답변 생성 과정을 실시간으로 확인 가능\n- **대화 기록 유지**: 질문 간 문맥을 유지해 더 일관된 대화 가능\n\n### DeepResearch 기능\n\nDeepResearch는 다중 턴 연구 프로세스를 통해 저장소 분석을 한층 심화합니다:\n\n- **심층 조사**: 여러 연구 반복을 통해 복잡한 주제 철저히 탐구\n- **구조화된 프로세스**: 연구 계획, 업데이트, 최종 결론 단계로 진행\n- **자동 연속 진행**: AI가 최대 5회 반복해 연구를 계속 진행\n- **연구 단계**:\n  1. **연구 계획**: 접근법과 초기 발견 사항 개요 작성\n  2. **연구 업데이트**: 이전 반복 내용을 바탕으로 새로운 통찰 추가\n  3. **최종 결론**: 모든 반복을 종합한 포괄적 답변 제공\n\nDeepResearch를 사용하려면 질문 제출 전 Ask 인터페이스에서 \"Deep Research\" 스위치를 켜세요.\n\n## 📱 스크린샷\n\n![DeepWiki Main Interface](screenshots/Interface.png)\n*DeepWiki의 메인 인터페이스*\n\n![Private Repository Support](screenshots/privaterepo.png)\n*개인 액세스 토큰으로 비공개 저장소 접근*\n\n![DeepResearch Feature](screenshots/DeepResearch.png)\n*DeepResearch는 복잡한 주제에 대해 다중 턴 조사를 수행*\n\n### 데모 영상\n\n[![DeepWiki Demo Video](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*DeepWiki 작동 영상 보기!*\n\n## ❓ 문제 해결\n\n### API 키 문제\n- **\"환경 변수 누락\"**: `.env` 파일이 프로젝트 루트에 있고 필요한 API 키가 포함되어 있는지 확인\n- **\"API 키가 유효하지 않음\"**: 키를 정확히 복사했는지, 공백이 없는지 확인\n- **\"OpenRouter API 오류\"**: OpenRouter API 키가 유효하고 충분한 크레딧이 있는지 확인\n\n### 연결 문제\n- **\"API 서버에 연결할 수 없음\"**: API 서버가 포트 8001에서 실행 중인지 확인\n- **\"CORS 오류\"**: API가 모든 출처를 허용하도록 설정되어 있지만 문제가 있으면 프론트엔드와 백엔드를 같은 머신에서 실행해 보세요\n\n### 생성 문제\n- **\"위키 생성 오류\"**: 아주 큰 저장소는 먼저 작은 저장소로 시도해 보세요\n- **\"잘못된 저장소 형식\"**: 유효한 GitHub, GitLab 또는 Bitbucket URL 형식인지 확인\n- **\"저장소 구조를 가져올 수 없음\"**: 비공개 저장소라면 적절한 권한의 개인 액세스 토큰을 입력했는지 확인\n- **\"다이어그램 렌더링 오류\"**: 앱이 자동으로 다이어그램 오류를 수정하려 시도합니다\n\n### 일반적인 해결법\n1. **서버 둘 다 재시작**: 간단한 재시작으로 대부분 문제 해결\n2. **콘솔 로그 확인**: 브라우저 개발자 도구에서 자바스크립트 오류 확인\n3. **API 로그 확인**: API 실행 터미널에서 Python 오류 확인\n\n## 🤝 기여\n\n기여를 환영합니다! 다음을 자유롭게 해주세요:\n- 버그나 기능 요청을 위한 이슈 열기\n- 코드 개선을 위한 풀 리퀘스트 제출\n- 피드백과 아이디어 공유\n\n## 📄 라이선스\n\n이 프로젝트는 MIT 라이선스 하에 있습니다 - 자세한 내용은 [LICENSE](LICENSE) 파일 참고.\n\n## ⭐ 스타 히스토리\n\n[![Star History Chart](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n"
  },
  {
    "path": "README.md",
    "content": "\n### ⚠️ Announcement: Shifting focus to AsyncReview\n---\n\n**IMPORTANT UPDATE** DeepWiki-Open maintenance is ongoing, but primary active development is moving to **[AsyncReview](https://github.com/AsyncFuncAI/AsyncReview/)**. Thank you for the support on this project; please join me in the new repository for this year's primary effort.\n\n---\n---\n\n# DeepWiki-Open\n\n![DeepWiki Banner](screenshots/Deepwiki.png)\n\n**DeepWiki** is my own implementation attempt of DeepWiki, automatically creates beautiful, interactive wikis for any GitHub, GitLab, or BitBucket repository! Just enter a repo name, and DeepWiki will:\n\n1. Analyze the code structure\n2. Generate comprehensive documentation\n3. Create visual diagrams to explain how everything works\n4. Organize it all into an easy-to-navigate wiki\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n[![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc)\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Features\n\n- **Instant Documentation**: Turn any GitHub, GitLab or BitBucket repo into a wiki in seconds\n- **Private Repository Support**: Securely access private repositories with personal access tokens\n- **Smart Analysis**: AI-powered understanding of code structure and relationships\n- **Beautiful Diagrams**: Automatic Mermaid diagrams to visualize architecture and data flow\n- **Easy Navigation**: Simple, intuitive interface to explore the wiki\n- **Ask Feature**: Chat with your repository using RAG-powered AI to get accurate answers\n- **DeepResearch**: Multi-turn research process that thoroughly investigates complex topics\n- **Multiple Model Providers**: Support for Google Gemini, OpenAI, OpenRouter, and local Ollama models\n- **Flexible Embeddings**: Choose between OpenAI, Google AI, or local Ollama embeddings for optimal performance\n\n## 🚀 Quick Start (Super Easy!)\n\n### Option 1: Using Docker\n\n```bash\n# Clone the repository\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Create a .env file with your API keys\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# Optional: Use Google AI embeddings instead of OpenAI (recommended if using Google models)\necho \"DEEPWIKI_EMBEDDER_TYPE=google\" >> .env\n# Optional: Add OpenRouter API key if you want to use OpenRouter models\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n# Optional: Add Ollama host if not local. defaults to http://localhost:11434\necho \"OLLAMA_HOST=your_ollama_host\" >> .env\n# Optional: Add Azure API key, endpoint and version if you want to use azure openai models\necho \"AZURE_OPENAI_API_KEY=your_azure_openai_api_key\" >> .env\necho \"AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint\" >> .env\necho \"AZURE_OPENAI_VERSION=your_azure_openai_version\" >> .env\n# Run with Docker Compose\ndocker-compose up\n```\n\nFor detailed instructions on using DeepWiki with Ollama and Docker, see [Ollama Instructions](Ollama-instruction.md).\n\n> 💡 **Where to get these keys:**\n> - Get a Google API key from [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Get an OpenAI API key from [OpenAI Platform](https://platform.openai.com/api-keys)\n> - Get Azure OpenAI credentials from [Azure Portal](https://portal.azure.com/) - create an Azure OpenAI resource and get the API key, endpoint, and API version\n\n### Option 2: Manual Setup (Recommended)\n\n#### Step 1: Set Up Your API Keys\n\nCreate a `.env` file in the project root with these keys:\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# Optional: Use Google AI embeddings (recommended if using Google models)\nDEEPWIKI_EMBEDDER_TYPE=google\n# Optional: Add this if you want to use OpenRouter models\nOPENROUTER_API_KEY=your_openrouter_api_key\n# Optional: Add this if you want to use Azure OpenAI models\nAZURE_OPENAI_API_KEY=your_azure_openai_api_key\nAZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint\nAZURE_OPENAI_VERSION=your_azure_openai_version\n# Optional: Add Ollama host if not local. default: http://localhost:11434\nOLLAMA_HOST=your_ollama_host\n```\n\n#### Step 2: Start the Backend\n\n```bash\n# Install Python dependencies\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Start the API server\npython -m api.main\n```\n\n#### Step 3: Start the Frontend\n\n```bash\n# Install JavaScript dependencies\nnpm install\n# or\nyarn install\n\n# Start the web app\nnpm run dev\n# or\nyarn dev\n```\n\n#### Step 4: Use DeepWiki!\n\n1. Open [http://localhost:3000](http://localhost:3000) in your browser\n2. Enter a GitHub, GitLab, or Bitbucket repository (like `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, or `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. For private repositories, click \"+ Add access tokens\" and enter your GitHub or GitLab personal access token\n4. Click \"Generate Wiki\" and watch the magic happen!\n\n## 🔍 How It Works\n\nDeepWiki uses AI to:\n\n1. Clone and analyze the GitHub, GitLab, or Bitbucket repository (including private repos with token authentication)\n2. Create embeddings of the code for smart retrieval\n3. Generate documentation with context-aware AI (using Google Gemini, OpenAI, OpenRouter, Azure OpenAI, or local Ollama models)\n4. Create visual diagrams to explain code relationships\n5. Organize everything into a structured wiki\n6. Enable intelligent Q&A with the repository through the Ask feature\n7. Provide in-depth research capabilities with DeepResearch\n\n```mermaid\ngraph TD\n    A[User inputs GitHub/GitLab/Bitbucket repo] --> AA{Private repo?}\n    AA -->|Yes| AB[Add access token]\n    AA -->|No| B[Clone Repository]\n    AB --> B\n    B --> C[Analyze Code Structure]\n    C --> D[Create Code Embeddings]\n\n    D --> M{Select Model Provider}\n    M -->|Google Gemini| E1[Generate with Gemini]\n    M -->|OpenAI| E2[Generate with OpenAI]\n    M -->|OpenRouter| E3[Generate with OpenRouter]\n    M -->|Local Ollama| E4[Generate with Ollama]\n    M -->|Azure| E5[Generate with Azure]\n\n    E1 --> E[Generate Documentation]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n    E5 --> E\n\n    D --> F[Create Visual Diagrams]\n    E --> G[Organize as Wiki]\n    F --> G\n    G --> H[Interactive DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4,E5 process;\n    class H result;\n```\n\n## 🛠️ Project Structure\n\n```\ndeepwiki/\n├── api/                  # Backend API server\n│   ├── main.py           # API entry point\n│   ├── api.py            # FastAPI implementation\n│   ├── rag.py            # Retrieval Augmented Generation\n│   ├── data_pipeline.py  # Data processing utilities\n│   ├── pyproject.toml     # Python dependencies (Poetry)\n│   └── poetry.lock        # Locked Python dependency versions\n│\n├── src/                  # Frontend Next.js app\n│   ├── app/              # Next.js app directory\n│   │   └── page.tsx      # Main application page\n│   └── components/       # React components\n│       └── Mermaid.tsx   # Mermaid diagram renderer\n│\n├── public/               # Static assets\n├── package.json          # JavaScript dependencies\n└── .env                  # Environment variables (create this)\n```\n\n## 🤖 Provider-Based Model Selection System\n\nDeepWiki now implements a flexible provider-based model selection system supporting multiple LLM providers:\n\n### Supported Providers and Models\n\n- **Google**: Default `gemini-2.5-flash`, also supports `gemini-2.5-flash-lite`, `gemini-2.5-pro`, etc.\n- **OpenAI**: Default `gpt-5-nano`, also supports `gpt-5`, `4o`, etc.\n- **OpenRouter**: Access to multiple models via a unified API, including Claude, Llama, Mistral, etc.\n- **Azure OpenAI**: Default `gpt-4o`, also supports `o4-mini`, etc.\n- **Ollama**: Support for locally running open-source models like `llama3`\n\n### Environment Variables\n\nEach provider requires its corresponding API key environment variables:\n\n```\n# API Keys\nGOOGLE_API_KEY=your_google_api_key        # Required for Google Gemini models\nOPENAI_API_KEY=your_openai_api_key        # Required for OpenAI models\nOPENROUTER_API_KEY=your_openrouter_api_key # Required for OpenRouter models\nAZURE_OPENAI_API_KEY=your_azure_openai_api_key  #Required for Azure OpenAI models\nAZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint  #Required for Azure OpenAI models\nAZURE_OPENAI_VERSION=your_azure_openai_version  #Required for Azure OpenAI models\n\n# OpenAI API Base URL Configuration\nOPENAI_BASE_URL=https://custom-api-endpoint.com/v1  # Optional, for custom OpenAI API endpoints\n\n# Ollama host\nOLLAMA_HOST=your_ollama_host # Optional, if Ollama is not local. default: http://localhost:11434\n\n# Configuration Directory\nDEEPWIKI_CONFIG_DIR=/path/to/custom/config/dir  # Optional, for custom config file location\n```\n\n### Configuration Files\n\nDeepWiki uses JSON configuration files to manage various aspects of the system:\n\n1. **`generator.json`**: Configuration for text generation models\n   - Defines available model providers (Google, OpenAI, OpenRouter, Azure, Ollama)\n   - Specifies default and available models for each provider\n   - Contains model-specific parameters like temperature and top_p\n\n2. **`embedder.json`**: Configuration for embedding models and text processing\n   - Defines embedding models for vector storage\n   - Contains retriever configuration for RAG\n   - Specifies text splitter settings for document chunking\n\n3. **`repo.json`**: Configuration for repository handling\n   - Contains file filters to exclude certain files and directories\n   - Defines repository size limits and processing rules\n\nBy default, these files are located in the `api/config/` directory. You can customize their location using the `DEEPWIKI_CONFIG_DIR` environment variable.\n\n### Custom Model Selection for Service Providers\n\nThe custom model selection feature is specifically designed for service providers who need to:\n\n- You can offer multiple AI model choices to users within your organization\n- You can quickly adapt to the rapidly evolving LLM landscape without code changes\n- You can support specialized or fine-tuned models that aren't in the predefined list\n\nService providers can implement their model offerings by selecting from the predefined options or entering custom model identifiers in the frontend interface.\n\n### Base URL Configuration for Enterprise Private Channels\n\nThe OpenAI Client's base_url configuration is designed primarily for enterprise users with private API channels. This feature:\n\n- Enables connection to private or enterprise-specific API endpoints\n- Allows organizations to use their own self-hosted or custom-deployed LLM services\n- Supports integration with third-party OpenAI API-compatible services\n\n**Coming Soon**: In future updates, DeepWiki will support a mode where users need to provide their own API keys in requests. This will allow enterprise customers with private channels to use their existing API arrangements without sharing credentials with the DeepWiki deployment.\n\n## 🧩 Using OpenAI-Compatible Embedding Models (e.g., Alibaba Qwen)\n\nIf you want to use embedding models compatible with the OpenAI API (such as Alibaba Qwen), follow these steps:\n\n1. Replace the contents of `api/config/embedder.json` with those from `api/config/embedder_openai_compatible.json`.\n2. In your project root `.env` file, set the relevant environment variables, for example:\n   ```\n   OPENAI_API_KEY=your_api_key\n   OPENAI_BASE_URL=your_openai_compatible_endpoint\n   ```\n3. The program will automatically substitute placeholders in embedder.json with the values from your environment variables.\n\nThis allows you to seamlessly switch to any OpenAI-compatible embedding service without code changes.\n\n## 🧠 Using Google AI Embeddings\n\nDeepWiki now supports Google AI's latest embedding models as an alternative to OpenAI embeddings. This provides better integration when you're already using Google Gemini models for text generation.\n\n### Features\n\n- **Latest Model**: Uses Google's `text-embedding-004` model\n- **Same API Key**: Uses your existing `GOOGLE_API_KEY` (no additional setup required)\n- **Better Integration**: Optimized for use with Google Gemini text generation models\n- **Task-Specific**: Supports semantic similarity, retrieval, and classification tasks\n- **Batch Processing**: Efficient processing of multiple texts\n\n### How to Enable Google AI Embeddings\n\n**Option 1: Environment Variable (Recommended)**\n\nSet the embedder type in your `.env` file:\n\n```bash\n# Your existing Google API key\nGOOGLE_API_KEY=your_google_api_key\n\n# Enable Google AI embeddings\nDEEPWIKI_EMBEDDER_TYPE=google\n```\n\n**Option 2: Docker Environment**\n\n```bash\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e DEEPWIKI_EMBEDDER_TYPE=google \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n**Option 3: Docker Compose**\n\nAdd to your `.env` file:\n\n```bash\nGOOGLE_API_KEY=your_google_api_key\nDEEPWIKI_EMBEDDER_TYPE=google\n```\n\nThen run:\n\n```bash\ndocker-compose up\n```\n\n### Available Embedder Types\n\n| Type | Description | API Key Required | Notes |\n|------|-------------|------------------|-------|\n| `openai` | OpenAI embeddings (default) | `OPENAI_API_KEY` | Uses `text-embedding-3-small` model |\n| `google` | Google AI embeddings | `GOOGLE_API_KEY` | Uses `text-embedding-004` model |\n| `ollama` | Local Ollama embeddings | None | Requires local Ollama installation |\n\n### Why Use Google AI Embeddings?\n\n- **Consistency**: If you're using Google Gemini for text generation, using Google embeddings provides better semantic consistency\n- **Performance**: Google's latest embedding model offers excellent performance for retrieval tasks\n- **Cost**: Competitive pricing compared to OpenAI\n- **No Additional Setup**: Uses the same API key as your text generation models\n\n### Switching Between Embedders\n\nYou can easily switch between different embedding providers:\n\n```bash\n# Use OpenAI embeddings (default)\nexport DEEPWIKI_EMBEDDER_TYPE=openai\n\n# Use Google AI embeddings\nexport DEEPWIKI_EMBEDDER_TYPE=google\n\n# Use local Ollama embeddings\nexport DEEPWIKI_EMBEDDER_TYPE=ollama\n```\n\n**Note**: When switching embedders, you may need to regenerate your repository embeddings as different models produce different vector spaces.\n\n### Logging\n\nDeepWiki uses Python's built-in `logging` module for diagnostic output. You can configure the verbosity and log file destination via environment variables:\n\n| Variable        | Description                                                        | Default                      |\n|-----------------|--------------------------------------------------------------------|------------------------------|\n| `LOG_LEVEL`     | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).             | INFO                         |\n| `LOG_FILE_PATH` | Path to the log file. If set, logs will be written to this file.   | `api/logs/application.log`   |\n\nTo enable debug logging and direct logs to a custom file:\n```bash\nexport LOG_LEVEL=DEBUG\nexport LOG_FILE_PATH=./debug.log\npython -m api.main\n```\nOr with Docker Compose:\n```bash\nLOG_LEVEL=DEBUG LOG_FILE_PATH=./debug.log docker-compose up\n```\n\nWhen running with Docker Compose, the container's `api/logs` directory is bind-mounted to `./api/logs` on your host (see the `volumes` section in `docker-compose.yml`), ensuring log files persist across restarts.\n\nAlternatively, you can store these settings in your `.env` file:\n\n```bash\nLOG_LEVEL=DEBUG\nLOG_FILE_PATH=./debug.log\n```\nThen simply run:\n\n```bash\ndocker-compose up\n```\n\n**Logging Path Security Considerations:** In production environments, ensure the `api/logs` directory and any custom log file path are secured with appropriate filesystem permissions and access controls. The application enforces that `LOG_FILE_PATH` resides within the project's `api/logs` directory to prevent path traversal or unauthorized writes.\n\n## 🛠️ Advanced Setup\n\n### Environment Variables\n\n| Variable             | Description                                                  | Required | Note                                                                                                     |\n|----------------------|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------|\n| `GOOGLE_API_KEY`     | Google Gemini API key for AI generation and embeddings      | No | Required for Google Gemini models and Google AI embeddings                                               \n| `OPENAI_API_KEY`     | OpenAI API key for embeddings and models                     | Conditional | Required if using OpenAI embeddings or models                                                            |\n| `OPENROUTER_API_KEY` | OpenRouter API key for alternative models                    | No | Required only if you want to use OpenRouter models                                                       |\n| `AWS_ACCESS_KEY_ID`  | AWS access key ID for Bedrock                                 | No | Required for Bedrock if not using instance/role-based credentials                                        |\n| `AWS_SECRET_ACCESS_KEY` | AWS secret access key for Bedrock                          | No | Required for Bedrock if not using instance/role-based credentials                                        |\n| `AWS_SESSION_TOKEN`  | AWS session token for Bedrock (STS)                            | No | Required when using temporary credentials                                                                |\n| `AWS_REGION`         | AWS region for Bedrock (default: `us-east-1`)                  | No | Used by Bedrock client                                                                                   |\n| `AWS_ROLE_ARN`       | AWS role ARN to assume for Bedrock                             | No | If set, the Bedrock client will call STS AssumeRole                                                     |\n| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key                    | No | Required only if you want to use Azure OpenAI models                                                       |\n| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint                    | No | Required only if you want to use Azure OpenAI models                                                       |\n| `AZURE_OPENAI_VERSION` | Azure OpenAI version                     | No | Required only if you want to use Azure OpenAI models                                                       |\n| `OLLAMA_HOST`        | Ollama Host (default: http://localhost:11434)                | No | Required only if you want to use external Ollama server                                                  |\n| `DEEPWIKI_EMBEDDER_TYPE` | Embedder type: `openai`, `google`, `ollama`, or `bedrock` (default: `openai`) | No | Controls which embedding provider to use                                                              |\n| `PORT`               | Port for the API server (default: 8001)                      | No | If you host API and frontend on the same machine, make sure change port of `SERVER_BASE_URL` accordingly |\n| `SERVER_BASE_URL`    | Base URL for the API server (default: http://localhost:8001) | No |\n| `DEEPWIKI_AUTH_MODE` | Set to `true` or `1` to enable authorization mode. | No | Defaults to `false`. If enabled, `DEEPWIKI_AUTH_CODE` is required. |\n| `DEEPWIKI_AUTH_CODE` | The secret code required for wiki generation when `DEEPWIKI_AUTH_MODE` is enabled. | No | Only used if `DEEPWIKI_AUTH_MODE` is `true` or `1`. |\n\n**API Key Requirements:**\n- If using `DEEPWIKI_EMBEDDER_TYPE=openai` (default): `OPENAI_API_KEY` is required\n- If using `DEEPWIKI_EMBEDDER_TYPE=google`: `GOOGLE_API_KEY` is required  \n- If using `DEEPWIKI_EMBEDDER_TYPE=ollama`: No API key required (local processing)\n- If using `DEEPWIKI_EMBEDDER_TYPE=bedrock`: AWS credentials (or role-based credentials) are required\n\nOther API keys are only required when configuring and using models from the corresponding providers.\n\n## Authorization Mode\n\nDeepWiki can be configured to run in an authorization mode, where wiki generation requires a valid authorization code. This is useful if you want to control who can use the generation feature.\nRestricts frontend initiation and protects cache deletion, but doesn't fully prevent backend generation if API endpoints are hit directly.\n\nTo enable authorization mode, set the following environment variables:\n\n- `DEEPWIKI_AUTH_MODE`: Set this to `true` or `1`. When enabled, the frontend will display an input field for the authorization code.\n- `DEEPWIKI_AUTH_CODE`: Set this to the desired secret code. Restricts frontend initiation and protects cache deletion, but doesn't fully prevent backend generation if API endpoints are hit directly.\n\nIf `DEEPWIKI_AUTH_MODE` is not set or is set to `false` (or any other value than `true`/`1`), the authorization feature will be disabled, and no code will be required.\n\n### Docker Setup\n\nYou can use Docker to run DeepWiki:\n\n#### Running the Container\n\n```bash\n# Pull the image from GitHub Container Registry\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# Run the container with environment variables\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -e OLLAMA_HOST=your_ollama_host \\\n  -e AZURE_OPENAI_API_KEY=your_azure_openai_api_key \\\n  -e AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint \\\n  -e AZURE_OPENAI_VERSION=your_azure_openai_version \\\n\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nThis command also mounts `~/.adalflow` on your host to `/root/.adalflow` in the container. This path is used to store:\n- Cloned repositories (`~/.adalflow/repos/`)\n- Their embeddings and indexes (`~/.adalflow/databases/`)\n- Cached generated wiki content (`~/.adalflow/wikicache/`)\n\nThis ensures that your data persists even if the container is stopped or removed.\n\nOr use the provided `docker-compose.yml` file:\n\n```bash\n# Edit the .env file with your API keys first\ndocker-compose up\n```\n\n(The `docker-compose.yml` file is pre-configured to mount `~/.adalflow` for data persistence, similar to the `docker run` command above.)\n\n#### Using a .env file with Docker\n\nYou can also mount a .env file to the container:\n\n```bash\n# Create a .env file with your API keys\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\necho \"AZURE_OPENAI_API_KEY=your_azure_openai_api_key\" >> .env\necho \"AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint\" >> .env\necho \"AZURE_OPENAI_VERSION=your_azure_openai_version\"  >>.env\necho \"OLLAMA_HOST=your_ollama_host\" >> .env\n\n# Run the container with the .env file mounted\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nThis command also mounts `~/.adalflow` on your host to `/root/.adalflow` in the container. This path is used to store:\n- Cloned repositories (`~/.adalflow/repos/`)\n- Their embeddings and indexes (`~/.adalflow/databases/`)\n- Cached generated wiki content (`~/.adalflow/wikicache/`)\n\nThis ensures that your data persists even if the container is stopped or removed.\n\n#### Building the Docker image locally\n\nIf you want to build the Docker image locally:\n\n```bash\n# Clone the repository\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Build the Docker image\ndocker build -t deepwiki-open .\n\n# Run the container\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -e AZURE_OPENAI_API_KEY=your_azure_openai_api_key \\\n  -e AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint \\\n  -e AZURE_OPENAI_VERSION=your_azure_openai_version \\\n  -e OLLAMA_HOST=your_ollama_host \\\n  deepwiki-open\n```\n\n#### Using Self-Signed Certificates in Docker\n\nIf you're in an environment that uses self-signed certificates, you can include them in the Docker build:\n\n1. Create a directory for your certificates (default is `certs` in your project root)\n2. Copy your `.crt` or `.pem` certificate files into this directory\n3. Build the Docker image:\n\n```bash\n# Build with default certificates directory (certs)\ndocker build .\n\n# Or build with a custom certificates directory\ndocker build --build-arg CUSTOM_CERT_DIR=my-custom-certs .\n```\n\n### API Server Details\n\nThe API server provides:\n- Repository cloning and indexing\n- RAG (Retrieval Augmented Generation)\n- Streaming chat completions\n\nFor more details, see the [API README](./api/README.md).\n\n## 🔌 OpenRouter Integration\n\nDeepWiki now supports [OpenRouter](https://openrouter.ai/) as a model provider, giving you access to hundreds of AI models through a single API:\n\n- **Multiple Model Options**: Access models from OpenAI, Anthropic, Google, Meta, Mistral, and more\n- **Simple Configuration**: Just add your OpenRouter API key and select the model you want to use\n- **Cost Efficiency**: Choose models that fit your budget and performance needs\n- **Easy Switching**: Toggle between different models without changing your code\n\n### How to Use OpenRouter with DeepWiki\n\n1. **Get an API Key**: Sign up at [OpenRouter](https://openrouter.ai/) and get your API key\n2. **Add to Environment**: Add `OPENROUTER_API_KEY=your_key` to your `.env` file\n3. **Enable in UI**: Check the \"Use OpenRouter API\" option on the homepage\n4. **Select Model**: Choose from popular models like GPT-4o, Claude 3.5 Sonnet, Gemini 2.0, and more\n\nOpenRouter is particularly useful if you want to:\n- Try different models without signing up for multiple services\n- Access models that might be restricted in your region\n- Compare performance across different model providers\n- Optimize for cost vs. performance based on your needs\n\n## 🤖 Ask & DeepResearch Features\n\n### Ask Feature\n\nThe Ask feature allows you to chat with your repository using Retrieval Augmented Generation (RAG):\n\n- **Context-Aware Responses**: Get accurate answers based on the actual code in your repository\n- **RAG-Powered**: The system retrieves relevant code snippets to provide grounded responses\n- **Real-Time Streaming**: See responses as they're generated for a more interactive experience\n- **Conversation History**: The system maintains context between questions for more coherent interactions\n\n### DeepResearch Feature\n\nDeepResearch takes repository analysis to the next level with a multi-turn research process:\n\n- **In-Depth Investigation**: Thoroughly explores complex topics through multiple research iterations\n- **Structured Process**: Follows a clear research plan with updates and a comprehensive conclusion\n- **Automatic Continuation**: The AI automatically continues research until reaching a conclusion (up to 5 iterations)\n- **Research Stages**:\n  1. **Research Plan**: Outlines the approach and initial findings\n  2. **Research Updates**: Builds on previous iterations with new insights\n  3. **Final Conclusion**: Provides a comprehensive answer based on all iterations\n\nTo use DeepResearch, simply toggle the \"Deep Research\" switch in the Ask interface before submitting your question.\n\n## Screenshots\n\n![DeepWiki Main Interface](screenshots/Interface.png)\n*The main interface of DeepWiki*\n\n![Private Repository Support](screenshots/privaterepo.png)\n*Access private repositories with personal access tokens*\n\n![DeepResearch Feature](screenshots/DeepResearch.png)\n*DeepResearch conducts multi-turn investigations for complex topics*\n\n### Demo Video\n\n[![DeepWiki Demo Video](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*Watch DeepWiki in action!*\n\n## ❓ Troubleshooting\n\n### API Key Issues\n- **\"Missing environment variables\"**: Make sure your `.env` file is in the project root and contains the required API keys\n- **\"API key not valid\"**: Check that you've copied the full key correctly with no extra spaces\n- **\"OpenRouter API error\"**: Verify your OpenRouter API key is valid and has sufficient credits\n- **\"Azure OpenAI API error\"**: Verify your Azure OpenAI credentials (API key, endpoint, and version) are correct and the service is properly deployed\n\n### Connection Problems\n- **\"Cannot connect to API server\"**: Make sure the API server is running on port 8001\n- **\"CORS error\"**: The API is configured to allow all origins, but if you're having issues, try running both frontend and backend on the same machine\n\n### Generation Issues\n- **\"Error generating wiki\"**: For very large repositories, try a smaller one first\n- **\"Invalid repository format\"**: Make sure you're using a valid GitHub, GitLab or Bitbucket URL format\n- **\"Could not fetch repository structure\"**: For private repositories, ensure you've entered a valid personal access token with appropriate permissions\n- **\"Diagram rendering error\"**: The app will automatically try to fix broken diagrams\n\n### Common Solutions\n1. **Restart both servers**: Sometimes a simple restart fixes most issues\n2. **Check console logs**: Open browser developer tools to see any JavaScript errors\n3. **Check API logs**: Look at the terminal where the API is running for Python errors\n\n## 🤝 Contributing\n\nContributions are welcome! Feel free to:\n- Open issues for bugs or feature requests\n- Submit pull requests to improve the code\n- Share your feedback and ideas\n\n## 📄 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## ⭐ Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n"
  },
  {
    "path": "README.pt-br.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki Banner](screenshots/Deepwiki.png)\n\n**DeepWiki** é minha própria tentativa de implementação do DeepWiki, que cria automaticamente wikis bonitas e interativas para qualquer repositório GitHub, GitLab ou BitBucket! Basta inserir o nome de um repositório, e o DeepWiki irá:\n\n1. Analisar a estrutura do código\n2. Gerar documentação abrangente\n3. Criar diagramas visuais para explicar como tudo funciona\n4. Organizar tudo em uma wiki fácil de navegar\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n[![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc)\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Recursos\n\n- **Documentação Instantânea**: Transforme qualquer repositório GitHub, GitLab ou BitBucket em uma wiki em segundos\n- **Suporte a Repositórios Privados**: Acesse repositórios privados com segurança usando tokens de acesso pessoal\n- **Análise Inteligente**: Compreensão da estrutura e relacionamentos do código com IA\n- **Diagramas Bonitos**: Diagramas Mermaid automáticos para visualizar arquitetura e fluxo de dados\n- **Navegação Fácil**: Interface simples e intuitiva para explorar a wiki\n- **Recurso de Perguntas**: Converse com seu repositório usando IA com RAG para obter respostas precisas\n- **DeepResearch**: Processo de pesquisa em várias etapas que investiga minuciosamente tópicos complexos\n- **Múltiplos Provedores de Modelos**: Suporte para Google Gemini, OpenAI, OpenRouter e modelos locais Ollama\n\n## 🚀 Início Rápido (Super Fácil!)\n\n### Opção 1: Usando Docker\n\n```bash\n# Clone o repositório\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Crie um arquivo .env com suas chaves de API\necho \"GOOGLE_API_KEY=sua_chave_api_google\" > .env\necho \"OPENAI_API_KEY=sua_chave_api_openai\" >> .env\n# Opcional: Adicione a chave API OpenRouter se quiser usar modelos OpenRouter\necho \"OPENROUTER_API_KEY=sua_chave_api_openrouter\" >> .env\n# Opcional: Adicione o host Ollama se não for local. padrão: http://localhost:11434\necho \"OLLAMA_HOST=seu_host_ollama\" >> .env\n\n# Execute com Docker Compose\ndocker-compose up\n```\n\nPara instruções detalhadas sobre como usar o DeepWiki com Ollama e Docker, veja [Instruções do Ollama (em inglês)](Ollama-instruction.md).\n\n> 💡 **Onde obter essas chaves:**\n> - Obtenha uma chave API Google no [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Obtenha uma chave API OpenAI na [Plataforma OpenAI](https://platform.openai.com/api-keys)\n\n### Opção 2: Configuração Manual (Recomendada)\n\n#### Passo 1: Configure Suas Chaves API\n\nCrie um arquivo `.env` na raiz do projeto com estas chaves:\n\n```\nGOOGLE_API_KEY=sua_chave_api_google\nOPENAI_API_KEY=sua_chave_api_openai\n# Opcional: Adicione isso se quiser usar modelos OpenRouter\nOPENROUTER_API_KEY=sua_chave_api_openrouter\n# Opcional: Adicione o host Ollama se não for local. padrão: http://localhost:11434\nOLLAMA_HOST=seu_host_ollama\n```\n\n#### Passo 2: Inicie o Backend\n\n```bash\n# Instale as dependências Python\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Inicie o servidor API\npython -m api.main\n```\n\n#### Passo 3: Inicie o Frontend\n\n```bash\n# Instale as dependências JavaScript\nnpm install\n# ou\nyarn install\n\n# Inicie o aplicativo web\nnpm run dev\n# ou\nyarn dev\n```\n\n#### Passo 4: Use o DeepWiki!\n\n1. Abra [http://localhost:3000](http://localhost:3000) no seu navegador\n2. Insira um repositório GitHub, GitLab ou Bitbucket (como `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, ou `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. Para repositórios privados, clique em \"+ Adicionar tokens de acesso\" e insira seu token de acesso pessoal do GitHub ou GitLab\n4. Clique em \"Gerar Wiki\" e veja a mágica acontecer!\n\n## 🔍 Como Funciona\n\nO DeepWiki usa IA para:\n\n1. Clonar e analisar o repositório GitHub, GitLab ou Bitbucket (incluindo repositórios privados com autenticação por token)\n2. Criar embeddings do código para recuperação inteligente\n3. Gerar documentação com IA contextual (usando modelos Google Gemini, OpenAI, OpenRouter ou Ollama local)\n4. Criar diagramas visuais para explicar relações de código\n5. Organizar tudo em uma wiki estruturada\n6. Permitir perguntas e respostas inteligentes com o repositório através do recurso de Perguntas\n7. Fornecer capacidades de pesquisa aprofundada com DeepResearch\n\n```mermaid\ngraph TD\n    A[Usuário insere repo GitHub/GitLab/Bitbucket] --> AA{Repo privado?}\n    AA -->|Sim| AB[Adicionar token de acesso]\n    AA -->|Não| B[Clonar Repositório]\n    AB --> B\n    B --> C[Analisar Estrutura do Código]\n    C --> D[Criar Embeddings do Código]\n\n    D --> M{Selecionar Provedor de Modelo}\n    M -->|Google Gemini| E1[Gerar com Gemini]\n    M -->|OpenAI| E2[Gerar com OpenAI]\n    M -->|OpenRouter| E3[Gerar com OpenRouter]\n    M -->|Ollama Local| E4[Gerar com Ollama]\n\n    E1 --> E[Gerar Documentação]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[Criar Diagramas Visuais]\n    E --> G[Organizar como Wiki]\n    F --> G\n    G --> H[DeepWiki Interativo]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ Estrutura do Projeto\n\n```\ndeepwiki/\n├── api/                  # Servidor API backend\n│   ├── main.py           # Ponto de entrada da API\n│   ├── api.py            # Implementação FastAPI\n│   ├── rag.py            # Retrieval Augmented Generation\n│   ├── data_pipeline.py  # Utilitários de processamento de dados\n│   └── requirements.txt  # Dependências Python\n│\n├── src/                  # Aplicativo Next.js frontend\n│   ├── app/              # Diretório do aplicativo Next.js\n│   │   └── page.tsx      # Página principal do aplicativo\n│   └── components/       # Componentes React\n│       └── Mermaid.tsx   # Renderizador de diagramas Mermaid\n│\n├── public/               # Ativos estáticos\n├── package.json          # Dependências JavaScript\n└── .env                  # Variáveis de ambiente (crie este arquivo)\n```\n\n## 🤖 Sistema de Seleção de Modelos Baseado em Provedores\n\nO DeepWiki agora implementa um sistema flexível de seleção de modelos baseado em provedores, suportando múltiplos provedores de LLM:\n\n### Provedores e Modelos Suportados\n\n- **Google**: Padrão `gemini-2.5-flash`, também suporta `gemini-2.5-flash-lite`, `gemini-2.5-pro`, etc.\n- **OpenAI**: Padrão `gpt-5-nano`, também suporta `gpt-5`, `4o`, etc.\n- **OpenRouter**: Acesso a múltiplos modelos via uma API unificada, incluindo Claude, Llama, Mistral, etc.\n- **Ollama**: Suporte para modelos de código aberto executados localmente como `llama3`\n\n### Variáveis de Ambiente\n\nCada provedor requer suas variáveis de ambiente de chave API correspondentes:\n\n```\n# Chaves API\nGOOGLE_API_KEY=sua_chave_api_google        # Necessária para modelos Google Gemini\nOPENAI_API_KEY=sua_chave_api_openai        # Necessária para modelos OpenAI\nOPENROUTER_API_KEY=sua_chave_api_openrouter # Necessária para modelos OpenRouter\n\n# Configuração de URL Base da API OpenAI\nOPENAI_BASE_URL=https://endpoint-api-personalizado.com/v1  # Opcional, para endpoints de API OpenAI personalizados\n\n# Host Ollama\nOLLAMA_HOST=seu_host_ollama # Opcional, se Ollama não for local. padrão: http://localhost:11434\n\n# Diretório de Configuração\nDEEPWIKI_CONFIG_DIR=/caminho/para/dir/config/personalizado  # Opcional, para localização personalizada de arquivos de configuração\n```\n\n### Arquivos de Configuração\n\nO DeepWiki usa arquivos de configuração JSON para gerenciar vários aspectos do sistema:\n\n1. **`generator.json`**: Configuração para modelos de geração de texto\n   - Define provedores de modelos disponíveis (Google, OpenAI, OpenRouter, Ollama)\n   - Especifica modelos padrão e disponíveis para cada provedor\n   - Contém parâmetros específicos de modelo como temperatura e top_p\n\n2. **`embedder.json`**: Configuração para modelos de embedding e processamento de texto\n   - Define modelos de embedding para armazenamento de vetores\n   - Contém configuração do recuperador para RAG\n   - Especifica configurações do divisor de texto para divisão de documentos\n\n3. **`repo.json`**: Configuração para manipulação de repositórios\n   - Contém filtros de arquivos para excluir certos arquivos e diretórios\n   - Define limites de tamanho de repositório e regras de processamento\n\nPor padrão, esses arquivos estão localizados no diretório `api/config/`. Você pode personalizar sua localização usando a variável de ambiente `DEEPWIKI_CONFIG_DIR`.\n\n### Seleção de Modelo Personalizado para Provedores de Serviço\n\nO recurso de seleção de modelo personalizado é especificamente projetado para provedores de serviço que precisam:\n\n- Oferecer múltiplas opções de modelo de IA para usuários dentro de sua organização\n- Adaptar-se rapidamente ao panorama de LLM em rápida evolução sem mudanças de código\n- Suportar modelos especializados ou ajustados que não estão na lista predefinida\n\nProvedores de serviço podem implementar suas ofertas de modelo selecionando entre as opções predefinidas ou inserindo identificadores de modelo personalizados na interface do frontend.\n\n### Configuração de URL Base para Canais Privados Empresariais\n\nA configuração base_url do Cliente OpenAI é projetada principalmente para usuários empresariais com canais de API privados. Este recurso:\n\n- Permite conexão a endpoints de API privados ou específicos da empresa\n- Permite que organizações usem seus próprios serviços LLM auto-hospedados ou implantados personalizados\n- Suporta integração com serviços compatíveis com a API OpenAI de terceiros\n\n**Em Breve**: Em atualizações futuras, o DeepWiki suportará um modo onde os usuários precisam fornecer suas próprias chaves API nas solicitações. Isso permitirá que clientes empresariais com canais privados usem seus arranjos de API existentes sem compartilhar credenciais com a implantação do DeepWiki.\n\n## 🤩 Usando Modelos de Embedding Compatíveis com OpenAI (ex., Alibaba Qwen)\n\nSe você deseja usar modelos de embedding compatíveis com a API OpenAI (como Alibaba Qwen), siga estas etapas:\n\n1. Substitua o conteúdo de `api/config/embedder.json` pelo de `api/config/embedder_openai_compatible.json`.\n2. No arquivo `.env` da raiz do seu projeto, defina as variáveis de ambiente relevantes, por exemplo:\n   ```\n   OPENAI_API_KEY=sua_chave_api\n   OPENAI_BASE_URL=seu_endpoint_compativel_openai\n   ```\n3. O programa substituirá automaticamente os espaços reservados em embedder.json pelos valores de suas variáveis de ambiente.\n\nIsso permite que você mude perfeitamente para qualquer serviço de embedding compatível com OpenAI sem mudanças de código.\n\n### Logging\n\nO DeepWiki usa o módulo `logging` integrado do Python para saída de diagnóstico. Você pode configurar a verbosidade e o destino do arquivo de log via variáveis de ambiente:\n\n| Variável        | Descrição                                                        | Padrão                      |\n|-----------------|--------------------------------------------------------------------|------------------------------|\n| `LOG_LEVEL`     | Nível de logging (DEBUG, INFO, WARNING, ERROR, CRITICAL).          | INFO                         |\n| `LOG_FILE_PATH` | Caminho para o arquivo de log. Se definido, logs serão escritos neste arquivo. | `api/logs/application.log`   |\n\nPara habilitar logging de depuração e direcionar logs para um arquivo personalizado:\n```bash\nexport LOG_LEVEL=DEBUG\nexport LOG_FILE_PATH=./debug.log\npython -m api.main\n```\nOu com Docker Compose:\n```bash\nLOG_LEVEL=DEBUG LOG_FILE_PATH=./debug.log docker-compose up\n```\n\nAo executar com Docker Compose, o diretório `api/logs` do container é montado em `./api/logs` no seu host (veja a seção `volumes` em `docker-compose.yml`), garantindo que os arquivos de log persistam entre reinicializações.\n\nAlternativamente, você pode armazenar essas configurações no seu arquivo `.env`:\n\n```bash\nLOG_LEVEL=DEBUG\nLOG_FILE_PATH=./debug.log\n```\nEntão simplesmente execute:\n\n```bash\ndocker-compose up\n```\n\n**Considerações de Segurança do Caminho de Logging:** Em ambientes de produção, garanta que o diretório `api/logs` e qualquer caminho de arquivo de log personalizado estejam protegidos com permissões de sistema de arquivos e controles de acesso apropriados. O aplicativo impõe que `LOG_FILE_PATH` resida dentro do diretório `api/logs` do projeto para evitar travessia de caminho ou escritas não autorizadas.\n\n## 🔧 Configuração Avançada\n\n### Variáveis de Ambiente\n\n| Variável             | Descrição                                                  | Obrigatória | Observação                                                                                                     |\n|----------------------|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------|\n| `GOOGLE_API_KEY`     | Chave API Google Gemini para geração com IA                      | Não | Necessária apenas se você quiser usar modelos Google Gemini                                                    \n| `OPENAI_API_KEY`     | Chave API OpenAI para embeddings                                | Sim | Nota: Isso é necessário mesmo se você não estiver usando modelos OpenAI, pois é usado para embeddings.              |\n| `OPENROUTER_API_KEY` | Chave API OpenRouter para modelos alternativos                    | Não | Necessária apenas se você quiser usar modelos OpenRouter                                                       |\n| `OLLAMA_HOST`        | Host Ollama (padrão: http://localhost:11434)                | Não | Necessária apenas se você quiser usar servidor Ollama externo                                                  |\n| `PORT`               | Porta para o servidor API (padrão: 8001)                      | Não | Se você hospedar API e frontend na mesma máquina, certifique-se de alterar a porta de `SERVER_BASE_URL` de acordo |\n| `SERVER_BASE_URL`    | URL base para o servidor API (padrão: http://localhost:8001) | Não |\n| `DEEPWIKI_AUTH_MODE` | Defina como `true` ou `1` para habilitar o modo de autorização. | Não | Padrão é `false`. Se habilitado, `DEEPWIKI_AUTH_CODE` é necessário. |\n| `DEEPWIKI_AUTH_CODE` | O código secreto necessário para geração de wiki quando `DEEPWIKI_AUTH_MODE` está habilitado. | Não | Usado apenas se `DEEPWIKI_AUTH_MODE` for `true` ou `1`. |\n\nSe você não estiver usando o modo ollama, você precisa configurar uma chave API OpenAI para embeddings. Outras chaves API são necessárias apenas ao configurar e usar modelos dos provedores correspondentes.\n\n## Modo de Autorização\n\nO DeepWiki pode ser configurado para executar em um modo de autorização, onde a geração de wiki requer um código de autorização válido. Isso é útil se você quiser controlar quem pode usar o recurso de geração.\nRestringe a iniciação do frontend e protege a exclusão de cache, mas não impede completamente a geração de backend se os endpoints da API forem acessados diretamente.\n\nPara habilitar o modo de autorização, defina as seguintes variáveis de ambiente:\n\n- `DEEPWIKI_AUTH_MODE`: Defina como `true` ou `1`. Quando habilitado, o frontend exibirá um campo de entrada para o código de autorização.\n- `DEEPWIKI_AUTH_CODE`: Defina como o código secreto desejado. Restringe a iniciação do frontend e protege a exclusão de cache, mas não impede completamente a geração de backend se os endpoints da API forem acessados diretamente.\n\nSe `DEEPWIKI_AUTH_MODE` não estiver definido ou estiver definido como `false` (ou qualquer outro valor diferente de `true`/`1`), o recurso de autorização será desativado, e nenhum código será necessário.\n\n### Configuração Docker\n\nVocê pode usar Docker para executar o DeepWiki:\n\n```bash\n# Baixe a imagem do GitHub Container Registry\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# Execute o container com variáveis de ambiente\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=sua_chave_api_google \\\n  -e OPENAI_API_KEY=sua_chave_api_openai \\\n  -e OPENROUTER_API_KEY=sua_chave_api_openrouter \\\n  -e OLLAMA_HOST=seu_host_ollama \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nEste comando também monta `~/.adalflow` no seu host para `/root/.adalflow` no container. Este caminho é usado para armazenar:\n- Repositórios clonados (`~/.adalflow/repos/`)\n- Seus embeddings e índices (`~/.adalflow/databases/`)\n- Conteúdo de wiki gerado em cache (`~/.adalflow/wikicache/`)\n\nIsso garante que seus dados persistam mesmo se o container for parado ou removido.\n\nOu use o arquivo `docker-compose.yml` fornecido:\n\n```bash\n# Edite o arquivo .env com suas chaves API primeiro\ndocker-compose up\n```\n\n(O arquivo `docker-compose.yml` é pré-configurado para montar `~/.adalflow` para persistência de dados, similar ao comando `docker run` acima.)\n\n#### Usando um arquivo .env com Docker\n\nVocê também pode montar um arquivo .env no container:\n\n```bash\n# Crie um arquivo .env com suas chaves API\necho \"GOOGLE_API_KEY=sua_chave_api_google\" > .env\necho \"OPENAI_API_KEY=sua_chave_api_openai\" >> .env\necho \"OPENROUTER_API_KEY=sua_chave_api_openrouter\" >> .env\necho \"OLLAMA_HOST=seu_host_ollama\" >> .env\n\n# Execute o container com o arquivo .env montado\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nEste comando também monta `~/.adalflow` no seu host para `/root/.adalflow` no container. Este caminho é usado para armazenar:\n- Repositórios clonados (`~/.adalflow/repos/`)\n- Seus embeddings e índices (`~/.adalflow/databases/`)\n- Conteúdo de wiki gerado em cache (`~/.adalflow/wikicache/`)\n\nIsso garante que seus dados persistam mesmo se o container for parado ou removido.\n#### Construindo a imagem Docker localmente\n\nSe você quiser construir a imagem Docker localmente:\n\n```bash\n# Clone o repositório\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Construa a imagem Docker\ndocker build -t deepwiki-open .\n\n# Execute o container\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=sua_chave_api_google \\\n  -e OPENAI_API_KEY=sua_chave_api_openai \\\n  -e OPENROUTER_API_KEY=sua_chave_api_openrouter \\\n  -e OLLAMA_HOST=seu_host_ollama \\\n  deepwiki-open\n```\n\n### Detalhes do Servidor API\n\nO servidor API fornece:\n- Clonagem e indexação de repositórios\n- RAG (Retrieval Augmented Generation)\n- Completions de chat com streaming\n\nPara mais detalhes, veja o [README da API](./api/README.md).\n\n## 🔌 Integração com OpenRouter\n\nO DeepWiki agora suporta [OpenRouter](https://openrouter.ai/) como provedor de modelos, dando acesso a centenas de modelos de IA através de uma única API:\n\n- **Múltiplas Opções de Modelos**: Acesse modelos da OpenAI, Anthropic, Google, Meta, Mistral e mais\n- **Configuração Simples**: Apenas adicione sua chave API OpenRouter e selecione o modelo que deseja usar\n- **Eficiência de Custo**: Escolha modelos que se adequem ao seu orçamento e necessidades de desempenho\n- **Troca Fácil**: Alterne entre diferentes modelos sem alterar seu código\n\n### Como Usar o OpenRouter com DeepWiki\n\n1. **Obtenha uma Chave API**: Cadastre-se no [OpenRouter](https://openrouter.ai/) e obtenha sua chave API\n2. **Adicione ao Ambiente**: Adicione `OPENROUTER_API_KEY=sua_chave` ao seu arquivo `.env`\n3. **Habilite na UI**: Marque a opção \"Usar API OpenRouter\" na página inicial\n4. **Selecione o Modelo**: Escolha entre modelos populares como GPT-4o, Claude 3.5 Sonnet, Gemini 2.0 e mais\n\nO OpenRouter é particularmente útil se você quiser:\n- Experimentar diferentes modelos sem se cadastrar em múltiplos serviços\n- Acessar modelos que podem estar restritos em sua região\n- Comparar desempenho entre diferentes provedores de modelos\n- Otimizar custo vs. desempenho com base em suas necessidades\n\n## 🤖 Recursos de Perguntas & DeepResearch\n\n### Recurso de Perguntas\n\nO recurso de Perguntas permite que você converse com seu repositório usando Retrieval Augmented Generation (RAG):\n\n- **Respostas Contextuais**: Obtenha respostas precisas baseadas no código real em seu repositório\n- **Alimentado por RAG**: O sistema recupera trechos de código relevantes para fornecer respostas fundamentadas\n- **Streaming em Tempo Real**: Veja as respostas conforme são geradas para uma experiência mais interativa\n- **Histórico de Conversação**: O sistema mantém contexto entre perguntas para interações mais coerentes\n\n### Recurso DeepResearch\n\nO DeepResearch leva a análise de repositórios a um novo nível com um processo de pesquisa em várias etapas:\n\n- **Investigação Aprofundada**: Explora minuciosamente tópicos complexos através de múltiplas iterações de pesquisa\n- **Processo Estruturado**: Segue um plano de pesquisa claro com atualizações e uma conclusão abrangente\n- **Continuação Automática**: A IA continua automaticamente a pesquisa até chegar a uma conclusão (até 5 iterações)\n- **Estágios de Pesquisa**:\n  1. **Plano de Pesquisa**: Descreve a abordagem e descobertas iniciais\n  2. **Atualizações de Pesquisa**: Construído sobre iterações anteriores com novos insights\n  3. **Conclusão Final**: Fornece uma resposta abrangente baseada em todas as iterações\n\nPara usar o DeepResearch, simplesmente alterne o interruptor \"Pesquisa Aprofundada\" na interface de Perguntas antes de enviar sua pergunta.\n\n## 📱 Capturas de Tela\n\n![Interface Principal do DeepWiki](screenshots/Interface.png)\n*A interface principal do DeepWiki*\n\n![Suporte a Repositórios Privados](screenshots/privaterepo.png)\n*Acesse repositórios privados com tokens de acesso pessoal*\n\n![Recurso DeepResearch](screenshots/DeepResearch.png)\n*DeepResearch conduz investigações em várias etapas para tópicos complexos*\n\n### Vídeo de Demonstração\n\n[![Vídeo de Demonstração do DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*Veja o DeepWiki em ação!*\n\n## ❓ Solução de Problemas\n\n### Problemas com Chaves API\n- **\"Variáveis de ambiente ausentes\"**: Certifique-se de que seu arquivo `.env` está na raiz do projeto e contém as chaves API necessárias\n- **\"Chave API não válida\"**: Verifique se você copiou a chave completa corretamente sem espaços extras\n- **\"Erro de API OpenRouter\"**: Verifique se sua chave API OpenRouter é válida e tem créditos suficientes\n\n### Problemas de Conexão\n- **\"Não é possível conectar ao servidor API\"**: Certifique-se de que o servidor API está em execução na porta 8001\n- **\"Erro CORS\"**: A API está configurada para permitir todas as origens, mas se você estiver tendo problemas, tente executar frontend e backend na mesma máquina\n\n### Problemas de Geração\n- **\"Erro ao gerar wiki\"**: Para repositórios muito grandes, tente um menor primeiro\n- **\"Formato de repositório inválido\"**: Certifique-se de que está usando um formato de URL GitHub, GitLab ou Bitbucket válido\n- **\"Não foi possível buscar a estrutura do repositório\"**: Para repositórios privados, certifique-se de ter inserido um token de acesso pessoal válido com as permissões apropriadas\n- **\"Erro de renderização de diagrama\"**: O aplicativo tentará corrigir automaticamente diagramas quebrados\n\n### Soluções Comuns\n1. **Reinicie ambos os servidores**: Às vezes um simples reinicio resolve a maioria dos problemas\n2. **Verifique os logs do console**: Abra as ferramentas de desenvolvedor do navegador para ver quaisquer erros JavaScript\n3. **Verifique os logs da API**: Olhe o terminal onde a API está em execução para erros Python\n\n## 🤝 Contribuindo\n\nContribuições são bem-vindas! Sinta-se à vontade para:\n- Abrir issues para bugs ou solicitações de recursos\n- Enviar pull requests para melhorar o código\n- Compartilhar seu feedback e ideias\n\n## 📄 Licença\n\nEste projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.\n\n## ⭐ Histórico de Estrelas\n\n[![Gráfico de Histórico de Estrelas](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n"
  },
  {
    "path": "README.ru.md",
    "content": "# DeepWiki-Open\n\n![Баннер DeepWiki](screenshots/Deepwiki.png)\n\n**DeepWiki** — это моя собственная реализация DeepWiki, автоматически создающая красивые, интерактивные вики по любому репозиторию на GitHub, GitLab или BitBucket! Просто укажите название репозитория, и DeepWiki выполнит:\n\n1. Анализ структуры кода\n2. Генерацию полноценной документации\n3. Построение визуальных диаграмм, объясняющих работу системы\n4. Организацию всего в удобную и структурированную вики\n\n[![\"Купить мне кофе\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)  \n[![Поддержать в криптовалюте](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc)  \n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)  \n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Возможности\n\n- **Мгновенная документация**: Превращение любого репозитория в вики за считанные секунды\n- **Поддержка приватных репозиториев**: Безопасный доступ с помощью персональных токенов\n- **Умный анализ**: Понимание структуры и взаимосвязей в коде с помощью ИИ\n- **Красивые диаграммы**: Автоматическая генерация диаграмм Mermaid для отображения архитектуры и потоков данных\n- **Простая навигация**: Интуитивный интерфейс для изучения вики\n- **Функция “Спросить”**: Общение с репозиторием через ИИ, основанный на RAG, для получения точных ответов\n- **DeepResearch**: Многошаговое исследование для глубокого анализа сложных тем\n- **Поддержка различных провайдеров моделей**: Google Gemini, OpenAI, OpenRouter и локальные модели Ollama\n\n## 🚀 Быстрый старт (максимально просто!)\n\n### Вариант 1: С использованием Docker\n\n```bash\n# Клонируйте репозиторий\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Создайте файл .env с вашими API-ключами\necho \"GOOGLE_API_KEY=ваш_google_api_key\" > .env\necho \"OPENAI_API_KEY=ваш_openai_api_key\" >> .env\n# Необязательно: ключ OpenRouter\necho \"OPENROUTER_API_KEY=ваш_openrouter_api_key\" >> .env\n# Необязательно: указать хост Ollama, если он не локальный (по умолчанию http://localhost:11434)\necho \"OLLAMA_HOST=ваш_ollama_host\" >> .env\n# Необязательно: ключ и параметры Azure OpenAI\necho \"AZURE_OPENAI_API_KEY=ваш_azure_api_key\" >> .env\necho \"AZURE_OPENAI_ENDPOINT=ваш_azure_endpoint\" >> .env\necho \"AZURE_OPENAI_VERSION=ваша_azure_version\" >> .env\n# Запуск через Docker Compose\ndocker-compose up\n```\n\nПодробную инструкцию по работе с Ollama и Docker см. в [Ollama Instructions](Ollama-instruction.md).\n\n> 💡 **Где взять ключи API:**\n> - [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - [OpenAI Platform](https://platform.openai.com/api-keys)\n> - [Azure Portal](https://portal.azure.com/)\n\n### Вариант 2: Ручная установка (рекомендуется)\n\n#### Шаг 1: Установка ключей API\n\nСоздайте файл `.env` в корне проекта со следующим содержанием:\n\n```\nGOOGLE_API_KEY=ваш_google_api_key\nOPENAI_API_KEY=ваш_openai_api_key\n# Необязательно: для OpenRouter\nOPENROUTER_API_KEY=ваш_openrouter_api_key\n# Необязательно: для Azure OpenAI\nAZURE_OPENAI_API_KEY=ваш_azure_api_key\nAZURE_OPENAI_ENDPOINT=ваш_azure_endpoint\nAZURE_OPENAI_VERSION=ваша_azure_version\n# Необязательно: если Ollama не локальная\nOLLAMA_HOST=ваш_ollama_host\n```\n\n#### Шаг 2: Запуск backend-сервера\n\n```bash\n# Установка зависимостей\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Запуск API\npython -m api.main\n```\n\n#### Шаг 3: Запуск frontend-интерфейса\n\n```bash\n# Установка JS-зависимостей\nnpm install\n# или\nyarn install\n\n# Запуск веб-интерфейса\nnpm run dev\n# или\nyarn dev\n```\n\n#### Шаг 4: Используйте DeepWiki!\n\n1. Откройте [http://localhost:3000](http://localhost:3000) в браузере\n2. Введите URL репозитория (например, `https://github.com/openai/codex`)\n3. Для приватных репозиториев нажмите “+ Add access tokens” и введите токен\n4. Нажмите “Generate Wiki” и наблюдайте за магией!\n\n## 🔍 Как это работает\n\nDeepWiki использует искусственный интеллект, чтобы:\n\n1. Клонировать и проанализировать репозиторий GitHub, GitLab или Bitbucket (включая приватные — с использованием токенов)\n2. Создать эмбеддинги кода для интеллектуального поиска\n3. Сгенерировать документацию с учетом контекста (с помощью Google Gemini, OpenAI, OpenRouter, Azure OpenAI или локальных моделей Ollama)\n4. Построить визуальные диаграммы для отображения связей в коде\n5. Организовать всё в структурированную вики\n6. Включить интеллектуальное взаимодействие через функцию \"Спросить\"\n7. Обеспечить углубленный анализ через DeepResearch\n\n```mermaid\ngraph TD\n    A[Пользователь вводит ссылку на репозиторий] --> AA{Приватный репозиторий?}\n    AA -->|Да| AB[Добавить токен доступа]\n    AA -->|Нет| B[Клонировать репозиторий]\n    AB --> B\n    B --> C[Анализ структуры кода]\n    C --> D[Создание эмбеддингов]\n\n    D --> M{Выбор провайдера модели}\n    M -->|Google Gemini| E1[Генерация через Gemini]\n    M -->|OpenAI| E2[Генерация через OpenAI]\n    M -->|OpenRouter| E3[Генерация через OpenRouter]\n    M -->|Локальная Ollama| E4[Генерация через Ollama]\n    M -->|Azure| E5[Генерация через Azure]\n\n    E1 --> E[Создание документации]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n    E5 --> E\n\n    D --> F[Создание диаграмм]\n    E --> G[Формирование вики]\n    F --> G\n    G --> H[Интерактивная DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4,E5 process;\n    class H result;\n```\n\n## 🛠️ Структура проекта\n\n```\ndeepwiki/\n├── api/                  # Backend API сервер\n│   ├── main.py           # Точка входа API\n│   ├── api.py            # Реализация через FastAPI\n│   ├── rag.py            # RAG: генерация с дополнением\n│   ├── data_pipeline.py  # Утилиты обработки данных\n│   └── requirements.txt  # Зависимости Python\n│\n├── src/                  # Клиентское приложение на Next.js\n│   ├── app/              # Каталог приложения Next.js\n│   │   └── page.tsx      # Главная страница приложения\n│   └── components/       # React-компоненты\n│       └── Mermaid.tsx   # Рендеринг диаграмм Mermaid\n│\n├── public/               # Статические ресурсы\n├── package.json          # JS-зависимости\n└── .env                  # Переменные окружения\n```\n\n## 🤖 Система выбора моделей по провайдерам\n\nDeepWiki поддерживает гибкую систему выбора моделей от разных поставщиков:\n\n### Поддерживаемые провайдеры и модели\n\n- **Google**: По умолчанию `gemini-2.5-flash`, также доступны `gemini-2.5-flash-lite`, `gemini-2.5-pro`, и др.\n- **OpenAI**: По умолчанию `gpt-5-nano`, также поддерживает `gpt-5`, `4o` и другие\n- **OpenRouter**: Доступ к множеству моделей через единый API (Claude, Llama, Mistral и др.)\n- **Azure OpenAI**: По умолчанию `gpt-4o`, поддерживаются и другие\n- **Ollama**: Локальные open-source модели, например `llama3`\n\n### Переменные окружения\n\nКаждому провайдеру соответствуют свои ключи:\n\n```bash\nGOOGLE_API_KEY=...         # Для моделей Google Gemini\nOPENAI_API_KEY=...         # Для моделей OpenAI\nOPENROUTER_API_KEY=...     # Для моделей OpenRouter\nAZURE_OPENAI_API_KEY=...   # Для моделей Azure\nAZURE_OPENAI_ENDPOINT=...\nAZURE_OPENAI_VERSION=...\n\n# Кастомный адрес для OpenAI API\nOPENAI_BASE_URL=https://ваш-кастомный-api/v1\n\n# Хост Ollama\nOLLAMA_HOST=http://localhost:11434\n\n# Каталог конфигурации\nDEEPWIKI_CONFIG_DIR=/путь/к/конфигурации\n```\n\n### Конфигурационные файлы\n\nDeepWiki использует JSON-файлы для настройки:\n\n1. **`generator.json`** — конфигурация генерации текста и моделей\n2. **`embedder.json`** — настройки эмбеддингов и ретривера\n3. **`repo.json`** — правила обработки репозиториев\n\nПо умолчанию хранятся в `api/config/`, путь можно изменить через `DEEPWIKI_CONFIG_DIR`.\n\n### Кастомизация для сервис-провайдеров\n\nФункция выбора модели позволяет:\n\n- Предоставлять выбор моделей пользователям вашей системы\n- Легко адаптироваться к новым LLM без изменения кода\n- Поддерживать кастомные или специализированные модели\n\nПользователи могут выбрать модель через интерфейс или указать свой идентификатор.\n\n### Настройка OpenAI base_url для корпоративных клиентов\n\nПозволяет:\n\n- Использовать приватные API OpenAI\n- Подключаться к self-hosted решениям\n- Интегрироваться с совместимыми сторонними сервисами\n\n**Скоро**: DeepWiki получит режим, в котором пользователи будут указывать свои API-ключи напрямую в запросах — удобно для корпоративных решений.\n\n## 🧩 Использование совместимых с OpenAI моделей (например, Alibaba Qwen)\n\nЧтобы использовать модели эмбеддингов, совместимые с OpenAI:\n\n1. Замените `api/config/embedder.json` на `embedder_openai_compatible.json`\n2. В `.env` добавьте:\n```bash\nOPENAI_API_KEY=ваш_ключ\nOPENAI_BASE_URL=совместимый_endpoint\n```\n\nПрограмма автоматически подставит значения из переменных окружения.\n\n### Логирование\n\nDeepWiki использует стандартный `logging` из Python. Настраивается через:\n\n| Переменная        | Описание                                      | Значение по умолчанию         |\n|------------------|-----------------------------------------------|-------------------------------|\n| `LOG_LEVEL`      | Уровень логов (DEBUG, INFO, WARNING и т.д.)   | INFO                          |\n| `LOG_FILE_PATH`  | Путь к файлу логов                             | `api/logs/application.log`    |\n\nПример:\n```bash\nexport LOG_LEVEL=DEBUG\nexport LOG_FILE_PATH=./debug.log\npython -m api.main\n```\n\nИли через Docker Compose:\n```bash\nLOG_LEVEL=DEBUG LOG_FILE_PATH=./debug.log docker-compose up\n```\n\nДля постоянства логов при перезапуске контейнера `api/logs` монтируется в `./api/logs`.\n\nТакже можно указать переменные в `.env`:\n\n```bash\nLOG_LEVEL=DEBUG\nLOG_FILE_PATH=./debug.log\n```\n\nИ просто выполнить:\n\n```bash\ndocker-compose up\n```\n\n**Безопасность логов:** в продакшене важно настроить права доступа к `api/logs`, чтобы исключить несанкционированный доступ или запись.\n\n## 🛠️ Расширенная настройка\n\n### Переменные окружения\n\n| Переменная               | Назначение                                                             | Обязательно | Примечание                                                                                     |\n|--------------------------|------------------------------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------|\n| `GOOGLE_API_KEY`         | Ключ API для Google Gemini                                             | Нет         | Только если используете модели от Google                                                      |\n| `OPENAI_API_KEY`         | Ключ API для OpenAI (нужен даже для эмбеддингов)                       | Да          | Обязателен для генерации эмбеддингов                                                          |\n| `OPENROUTER_API_KEY`     | Ключ API для OpenRouter                                                | Нет         | Только если используете модели OpenRouter                                                     |\n| `AZURE_OPENAI_API_KEY`   | Ключ Azure OpenAI                                                      | Нет         | Только если используете Azure                                                                 |\n| `AZURE_OPENAI_ENDPOINT`  | Endpoint Azure                                                         | Нет         | Только если используете Azure                                                                 |\n| `AZURE_OPENAI_VERSION`   | Версия API Azure                                                       | Нет         | Только если используете Azure                                                                 |\n| `OLLAMA_HOST`            | Хост Ollama (по умолчанию http://localhost:11434)                      | Нет         | Указывается при использовании внешнего сервера Ollama                                         |\n| `PORT`                   | Порт API-сервера (по умолчанию 8001)                                   | Нет         | Меняйте, если frontend и backend работают на одной машине                                     |\n| `SERVER_BASE_URL`        | Базовый URL для API (по умолчанию http://localhost:8001)               | Нет         |                                                                                               |\n| `DEEPWIKI_AUTH_MODE`     | Включает режим авторизации (true или 1)                                | Нет         | Если включён, потребуется код из `DEEPWIKI_AUTH_CODE`                                         |\n| `DEEPWIKI_AUTH_CODE`     | Секретный код для запуска генерации                                    | Нет         | Только если включён режим авторизации                                                         |\n\nЕсли не используете Ollama, обязательно настройте OpenAI API ключ.\n\n## Режим авторизации\n\nDeepWiki может быть запущен в режиме авторизации — для генерации вики потребуется ввести секретный код. Это полезно, если вы хотите ограничить доступ к функциональности.\n\nДля включения:\n\n- `DEEPWIKI_AUTH_MODE=true`\n- `DEEPWIKI_AUTH_CODE=секретный_код`\n\nЭто ограничивает доступ с фронтенда и защищает кэш, но не блокирует прямые вызовы API.\n\n### Запуск через Docker\n\nВы можете использовать Docker:\n\n#### Запуск контейнера\n\n```bash\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=... \\\n  -e OPENAI_API_KEY=... \\\n  -e OPENROUTER_API_KEY=... \\\n  -e OLLAMA_HOST=... \\\n  -e AZURE_OPENAI_API_KEY=... \\\n  -e AZURE_OPENAI_ENDPOINT=... \\\n  -e AZURE_OPENAI_VERSION=... \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nКаталог `~/.adalflow` содержит:\n\n- Клонированные репозитории\n- Эмбеддинги и индексы\n- Сгенерированные кэшированные вики\n\n#### Docker Compose\n\n```bash\n# Убедитесь, что .env заполнен\ndocker-compose up\n```\n\n#### Использование .env\n\n```bash\necho \"GOOGLE_API_KEY=...\" > .env\n...\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n#### Локальная сборка Docker-образа\n\n```bash\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\ndocker build -t deepwiki-open .\n\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=... \\\n  -e OPENAI_API_KEY=... \\\n  ... \\\n  deepwiki-open\n```\n\n#### Самоподписанные сертификаты\n\n1. Создайте каталог `certs` (или свой)\n2. Поместите сертификаты `.crt` или `.pem`\n3. Соберите образ:\n\n```bash\ndocker build --build-arg CUSTOM_CERT_DIR=certs .\n```\n\n### Описание API\n\nСервер API:\n\n- Клонирует и индексирует репозитории\n- Реализует RAG\n- Поддерживает потоковую генерацию\n\nСм. подробности в [API README](./api/README.md)\n\n## 🔌 Интеграция с OpenRouter\n\nПлатформа [OpenRouter](https://openrouter.ai/) предоставляет доступ ко множеству моделей:\n\n- **Много моделей**: OpenAI, Anthropic, Google, Meta и др.\n- **Простая настройка**: достаточно API-ключа\n- **Гибкость и экономия**: выбирайте модели по цене и производительности\n- **Быстрое переключение**: без изменения кода\n\n### Как использовать\n\n1. Получите ключ на [OpenRouter](https://openrouter.ai/)\n2. Добавьте `OPENROUTER_API_KEY=...` в `.env`\n3. Активируйте в интерфейсе\n4. Выберите модель (например GPT-4o, Claude 3.5, Gemini 2.0 и др.)\n\nПодходит для:\n\n- Тестирования разных моделей без регистрации в каждом сервисе\n- Доступа к моделям в регионах с ограничениями\n- Сравнения производительности\n- Оптимизации затрат\n\n## 🤖 Возможности Ask и DeepResearch\n\n### Ask\n\n- **Ответы по коду**: AI использует содержимое репозитория\n- **RAG**: подбираются релевантные фрагменты\n- **Потоковая генерация**: ответы формируются в реальном времени\n- **История общения**: поддерживается контекст\n\n### DeepResearch\n\nФункция глубокого анализа:\n\n- **Многошаговый подход**: AI сам исследует тему\n- **Этапы исследования**:\n  1. План\n  2. Промежуточные результаты\n  3. Итоговый вывод\n\nАктивируется переключателем \"Deep Research\".\n\n## 📱 Скриншоты\n\n![Интерфейс](screenshots/Interface.png)  \n*Основной интерфейс DeepWiki*\n\n![Приватный доступ](screenshots/privaterepo.png)  \n*Доступ к приватным репозиториям*\n\n![DeepResearch](screenshots/DeepResearch.png)  \n*DeepResearch анализирует сложные темы*\n\n### Видео-демо\n\n[![Видео](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n## ❓ Решение проблем\n\n### Проблемы с API-ключами\n\n- **“Отсутствуют переменные окружения”** — проверьте `.env`\n- **“Неверный ключ”** — уберите пробелы\n- **“Ошибка OpenRouter API”** — проверьте ключ и баланс\n- **“Ошибка Azure API”** — проверьте ключ, endpoint и версию\n\n### Проблемы с подключением\n\n- **“Нет подключения к API”** — убедитесь, что сервер запущен на 8001\n- **“CORS ошибка”** — пробуйте запускать frontend и backend на одной машине\n\n### Ошибки генерации\n\n- **“Ошибка генерации вики”** — попробуйте меньший репозиторий\n- **“Неверный формат ссылки”** — используйте корректные ссылки\n- **“Нет структуры репозитория”** — проверьте токен доступа\n- **“Ошибка диаграмм”** — система попытается автоматически исправить\n\n### Универсальные советы\n\n1. Перезапустите frontend и backend\n2. Проверьте консоль браузера\n3. Проверьте логи API\n\n## 🤝 Участие\n\nВы можете:\n\n- Заводить issues\n- Отправлять pull requests\n- Делиться идеями\n\n## 📄 Лицензия\n\nПроект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE)\n\n## ⭐ История звёзд\n\n[![График звёзд](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n"
  },
  {
    "path": "README.vi.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki Banner](screenshots/Deepwiki.png)\n\n**Open DeepWiki** là 1 triển khai thay thế cho DeepWiki, tự động tạo ra các trang wiki cho bất kỳ Repository  nào trên GitHub, GitLab hoặc BitBucket! Chỉ cần nhập đường dẫn Repository, và DeepWiki sẽ:\n\n1. Phân tích cấu trúc mã nguồn\n2. Tạo tài liệu đầy đủ và chi tiết\n3. Tạo sơ đồ trực quan để giải thích cách mọi thứ hoạt động\n4. Sắp xếp tất cả documents thành một wiki dễ hiểu\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ Tính năng\n\n- **Tạo Tài liệu tức thì**: Biến bất kỳ Repository GitHub, GitLab hoặc BitBucket nào thành wiki chỉ trong vài giây\n- **Hỗ trợ Private Repository**: Truy cập Private Repository một cách an toàn với personal access tokens\n- **Phân tích thông minh**: Hiểu cấu trúc và mối quan hệ của source codes nhờ AI\n- **Tự động tạo Sơ đồ**: Tự động tạo sơ đồ Mermaid để trực quan hóa kiến trúc và luồng dữ liệu\n- **Dễ dàng thao tác**:Giao diện wiki đơn giản, trực quan để khám phá\n- **Trò chuyện với repository**: Trò chuyện với repo của bạn bằng AI (tích hợp RAG) để nhận câu trả lời chính xác\n- **DeepResearch**:Quy trình Deep Research nhiều bước giúp phân tích kỹ lưỡng các chủ đề phức tạp\n- **Hỗ trợ nhiều mô hình**: Hỗ trợ Google Gemini, OpenAI, OpenRouter, và  local Ollama models\n\n## 🚀 Bắt đầu (Siêu dễ :))\n\n### Option 1: Sử dụng Docker\n\n```bash\n# Clone repository\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Tạo .env file với API keys\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# Optional: Thêm OpenRouter API key nếu bạn muốn OpenRouter models\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# Run với Docker Compose\ndocker-compose up\n```\n\n> 💡 **Hướng dẫn lấy Keys**\n> - Lấy Google API key từ [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Lấy OpenAI API key từ [OpenAI Platform](https://platform.openai.com/api-keys)\n\n### Option 2: Setup thủ công (Khuyên dùng)\n\n#### Bước 1: Set Up API Keys\n\nTạo  `.env` file trong thư mục gốc của project với những keys vừa tạo:\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# Optional: Thêm OpenRouter API key nếu bạn muốn OpenRouter models\nOPENROUTER_API_KEY=your_openrouter_api_key\n```\n\n#### Bước 2: Bắt đầu với Backend\n\n```bash\n# Cài đặt Python dependencies\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# Chạy API server\npython -m api.main\n```\n\n#### Bước 3: Bắt đầu với Frontend\n\n```bash\n# Cài đặt JavaScript dependencies\nnpm install\n# Hoặc\nyarn install\n\n# Chạy the web app\nnpm run dev\n# Hoặc\nyarn dev\n```\n\n#### Bước 4: Dùng DeepWiki!\n\n1. Mở [http://localhost:3000](http://localhost:3000) trên trình duyệt\n2. Nhập đường dẫn GitHub, GitLab, hoặt Bitbucket repository (ví dụ như `https://github.com/openai/codex`, `https://github.com/microsoft/autogen`, `https://gitlab.com/gitlab-org/gitlab`, hay `https://bitbucket.org/redradish/atlassian_app_versions`)\n3. Cho private repositories, Nhấn \"+ Add access tokens\" và nhập your GitHub hoặt GitLab personal access token\n4. Click \"Generate Wiki\" và xem kết quả!\n\n## 🔍 Cách Open Deepwiki hoạt động\n\nDeepWiki dùng AI để:\n\n1. Clone và phân tích GitHub, GitLab, hoặc Bitbucket repository (bao gồm private repos với token authentication)\n2. Tạo embeddings cho code (Rag support)\n3. Tạo documentation với context-aware AI (dùng Google Gemini, OpenAI, OpenRouter, hay local Ollama models)\n4. Tạo diagrams để giải thích code relationships\n5. Organize thông tin thành 1 trang wiki\n6. Cho phép Q&A với repository\n7. Cung cấp khả năng DeepResearch\n\n```mermaid\ngraph TD\n    A[User inputs GitHub/GitLab/Bitbucket repo] --> AA{Private repo?}\n    AA -->|Yes| AB[Add access token]\n    AA -->|No| B[Clone Repository]\n    AB --> B\n    B --> C[Analyze Code Structure]\n    C --> D[Create Code Embeddings]\n\n    D --> M{Select Model Provider}\n    M -->|Google Gemini| E1[Generate with Gemini]\n    M -->|OpenAI| E2[Generate with OpenAI]\n    M -->|OpenRouter| E3[Generate with OpenRouter]\n    M -->|Local Ollama| E4[Generate with Ollama]\n\n    E1 --> E[Generate Documentation]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[Create Visual Diagrams]\n    E --> G[Organize as Wiki]\n    F --> G\n    G --> H[Interactive DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ Cấu trúc dự án\n\n```\ndeepwiki/\n├── api/                  # Backend API server\n│   ├── main.py           # API\n│   ├── api.py            # FastAPI\n│   ├── rag.py            # Retrieval Augmented Generation (RAG)\n│   ├── data_pipeline.py  # Data processing utilities\n│   └── requirements.txt  # Python dependencies\n│\n├── src/                  # Frontend Next.js app\n│   ├── app/              # Next.js app directory\n│   │   └── page.tsx      # Main application page\n│   └── components/       # React components\n│       └── Mermaid.tsx   # Mermaid diagram renderer\n│\n├── public/               # Static assets\n├── package.json          # JavaScript dependencies\n└── .env                  # Environment variables (create this)\n```\n\n## 🛠️ Cài đặt nâng cao\n\n### Biến môi trường\n\n| Biến môi trường | Mô tả | bắt buộc | ghi chú |\n|----------|-------------|----------|------|\n| `GOOGLE_API_KEY` | Google Gemini API key  | Có |\n| `OPENAI_API_KEY` | OpenAI API key   | có |\n| `OPENROUTER_API_KEY` | OpenRouter API key   | không| Yêu cầu nếu bạn muốn dùng OpenRouter models |\n| `PORT` | Port của API server (mặc định: 8001) | không | Nếu bạn muốn chạy API và frontend trên cùng 1 máy, hãy điều chỉnh Port `SERVER_BASE_URL` |\n| `SERVER_BASE_URL` | Đường dẫnn mặt định của API server (mặc định: http://localhost:8001) | không |\n\n### Cài Đặt với Docker\n\nBạn có thể dùng Docker để run DeepWiki:\n\n```bash\n# Pull Docker image từ GitHub Container Registry\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# Chạy container với biến môi trường\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\nHoặc đơn giản hơn, sử dụng `docker-compose.yml` :\n\n```bash\n# Edit the .env file with your API keys first\ndocker-compose up\n```\n\n#### Sử dụng  .env file với Docker\n\nBạn có thể \"mount\"  .env file vào container:\n\n```bash\n# Tạo .env file với your API keys\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# Run container với .env file\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n#### Bạn có thể Building the Docker image trên máy cục bộ\n\n\n```bash\n# Clone repository\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# Build Docker image\ndocker build -t deepwiki-open .\n\n# Chạy container\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  deepwiki-open\n```\n\n### Chi tiết API Server\n\nAPI server cung cấp:\n- Repository cloning và indexing\n- RAG (Retrieval Augmented Generation)\n- Trò chuyện liên tục\n\nBiết thêm chi tiết truy cập [ API README](./api/README.md).\n\n## 🤖 Hệ thống lựa chọn mô hình dựa trên nhà cung cấp\n\nDeepWiki hiện đã triển khai một hệ thống lựa chọn mô hình linh hoạt dựa trên nhiều nhà cung cấp LLM:\n\n### Các nhà cung cấp và mô hình được hỗ trợ\n\n- **Google**: Mặc định là `gemini-2.5-flash`, cũng hỗ trợ `gemini-2.5-flash-lite`, `gemini-2.5-pro`, v.v.\n- **OpenAI**: Mặc định là `gpt-5-nano`, cũng hỗ trợ `gpt-5`, `4o`, v.v.\n- **OpenRouter**: Truy cập nhiều mô hình qua một API thống nhất, bao gồm Claude, Llama, Mistral, v.v.\n- **Ollama**: Hỗ trợ các mô hình mã nguồn mở chạy cục bộ như `llama3`\n\n### Biến môi trường\n\nMỗi nhà cung cấp yêu cầu các biến môi trường API key tương ứng:\n\n```\n# API Keys\nGOOGLE_API_KEY=google_api_key_của_bạn        # Bắt buộc cho các mô hình Google Gemini\nOPENAI_API_KEY=openai_key_của_bạn            # Bắt buộc cho các mô hình OpenAI\nOPENROUTER_API_KEY=openrouter_key_của_bạn    # Bắt buộc cho các mô hình OpenRouter\n\n# Cấu hình URL cơ sở cho OpenAI API\nOPENAI_BASE_URL=https://endpoint-tùy-chỉnh.com/v1  # Tùy chọn, cho các điểm cuối API OpenAI tùy chỉnh\n\n# Thư mục cấu hình\nDEEPWIKI_CONFIG_DIR=/đường/dẫn/đến/thư_mục/cấu_hình  # Tùy chọn, cho vị trí tệp cấu hình tùy chỉnh\n```\n\n### Tệp cấu hình\n\nDeepWiki sử dụng các tệp cấu hình JSON để quản lý các khía cạnh khác nhau của hệ thống:\n\n1. **`generator.json`**: Cấu hình cho các mô hình tạo văn bản\n   - Xác định các nhà cung cấp mô hình có sẵn (Google, OpenAI, OpenRouter, Ollama)\n   - Chỉ định các mô hình mặc định và có sẵn cho mỗi nhà cung cấp\n   - Chứa các tham số đặc thù cho mô hình như temperature và top_p\n\n2. **`embedder.json`**: Cấu hình cho mô hình embedding và xử lý văn bản\n   - Xác định mô hình embedding cho lưu trữ vector\n   - Chứa cấu hình bộ truy xuất cho RAG\n   - Chỉ định cài đặt trình chia văn bản để phân đoạn tài liệu\n\n3. **`repo.json`**: Cấu hình xử lý repository\n   - Chứa bộ lọc tệp để loại trừ một số tệp và thư mục nhất định\n   - Xác định giới hạn kích thước repository và quy tắc xử lý\n\nMặc định, các tệp này nằm trong thư mục `api/config/`. Bạn có thể tùy chỉnh vị trí của chúng bằng biến môi trường `DEEPWIKI_CONFIG_DIR`.\n\n### Lựa chọn mô hình tùy chỉnh cho nhà cung cấp dịch vụ\n\nTính năng lựa chọn mô hình tùy chỉnh được thiết kế đặc biệt cho các nhà cung cấp dịch vụ cần:\n\n- Bạn có thể cung cấp cho người dùng trong tổ chức của mình nhiều lựa chọn mô hình AI khác nhau\n- Bạn có thể thích ứng nhanh chóng với môi trường LLM đang phát triển nhanh chóng mà không cần thay đổi mã\n- Bạn có thể hỗ trợ các mô hình chuyên biệt hoặc được tinh chỉnh không có trong danh sách định nghĩa trước\n\nBạn có thể triển khai các mô hình cung cấp bằng cách chọn từ các tùy chọn định nghĩa trước hoặc nhập định danh mô hình tùy chỉnh trong giao diện người dùng.\n\n### Cấu hình URL cơ sở cho các kênh riêng doanh nghiệp\n\nCấu hình base_url của OpenAI Client được thiết kế chủ yếu cho người dùng doanh nghiệp có các kênh API riêng. Tính năng này:\n\n- Cho phép kết nối với các điểm cuối API riêng hoặc dành riêng cho doanh nghiệp\n- Cho phép các tổ chức sử dụng dịch vụ LLM tự lưu trữ hoặc triển khai tùy chỉnh\n- Hỗ trợ tích hợp với các dịch vụ tương thích API OpenAI của bên thứ ba\n\n**Sắp ra mắt**: Trong các bản cập nhật tương lai, DeepWiki sẽ hỗ trợ chế độ mà người dùng cần cung cấp API key của riêng họ trong các yêu cầu. Điều này sẽ cho phép khách hàng doanh nghiệp có kênh riêng sử dụng cấu hình API hiện có mà không cần chia sẻ thông tin đăng nhập với triển khai DeepWiki.\n\n## 🔌 Tích hợp OpenRouter\n\nDeepWiki hiện đã hỗ trợ [OpenRouter](https://openrouter.ai/) làm nhà cung cấp mô hình, cho phép bạn truy cập hàng trăm mô hình AI thông qua một API duy nhất:\n\n- **Nhiều tùy chọn mô hình**: Truy cập các mô hình từ OpenAI, Anthropic, Google, Meta, Mistral và nhiều nhà cung cấp khác\n- **Cấu hình đơn giản**: Chỉ cần thêm khóa API của bạn từ OpenRouter và chọn mô hình bạn muốn sử dụng\n- **Tiết kiệm chi phí**: Lựa chọn mô hình phù hợp với ngân sách và nhu cầu hiệu suất của bạn\n- **Chuyển đổi dễ dàng**: Chuyển đổi giữa các mô hình khác nhau mà không cần thay đổi mã nguồn\n\n\n### Cách sử dụng OpenRouter với DeepWiki\n\n1. **Lấy API Key**: Đăng ký tại [OpenRouter](https://openrouter.ai/) và lấy khóa API\n2. **Thêm vào biến môi trường**: Thêm `OPENROUTER_API_KEY=your_key` vào file `.env`\n3. **Bật trong giao diện**: Chọn \"Use OpenRouter API\" trên trang chủ\n4. **Chọn mô hình**: Lựa chọn từ các mô hình phổ biến như GPT-4o, Claude 3.5 Sonnet, Gemini 2.0 và nhiều hơn nữa\n\n\nOpenRouter đặc biệt hữu ích nếu bạn muốn:\n\n- Thử nhiều mô hình khác nhau mà không cần đăng ký nhiều dịch vụ\n- Truy cập các mô hình có thể bị giới hạn tại khu vực của bạn\n- So sánh hiệu năng giữa các nhà cung cấp mô hình khác nhau\n- Tối ưu hóa chi phí so với hiệu suất dựa trên nhu cầu của bạn\n\n\n## 🤖 Tính năng Hỏi & Nghiên cứu Sâu (DeepResearch)\n\n### Tính năng Hỏi (Ask)\n\nTính năng Hỏi cho phép bạn trò chuyện với kho mã của mình bằng cách sử dụng kỹ thuật RAG (Retrieval Augmented Generation):\n\n- **Phản hồi theo ngữ cảnh**: Nhận câu trả lời chính xác dựa trên mã thực tế trong kho của bạn\n- **Ứng dụng RAG**: Hệ thống truy xuất các đoạn mã liên quan để tạo ra câu trả lời có cơ sở\n- **Phản hồi theo thởi gian thực**: Xem câu trả lời được tạo ra trực tiếp, mang lại trải nghiệm tương tác hơn\n- **Lưu lịch sử cuộc trò chuyện**: Hệ thống duy trì ngữ cảnh giữa các câu hỏi để cuộc đối thoại liền mạch hơn\n\n\n### Tính năng DeepResearch\n\nDeepResearch nâng tầm phân tích kho mã với quy trình nghiện cứu nhiểu vòng:\n\n- **Ngieen cứu chuyên sâu**: Khám phá kỹ lưỡng các chủ đề phức tạp thông qua nhiểu vòng nghiện cứu\n- **Quy trình có cấu trúc**: Tuân theo kế hoạch nghiện cứu rõ ràng với các bản cập nhật và kết luận tổng thể\n- **Tự động tiếp tục**: AI sẽ tự động tiếp tục quá trình nghiện cứu cho đến khi đưa ra kết luận (tối đa 5 vòng)\n- **Các giai đoạn nghiện cứu**:\n  1. **Kế hoạch nghiện cứu**: Phác thảo phương pháp và những phát hiện ban đầu\n  2. **Cập nhật nghiện cứu**: Bổ sung kiến thức mới qua từng vòng lặp\n  3. **Kết luận cuối cùng**: Đưa ra câu trả lời toàn diện dựa trên tất cả các vòng nghiện cứu\n\nĐể sử dụng DeepResearch, chỉ cần bật công tắc \"Deep Research\" trong giao diện Hỏi (Ask) trước khi gửi câu hỏi của bạn.\n\n\n## 📱 Ảnh chụp màng hình\n\n![Giao diện chính của DeepWiki](screenshots/Interface.png)\n*Giao diện chính của DeepWiki*\n\n![Hỗ trợ kho riêng tư](screenshots/privaterepo.png)\n*Truy cập kho riêng tư bằng Personal Access Token*\n\n![Tính năng DeepResearch](screenshots/DeepResearch.png)\n*DeepResearch thực hiện nghiện cứu nhiểu vòng cho các chủ đề phức tạp*\n\n### Demo Video\n\n[![DeepWiki Demo Video](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n\n\n## ❓ Khắc phục sự cố\n\n### Vấn đề với API Key\n- **\"Thiếu biến môi trường\"**: Đảm bảo rằng file `.env` của bạn nằm ở thư mục gốc của dự án và chứa các API key cần thiết\n- **\"API key không hợp lệ\"**: Kiểm tra lại xem bạn đã sao chép đầy đủ API key mà không có khoảng trắng thừa chưa\n- **\"Lỗi API OpenRouter\"**: Xác minh rằng API key của OpenRouter là hợp lệ và có đủ tín dụng\n\n### Vấn đề kết nối\n- **\"Không thể kết nối với máy chủ API\"**: Đảm bảo máy chủ API đang chạy trên cổng 8001\n- **\"Lỗi CORS\"**: API được cấu hình để cho phép tất cả các nguồn gốc, nhưng nếu gặp sự cố, thử chạy cả frontend và backend trên cùng một máy tính\n\n### Vấn đề khi tạo nội dung\n- **\"Lỗi khi tạo wiki\"**: Với các kho mã rất lớn, hãy thử trước với kho mã nhỏ hơn\n- **\"Định dạng kho mã không hợp lệ\"**: Đảm bảo bạn đang sử dụng định dạng URL hợp lệ cho GitHub, GitLab hoặc Bitbucket\n- **\"Không thể lấy cấu trúc kho mã\"**: Với các kho mã riêng tư, hãy đảm bảo bạn đã nhập token truy cập cá nhân hợp lệ và có quyền truy cập phù hợp\n- **\"Lỗi khi render sơ đồ\"**: Ứng dụng sẽ tự động thử khắc phục các sơ đồ bị lỗi\n\n### Các giải pháp phổ biến\n1. **Khởi động lại cả hai máy chủ**: Đôi khi, một lần khởi động lại đơn giản có thể giải quyết hầu hết các vấn đề\n2. **Kiểm tra nhật ký trình duyệt**: Mở công cụ phát triển của trình duyệt để xem các lỗi JavaScript\n3. **Kiểm tra nhật ký API**: Xem các lỗi Python trong terminal nơi API đang chạy\n\n\n## 🤝 Đóng góp\n\nChúng tôi hoan nghênh mọi đóng góp! Bạn có thể:\n- Mở các vấn đề (issues) để báo lỗi hoặc yêu cầu tính năng\n- Gửi pull request để cải thiện mã nguồn\n- Chia sẻ phản hồi và ý tưởng của bạn\n\n## 📄 Giấy phép\n\nDự án này được cấp phép theo Giấy phép MIT - xem file [LICENSE](LICENSE) để biết chi tiết.\n\n## ⭐ Lịch sử\n\n[![Biểu đồ lịch sử](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n"
  },
  {
    "path": "README.zh-tw.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki 橫幅](screenshots/Deepwiki.png)\n\n**DeepWiki** 可以為任何 GitHub、GitLab 或 BitBucket 程式碼儲存庫自動建立美觀、互動式的 Wiki！只需輸入儲存庫名稱，DeepWiki 將：\n\n1. 分析程式碼結構\n2. 產生全面的文件\n3. 建立視覺化圖表解釋一切如何運作\n4. 將所有內容整理成易於導覽的 Wiki\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n[![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc)\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ 特點\n\n- **即時文件**：幾秒鐘內將任何 GitHub、GitLab 或 BitBucket 儲存庫轉換為 Wiki\n- **私人儲存庫支援**：使用個人存取權杖安全存取私人儲存庫\n- **智慧分析**：AI 驅動的程式碼結構和關係理解\n- **精美圖表**：自動產生 Mermaid 圖表視覺化架構和資料流\n- **簡易導覽**：簡單、直觀的介面探索 Wiki\n- **提問功能**：使用 RAG 驅動的 AI 與您的儲存庫聊天，取得準確答案\n- **深度研究**：多輪研究過程，徹底調查複雜主題\n- **多模型提供商**：支援 Google Gemini、OpenAI、OpenRouter 和本機 Ollama 模型\n\n## 🚀 快速開始（超級簡單！）\n\n### 選項 1：使用 Docker\n\n```bash\n# 複製儲存庫\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# 建立包含 API 金鑰的 .env 檔案\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# 可選：如果您想使用 OpenRouter 模型，新增 OpenRouter API 金鑰\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n# 可選：如果 Ollama 不在本機執行，新增 Ollama 主機位址，預設為 http://localhost:11434\necho \"OLLAMA_HOST=your_ollama_host\" >> .env\n\n# 使用 Docker Compose 執行\ndocker-compose up\n```\n\n有關使用 DeepWiki 搭配 Ollama 和 Docker 的詳細說明，請參閱 [Ollama 操作說明](Ollama-instruction.md)。\n\n(上述 Docker 命令以及 `docker-compose.yml` 設定會掛載您主機上的 `~/.adalflow` 目錄到容器內的 `/root/.adalflow`。此路徑用於儲存：\n- 複製的儲存庫 (`~/.adalflow/repos/`)\n- 儲存庫的嵌入和索引 (`~/.adalflow/databases/`)\n- 快取的已產生 Wiki 內容 (`~/.adalflow/wikicache/`)\n\n這確保了即使容器停止或移除，您的資料也能持久保存。)\n\n> 💡 **取得這些金鑰的地方：**\n> - 從 [Google AI Studio](https://makersuite.google.com/app/apikey) 取得 Google API 金鑰\n> - 從 [OpenAI Platform](https://platform.openai.com/api-keys) 取得 OpenAI API 金鑰\n\n### 選項 2：手動設定（推薦）\n\n#### 步驟 1：設定 API 金鑰\n\n在專案根目錄建立一個 `.env` 檔案，包含以下金鑰：\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# 可選：如果您想使用 OpenRouter 模型，新增此項\nOPENROUTER_API_KEY=your_openrouter_api_key\n# 可選：如果 Ollama 不在本機執行，新增 Ollama 主機位址，預設為 http://localhost:11434\nOLLAMA_HOST=your_ollama_host\n```\n\n#### 步驟 2：啟動後端\n\n```bash\n# 安裝 Python 相依性\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# 啟動 API 伺服器\npython -m api.main\n```\n\n#### 步驟 3：啟動前端\n\n```bash\n# 安裝 JavaScript 相依性\nnpm install\n# 或\nyarn install\n\n# 啟動 Web 應用\nnpm run dev\n# 或\nyarn dev\n```\n\n#### 步驟 4：使用 DeepWiki！\n\n1. 在瀏覽器中開啟 [http://localhost:3000](http://localhost:3000)\n2. 輸入 GitHub、GitLab 或 Bitbucket 儲存庫（如 `https://github.com/openai/codex`、`https://github.com/microsoft/autogen`、`https://gitlab.com/gitlab-org/gitlab` 或 `https://bitbucket.org/redradish/atlassian_app_versions`）\n3. 對於私人儲存庫，點擊「+ 新增存取權杖」並輸入您的 GitHub 或 GitLab 個人存取權杖\n4. 點擊「產生 Wiki」，見證奇蹟的發生！\n\n## 🔍 工作原理\n\nDeepWiki 使用 AI 來：\n\n1. 複製並分析 GitHub、GitLab 或 Bitbucket 儲存庫（包括使用權杖驗證的私人儲存庫）\n2. 建立程式碼嵌入用於智慧檢索\n3. 使用上下文感知 AI 產生文件（使用 Google Gemini、OpenAI、OpenRouter 或本機 Ollama 模型）\n4. 建立視覺化圖表解釋程式碼關係\n5. 將所有內容組織成結構化 Wiki\n6. 透過提問功能實現與儲存庫的智慧問答\n7. 透過深度研究功能提供深入研究能力\n\n```mermaid\ngraph TD\n    A[使用者輸入 GitHub/GitLab/Bitbucket 儲存庫] --> AA{私人儲存庫?}\n    AA -->|是| AB[新增存取權杖]\n    AA -->|否| B[複製儲存庫]\n    AB --> B\n    B --> C[分析程式碼結構]\n    C --> D[建立程式碼嵌入]\n\n    D --> M{選擇模型提供商}\n    M -->|Google Gemini| E1[使用 Gemini 產生]\n    M -->|OpenAI| E2[使用 OpenAI 產生]\n    M -->|OpenRouter| E3[使用 OpenRouter 產生]\n    M -->|本機 Ollama| E4[使用 Ollama 產生]\n\n    E1 --> E[產生文件]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[建立視覺化圖表]\n    E --> G[組織為 Wiki]\n    F --> G\n    G --> H[互動式 DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ 專案結構\n\n```\ndeepwiki/\n├── api/                  # 後端 API 伺服器\n│   ├── main.py           # API 進入點\n│   ├── api.py            # FastAPI 實作\n│   ├── rag.py            # 檢索增強產生\n│   ├── data_pipeline.py  # 資料處理工具\n│   └── requirements.txt  # Python 相依性\n│\n├── src/                  # 前端 Next.js 應用\n│   ├── app/              # Next.js 應用目錄\n│   │   └── page.tsx      # 主應用頁面\n│   └── components/       # React 元件\n│       └── Mermaid.tsx   # Mermaid 圖表渲染器\n│\n├── public/               # 靜態資源\n├── package.json          # JavaScript 相依性\n└── .env                  # 環境變數（需要建立）\n```\n\n## 🤖 基於提供商的模型選擇系統\n\nDeepWiki 現在實作了靈活的基於提供商的模型選擇系統，支援多種 LLM 提供商：\n\n### 支援的提供商和模型\n\n- **Google**：預設 `gemini-2.5-flash`，也支援 `gemini-2.5-flash-lite`、`gemini-2.5-pro` 等\n- **OpenAI**：預設 `gpt-5-nano`，也支援 `gpt-5`, `4o` 等\n- **OpenRouter**：透過統一 API 存取多種模型，包括 Claude、Llama、Mistral 等\n- **Ollama**：支援本機執行的開源模型，如 `llama3`\n\n### 環境變數\n\n每個提供商都需要對應的 API 金鑰環境變數：\n\n```\n# API 金鑰\nGOOGLE_API_KEY=your_google_api_key        # 使用 Google Gemini 模型時必需\nOPENAI_API_KEY=your_openai_api_key        # 使用 OpenAI 模型時必需\nOPENROUTER_API_KEY=your_openrouter_api_key # 使用 OpenRouter 模型時必需\n\n# OpenAI API 基礎 URL 設定\nOPENAI_BASE_URL=https://custom-api-endpoint.com/v1  # 可選，用於自訂 OpenAI API 端點\n\n# Ollama 主機\nOLLAMA_HOST=your_ollama_host # 可選，如果 Ollama 不在本機執行，預設為 http://localhost:11434\n\n# 設定檔目錄\nDEEPWIKI_CONFIG_DIR=/path/to/custom/config/dir  # 可選，用於自訂設定檔位置\n```\n\n### 設定檔\n\nDeepWiki 使用 JSON 設定檔來管理系統的各個層面：\n\n1. **`generator.json`**：文字產生模型設定\n   - 定義可用的模型提供商（Google、OpenAI、OpenRouter、Ollama）\n   - 指定每個提供商的預設和可用模型\n   - 包含模型特定參數，如 temperature 和 top_p\n\n2. **`embedder.json`**：嵌入模型和文字處理設定\n   - 定義用於向量儲存的嵌入模型\n   - 包含用於 RAG 的檢索器設定\n   - 指定文件分塊的文字分割器設定\n\n3. **`repo.json`**：儲存庫處理設定\n   - 包含排除特定檔案和目錄的檔案篩選器\n   - 定義儲存庫大小限制和處理規則\n\n預設情況下，這些檔案位於 `api/config/` 目錄中。您可以使用 `DEEPWIKI_CONFIG_DIR` 環境變數自訂它們的位置。\n\n### 為服務提供商設計的自訂模型選擇\n\n自訂模型選擇功能專為需要以下功能的服務提供商設計：\n\n- 您可以在組織內為使用者提供多種 AI 模型選擇\n- 您可以快速適應快速發展的 LLM 領域，無需變更程式碼\n- 您可以支援不在預定義清單中的專業或微調模型\n\n服務提供商可以透過從預定義選項中選擇或在前端介面中輸入自訂模型識別符來實作其模型提供方案。\n\n### 為企業私有通道設計的基礎 URL 設定\n\nOpenAI 客戶端的 base_url 設定主要為擁有私有 API 通道的企業使用者設計。此功能：\n\n- 支援連線到私有或企業特定的 API 端點\n- 允許組織使用自己的自主託管或自訂部署的 LLM 服務\n- 支援與第三方 OpenAI API 相容服務的整合\n\n**即將推出**：在未來的更新中，DeepWiki 將支援一種模式，讓使用者需要在請求中提供自己的 API 金鑰。這將允許擁有私有通道的企業客戶使用其現有的 API 安排，而不必與 DeepWiki 部署共享憑證。\n\n## 🧩 使用 OpenAI 相容的嵌入模型（如阿里巴巴 Qwen）\n\n如果您想使用與 OpenAI API 相容的嵌入模型（如阿里巴巴 Qwen），請按照以下步驟操作：\n\n1. 用 `api/config/embedder_openai_compatible.json` 的內容替換 `api/config/embedder.json` 的內容。\n2. 在專案根目錄的 `.env` 檔案中，設定相關的環境變數，例如：\n   ```\n   OPENAI_API_KEY=your_api_key\n   OPENAI_BASE_URL=your_openai_compatible_endpoint\n   ```\n3. 程式會自動用環境變數的值替換 embedder.json 中的預留位置。\n\n這讓您可以無縫切換到任何 OpenAI 相容的嵌入服務，無需變更程式碼。\n\n### 日誌記錄\n\nDeepWiki 使用 Python 的內建 `logging` 模組進行診斷輸出。您可以透過環境變數設定詳細程度和日誌檔案目標：\n\n| 變數             | 說明                                                                 | 預設值                        |\n|-----------------|----------------------------------------------------------------------|------------------------------|\n| `LOG_LEVEL`     | 日誌記錄等級（DEBUG、INFO、WARNING、ERROR、CRITICAL）                    | INFO                         |\n| `LOG_FILE_PATH` | 日誌檔案的路徑。如果設定，日誌將寫入此檔案   | `api/logs/application.log`   |\n\n要啟用除錯日誌並將日誌導向自訂檔案：\n```bash\nexport LOG_LEVEL=DEBUG\nexport LOG_FILE_PATH=./debug.log\npython -m api.main\n```\n或使用 Docker Compose：\n```bash\nLOG_LEVEL=DEBUG LOG_FILE_PATH=./debug.log docker-compose up\n```\n\n使用 Docker Compose 執行時，容器的 `api/logs` 目錄會掛載到主機上的 `./api/logs`（請參閱 `docker-compose.yml` 中的 `volumes` 區段），確保日誌檔案在重新啟動後仍然存在。\n\n您也可以將這些設定儲存在 `.env` 檔案中：\n\n```bash\nLOG_LEVEL=DEBUG\nLOG_FILE_PATH=./debug.log\n```\n然後簡單執行：\n\n```bash\ndocker-compose up\n```\n\n**日誌路徑安全性考量：** 在生產環境中，請確保 `api/logs` 目錄和任何自訂日誌檔案路徑都受到適當的檔案系統權限和存取控制保護。應用程式會強制要求 `LOG_FILE_PATH` 位於專案的 `api/logs` 目錄內，以防止路徑遍歷或未授權的寫入。\n\n## 🛠️ 進階設定\n\n### 環境變數\n\n| 變數             | 說明                                                  | 必需 | 備註                                                                                                     |\n|----------------------|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------|\n| `GOOGLE_API_KEY`     | Google Gemini API 金鑰，用於 AI 產生                      | 否 | 只有在您想使用 Google Gemini 模型時才需要                                                    \n| `OPENAI_API_KEY`     | OpenAI API 金鑰，用於嵌入                                | 是 | 備註：即使您不使用 OpenAI 模型，這個也是必需的，因為它用於嵌入              |\n| `OPENROUTER_API_KEY` | OpenRouter API 金鑰，用於替代模型                    | 否 | 只有在您想使用 OpenRouter 模型時才需要                                                       |\n| `OLLAMA_HOST`        | Ollama 主機（預設：http://localhost:11434）                | 否 | 只有在您想使用外部 Ollama 伺服器時才需要                                                  |\n| `PORT`               | API 伺服器的連接埠（預設：8001）                      | 否 | 如果您在同一台機器上託管 API 和前端，請確保相應地變更 `SERVER_BASE_URL` 的連接埠 |\n| `SERVER_BASE_URL`    | API 伺服器的基礎 URL（預設：http://localhost:8001） | 否 |\n| `DEEPWIKI_AUTH_MODE` | 設定為 `true` 或 `1` 以啟用授權模式 | 否 | 預設為 `false`。如果啟用，則需要 `DEEPWIKI_AUTH_CODE` |\n| `DEEPWIKI_AUTH_CODE` | 當 `DEEPWIKI_AUTH_MODE` 啟用時，Wiki 產生所需的秘密代碼 | 否 | 只有在 `DEEPWIKI_AUTH_MODE` 為 `true` 或 `1` 時才使用 |\n\n如果您不使用 ollama 模式，您需要設定 OpenAI API 金鑰用於嵌入。其他 API 金鑰只有在設定並使用對應提供商的模型時才需要。\n\n## 授權模式\n\nDeepWiki 可以設定為在授權模式下執行，在此模式下，Wiki 產生需要有效的授權代碼。如果您想控制誰可以使用產生功能，這會很有用。\n限制前端啟動並保護快取刪除，但如果直接存取 API 端點，無法完全防止後端產生。\n\n要啟用授權模式，請設定以下環境變數：\n\n- `DEEPWIKI_AUTH_MODE`：將此設定為 `true` 或 `1`。啟用時，前端將顯示授權代碼的輸入欄位。\n- `DEEPWIKI_AUTH_CODE`：將此設定為所需的秘密代碼。限制前端啟動並保護快取刪除，但如果直接存取 API 端點，無法完全防止後端產生。\n\n如果未設定 `DEEPWIKI_AUTH_MODE` 或設定為 `false`（或除 `true`/`1` 以外的任何其他值），授權功能將被停用，不需要任何代碼。\n\n### Docker 設定\n\n您可以使用 Docker 來執行 DeepWiki：\n\n```bash\n# 從 GitHub Container Registry 拉取映像\ndocker pull ghcr.io/asyncfuncai/deepwiki-open:latest\n\n# 使用環境變數執行容器\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -e OLLAMA_HOST=your_ollama_host \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n此命令也會將主機上的 `~/.adalflow` 掛載到容器中的 `/root/.adalflow`。此路徑用於儲存：\n- 複製的儲存庫（`~/.adalflow/repos/`）\n- 它們的嵌入和索引（`~/.adalflow/databases/`）\n- 快取的已產生 Wiki 內容（`~/.adalflow/wikicache/`）\n\n這確保即使容器停止或移除，您的資料也會持續存在。\n\n或使用提供的 `docker-compose.yml` 檔案：\n\n```bash\n# 首先使用您的 API 金鑰編輯 .env 檔案\ndocker-compose up\n```\n\n（`docker-compose.yml` 檔案預先設定為掛載 `~/.adalflow` 以保持資料持續性，類似於上面的 `docker run` 命令。）\n\n#### 在 Docker 中使用 .env 檔案\n\n您也可以將 .env 檔案掛載到容器：\n\n```bash\n# 使用您的 API 金鑰建立 .env 檔案\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\necho \"OLLAMA_HOST=your_ollama_host\" >> .env\n\n# 使用掛載的 .env 檔案執行容器\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -v $(pwd)/.env:/app/.env \\\n  -v ~/.adalflow:/root/.adalflow \\\n  ghcr.io/asyncfuncai/deepwiki-open:latest\n```\n\n此命令也會將主機上的 `~/.adalflow` 掛載到容器中的 `/root/.adalflow`。此路徑用於儲存：\n- 複製的儲存庫（`~/.adalflow/repos/`）\n- 它們的嵌入和索引（`~/.adalflow/databases/`）\n- 快取的已產生 Wiki 內容（`~/.adalflow/wikicache/`）\n\n這確保即使容器停止或移除，您的資料也會持續存在。\n\n#### 在本機建置 Docker 映像\n\n如果您想在本機建置 Docker 映像：\n\n```bash\n# 複製儲存庫\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# 建置 Docker 映像\ndocker build -t deepwiki-open .\n\n# 執行容器\ndocker run -p 8001:8001 -p 3000:3000 \\\n  -e GOOGLE_API_KEY=your_google_api_key \\\n  -e OPENAI_API_KEY=your_openai_api_key \\\n  -e OPENROUTER_API_KEY=your_openrouter_api_key \\\n  -e OLLAMA_HOST=your_ollama_host \\\n  deepwiki-open\n```\n\n### API 伺服器詳細資訊\n\nAPI 伺服器提供：\n- 儲存庫複製和索引\n- RAG（檢索增強產生）\n- 串流聊天完成\n\n更多詳細資訊，請參閱 [API README](./api/README.md)。\n\n## 🔌 OpenRouter 整合\n\nDeepWiki 現在支援 [OpenRouter](https://openrouter.ai/) 作為模型提供商，讓您可以透過單一 API 存取數百個 AI 模型：\n\n- **多種模型選項**：存取來自 OpenAI、Anthropic、Google、Meta、Mistral 等的模型\n- **簡單設定**：只需新增您的 OpenRouter API 金鑰並選擇您想使用的模型\n- **成本效益**：選擇符合您預算和效能需求的模型\n- **輕鬆切換**：在不同模型之間切換，無需變更程式碼\n\n### 如何在 DeepWiki 中使用 OpenRouter\n\n1. **取得 API 金鑰**：在 [OpenRouter](https://openrouter.ai/) 註冊並取得您的 API 金鑰\n2. **新增到環境**：在您的 `.env` 檔案中新增 `OPENROUTER_API_KEY=your_key`\n3. **在 UI 中啟用**：在首頁勾選「使用 OpenRouter API」選項\n4. **選擇模型**：從熱門模型中選擇，如 GPT-4o、Claude 3.5 Sonnet、Gemini 2.0 等\n\nOpenRouter 特別適用於以下情況：\n- 想嘗試不同模型而不用註冊多個服務\n- 存取在您所在地區可能受限的模型\n- 比較不同模型提供商的效能\n- 根據您的需求最佳化成本與效能的平衡\n\n## 🤖 提問和深度研究功能\n\n### 提問功能\n\n提問功能允許您使用檢索增強產生（RAG）與您的儲存庫聊天：\n\n- **上下文感知回應**：基於儲存庫中實際程式碼取得準確答案\n- **RAG 驅動**：系統檢索相關程式碼片段，提供有根據的回應\n- **即時串流傳輸**：即時檢視產生的回應，取得更互動式的體驗\n- **對話歷史**：系統在問題之間保持上下文，實現更連貫的互動\n\n### 深度研究功能\n\n深度研究透過多輪研究過程將儲存庫分析提升到新水平：\n\n- **深入調查**：透過多次研究迭代徹底探索複雜主題\n- **結構化過程**：遵循清晰的研究計畫，包含更新和全面結論\n- **自動繼續**：AI 自動繼續研究直到達成結論（最多 5 次迭代）\n- **研究階段**：\n  1. **研究計畫**：概述方法和初步發現\n  2. **研究更新**：在前一輪迭代基礎上增加新見解\n  3. **最終結論**：基於所有迭代提供全面答案\n\n要使用深度研究，只需在提交問題前在提問介面中切換「深度研究」開關。\n\n## 📱 螢幕截圖\n\n### 主頁面\n![主頁面](screenshots/home.png)\n\n### Wiki 頁面\n![Wiki 頁面](screenshots/wiki-page.png)\n\n### 提問功能\n![提問功能](screenshots/ask.png)\n\n### 深度研究\n![深度研究](screenshots/deep-research.png)\n\n### 展示影片\n\n[![DeepWiki 展示影片](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*觀看 DeepWiki 實際操作！*\n\n## 🔧 配置選項\n\n### 模型提供商\n\nDeepWiki 支援多個 AI 模型提供商：\n\n1. **Google Gemini**（預設）\n   - 快速且經濟實惠\n   - 良好的程式碼理解能力\n\n2. **OpenAI**\n   - 高品質輸出\n   - 支援 GPT-4 和 GPT-3.5\n\n3. **OpenRouter**\n   - 存取多個模型\n   - 靈活的定價選項\n\n4. **本機 Ollama**\n   - 隱私保護\n   - 離線執行\n   - 需要本機設定\n\n### Wiki 類型\n\n- **全面型**：包含詳細分析、程式碼範例和完整文件\n- **簡潔型**：專注於核心功能和關鍵見解\n\n## 🌍 支援的平台\n\n- **GitHub**：公開和私人儲存庫\n- **GitLab**：GitLab.com 和自主託管實例\n- **Bitbucket**：Atlassian 託管的儲存庫\n\n## 📚 API 端點\n\n### `/api/wiki_cache`\n- **方法**：GET\n- **描述**：檢索快取的 Wiki 資料\n- **參數**：\n  - `repo`: 儲存庫識別符\n  - `platform`: git 平台（github、gitlab、bitbucket）\n\n### `/export/wiki`\n- **方法**：GET\n- **描述**：匯出 Wiki 為 Markdown 或 JSON\n- **參數**：\n  - `repo`: 儲存庫識別符\n  - `format`: 匯出格式（markdown、json）\n\n## ❓ 故障排除\n\n### API 金鑰問題\n- **「缺少環境變數」**：確保您的 `.env` 檔案位於專案根目錄並包含所需的 API 金鑰\n- **「API 金鑰無效」**：檢查您是否正確複製了完整金鑰，沒有多餘空格\n- **「OpenRouter API 錯誤」**：驗證您的 OpenRouter API 金鑰有效且有足夠的額度\n\n### 連線問題\n- **「無法連線到 API 伺服器」**：確保 API 伺服器在連接埠 8001 上執行\n- **「CORS 錯誤」**：API 設定為允許所有來源，但如果您遇到問題，請嘗試在同一台機器上執行前端和後端\n\n### 產生問題\n- **「產生 Wiki 時出錯」**：對於非常大的儲存庫，請先嘗試較小的儲存庫\n- **「無效的儲存庫格式」**：確保您使用有效的 GitHub、GitLab 或 Bitbucket URL 格式\n- **「無法擷取儲存庫結構」**：對於私人儲存庫，確保您輸入了具有適當權限的有效個人存取權杖\n- **「圖表轉譯錯誤」**：應用程式將自動嘗試修復損壞的圖表\n\n### 常見解決方案\n1. **重新啟動兩個伺服器**：有時簡單的重新啟動可以解決大多數問題\n2. **檢查主控台日誌**：開啟瀏覽器開發者工具查看任何 JavaScript 錯誤\n3. **檢查 API 日誌**：查看執行 API 的終端中的 Python 錯誤\n\n## 🤝 貢獻\n\n我們歡迎各種形式的貢獻！無論是錯誤報告、功能請求還是程式碼貢獻。\n\n### 開發設定\n\n1. Fork 此儲存庫\n2. 建立功能分支：`git checkout -b feature/amazing-feature`\n3. 提交您的變更：`git commit -m 'Add amazing feature'`\n4. 推送到分支：`git push origin feature/amazing-feature`\n5. 開啟 Pull Request\n\n### 新增新語言支援\n\n1. 在 `src/messages/` 中新增新的翻譯檔案\n2. 更新 `src/i18n.ts` 中的 `locales` 陣列\n3. 建立相對應的 README 檔案\n4. 測試翻譯\n\n## 📄 授權\n\n此專案根據 MIT 授權條款授權 - 詳情請參閱 [LICENSE](LICENSE) 檔案。\n\n## 🙏 致謝\n\n- 感謝所有貢獻者的努力\n- 基於 Next.js、FastAPI 和各種開源程式庫建構\n- 特別感謝 AI 模型提供商讓此專案成為可能\n\n## 🐛 問題回報\n\n如果您遇到任何問題，請在 GitHub Issues 中建立問題報告。請包含：\n\n- 錯誤描述\n- 重現步驟\n- 預期行為\n- 螢幕截圖（如果適用）\n- 系統資訊\n\n## 🔮 未來計劃\n\n- [ ] 更多 AI 模型整合\n- [ ] 進階程式碼分析功能\n- [ ] 即時協作編輯\n- [ ] 行動應用支援\n- [ ] 企業級功能\n\n## ⭐ Star 歷史\n\n[![Star 歷史圖表](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n"
  },
  {
    "path": "README.zh.md",
    "content": "# DeepWiki-Open\n\n![DeepWiki 横幅](screenshots/Deepwiki.png)\n\n**DeepWiki**可以为任何GitHub、GitLab或BitBucket代码仓库自动创建美观、交互式的Wiki！只需输入仓库名称，DeepWiki将：\n\n1. 分析代码结构\n2. 生成全面的文档\n3. 创建可视化图表解释一切如何运作\n4. 将所有内容整理成易于导航的Wiki\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing)\n\n[![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void)\n[![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v)\n\n[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md)\n\n## ✨ 特点\n\n- **即时文档**：几秒钟内将任何GitHub、GitLab或BitBucket仓库转换为Wiki\n- **私有仓库支持**：使用个人访问令牌安全访问私有仓库\n- **智能分析**：AI驱动的代码结构和关系理解\n- **精美图表**：自动生成Mermaid图表可视化架构和数据流\n- **简易导航**：简单、直观的界面探索Wiki\n- **提问功能**：使用RAG驱动的AI与您的仓库聊天，获取准确答案\n- **深度研究**：多轮研究过程，彻底调查复杂主题\n- **多模型提供商**：支持Google Gemini、OpenAI、OpenRouter和本地Ollama模型\n\n## 🚀 快速开始（超级简单！）\n\n### 选项1：使用Docker\n\n```bash\n# 克隆仓库\ngit clone https://github.com/AsyncFuncAI/deepwiki-open.git\ncd deepwiki-open\n\n# 创建包含API密钥的.env文件\necho \"GOOGLE_API_KEY=your_google_api_key\" > .env\necho \"OPENAI_API_KEY=your_openai_api_key\" >> .env\n# 可选：如果您想使用OpenRouter模型，添加OpenRouter API密钥\necho \"OPENROUTER_API_KEY=your_openrouter_api_key\" >> .env\n\n# 使用Docker Compose运行\ndocker-compose up\n```\n\n(上述 Docker 命令以及 `docker-compose.yml` 配置会挂载您主机上的 `~/.adalflow` 目录到容器内的 `/root/.adalflow`。此路径用于存储：\n- 克隆的仓库 (`~/.adalflow/repos/`)\n- 仓库的嵌入和索引 (`~/.adalflow/databases/`)\n- 缓存的已生成 Wiki 内容 (`~/.adalflow/wikicache/`)\n\n这确保了即使容器停止或移除，您的数据也能持久保存。)\n\n> 💡 **获取这些密钥的地方：**\n> - 从[Google AI Studio](https://makersuite.google.com/app/apikey)获取Google API密钥\n> - 从[OpenAI Platform](https://platform.openai.com/api-keys)获取OpenAI API密钥\n\n### 选项2：手动设置（推荐）\n\n#### 步骤1：设置API密钥\n\n在项目根目录创建一个`.env`文件，包含以下密钥：\n\n```\nGOOGLE_API_KEY=your_google_api_key\nOPENAI_API_KEY=your_openai_api_key\n# 可选：如果您想使用OpenRouter模型，添加此项\nOPENROUTER_API_KEY=your_openrouter_api_key\n```\n\n#### 步骤2：启动后端\n\n```bash\n# 安装Python依赖\npython -m pip install poetry==2.0.1 && poetry install -C api\n\n# 启动API服务器\npython -m api.main\n```\n\n#### 步骤3：启动前端\n\n```bash\n# 安装JavaScript依赖\nnpm install\n# 或\nyarn install\n\n# 启动Web应用\nnpm run dev\n# 或\nyarn dev\n```\n\n#### 步骤4：使用DeepWiki！\n\n1. 在浏览器中打开[http://localhost:3000](http://localhost:3000)\n2. 输入GitHub、GitLab或Bitbucket仓库（如`https://github.com/openai/codex`、`https://github.com/microsoft/autogen`、`https://gitlab.com/gitlab-org/gitlab`或`https://bitbucket.org/redradish/atlassian_app_versions`）\n3. 对于私有仓库，点击\"+ 添加访问令牌\"并输入您的GitHub或GitLab个人访问令牌\n4. 点击\"生成Wiki\"，见证奇迹的发生！\n\n## 🔍 工作原理\n\nDeepWiki使用AI来：\n\n1. 克隆并分析GitHub、GitLab或Bitbucket仓库（包括使用令牌认证的私有仓库）\n2. 创建代码嵌入用于智能检索\n3. 使用上下文感知AI生成文档（使用Google Gemini、OpenAI、OpenRouter或本地Ollama模型）\n4. 创建可视化图表解释代码关系\n5. 将所有内容组织成结构化Wiki\n6. 通过提问功能实现与仓库的智能问答\n7. 通过深度研究功能提供深入研究能力\n\n```mermaid\ngraph TD\n    A[用户输入GitHub/GitLab/Bitbucket仓库] --> AA{私有仓库?}\n    AA -->|是| AB[添加访问令牌]\n    AA -->|否| B[克隆仓库]\n    AB --> B\n    B --> C[分析代码结构]\n    C --> D[创建代码嵌入]\n\n    D --> M{选择模型提供商}\n    M -->|Google Gemini| E1[使用Gemini生成]\n    M -->|OpenAI| E2[使用OpenAI生成]\n    M -->|OpenRouter| E3[使用OpenRouter生成]\n    M -->|本地Ollama| E4[使用Ollama生成]\n\n    E1 --> E[生成文档]\n    E2 --> E\n    E3 --> E\n    E4 --> E\n\n    D --> F[创建可视化图表]\n    E --> G[组织为Wiki]\n    F --> G\n    G --> H[交互式DeepWiki]\n\n    classDef process stroke-width:2px;\n    classDef data stroke-width:2px;\n    classDef result stroke-width:2px;\n    classDef decision stroke-width:2px;\n\n    class A,D data;\n    class AA,M decision;\n    class B,C,E,F,G,AB,E1,E2,E3,E4 process;\n    class H result;\n```\n\n## 🛠️ 项目结构\n\n```\ndeepwiki/\n├── api/                  # 后端API服务器\n│   ├── main.py           # API入口点\n│   ├── api.py            # FastAPI实现\n│   ├── rag.py            # 检索增强生成\n│   ├── data_pipeline.py  # 数据处理工具\n│   └── requirements.txt  # Python依赖\n│\n├── src/                  # 前端Next.js应用\n│   ├── app/              # Next.js应用目录\n│   │   └── page.tsx      # 主应用页面\n│   └── components/       # React组件\n│       └── Mermaid.tsx   # Mermaid图表渲染器\n│\n├── public/               # 静态资源\n├── package.json          # JavaScript依赖\n└── .env                  # 环境变量（需要创建）\n```\n\n## 🤖 提问和深度研究功能\n\n### 提问功能\n\n提问功能允许您使用检索增强生成（RAG）与您的仓库聊天：\n\n- **上下文感知响应**：基于仓库中实际代码获取准确答案\n- **RAG驱动**：系统检索相关代码片段，提供有根据的响应\n- **实时流式传输**：实时查看生成的响应，获得更交互式的体验\n- **对话历史**：系统在问题之间保持上下文，实现更连贯的交互\n\n### 深度研究功能\n\n深度研究通过多轮研究过程将仓库分析提升到新水平：\n\n- **深入调查**：通过多次研究迭代彻底探索复杂主题\n- **结构化过程**：遵循清晰的研究计划，包含更新和全面结论\n- **自动继续**：AI自动继续研究直到达成结论（最多5次迭代）\n- **研究阶段**：\n  1. **研究计划**：概述方法和初步发现\n  2. **研究更新**：在前一轮迭代基础上增加新见解\n  3. **最终结论**：基于所有迭代提供全面答案\n\n要使用深度研究，只需在提交问题前在提问界面中切换\"深度研究\"开关。\n\n## 📱 截图\n\n![DeepWiki主界面](screenshots/Interface.png)\n*DeepWiki的主界面*\n\n![私有仓库支持](screenshots/privaterepo.png)\n*使用个人访问令牌访问私有仓库*\n\n![深度研究功能](screenshots/DeepResearch.png)\n*深度研究为复杂主题进行多轮调查*\n\n### 演示视频\n\n[![DeepWiki演示视频](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4)\n\n*观看DeepWiki实际操作！*\n\n## ❓ 故障排除\n\n### API密钥问题\n- **\"缺少环境变量\"**：确保您的`.env`文件位于项目根目录并包含所需的API密钥\n- **\"API密钥无效\"**：检查您是否正确复制了完整密钥，没有多余空格\n- **\"OpenRouter API错误\"**：验证您的OpenRouter API密钥有效且有足够的额度\n\n### 连接问题\n- **\"无法连接到API服务器\"**：确保API服务器在端口8001上运行\n- **\"CORS错误\"**：API配置为允许所有来源，但如果您遇到问题，请尝试在同一台机器上运行前端和后端\n\n### 生成问题\n- **\"生成Wiki时出错\"**：对于非常大的仓库，请先尝试较小的仓库\n- **\"无效的仓库格式\"**：确保您使用有效的GitHub、GitLab或Bitbucket URL格式\n- **\"无法获取仓库结构\"**：对于私有仓库，确保您输入了具有适当权限的有效个人访问令牌\n- **\"图表渲染错误\"**：应用程序将自动尝试修复损坏的图表\n\n### 常见解决方案\n1. **重启两个服务器**：有时简单的重启可以解决大多数问题\n2. **检查控制台日志**：打开浏览器开发者工具查看任何JavaScript错误\n3. **检查API日志**：查看运行API的终端中的Python错误\n\n## 🤝 贡献\n\n欢迎贡献！随时：\n- 为bug或功能请求开issue\n- 提交pull request改进代码\n- 分享您的反馈和想法\n\n## 📄 许可证\n\n本项目根据MIT许可证授权 - 详情请参阅[LICENSE](LICENSE)文件。\n\n## ⭐ 星标历史\n\n[![星标历史图表](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date)\n\n## 🤖 基于提供者的模型选择系统\n\nDeepWiki 现在实现了灵活的基于提供者的模型选择系统，支持多种 LLM 提供商：\n\n### 支持的提供商和模型\n\n- **Google**: 默认使用 `gemini-2.5-flash`，还支持 `gemini-2.5-flash-lite`、`gemini-2.5-pro` 等\n- **OpenAI**: 默认使用 `gpt-5-nano`，还支持 `gpt-5`, `4o` 等\n- **OpenRouter**: 通过统一 API 访问多种模型，包括 Claude、Llama、Mistral 等\n- **Ollama**: 支持本地运行的开源模型，如 `llama3`\n\n### 环境变量\n\n每个提供商需要相应的 API 密钥环境变量：\n\n```\n# API 密钥\nGOOGLE_API_KEY=你的谷歌API密钥        # 使用 Google Gemini 模型必需\nOPENAI_API_KEY=你的OpenAI密钥        # 使用 OpenAI 模型必需\nOPENROUTER_API_KEY=你的OpenRouter密钥 # 使用 OpenRouter 模型必需\n\n# OpenAI API 基础 URL 配置\nOPENAI_BASE_URL=https://自定义API端点.com/v1  # 可选，用于自定义 OpenAI API 端点\n```\n\n### 为服务提供者设计的自定义模型选择\n\n自定义模型选择功能专为需要以下功能的服务提供者设计：\n\n- 您可在您的组织内部为用户提供多种 AI 模型选择\n- 您无需代码更改即可快速适应快速发展的 LLM 领域\n- 您可支持预定义列表中没有的专业或微调模型\n\n使用者可以通过从服务提供者预定义选项中选择或在前端界面中输入自定义模型标识符来实现其模型产品。\n\n### 为企业私有渠道设计的基础 URL 配置\n\nOpenAI 客户端的 base_url 配置主要为拥有私有 API 渠道的企业用户设计。此功能：\n\n- 支持连接到私有或企业特定的 API 端点\n- 允许组织使用自己的自托管或自定义部署的 LLM 服务\n- 支持与第三方 OpenAI API 兼容服务的集成\n\n**即将推出**：在未来的更新中，DeepWiki 将支持一种模式，用户需要在请求中提供自己的 API 密钥。这将允许拥有私有渠道的企业客户使用其现有的 API 安排，而不是与 DeepWiki 部署共享凭据。\n\n### 环境变量\n\n每个提供商需要其相应的API密钥环境变量：\n\n```\n# API密钥\nGOOGLE_API_KEY=your_google_api_key        # Google Gemini模型必需\nOPENAI_API_KEY=your_openai_api_key        # OpenAI模型必需\nOPENROUTER_API_KEY=your_openrouter_api_key # OpenRouter模型必需\n\n# OpenAI API基础URL配置\nOPENAI_BASE_URL=https://custom-api-endpoint.com/v1  # 可选，用于自定义OpenAI API端点\n\n# 配置目录\nDEEPWIKI_CONFIG_DIR=/path/to/custom/config/dir  # 可选，用于自定义配置文件位置\n\n# 授权模式\nDEEPWIKI_AUTH_MODE=true  # 设置为 true 或 1 以启用授权模式\nDEEPWIKI_AUTH_CODE=your_secret_code # 当 DEEPWIKI_AUTH_MODE 启用时所需的授权码\n```\n如果不使用ollama模式，那么需要配置OpenAI API密钥用于embeddings。其他密钥只有配置并使用使用对应提供商的模型时才需要。\n\n## 授权模式\n\nDeepWiki 可以配置为在授权模式下运行，在该模式下，生成 Wiki 需要有效的授权码。如果您想控制谁可以使用生成功能，这将非常有用。\n限制使用前端页面生成wiki并保护已生成页面的缓存删除，但无法完全阻止直接访问 API 端点生成wiki。主要目的是为了保护管理员已生成的wiki页面，防止被访问者重新生成。\n\n要启用授权模式，请设置以下环境变量：\n\n- `DEEPWIKI_AUTH_MODE`: 将此设置为 `true` 或 `1`。启用后，前端将显示一个用于输入授权码的字段。\n- `DEEPWIKI_AUTH_CODE`: 将此设置为所需的密钥。限制使用前端页面生成wiki并保护已生成页面的缓存删除，但无法完全阻止直接访问 API 端点生成wiki。\n\n如果未设置 `DEEPWIKI_AUTH_MODE` 或将其设置为 `false`（或除 `true`/`1` 之外的任何其他值），则授权功能将被禁用，并且不需要任何代码。\n\n### 配置文件\n\nDeepWiki使用JSON配置文件管理系统的各个方面：\n\n1. **`generator.json`**：文本生成模型配置\n   - 定义可用的模型提供商（Google、OpenAI、OpenRouter、Ollama）\n   - 指定每个提供商的默认和可用模型\n   - 包含特定模型的参数，如temperature和top_p\n\n2. **`embedder.json`**：嵌入模型和文本处理配置\n   - 定义用于向量存储的嵌入模型\n   - 包含用于RAG的检索器配置\n   - 指定文档分块的文本分割器设置\n\n3. **`repo.json`**：仓库处理配置\n   - 包含排除特定文件和目录的文件过滤器\n   - 定义仓库大小限制和处理规则\n\n默认情况下，这些文件位于`api/config/`目录中。您可以使用`DEEPWIKI_CONFIG_DIR`环境变量自定义它们的位置。\n\n### 面向服务提供商的自定义模型选择\n\n自定义模型选择功能专为需要以下功能的服务提供者设计：\n\n- 您可在您的组织内部为用户提供多种 AI 模型选择\n- 您无需代码更改即可快速适应快速发展的 LLM 领域\n- 您可支持预定义列表中没有的专业或微调模型\n\n使用者可以通过从服务提供者预定义选项中选择或在前端界面中输入自定义模型标识符来实现其模型产品。\n\n### 为企业私有渠道设计的基础 URL 配置\n\nOpenAI 客户端的 base_url 配置主要为拥有私有 API 渠道的企业用户设计。此功能：\n\n- 支持连接到私有或企业特定的 API 端点\n- 允许组织使用自己的自托管或自定义部署的 LLM 服务\n- 支持与第三方 OpenAI API 兼容服务的集成\n\n**即将推出**：在未来的更新中，DeepWiki 将支持一种模式，用户需要在请求中提供自己的 API 密钥。这将允许拥有私有渠道的企业客户使用其现有的 API 安排，而不是与 DeepWiki 部署共享凭据。\n\n## 🧩 使用 OpenAI 兼容的 Embedding 模型（如阿里巴巴 Qwen）\n\n如果你希望使用 OpenAI 以外、但兼容 OpenAI 接口的 embedding 模型（如阿里巴巴 Qwen），请参考以下步骤：\n\n1. 用 `api/config/embedder_openai_compatible.json` 的内容替换 `api/config/embedder.json`。\n2. 在项目根目录的 `.env` 文件中，配置相应的环境变量，例如：\n   ```\n   OPENAI_API_KEY=你的_api_key\n   OPENAI_BASE_URL=你的_openai_兼容接口地址\n   ```\n3. 程序会自动用环境变量的值替换 embedder.json 里的占位符。\n\n这样即可无缝切换到 OpenAI 兼容的 embedding 服务，无需修改代码。\n\n"
  },
  {
    "path": "api/README.md",
    "content": "# 🚀 DeepWiki API\n\nThis is the backend API for DeepWiki, providing smart code analysis and AI-powered documentation generation.\n\n## ✨ Features\n\n- **Streaming AI Responses**: Real-time responses using Google's Generative AI (Gemini)\n- **Smart Code Analysis**: Automatically analyzes GitHub repositories\n- **RAG Implementation**: Retrieval Augmented Generation for context-aware responses\n- **Local Storage**: All data stored locally - no cloud dependencies\n- **Conversation History**: Maintains context across multiple questions\n\n## 🔧 Quick Setup\n\n### Step 1: Install Dependencies\n\n```bash\n# From the project root\npython -m pip install poetry==2.0.1 && poetry install -C api\n```\n\n### Step 2: Set Up Environment Variables\n\nCreate a `.env` file in the project root:\n\n```\n# Required API Keys\nGOOGLE_API_KEY=your_google_api_key        # Required for Google Gemini models\nOPENAI_API_KEY=your_openai_api_key        # Required for embeddings and OpenAI models\n\n# Optional API Keys\nOPENROUTER_API_KEY=your_openrouter_api_key  # Required only if using OpenRouter models\n\n# AWS Bedrock Configuration\nAWS_ACCESS_KEY_ID=your_aws_access_key_id      # Required for AWS Bedrock models\nAWS_SECRET_ACCESS_KEY=your_aws_secret_key     # Required for AWS Bedrock models\nAWS_REGION=us-east-1                          # Optional, defaults to us-east-1\nAWS_ROLE_ARN=your_aws_role_arn                # Optional, for role-based authentication\n\n# OpenAI API Configuration\nOPENAI_BASE_URL=https://custom-api-endpoint.com/v1  # Optional, for custom OpenAI API endpoints\n\n# Ollama host\nOLLAMA_HOST=https://your_ollama_host\"  # Optional: Add Ollama host if not local. default: http://localhost:11434\n\n# Server Configuration\nPORT=8001  # Optional, defaults to 8001\n```\n\nIf you're not using Ollama mode, you need to configure an OpenAI API key for embeddings. Other API keys are only required when configuring and using models from the corresponding providers.\n\n> 💡 **Where to get these keys:**\n> - Get a Google API key from [Google AI Studio](https://makersuite.google.com/app/apikey)\n> - Get an OpenAI API key from [OpenAI Platform](https://platform.openai.com/api-keys)\n> - Get an OpenRouter API key from [OpenRouter](https://openrouter.ai/keys)\n> - Get AWS credentials from [AWS IAM Console](https://console.aws.amazon.com/iam/)\n\n#### Advanced Environment Configuration\n\n##### Provider-Based Model Selection\nDeepWiki supports multiple LLM providers. The environment variables above are required depending on which providers you want to use:\n\n- **Google Gemini**: Requires `GOOGLE_API_KEY`\n- **OpenAI**: Requires `OPENAI_API_KEY`\n- **OpenRouter**: Requires `OPENROUTER_API_KEY`\n- **AWS Bedrock**: Requires `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`\n- **Ollama**: No API key required (runs locally)\n\n##### Custom OpenAI API Endpoints\nThe `OPENAI_BASE_URL` variable allows you to specify a custom endpoint for the OpenAI API. This is useful for:\n\n- Enterprise users with private API channels\n- Organizations using self-hosted or custom-deployed LLM services\n- Integration with third-party OpenAI API-compatible services\n\n**Example:** you can use the endpoint which support the OpenAI protocol provided by any organization\n```\nOPENAI_BASE_URL=https://custom-openai-endpoint.com/v1\n```\n\n##### Configuration Files\nDeepWiki now uses JSON configuration files to manage various system components instead of hardcoded values:\n\n1. **`generator.json`**: Configuration for text generation models\n   - Located in `api/config/` by default\n   - Defines available model providers (Google, OpenAI, OpenRouter, AWS Bedrock, Ollama)\n   - Specifies default and available models for each provider\n   - Contains model-specific parameters like temperature and top_p\n\n2. **`embedder.json`**: Configuration for embedding models and text processing\n   - Located in `api/config/` by default\n   - Defines embedding models for vector storage\n   - Contains retriever configuration for RAG\n   - Specifies text splitter settings for document chunking\n\n3. **`repo.json`**: Configuration for repository handling\n   - Located in `api/config/` by default\n   - Contains file filters to exclude certain files and directories\n   - Defines repository size limits and processing rules\n\nYou can customize the configuration directory location using the environment variable:\n\n```\nDEEPWIKI_CONFIG_DIR=/path/to/custom/config/dir  # Optional, for custom config file location\n```\n\nThis allows you to maintain different configurations for various environments or deployment scenarios without modifying the code.\n\n### Step 3: Start the API Server\n\n```bash\n# From the project root\npython -m api.main\n```\n\nThe API will be available at `http://localhost:8001`\n\n## 🧠 How It Works\n\n### 1. Repository Indexing\nWhen you provide a GitHub repository URL, the API:\n- Clones the repository locally (if not already cloned)\n- Reads all files in the repository\n- Creates embeddings for the files using OpenAI\n- Stores the embeddings in a local database\n\n### 2. Smart Retrieval (RAG)\nWhen you ask a question:\n- The API finds the most relevant code snippets\n- These snippets are used as context for the AI\n- The AI generates a response based on this context\n\n### 3. Real-Time Streaming\n- Responses are streamed in real-time\n- You see the answer as it's being generated\n- This creates a more interactive experience\n\n## 📡 API Endpoints\n\n### GET /\nReturns basic API information and available endpoints.\n\n### POST /chat/completions/stream\nStreams an AI-generated response about a GitHub repository.\n\n**Request Body:**\n\n```json\n{\n  \"repo_url\": \"https://github.com/username/repo\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What does this repository do?\"\n    }\n  ],\n  \"filePath\": \"optional/path/to/file.py\"  // Optional\n}\n```\n\n**Response:**\nA streaming response with the generated text.\n\n## 📝 Example Code\n\n```python\nimport requests\n\n# API endpoint\nurl = \"http://localhost:8001/chat/completions/stream\"\n\n# Request data\npayload = {\n    \"repo_url\": \"https://github.com/AsyncFuncAI/deepwiki-open\",\n    \"messages\": [\n        {\n            \"role\": \"user\",\n            \"content\": \"Explain how React components work\"\n        }\n    ]\n}\n\n# Make streaming request\nresponse = requests.post(url, json=payload, stream=True)\n\n# Process the streaming response\nfor chunk in response.iter_content(chunk_size=None):\n    if chunk:\n        print(chunk.decode('utf-8'), end='', flush=True)\n```\n\n## 💾 Storage\n\nAll data is stored locally on your machine:\n- Cloned repositories: `~/.adalflow/repos/`\n- Embeddings and indexes: `~/.adalflow/databases/`\n- Generated wiki cache: `~/.adalflow/wikicache/`\n\nNo cloud storage is used - everything runs on your computer!\n"
  },
  {
    "path": "api/__init__.py",
    "content": "# Make the api package importable\n\n# api package\n"
  },
  {
    "path": "api/api.py",
    "content": "import os\nimport logging\nfrom fastapi import FastAPI, HTTPException, Query, Request, WebSocket\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import JSONResponse, Response\nfrom typing import List, Optional, Dict, Any, Literal\nimport json\nfrom datetime import datetime\nfrom pydantic import BaseModel, Field\nimport google.generativeai as genai\nimport asyncio\n\n# Configure logging\nfrom api.logging_config import setup_logging\n\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\n\n# Initialize FastAPI app\napp = FastAPI(\n    title=\"Streaming API\",\n    description=\"API for streaming chat completions\"\n)\n\n# Configure CORS\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],  # Allows all origins\n    allow_credentials=True,\n    allow_methods=[\"*\"],  # Allows all methods\n    allow_headers=[\"*\"],  # Allows all headers\n)\n\n# Helper function to get adalflow root path\ndef get_adalflow_default_root_path():\n    return os.path.expanduser(os.path.join(\"~\", \".adalflow\"))\n\n# --- Pydantic Models ---\nclass WikiPage(BaseModel):\n    \"\"\"\n    Model for a wiki page.\n    \"\"\"\n    id: str\n    title: str\n    content: str\n    filePaths: List[str]\n    importance: str # Should ideally be Literal['high', 'medium', 'low']\n    relatedPages: List[str]\n\nclass ProcessedProjectEntry(BaseModel):\n    id: str  # Filename\n    owner: str\n    repo: str\n    name: str  # owner/repo\n    repo_type: str # Renamed from type to repo_type for clarity with existing models\n    submittedAt: int # Timestamp\n    language: str # Extracted from filename\n\nclass RepoInfo(BaseModel):\n    owner: str\n    repo: str\n    type: str\n    token: Optional[str] = None\n    localPath: Optional[str] = None\n    repoUrl: Optional[str] = None\n\n\nclass WikiSection(BaseModel):\n    \"\"\"\n    Model for the wiki sections.\n    \"\"\"\n    id: str\n    title: str\n    pages: List[str]\n    subsections: Optional[List[str]] = None\n\n\nclass WikiStructureModel(BaseModel):\n    \"\"\"\n    Model for the overall wiki structure.\n    \"\"\"\n    id: str\n    title: str\n    description: str\n    pages: List[WikiPage]\n    sections: Optional[List[WikiSection]] = None\n    rootSections: Optional[List[str]] = None\n\nclass WikiCacheData(BaseModel):\n    \"\"\"\n    Model for the data to be stored in the wiki cache.\n    \"\"\"\n    wiki_structure: WikiStructureModel\n    generated_pages: Dict[str, WikiPage]\n    repo_url: Optional[str] = None  #compatible for old cache\n    repo: Optional[RepoInfo] = None\n    provider: Optional[str] = None\n    model: Optional[str] = None\n\nclass WikiCacheRequest(BaseModel):\n    \"\"\"\n    Model for the request body when saving wiki cache.\n    \"\"\"\n    repo: RepoInfo\n    language: str\n    wiki_structure: WikiStructureModel\n    generated_pages: Dict[str, WikiPage]\n    provider: str\n    model: str\n\nclass WikiExportRequest(BaseModel):\n    \"\"\"\n    Model for requesting a wiki export.\n    \"\"\"\n    repo_url: str = Field(..., description=\"URL of the repository\")\n    pages: List[WikiPage] = Field(..., description=\"List of wiki pages to export\")\n    format: Literal[\"markdown\", \"json\"] = Field(..., description=\"Export format (markdown or json)\")\n\n# --- Model Configuration Models ---\nclass Model(BaseModel):\n    \"\"\"\n    Model for LLM model configuration\n    \"\"\"\n    id: str = Field(..., description=\"Model identifier\")\n    name: str = Field(..., description=\"Display name for the model\")\n\nclass Provider(BaseModel):\n    \"\"\"\n    Model for LLM provider configuration\n    \"\"\"\n    id: str = Field(..., description=\"Provider identifier\")\n    name: str = Field(..., description=\"Display name for the provider\")\n    models: List[Model] = Field(..., description=\"List of available models for this provider\")\n    supportsCustomModel: Optional[bool] = Field(False, description=\"Whether this provider supports custom models\")\n\nclass ModelConfig(BaseModel):\n    \"\"\"\n    Model for the entire model configuration\n    \"\"\"\n    providers: List[Provider] = Field(..., description=\"List of available model providers\")\n    defaultProvider: str = Field(..., description=\"ID of the default provider\")\n\nclass AuthorizationConfig(BaseModel):\n    code: str = Field(..., description=\"Authorization code\")\n\nfrom api.config import configs, WIKI_AUTH_MODE, WIKI_AUTH_CODE\n\n@app.get(\"/lang/config\")\nasync def get_lang_config():\n    return configs[\"lang_config\"]\n\n@app.get(\"/auth/status\")\nasync def get_auth_status():\n    \"\"\"\n    Check if authentication is required for the wiki.\n    \"\"\"\n    return {\"auth_required\": WIKI_AUTH_MODE}\n\n@app.post(\"/auth/validate\")\nasync def validate_auth_code(request: AuthorizationConfig):\n    \"\"\"\n    Check authorization code.\n    \"\"\"\n    return {\"success\": WIKI_AUTH_CODE == request.code}\n\n@app.get(\"/models/config\", response_model=ModelConfig)\nasync def get_model_config():\n    \"\"\"\n    Get available model providers and their models.\n\n    This endpoint returns the configuration of available model providers and their\n    respective models that can be used throughout the application.\n\n    Returns:\n        ModelConfig: A configuration object containing providers and their models\n    \"\"\"\n    try:\n        logger.info(\"Fetching model configurations\")\n\n        # Create providers from the config file\n        providers = []\n        default_provider = configs.get(\"default_provider\", \"google\")\n\n        # Add provider configuration based on config.py\n        for provider_id, provider_config in configs[\"providers\"].items():\n            models = []\n            # Add models from config\n            for model_id in provider_config[\"models\"].keys():\n                # Get a more user-friendly display name if possible\n                models.append(Model(id=model_id, name=model_id))\n\n            # Add provider with its models\n            providers.append(\n                Provider(\n                    id=provider_id,\n                    name=f\"{provider_id.capitalize()}\",\n                    supportsCustomModel=provider_config.get(\"supportsCustomModel\", False),\n                    models=models\n                )\n            )\n\n        # Create and return the full configuration\n        config = ModelConfig(\n            providers=providers,\n            defaultProvider=default_provider\n        )\n        return config\n\n    except Exception as e:\n        logger.error(f\"Error creating model configuration: {str(e)}\")\n        # Return some default configuration in case of error\n        return ModelConfig(\n            providers=[\n                Provider(\n                    id=\"google\",\n                    name=\"Google\",\n                    supportsCustomModel=True,\n                    models=[\n                        Model(id=\"gemini-2.5-flash\", name=\"Gemini 2.5 Flash\")\n                    ]\n                )\n            ],\n            defaultProvider=\"google\"\n        )\n\n@app.post(\"/export/wiki\")\nasync def export_wiki(request: WikiExportRequest):\n    \"\"\"\n    Export wiki content as Markdown or JSON.\n\n    Args:\n        request: The export request containing wiki pages and format\n\n    Returns:\n        A downloadable file in the requested format\n    \"\"\"\n    try:\n        logger.info(f\"Exporting wiki for {request.repo_url} in {request.format} format\")\n\n        # Extract repository name from URL for the filename\n        repo_parts = request.repo_url.rstrip('/').split('/')\n        repo_name = repo_parts[-1] if len(repo_parts) > 0 else \"wiki\"\n\n        # Get current timestamp for the filename\n        timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n\n        if request.format == \"markdown\":\n            # Generate Markdown content\n            content = generate_markdown_export(request.repo_url, request.pages)\n            filename = f\"{repo_name}_wiki_{timestamp}.md\"\n            media_type = \"text/markdown\"\n        else:  # JSON format\n            # Generate JSON content\n            content = generate_json_export(request.repo_url, request.pages)\n            filename = f\"{repo_name}_wiki_{timestamp}.json\"\n            media_type = \"application/json\"\n\n        # Create response with appropriate headers for file download\n        response = Response(\n            content=content,\n            media_type=media_type,\n            headers={\n                \"Content-Disposition\": f\"attachment; filename={filename}\"\n            }\n        )\n\n        return response\n\n    except Exception as e:\n        error_msg = f\"Error exporting wiki: {str(e)}\"\n        logger.error(error_msg)\n        raise HTTPException(status_code=500, detail=error_msg)\n\n@app.get(\"/local_repo/structure\")\nasync def get_local_repo_structure(path: str = Query(None, description=\"Path to local repository\")):\n    \"\"\"Return the file tree and README content for a local repository.\"\"\"\n    if not path:\n        return JSONResponse(\n            status_code=400,\n            content={\"error\": \"No path provided. Please provide a 'path' query parameter.\"}\n        )\n\n    if not os.path.isdir(path):\n        return JSONResponse(\n            status_code=404,\n            content={\"error\": f\"Directory not found: {path}\"}\n        )\n\n    try:\n        logger.info(f\"Processing local repository at: {path}\")\n        file_tree_lines = []\n        readme_content = \"\"\n\n        for root, dirs, files in os.walk(path):\n            # Exclude hidden dirs/files and virtual envs\n            dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__' and d != 'node_modules' and d != '.venv']\n            for file in files:\n                if file.startswith('.') or file == '__init__.py' or file == '.DS_Store':\n                    continue\n                rel_dir = os.path.relpath(root, path)\n                rel_file = os.path.join(rel_dir, file) if rel_dir != '.' else file\n                file_tree_lines.append(rel_file)\n                # Find README.md (case-insensitive)\n                if file.lower() == 'readme.md' and not readme_content:\n                    try:\n                        with open(os.path.join(root, file), 'r', encoding='utf-8') as f:\n                            readme_content = f.read()\n                    except Exception as e:\n                        logger.warning(f\"Could not read README.md: {str(e)}\")\n                        readme_content = \"\"\n\n        file_tree_str = '\\n'.join(sorted(file_tree_lines))\n        return {\"file_tree\": file_tree_str, \"readme\": readme_content}\n    except Exception as e:\n        logger.error(f\"Error processing local repository: {str(e)}\")\n        return JSONResponse(\n            status_code=500,\n            content={\"error\": f\"Error processing local repository: {str(e)}\"}\n        )\n\ndef generate_markdown_export(repo_url: str, pages: List[WikiPage]) -> str:\n    \"\"\"\n    Generate Markdown export of wiki pages.\n\n    Args:\n        repo_url: The repository URL\n        pages: List of wiki pages\n\n    Returns:\n        Markdown content as string\n    \"\"\"\n    # Start with metadata\n    markdown = f\"# Wiki Documentation for {repo_url}\\n\\n\"\n    markdown += f\"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\\n\\n\"\n\n    # Add table of contents\n    markdown += \"## Table of Contents\\n\\n\"\n    for page in pages:\n        markdown += f\"- [{page.title}](#{page.id})\\n\"\n    markdown += \"\\n\"\n\n    # Add each page\n    for page in pages:\n        markdown += f\"<a id='{page.id}'></a>\\n\\n\"\n        markdown += f\"## {page.title}\\n\\n\"\n\n\n\n        # Add related pages\n        if page.relatedPages and len(page.relatedPages) > 0:\n            markdown += \"### Related Pages\\n\\n\"\n            related_titles = []\n            for related_id in page.relatedPages:\n                # Find the title of the related page\n                related_page = next((p for p in pages if p.id == related_id), None)\n                if related_page:\n                    related_titles.append(f\"[{related_page.title}](#{related_id})\")\n\n            if related_titles:\n                markdown += \"Related topics: \" + \", \".join(related_titles) + \"\\n\\n\"\n\n        # Add page content\n        markdown += f\"{page.content}\\n\\n\"\n        markdown += \"---\\n\\n\"\n\n    return markdown\n\ndef generate_json_export(repo_url: str, pages: List[WikiPage]) -> str:\n    \"\"\"\n    Generate JSON export of wiki pages.\n\n    Args:\n        repo_url: The repository URL\n        pages: List of wiki pages\n\n    Returns:\n        JSON content as string\n    \"\"\"\n    # Create a dictionary with metadata and pages\n    export_data = {\n        \"metadata\": {\n            \"repository\": repo_url,\n            \"generated_at\": datetime.now().isoformat(),\n            \"page_count\": len(pages)\n        },\n        \"pages\": [page.model_dump() for page in pages]\n    }\n\n    # Convert to JSON string with pretty formatting\n    return json.dumps(export_data, indent=2)\n\n# Import the simplified chat implementation\nfrom api.simple_chat import chat_completions_stream\nfrom api.websocket_wiki import handle_websocket_chat\n\n# Add the chat_completions_stream endpoint to the main app\napp.add_api_route(\"/chat/completions/stream\", chat_completions_stream, methods=[\"POST\"])\n\n# Add the WebSocket endpoint\napp.add_websocket_route(\"/ws/chat\", handle_websocket_chat)\n\n# --- Wiki Cache Helper Functions ---\n\nWIKI_CACHE_DIR = os.path.join(get_adalflow_default_root_path(), \"wikicache\")\nos.makedirs(WIKI_CACHE_DIR, exist_ok=True)\n\ndef get_wiki_cache_path(owner: str, repo: str, repo_type: str, language: str) -> str:\n    \"\"\"Generates the file path for a given wiki cache.\"\"\"\n    filename = f\"deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json\"\n    return os.path.join(WIKI_CACHE_DIR, filename)\n\nasync def read_wiki_cache(owner: str, repo: str, repo_type: str, language: str) -> Optional[WikiCacheData]:\n    \"\"\"Reads wiki cache data from the file system.\"\"\"\n    cache_path = get_wiki_cache_path(owner, repo, repo_type, language)\n    if os.path.exists(cache_path):\n        try:\n            with open(cache_path, 'r', encoding='utf-8') as f:\n                data = json.load(f)\n                return WikiCacheData(**data)\n        except Exception as e:\n            logger.error(f\"Error reading wiki cache from {cache_path}: {e}\")\n            return None\n    return None\n\nasync def save_wiki_cache(data: WikiCacheRequest) -> bool:\n    \"\"\"Saves wiki cache data to the file system.\"\"\"\n    cache_path = get_wiki_cache_path(data.repo.owner, data.repo.repo, data.repo.type, data.language)\n    logger.info(f\"Attempting to save wiki cache. Path: {cache_path}\")\n    try:\n        payload = WikiCacheData(\n            wiki_structure=data.wiki_structure,\n            generated_pages=data.generated_pages,\n            repo=data.repo,\n            provider=data.provider,\n            model=data.model\n        )\n        # Log size of data to be cached for debugging (avoid logging full content if large)\n        try:\n            payload_json = payload.model_dump_json()\n            payload_size = len(payload_json.encode('utf-8'))\n            logger.info(f\"Payload prepared for caching. Size: {payload_size} bytes.\")\n        except Exception as ser_e:\n            logger.warning(f\"Could not serialize payload for size logging: {ser_e}\")\n\n\n        logger.info(f\"Writing cache file to: {cache_path}\")\n        with open(cache_path, 'w', encoding='utf-8') as f:\n            json.dump(payload.model_dump(), f, indent=2)\n        logger.info(f\"Wiki cache successfully saved to {cache_path}\")\n        return True\n    except IOError as e:\n        logger.error(f\"IOError saving wiki cache to {cache_path}: {e.strerror} (errno: {e.errno})\", exc_info=True)\n        return False\n    except Exception as e:\n        logger.error(f\"Unexpected error saving wiki cache to {cache_path}: {e}\", exc_info=True)\n        return False\n\n# --- Wiki Cache API Endpoints ---\n\n@app.get(\"/api/wiki_cache\", response_model=Optional[WikiCacheData])\nasync def get_cached_wiki(\n    owner: str = Query(..., description=\"Repository owner\"),\n    repo: str = Query(..., description=\"Repository name\"),\n    repo_type: str = Query(..., description=\"Repository type (e.g., github, gitlab)\"),\n    language: str = Query(..., description=\"Language of the wiki content\")\n):\n    \"\"\"\n    Retrieves cached wiki data (structure and generated pages) for a repository.\n    \"\"\"\n    # Language validation\n    supported_langs = configs[\"lang_config\"][\"supported_languages\"]\n    if not supported_langs.__contains__(language):\n        language = configs[\"lang_config\"][\"default\"]\n\n    logger.info(f\"Attempting to retrieve wiki cache for {owner}/{repo} ({repo_type}), lang: {language}\")\n    cached_data = await read_wiki_cache(owner, repo, repo_type, language)\n    if cached_data:\n        return cached_data\n    else:\n        # Return 200 with null body if not found, as frontend expects this behavior\n        # Or, raise HTTPException(status_code=404, detail=\"Wiki cache not found\") if preferred\n        logger.info(f\"Wiki cache not found for {owner}/{repo} ({repo_type}), lang: {language}\")\n        return None\n\n@app.post(\"/api/wiki_cache\")\nasync def store_wiki_cache(request_data: WikiCacheRequest):\n    \"\"\"\n    Stores generated wiki data (structure and pages) to the server-side cache.\n    \"\"\"\n    # Language validation\n    supported_langs = configs[\"lang_config\"][\"supported_languages\"]\n\n    if not supported_langs.__contains__(request_data.language):\n        request_data.language = configs[\"lang_config\"][\"default\"]\n\n    logger.info(f\"Attempting to save wiki cache for {request_data.repo.owner}/{request_data.repo.repo} ({request_data.repo.type}), lang: {request_data.language}\")\n    success = await save_wiki_cache(request_data)\n    if success:\n        return {\"message\": \"Wiki cache saved successfully\"}\n    else:\n        raise HTTPException(status_code=500, detail=\"Failed to save wiki cache\")\n\n@app.delete(\"/api/wiki_cache\")\nasync def delete_wiki_cache(\n    owner: str = Query(..., description=\"Repository owner\"),\n    repo: str = Query(..., description=\"Repository name\"),\n    repo_type: str = Query(..., description=\"Repository type (e.g., github, gitlab)\"),\n    language: str = Query(..., description=\"Language of the wiki content\"),\n    authorization_code: Optional[str] = Query(None, description=\"Authorization code\")\n):\n    \"\"\"\n    Deletes a specific wiki cache from the file system.\n    \"\"\"\n    # Language validation\n    supported_langs = configs[\"lang_config\"][\"supported_languages\"]\n    if not supported_langs.__contains__(language):\n        raise HTTPException(status_code=400, detail=\"Language is not supported\")\n\n    if WIKI_AUTH_MODE:\n        logger.info(\"check the authorization code\")\n        if not authorization_code or WIKI_AUTH_CODE != authorization_code:\n            raise HTTPException(status_code=401, detail=\"Authorization code is invalid\")\n\n    logger.info(f\"Attempting to delete wiki cache for {owner}/{repo} ({repo_type}), lang: {language}\")\n    cache_path = get_wiki_cache_path(owner, repo, repo_type, language)\n\n    if os.path.exists(cache_path):\n        try:\n            os.remove(cache_path)\n            logger.info(f\"Successfully deleted wiki cache: {cache_path}\")\n            return {\"message\": f\"Wiki cache for {owner}/{repo} ({language}) deleted successfully\"}\n        except Exception as e:\n            logger.error(f\"Error deleting wiki cache {cache_path}: {e}\")\n            raise HTTPException(status_code=500, detail=f\"Failed to delete wiki cache: {str(e)}\")\n    else:\n        logger.warning(f\"Wiki cache not found, cannot delete: {cache_path}\")\n        raise HTTPException(status_code=404, detail=\"Wiki cache not found\")\n\n@app.get(\"/health\")\nasync def health_check():\n    \"\"\"Health check endpoint for Docker and monitoring\"\"\"\n    return {\n        \"status\": \"healthy\",\n        \"timestamp\": datetime.now().isoformat(),\n        \"service\": \"deepwiki-api\"\n    }\n\n@app.get(\"/\")\nasync def root():\n    \"\"\"Root endpoint to check if the API is running and list available endpoints dynamically.\"\"\"\n    # Collect routes dynamically from the FastAPI app\n    endpoints = {}\n    for route in app.routes:\n        if hasattr(route, \"methods\") and hasattr(route, \"path\"):\n            # Skip docs and static routes\n            if route.path in [\"/openapi.json\", \"/docs\", \"/redoc\", \"/favicon.ico\"]:\n                continue\n            # Group endpoints by first path segment\n            path_parts = route.path.strip(\"/\").split(\"/\")\n            group = path_parts[0].capitalize() if path_parts[0] else \"Root\"\n            method_list = list(route.methods - {\"HEAD\", \"OPTIONS\"})\n            for method in method_list:\n                endpoints.setdefault(group, []).append(f\"{method} {route.path}\")\n\n    # Optionally, sort endpoints for readability\n    for group in endpoints:\n        endpoints[group].sort()\n\n    return {\n        \"message\": \"Welcome to Streaming API\",\n        \"version\": \"1.0.0\",\n        \"endpoints\": endpoints\n    }\n\n# --- Processed Projects Endpoint --- (New Endpoint)\n@app.get(\"/api/processed_projects\", response_model=List[ProcessedProjectEntry])\nasync def get_processed_projects():\n    \"\"\"\n    Lists all processed projects found in the wiki cache directory.\n    Projects are identified by files named like: deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json\n    \"\"\"\n    project_entries: List[ProcessedProjectEntry] = []\n    # WIKI_CACHE_DIR is already defined globally in the file\n\n    try:\n        if not os.path.exists(WIKI_CACHE_DIR):\n            logger.info(f\"Cache directory {WIKI_CACHE_DIR} not found. Returning empty list.\")\n            return []\n\n        logger.info(f\"Scanning for project cache files in: {WIKI_CACHE_DIR}\")\n        filenames = await asyncio.to_thread(os.listdir, WIKI_CACHE_DIR) # Use asyncio.to_thread for os.listdir\n\n        for filename in filenames:\n            if filename.startswith(\"deepwiki_cache_\") and filename.endswith(\".json\"):\n                file_path = os.path.join(WIKI_CACHE_DIR, filename)\n                try:\n                    stats = await asyncio.to_thread(os.stat, file_path) # Use asyncio.to_thread for os.stat\n                    parts = filename.replace(\"deepwiki_cache_\", \"\").replace(\".json\", \"\").split('_')\n\n                    # Expecting repo_type_owner_repo_language\n                    # Example: deepwiki_cache_github_AsyncFuncAI_deepwiki-open_en.json\n                    # parts = [github, AsyncFuncAI, deepwiki-open, en]\n                    if len(parts) >= 4:\n                        repo_type = parts[0]\n                        owner = parts[1]\n                        language = parts[-1] # language is the last part\n                        repo = \"_\".join(parts[2:-1]) # repo can contain underscores\n\n                        project_entries.append(\n                            ProcessedProjectEntry(\n                                id=filename,\n                                owner=owner,\n                                repo=repo,\n                                name=f\"{owner}/{repo}\",\n                                repo_type=repo_type,\n                                submittedAt=int(stats.st_mtime * 1000), # Convert to milliseconds\n                                language=language\n                            )\n                        )\n                    else:\n                        logger.warning(f\"Could not parse project details from filename: {filename}\")\n                except Exception as e:\n                    logger.error(f\"Error processing file {file_path}: {e}\")\n                    continue # Skip this file on error\n\n        # Sort by most recent first\n        project_entries.sort(key=lambda p: p.submittedAt, reverse=True)\n        logger.info(f\"Found {len(project_entries)} processed project entries.\")\n        return project_entries\n\n    except Exception as e:\n        logger.error(f\"Error listing processed projects from {WIKI_CACHE_DIR}: {e}\", exc_info=True)\n        raise HTTPException(status_code=500, detail=\"Failed to list processed projects from server cache.\")\n"
  },
  {
    "path": "api/azureai_client.py",
    "content": "\"\"\"AzureOpenAI ModelClient integration.\"\"\"\n\nimport os\nfrom typing import (\n    Dict,\n    Sequence,\n    Optional,\n    List,\n    Any,\n    TypeVar,\n    Callable,\n    Generator,\n    Union,\n    Literal,\n)\nimport re\n\nimport logging\nimport backoff\n\n# optional import\nfrom adalflow.utils.lazy_import import safe_import, OptionalPackages\n\nimport sys\n\nopenai = safe_import(OptionalPackages.OPENAI.value[0], OptionalPackages.OPENAI.value[1])\n# Importing all Azure packages together\nazure_modules = safe_import(\n    OptionalPackages.AZURE.value[0],  # List of package names\n    OptionalPackages.AZURE.value[1],  # Error message\n)\n# Manually add each module to sys.modules to make them available globally as if imported normally\nazure_module_names = OptionalPackages.AZURE.value[0]\nfor name, module in zip(azure_module_names, azure_modules):\n    sys.modules[name] = module\n\n# Use the modules as if they were imported normally\nfrom azure.identity import DefaultAzureCredential, get_bearer_token_provider\n\n# from azure.core.credentials import AccessToken\nfrom openai import AzureOpenAI, AsyncAzureOpenAI, Stream\nfrom openai import (\n    APITimeoutError,\n    InternalServerError,\n    RateLimitError,\n    UnprocessableEntityError,\n    BadRequestError,\n)\nfrom openai.types import (\n    Completion,\n    CreateEmbeddingResponse,\n)\nfrom openai.types.chat import ChatCompletionChunk, ChatCompletion\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import (\n    ModelType,\n    EmbedderOutput,\n    TokenLogProb,\n    CompletionUsage,\n    GeneratorOutput,\n)\nfrom adalflow.components.model_client.utils import parse_embedding_response\n\nlog = logging.getLogger(__name__)\nT = TypeVar(\"T\")\n\n\n__all__ = [\"AzureAIClient\"]\n\n# TODO: this overlaps with openai client largely, might need to refactor to subclass openai client to simplify the code\n\n\n# completion parsing functions and you can combine them into one singple chat completion parser\ndef get_first_message_content(completion: ChatCompletion) -> str:\n    r\"\"\"When we only need the content of the first message.\n    It is the default parser for chat completion.\"\"\"\n    return completion.choices[0].message.content\n\n\n# def _get_chat_completion_usage(completion: ChatCompletion) -> OpenAICompletionUsage:\n#     return completion.usage\n\n\ndef parse_stream_response(completion: ChatCompletionChunk) -> str:\n    r\"\"\"Parse the response of the stream API.\"\"\"\n    return completion.choices[0].delta.content\n\n\ndef handle_streaming_response(generator: Stream[ChatCompletionChunk]):\n    r\"\"\"Handle the streaming response.\"\"\"\n    for completion in generator:\n        log.debug(f\"Raw chunk completion: {completion}\")\n        parsed_content = parse_stream_response(completion)\n        yield parsed_content\n\n\ndef get_all_messages_content(completion: ChatCompletion) -> List[str]:\n    r\"\"\"When the n > 1, get all the messages content.\"\"\"\n    return [c.message.content for c in completion.choices]\n\n\ndef get_probabilities(completion: ChatCompletion) -> List[List[TokenLogProb]]:\n    r\"\"\"Get the probabilities of each token in the completion.\"\"\"\n    log_probs = []\n    for c in completion.choices:\n        content = c.logprobs.content\n        print(content)\n        log_probs_for_choice = []\n        for openai_token_logprob in content:\n            token = openai_token_logprob.token\n            logprob = openai_token_logprob.logprob\n            log_probs_for_choice.append(TokenLogProb(token=token, logprob=logprob))\n        log_probs.append(log_probs_for_choice)\n    return log_probs\n\n\nclass AzureAIClient(ModelClient):\n    __doc__ = r\"\"\"\n    A client wrapper for interacting with Azure OpenAI's API.\n\n    This class provides support for both embedding and chat completion API calls.\n    Users can use this class to simplify their interactions with Azure OpenAI models\n    through the `Embedder` and `Generator` components.\n\n    **Initialization:**\n\n    You can initialize the `AzureAIClient` with either an API key or Azure Active Directory (AAD) token\n    authentication. It is recommended to set environment variables for sensitive data like API keys.\n\n    Args:\n        api_key (Optional[str]): Azure OpenAI API key. Default is None.\n        api_version (Optional[str]): API version to use. Default is None.\n        azure_endpoint (Optional[str]): Azure OpenAI endpoint URL. Default is None.\n        credential (Optional[DefaultAzureCredential]): Azure AD credential for token-based authentication. Default is None.\n        chat_completion_parser (Callable[[Completion], Any]): Function to parse chat completions. Default is `get_first_message_content`.\n        input_type (Literal[\"text\", \"messages\"]): Format for input, either \"text\" or \"messages\". Default is \"text\".\n\n    **Setup Instructions:**\n\n    - **Using API Key:**\n      Set up the following environment variables:\n      ```bash\n      export AZURE_OPENAI_API_KEY=\"your_api_key\"\n      export AZURE_OPENAI_ENDPOINT=\"your_endpoint\"\n      export AZURE_OPENAI_VERSION=\"your_version\"\n      ```\n\n    - **Using Azure AD Token:**\n      Ensure you have configured Azure AD credentials. The `DefaultAzureCredential` will automatically use your configured credentials.\n\n    **Example Usage:**\n\n    .. code-block:: python\n\n        from azure.identity import DefaultAzureCredential\n        from your_module import AzureAIClient  # Adjust import based on your module name\n\n        # Initialize with API key\n        client = AzureAIClient(\n            api_key=\"your_api_key\",\n            api_version=\"2023-05-15\",\n            azure_endpoint=\"https://your-endpoint.openai.azure.com/\"\n        )\n\n        # Or initialize with Azure AD token\n        client = AzureAIClient(\n            api_version=\"2023-05-15\",\n            azure_endpoint=\"https://your-endpoint.openai.azure.com/\",\n            credential=DefaultAzureCredential()\n        )\n\n        # Example call to the chat completion API\n        api_kwargs = {\n            \"model\": \"gpt-3.5-turbo\",\n            \"messages\": [{\"role\": \"user\", \"content\": \"What is the meaning of life?\"}],\n            \"stream\": True\n        }\n        response = client.call(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n\n        for chunk in response:\n            print(chunk)\n\n\n    **Notes:**\n    - Ensure that the API key or credentials are correctly set up and accessible to avoid authentication errors.\n    - Use `chat_completion_parser` to define how to extract and handle the chat completion responses.\n    - The `input_type` parameter determines how input is formatted for the API call.\n\n    **References:**\n    - [Azure OpenAI API Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview)\n    - [OpenAI API Documentation](https://platform.openai.com/docs/guides/text-generation)\n    \"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        api_version: Optional[str] = None,\n        azure_endpoint: Optional[str] = None,\n        credential: Optional[DefaultAzureCredential] = None,\n        chat_completion_parser: Callable[[Completion], Any] = None,\n        input_type: Literal[\"text\", \"messages\"] = \"text\",\n    ):\n        r\"\"\"It is recommended to set the API_KEY into the  environment variable instead of passing it as an argument.\n\n\n        Initializes the Azure OpenAI client with either API key or AAD token authentication.\n\n        Args:\n            api_key: Azure OpenAI API key.\n            api_version: Azure OpenAI API version.\n            azure_endpoint: Azure OpenAI endpoint.\n            credential: Azure AD credential for token-based authentication.\n            chat_completion_parser: Function to parse chat completions.\n            input_type: Input format, either \"text\" or \"messages\".\n\n        \"\"\"\n        super().__init__()\n\n        # added api_type azure for azure Ai\n        self.api_type = \"azure\"\n        self._api_key = api_key\n        self._apiversion = api_version\n        self._azure_endpoint = azure_endpoint\n        self._credential = credential\n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # only initialize if the async call is called\n        self.chat_completion_parser = (\n            chat_completion_parser or get_first_message_content\n        )\n        self._input_type = input_type\n\n    def init_sync_client(self):\n        api_key = self._api_key or os.getenv(\"AZURE_OPENAI_API_KEY\")\n        azure_endpoint = self._azure_endpoint or os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n        api_version = self._apiversion or os.getenv(\"AZURE_OPENAI_VERSION\")\n        # credential = self._credential or DefaultAzureCredential\n        if not azure_endpoint:\n            raise ValueError(\"Environment variable AZURE_OPENAI_ENDPOINT must be set\")\n        if not api_version:\n            raise ValueError(\"Environment variable AZURE_OPENAI_VERSION must be set\")\n\n        if api_key:\n            return AzureOpenAI(\n                api_key=api_key, azure_endpoint=azure_endpoint, api_version=api_version\n            )\n        elif self._credential:\n            # credential = DefaultAzureCredential()\n            token_provider = get_bearer_token_provider(\n                DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n            )\n            return AzureOpenAI(\n                azure_ad_token_provider=token_provider,\n                azure_endpoint=azure_endpoint,\n                api_version=api_version,\n            )\n        else:\n            raise ValueError(\n                \"Environment variable AZURE_OPENAI_API_KEY must be set or credential must be provided\"\n            )\n\n    def init_async_client(self):\n        api_key = self._api_key or os.getenv(\"AZURE_OPENAI_API_KEY\")\n        azure_endpoint = self._azure_endpoint or os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n        api_version = self._apiversion or os.getenv(\"AZURE_OPENAI_VERSION\")\n        # credential = self._credential or DefaultAzureCredential()\n        if not azure_endpoint:\n            raise ValueError(\"Environment variable AZURE_OPENAI_ENDPOINT must be set\")\n        if not api_version:\n            raise ValueError(\"Environment variable AZURE_OPENAI_VERSION must be set\")\n\n        if api_key:\n            return AsyncAzureOpenAI(\n                api_key=api_key, azure_endpoint=azure_endpoint, api_version=api_version\n            )\n        elif self._credential:\n            # credential = DefaultAzureCredential()\n            token_provider = get_bearer_token_provider(\n                DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n            )\n            return AsyncAzureOpenAI(\n                azure_ad_token_provider=token_provider,\n                azure_endpoint=azure_endpoint,\n                api_version=api_version,\n            )\n        else:\n            raise ValueError(\n                \"Environment variable AZURE_OPENAI_API_KEY must be set or credential must be provided\"\n            )\n\n    # def _parse_chat_completion(self, completion: ChatCompletion) -> \"GeneratorOutput\":\n    #     # TODO: raw output it is better to save the whole completion as a source of truth instead of just the message\n    #     try:\n    #         data = self.chat_completion_parser(completion)\n    #         usage = self.track_completion_usage(completion)\n    #         return GeneratorOutput(\n    #             data=data, error=None, raw_response=str(data), usage=usage\n    #         )\n    #     except Exception as e:\n    #         log.error(f\"Error parsing the completion: {e}\")\n    #         return GeneratorOutput(data=None, error=str(e), raw_response=completion)\n\n    def parse_chat_completion(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> \"GeneratorOutput\":\n        \"\"\"Parse the completion, and put it into the raw_response.\"\"\"\n        log.debug(f\"completion: {completion}, parser: {self.chat_completion_parser}\")\n        try:\n            data = self.chat_completion_parser(completion)\n            usage = self.track_completion_usage(completion)\n            return GeneratorOutput(\n                data=None, error=None, raw_response=data, usage=usage\n            )\n        except Exception as e:\n            log.error(f\"Error parsing the completion: {e}\")\n            return GeneratorOutput(data=None, error=str(e), raw_response=completion)\n\n    def track_completion_usage(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> CompletionUsage:\n        if isinstance(completion, ChatCompletion):\n            usage: CompletionUsage = CompletionUsage(\n                completion_tokens=completion.usage.completion_tokens,\n                prompt_tokens=completion.usage.prompt_tokens,\n                total_tokens=completion.usage.total_tokens,\n            )\n            return usage\n        else:\n            raise NotImplementedError(\n                \"streaming completion usage tracking is not implemented\"\n            )\n\n    def parse_embedding_response(\n        self, response: CreateEmbeddingResponse\n    ) -> EmbedderOutput:\n        r\"\"\"Parse the embedding response to a structure AdalFlow components can understand.\n\n        Should be called in ``Embedder``.\n        \"\"\"\n        try:\n            return parse_embedding_response(response)\n        except Exception as e:\n            log.error(f\"Error parsing the embedding response: {e}\")\n            return EmbedderOutput(data=[], error=str(e), raw_response=response)\n\n    def convert_inputs_to_api_kwargs(\n        self,\n        input: Optional[Any] = None,\n        model_kwargs: Dict = {},\n        model_type: ModelType = ModelType.UNDEFINED,\n    ) -> Dict:\n        r\"\"\"\n        Specify the API input type and output api_kwargs that will be used in _call and _acall methods.\n        Convert the Component's standard input, and system_input(chat model) and model_kwargs into API-specific format\n        \"\"\"\n\n        final_model_kwargs = model_kwargs.copy()\n        if model_type == ModelType.EMBEDDER:\n            if isinstance(input, str):\n                input = [input]\n            # convert input to input\n            if not isinstance(input, Sequence):\n                raise TypeError(\"input must be a sequence of text\")\n            final_model_kwargs[\"input\"] = input\n        elif model_type == ModelType.LLM:\n            # convert input to messages\n            messages: List[Dict[str, str]] = []\n\n            if self._input_type == \"messages\":\n                system_start_tag = \"<START_OF_SYSTEM_PROMPT>\"\n                system_end_tag = \"<END_OF_SYSTEM_PROMPT>\"\n                user_start_tag = \"<START_OF_USER_PROMPT>\"\n                user_end_tag = \"<END_OF_USER_PROMPT>\"\n                pattern = f\"{system_start_tag}(.*?){system_end_tag}{user_start_tag}(.*?){user_end_tag}\"\n                # Compile the regular expression\n                regex = re.compile(pattern)\n                # Match the pattern\n                match = regex.search(input)\n                system_prompt, input_str = None, None\n\n                if match:\n                    system_prompt = match.group(1)\n                    input_str = match.group(2)\n\n                else:\n                    print(\"No match found.\")\n                if system_prompt and input_str:\n                    messages.append({\"role\": \"system\", \"content\": system_prompt})\n                    messages.append({\"role\": \"user\", \"content\": input_str})\n            if len(messages) == 0:\n                messages.append({\"role\": \"system\", \"content\": input})\n            final_model_kwargs[\"messages\"] = messages\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n        return final_model_kwargs\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED):\n        \"\"\"\n        kwargs is the combined input and model_kwargs.  Support streaming call.\n        \"\"\"\n        log.info(f\"api_kwargs: {api_kwargs}\")\n        if model_type == ModelType.EMBEDDER:\n            return self.sync_client.embeddings.create(**api_kwargs)\n        elif model_type == ModelType.LLM:\n            if \"stream\" in api_kwargs and api_kwargs.get(\"stream\", False):\n                log.debug(\"streaming call\")\n                self.chat_completion_parser = handle_streaming_response\n                return self.sync_client.chat.completions.create(**api_kwargs)\n            return self.sync_client.chat.completions.create(**api_kwargs)\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    async def acall(\n        self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED\n    ):\n        \"\"\"\n        kwargs is the combined input and model_kwargs\n        \"\"\"\n        if self.async_client is None:\n            self.async_client = self.init_async_client()\n        if model_type == ModelType.EMBEDDER:\n            return await self.async_client.embeddings.create(**api_kwargs)\n        elif model_type == ModelType.LLM:\n            return await self.async_client.chat.completions.create(**api_kwargs)\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @classmethod\n    def from_dict(cls: type[T], data: Dict[str, Any]) -> T:\n        obj = super().from_dict(data)\n        # recreate the existing clients\n        obj.sync_client = obj.init_sync_client()\n        obj.async_client = obj.init_async_client()\n        return obj\n\n    def to_dict(self) -> Dict[str, Any]:\n        r\"\"\"Convert the component to a dictionary.\"\"\"\n        # TODO: not exclude but save yes or no for recreating the clients\n        exclude = [\n            \"sync_client\",\n            \"async_client\",\n        ]  # unserializable object\n        output = super().to_dict(exclude=exclude)\n        return output\n\n\n# if __name__ == \"__main__\":\n#     from adalflow.core import Generator\n#     from adalflow.utils import setup_env, get_logger\n\n#     log = get_logger(level=\"DEBUG\")\n\n#     setup_env()\n#     prompt_kwargs = {\"input_str\": \"What is the meaning of life?\"}\n\n#     gen = Generator(\n#         model_client=OpenAIClient(),\n#         model_kwargs={\"model\": \"gpt-3.5-turbo\", \"stream\": True},\n#     )\n#     gen_response = gen(prompt_kwargs)\n#     print(f\"gen_response: {gen_response}\")\n\n#     for genout in gen_response.data:\n#         print(f\"genout: {genout}\")"
  },
  {
    "path": "api/bedrock_client.py",
    "content": "\"\"\"AWS Bedrock ModelClient integration.\"\"\"\n\nimport os\nimport json\nimport logging\nimport boto3\nimport botocore\nimport backoff\nfrom typing import Dict, Any, Optional, List, Generator, Union, AsyncGenerator, Sequence\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import ModelType, GeneratorOutput, EmbedderOutput\n\n# Configure logging\nfrom api.logging_config import setup_logging\n\nsetup_logging()\nlog = logging.getLogger(__name__)\n\nclass BedrockClient(ModelClient):\n    __doc__ = r\"\"\"A component wrapper for the AWS Bedrock API client.\n\n    AWS Bedrock provides a unified API that gives access to various foundation models\n    including Amazon's own models and third-party models like Anthropic Claude.\n\n    Example:\n        ```python\n        from api.bedrock_client import BedrockClient\n\n        client = BedrockClient()\n        generator = adal.Generator(\n            model_client=client,\n            model_kwargs={\"model\": \"anthropic.claude-3-sonnet-20240229-v1:0\"}\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        aws_access_key_id: Optional[str] = None,\n        aws_secret_access_key: Optional[str] = None,\n        aws_session_token: Optional[str] = None,\n        aws_region: Optional[str] = None,\n        aws_role_arn: Optional[str] = None,\n        *args,\n        **kwargs\n    ) -> None:\n        \"\"\"Initialize the AWS Bedrock client.\n        \n        Args:\n            aws_access_key_id: AWS access key ID. If not provided, will use environment variable AWS_ACCESS_KEY_ID.\n            aws_secret_access_key: AWS secret access key. If not provided, will use environment variable AWS_SECRET_ACCESS_KEY.\n            aws_session_token: AWS session token. If not provided, will use environment variable AWS_SESSION_TOKEN.\n            aws_region: AWS region. If not provided, will use environment variable AWS_REGION.\n            aws_role_arn: AWS IAM role ARN for role-based authentication. If not provided, will use environment variable AWS_ROLE_ARN.\n        \"\"\"\n        super().__init__(*args, **kwargs)\n        from api.config import (\n            AWS_ACCESS_KEY_ID,\n            AWS_SECRET_ACCESS_KEY,\n            AWS_SESSION_TOKEN,\n            AWS_REGION,\n            AWS_ROLE_ARN,\n        )\n\n        self.aws_access_key_id = aws_access_key_id or AWS_ACCESS_KEY_ID\n        self.aws_secret_access_key = aws_secret_access_key or AWS_SECRET_ACCESS_KEY\n        self.aws_session_token = aws_session_token or AWS_SESSION_TOKEN\n        self.aws_region = aws_region or AWS_REGION or \"us-east-1\"\n        self.aws_role_arn = aws_role_arn or AWS_ROLE_ARN\n        \n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # Initialize async client only when needed\n\n    @classmethod\n    def from_dict(cls, data: Dict[str, Any]):\n        \"\"\"Create an instance from a dictionary.\"\"\"\n        return cls(**data)\n\n    def to_dict(self) -> Dict[str, Any]:\n        \"\"\"Convert to dictionary.\"\"\"\n        return {\n            \"aws_access_key_id\": self.aws_access_key_id,\n            \"aws_secret_access_key\": self.aws_secret_access_key,\n            \"aws_session_token\": self.aws_session_token,\n            \"aws_region\": self.aws_region,\n            \"aws_role_arn\": self.aws_role_arn,\n        }\n\n    def __getstate__(self):\n        \"\"\"\n        Customize serialization to exclude non-picklable client objects.\n        This method is called by pickle when saving the object's state.\n        \"\"\"\n        state = self.__dict__.copy()\n        # Remove the unpicklable client instances\n        if 'sync_client' in state:\n            del state['sync_client']\n        if 'async_client' in state:\n            del state['async_client']\n        return state\n\n    def __setstate__(self, state):\n        \"\"\"\n        Customize deserialization to re-create the client objects.\n        This method is called by pickle when loading the object's state.\n        \"\"\"\n        self.__dict__.update(state)\n        # Re-initialize the clients after unpickling\n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # It will be lazily initialized when acall is used\n\n    def init_sync_client(self):\n        \"\"\"Initialize the synchronous AWS Bedrock client.\"\"\"\n        try:\n            # Create a session with the provided credentials\n            session = boto3.Session(\n                aws_access_key_id=self.aws_access_key_id,\n                aws_secret_access_key=self.aws_secret_access_key,\n                aws_session_token=self.aws_session_token,\n                region_name=self.aws_region\n            )\n            \n            # If a role ARN is provided, assume that role\n            if self.aws_role_arn:\n                sts_client = session.client('sts')\n                assumed_role = sts_client.assume_role(\n                    RoleArn=self.aws_role_arn,\n                    RoleSessionName=\"DeepWikiBedrockSession\"\n                )\n                credentials = assumed_role['Credentials']\n                \n                # Create a new session with the assumed role credentials\n                session = boto3.Session(\n                    aws_access_key_id=credentials['AccessKeyId'],\n                    aws_secret_access_key=credentials['SecretAccessKey'],\n                    aws_session_token=credentials['SessionToken'],\n                    region_name=self.aws_region\n                )\n            \n            # Create the Bedrock client\n            bedrock_runtime = session.client(\n                service_name='bedrock-runtime',\n                region_name=self.aws_region\n            )\n            \n            return bedrock_runtime\n            \n        except Exception as e:\n            log.error(f\"Error initializing AWS Bedrock client: {str(e)}\")\n            # Return None to indicate initialization failure\n            return None\n\n    def init_async_client(self):\n        \"\"\"Initialize the asynchronous AWS Bedrock client.\n        \n        Note: boto3 doesn't have native async support, so we'll use the sync client\n        in async methods and handle async behavior at a higher level.\n        \"\"\"\n        # For now, just return the sync client\n        return self.sync_client\n\n    def _get_model_provider(self, model_id: str) -> str:\n        \"\"\"Extract the provider from the model ID.\n        \n        Args:\n            model_id: The model inference ID, e.g., \"anthropic.claude-3-sonnet-20240229-v1:0\", \"global.anthropic.claude-sonnet-4-5-20250929-v1:0\", or \"global.cohere.embed-v4:0\"\n            \n        Returns:\n            The provider name, e.g., \"anthropic\"\n        \"\"\"\n        seg = model_id.split(\".\")\n        if len(seg) >= 3:\n            # regional format\n            return seg[1]\n        elif len(seg) == 2:\n            # non-regional format\n            return seg[0]\n        else:\n            # Default to Amazon if format is unexpected\n            return \"amazon\"\n\n    def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) -> Dict[str, Any]:\n        \"\"\"Format the prompt according to the provider's requirements.\n        \n        Args:\n            provider: The provider name, e.g., \"anthropic\"\n            prompt: The prompt text\n            messages: Optional list of messages for chat models\n            \n        Returns:\n            A dictionary with the formatted prompt\n        \"\"\"\n        if provider == \"anthropic\":\n            # Format for Claude models\n            if messages:\n                # Format as a conversation\n                formatted_messages = []\n                for msg in messages:\n                    role = \"user\" if msg.get(\"role\") == \"user\" else \"assistant\"\n                    formatted_messages.append({\n                        \"role\": role,\n                        \"content\": [{\"type\": \"text\", \"text\": msg.get(\"content\", \"\")}]\n                    })\n                return {\n                    \"anthropic_version\": \"bedrock-2023-05-31\",\n                    \"messages\": formatted_messages,\n                    \"max_tokens\": 4096\n                }\n            else:\n                # Format as a single prompt\n                return {\n                    \"anthropic_version\": \"bedrock-2023-05-31\",\n                    \"messages\": [\n                        {\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": prompt}]}\n                    ],\n                    \"max_tokens\": 4096\n                }\n        elif provider == \"amazon\":\n            # Format for Amazon Titan models\n            return {\n                \"inputText\": prompt,\n                \"textGenerationConfig\": {\n                    \"maxTokenCount\": 4096,\n                    \"stopSequences\": [],\n                    \"temperature\": 0.7,\n                    \"topP\": 0.8\n                }\n            }\n        elif provider == \"cohere\":\n            # Format for Cohere models\n            return {\n                \"prompt\": prompt,\n                \"max_tokens\": 4096,\n                \"temperature\": 0.7,\n                \"p\": 0.8\n            }\n        elif provider == \"ai21\":\n            # Format for AI21 models\n            return {\n                \"prompt\": prompt,\n                \"maxTokens\": 4096,\n                \"temperature\": 0.7,\n                \"topP\": 0.8\n            }\n        else:\n            # Default format\n            return {\"prompt\": prompt}\n\n    def _extract_response_text(self, provider: str, response: Dict[str, Any]) -> str:\n        \"\"\"Extract the generated text from the response.\n        \n        Args:\n            provider: The provider name, e.g., \"anthropic\"\n            response: The response from the Bedrock API\n            \n        Returns:\n            The generated text\n        \"\"\"\n        if provider == \"anthropic\":\n            return response.get(\"content\", [{}])[0].get(\"text\", \"\")\n        elif provider == \"amazon\":\n            return response.get(\"results\", [{}])[0].get(\"outputText\", \"\")\n        elif provider == \"cohere\":\n            return response.get(\"generations\", [{}])[0].get(\"text\", \"\")\n        elif provider == \"ai21\":\n            return response.get(\"completions\", [{}])[0].get(\"data\", {}).get(\"text\", \"\")\n        else:\n            # Try to extract text from the response\n            if isinstance(response, dict):\n                for key in [\"text\", \"content\", \"output\", \"completion\"]:\n                    if key in response:\n                        return response[key]\n            return str(response)\n\n    def parse_embedding_response(self, response: Any) -> EmbedderOutput:\n        \"\"\"Parse Bedrock embedding response to EmbedderOutput format.\"\"\"\n        from adalflow.core.types import Embedding\n\n        try:\n            embedding_data: List[Embedding] = []\n\n            if isinstance(response, dict) and \"embeddings\" in response:\n                embeddings = response.get(\"embeddings\") or []\n                embedding_data = [\n                    Embedding(embedding=emb, index=i) for i, emb in enumerate(embeddings)\n                ]\n            elif isinstance(response, dict) and \"embedding\" in response:\n                emb = response.get(\"embedding\") or []\n                embedding_data = [Embedding(embedding=emb, index=0)]\n            else:\n                raise ValueError(f\"Unexpected embedding response type: {type(response)}\")\n\n            return EmbedderOutput(data=embedding_data, error=None, raw_response=response)\n        except Exception as e:\n            log.error(f\"Error parsing Bedrock embedding response: {e}\")\n            return EmbedderOutput(data=[], error=str(e), raw_response=response)\n\n    @backoff.on_exception(\n        backoff.expo,\n        (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError),\n        max_time=5,\n    )\n    def call(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any:\n        \"\"\"Make a synchronous call to the AWS Bedrock API.\"\"\"\n        api_kwargs = api_kwargs or {}\n        \n        # Check if client is initialized\n        if not self.sync_client:\n            error_msg = \"AWS Bedrock client not initialized. Check your AWS credentials and region.\"\n            log.error(error_msg)\n            return error_msg\n        \n        if model_type == ModelType.LLM:\n            model_id = api_kwargs.get(\"model\", \"anthropic.claude-3-sonnet-20240229-v1:0\")\n            provider = self._get_model_provider(model_id)\n            \n            # Get the prompt from api_kwargs\n            prompt = api_kwargs.get(\"input\", \"\")\n            messages = api_kwargs.get(\"messages\")\n            \n            # Format the prompt according to the provider\n            request_body = self._format_prompt_for_provider(provider, prompt, messages)\n            \n            # Add model parameters if provided\n            if \"temperature\" in api_kwargs:\n                if provider == \"anthropic\":\n                    request_body[\"temperature\"] = api_kwargs[\"temperature\"]\n                elif provider == \"amazon\":\n                    request_body[\"textGenerationConfig\"][\"temperature\"] = api_kwargs[\"temperature\"]\n                elif provider == \"cohere\":\n                    request_body[\"temperature\"] = api_kwargs[\"temperature\"]\n                elif provider == \"ai21\":\n                    request_body[\"temperature\"] = api_kwargs[\"temperature\"]\n            \n            if \"top_p\" in api_kwargs:\n                if provider == \"anthropic\":\n                    request_body[\"top_p\"] = api_kwargs[\"top_p\"]\n                elif provider == \"amazon\":\n                    request_body[\"textGenerationConfig\"][\"topP\"] = api_kwargs[\"top_p\"]\n                elif provider == \"cohere\":\n                    request_body[\"p\"] = api_kwargs[\"top_p\"]\n                elif provider == \"ai21\":\n                    request_body[\"topP\"] = api_kwargs[\"top_p\"]\n            \n            # Convert request body to JSON\n            body = json.dumps(request_body)\n            \n            try:\n                # Make the API call\n                response = self.sync_client.invoke_model(\n                    modelId=model_id,\n                    body=body\n                )\n                \n                # Parse the response\n                response_body = json.loads(response[\"body\"].read())\n                \n                # Extract the generated text\n                generated_text = self._extract_response_text(provider, response_body)\n                \n                return generated_text\n                \n            except Exception as e:\n                log.error(f\"Error calling AWS Bedrock API: {str(e)}\")\n                return f\"Error: {str(e)}\"\n        elif model_type == ModelType.EMBEDDER:\n            model_id = api_kwargs.get(\"model\", \"amazon.titan-embed-text-v2:0\")\n            provider = self._get_model_provider(model_id)\n\n            texts = api_kwargs.get(\"input\", [])\n\n            model_kwargs = api_kwargs.get(\"model_kwargs\") or {}\n\n            embeddings: List[List[float]] = []\n            raw_responses: List[Dict[str, Any]] = []\n\n            if provider == \"amazon\":\n                # Amazon Titan Embed Text does not support batch; send one at a time.\n                for text in texts:\n                    request_body: Dict[str, Any] = {\"inputText\": text}\n\n                    dimensions = model_kwargs.get(\"dimensions\")\n                    if dimensions is not None:\n                        request_body[\"dimensions\"] = int(dimensions)\n\n                    normalize = model_kwargs.get(\"normalize\")\n                    if normalize is not None:\n                        request_body[\"normalize\"] = bool(normalize)\n\n                    # Make the API call\n                    response = self.sync_client.invoke_model(\n                        modelId=model_id,\n                        body=json.dumps(request_body),\n                    )\n\n                    # Parse the response\n                    response_body = json.loads(response[\"body\"].read())\n                    raw_responses.append(response_body)\n\n                    emb = response_body.get(\"embedding\")\n                    if emb is None:\n                        raise ValueError(f\"Embedding not found in response: {response_body}\")\n                    embeddings.append(emb)\n\n            elif provider == \"cohere\":\n                # Cohere supports batch; send all texts at once.\n                request_body = {\n                    \"texts\": texts,\n                    \"input_type\": model_kwargs.get(\"input_type\") or \"search_document\",\n                }\n\n                # Make the API call\n                response = self.sync_client.invoke_model(\n                    modelId=model_id,\n                    body=json.dumps(request_body),\n                )\n\n                # Parse the response\n                response_body = json.loads(response[\"body\"].read())\n                raw_responses.append(response_body)\n\n                batch_embeddings = response_body.get(\"embeddings\")\n                if isinstance(batch_embeddings, list):\n                    embeddings = batch_embeddings\n                elif isinstance(batch_embeddings, dict) and \"float\" in batch_embeddings:\n                    embeddings = batch_embeddings[\"float\"]\n                else:\n                    raise ValueError(f\"Embeddings not found in response: {response_body}\")\n            else:\n                raise NotImplementedError(f\"Embedding provider '{provider}' is not supported by the Bedrock client.\")\n            return {\"embeddings\": embeddings, \"raw_responses\": raw_responses}\n        else:\n            raise ValueError(f\"Model type {model_type} is not supported by AWS Bedrock client\")\n\n    async def acall(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any:\n        \"\"\"Make an asynchronous call to the AWS Bedrock API.\"\"\"\n        # For now, just call the sync method\n        # In a real implementation, you would use an async library or run the sync method in a thread pool\n        return self.call(api_kwargs, model_type)\n\n    def convert_inputs_to_api_kwargs(\n        self, input: Any = None, model_kwargs: Dict = None, model_type: ModelType = None\n    ) -> Dict:\n        \"\"\"Convert inputs to API kwargs for AWS Bedrock.\"\"\"\n        model_kwargs = model_kwargs or {}\n        api_kwargs = {}\n        \n        if model_type == ModelType.LLM:\n            api_kwargs[\"model\"] = model_kwargs.get(\"model\", \"anthropic.claude-3-sonnet-20240229-v1:0\")\n            api_kwargs[\"input\"] = input\n            \n            # Add model parameters\n            if \"temperature\" in model_kwargs:\n                api_kwargs[\"temperature\"] = model_kwargs[\"temperature\"]\n            if \"top_p\" in model_kwargs:\n                api_kwargs[\"top_p\"] = model_kwargs[\"top_p\"]\n            \n            return api_kwargs\n        elif model_type == ModelType.EMBEDDER:\n            if isinstance(input, str):\n                inputs = [input]\n            elif isinstance(input, Sequence):\n                inputs = list(input)\n            else:\n                raise TypeError(\"input must be a string or sequence of strings\")\n\n            api_kwargs[\"model\"] = model_kwargs.get(\"model\", \"amazon.titan-embed-text-v2:0\")\n            api_kwargs[\"input\"] = inputs\n            api_kwargs[\"model_kwargs\"] = model_kwargs\n            return api_kwargs\n        else:\n            raise ValueError(f\"Model type {model_type} is not supported by AWS Bedrock client\")\n"
  },
  {
    "path": "api/config/embedder.json",
    "content": "{\n  \"embedder\": {\n    \"client_class\": \"OpenAIClient\",\n    \"batch_size\": 500,\n    \"model_kwargs\": {\n      \"model\": \"text-embedding-3-small\",\n      \"dimensions\": 256,\n      \"encoding_format\": \"float\"\n    }\n  },\n  \"embedder_ollama\": {\n    \"client_class\": \"OllamaClient\",\n    \"model_kwargs\": {\n      \"model\": \"nomic-embed-text\"\n    }\n  },\n  \"embedder_google\": {\n    \"client_class\": \"GoogleEmbedderClient\",\n    \"batch_size\": 100,\n    \"model_kwargs\": {\n      \"model\": \"gemini-embedding-001\",\n      \"task_type\": \"SEMANTIC_SIMILARITY\"\n    }\n  },\n  \"embedder_bedrock\": {\n    \"client_class\": \"BedrockClient\",\n    \"batch_size\": 100,\n    \"model_kwargs\": {\n      \"model\": \"amazon.titan-embed-text-v2:0\",\n      \"dimensions\": 256\n    }\n  },\n  \"retriever\": {\n    \"top_k\": 20\n  },\n  \"text_splitter\": {\n    \"split_by\": \"word\",\n    \"chunk_size\": 350,\n    \"chunk_overlap\": 100\n  }\n}\n"
  },
  {
    "path": "api/config/embedder.json.bak",
    "content": "{\n  \"embedder\": {\n    \"client_class\": \"OpenAIClient\",\n    \"batch_size\": 500,\n    \"model_kwargs\": {\n      \"model\": \"text-embedding-3-small\",\n      \"dimensions\": 256,\n      \"encoding_format\": \"float\"\n    }\n  },\n  \"retriever\": {\n    \"top_k\": 20\n  },\n  \"text_splitter\": {\n    \"split_by\": \"word\",\n    \"chunk_size\": 350,\n    \"chunk_overlap\": 100\n  }\n}\n"
  },
  {
    "path": "api/config/embedder.ollama.json.bak",
    "content": "{\n  \"embedder_ollama\": {\n    \"client_class\": \"OllamaClient\",\n    \"model_kwargs\": {\n      \"model\": \"nomic-embed-text\"\n    }\n  },\n  \"embedder\": {\n    \"client_class\": \"OllamaClient\",\n    \"model_kwargs\": {\n      \"model\": \"nomic-embed-text\"\n    }\n  },\n  \"retriever\": {\n    \"top_k\": 20\n  },\n  \"text_splitter\": {\n    \"split_by\": \"word\",\n    \"chunk_size\": 350,\n    \"chunk_overlap\": 100\n  }\n}\n"
  },
  {
    "path": "api/config/embedder.openai_compatible.json.bak",
    "content": "{\n  \"embedder\": {\n    \"client_class\": \"OpenAIClient\",\n    \"initialize_kwargs\": {\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"base_url\": \"${OPENAI_BASE_URL}\"\n    },\n    \"batch_size\": 10,\n    \"model_kwargs\": {\n      \"model\": \"text-embedding-v3\",\n      \"dimensions\": 256,\n      \"encoding_format\": \"float\"\n    }\n  },\n  \"embedder_ollama\": {\n    \"client_class\": \"OllamaClient\",\n    \"model_kwargs\": {\n      \"model\": \"nomic-embed-text\"\n    }\n  },\n  \"retriever\": {\n    \"top_k\": 20\n  },\n  \"text_splitter\": {\n    \"split_by\": \"word\",\n    \"chunk_size\": 350,\n    \"chunk_overlap\": 100\n  }\n}\n"
  },
  {
    "path": "api/config/generator.json",
    "content": "{\n  \"default_provider\": \"google\",\n  \"providers\": {\n    \"dashscope\": {\n      \"default_model\": \"qwen-plus\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"qwen-plus\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"qwen-turbo\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"deepseek-r1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        }\n      }\n    },\n    \"google\": {\n      \"default_model\": \"gemini-2.5-flash\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"gemini-2.5-flash\": {\n          \"temperature\": 1.0,\n          \"top_p\": 0.8,\n          \"top_k\": 20\n        },\n        \"gemini-2.5-flash-lite\": {\n          \"temperature\": 1.0,\n          \"top_p\": 0.8,\n          \"top_k\": 20\n        },\n        \"gemini-2.5-pro\": {\n          \"temperature\": 1.0,\n          \"top_p\": 0.8,\n          \"top_k\": 20\n        }\n      }\n    },\n    \"openai\": {\n      \"default_model\": \"gpt-5-nano\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"gpt-5\": {\n          \"temperature\": 1.0\n        },\n        \"gpt-5-nano\": {\n          \"temperature\": 1.0\n        },\n        \"gpt-5-mini\": {\n          \"temperature\": 1.0\n        },\n        \"gpt-4o\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"gpt-4.1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"o1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"o3\": {\n          \"temperature\": 1.0\n        },\n        \"o4-mini\": {\n          \"temperature\": 1.0\n        }\n      }\n    },\n    \"openrouter\": {\n      \"default_model\": \"openai/gpt-5-nano\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"openai/gpt-5-nano\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"openai/gpt-4o\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"deepseek/deepseek-r1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"openai/gpt-4.1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"openai/o1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"openai/o3\": {\n          \"temperature\": 1.0\n        },\n        \"openai/o4-mini\": {\n          \"temperature\": 1.0\n        },\n        \"anthropic/claude-3.7-sonnet\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"anthropic/claude-3.5-sonnet\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        }\n      }\n    },\n    \"ollama\": {\n      \"default_model\": \"qwen3:1.7b\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"qwen3:1.7b\": {\n          \"options\": {\n            \"temperature\": 0.7,\n            \"top_p\": 0.8,\n            \"num_ctx\": 32000\n          }\n        },\n        \"llama3:8b\": {\n          \"options\": {\n            \"temperature\": 0.7,\n            \"top_p\": 0.8,\n            \"num_ctx\": 8000\n          }\n        },\n        \"qwen3:8b\": {\n          \"options\": {\n            \"temperature\": 0.7,\n            \"top_p\": 0.8,\n            \"num_ctx\": 32000\n          }\n        }\n      }\n    },\n    \"bedrock\": {\n      \"client_class\": \"BedrockClient\",\n      \"default_model\": \"anthropic.claude-3-sonnet-20240229-v1:0\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"anthropic.claude-3-sonnet-20240229-v1:0\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"anthropic.claude-3-haiku-20240307-v1:0\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"anthropic.claude-3-opus-20240229-v1:0\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"amazon.titan-text-express-v1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"cohere.command-r-v1:0\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"ai21.j2-ultra-v1\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        }\n      }\n    },\n    \"azure\": {\n      \"client_class\": \"AzureAIClient\",\n      \"default_model\": \"gpt-4o\",\n      \"supportsCustomModel\": true,\n      \"models\": {\n        \"gpt-4o\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"gpt-4\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"gpt-35-turbo\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        },\n        \"gpt-4-turbo\": {\n          \"temperature\": 0.7,\n          \"top_p\": 0.8\n        }\n      }\n    }\n  }\n}\n\n"
  },
  {
    "path": "api/config/lang.json",
    "content": "{\n  \"supported_languages\": {\n    \"en\": \"English\",\n    \"ja\": \"Japanese (日本語)\",\n    \"zh\": \"Mandarin Chinese (中文)\",\n    \"zh-tw\": \"Traditional Chinese (繁體中文)\",\n    \"es\": \"Spanish (Español)\",\n    \"kr\": \"Korean (한국어)\",\n    \"vi\": \"Vietnamese (Tiếng Việt)\",\n    \"pt-br\": \"Brazilian Portuguese (Português Brasileiro)\",\n    \"fr\": \"Français (French)\",\n    \"ru\": \"Русский (Russian)\"\n  },\n  \"default\": \"en\"\n}\n"
  },
  {
    "path": "api/config/repo.json",
    "content": "{\n  \"file_filters\": {\n    \"excluded_dirs\": [\n      \"./.venv/\", \n      \"./venv/\", \n      \"./env/\", \n      \"./virtualenv/\",\n      \"./node_modules/\", \n      \"./bower_components/\", \n      \"./jspm_packages/\",\n      \"./.git/\", \n      \"./.svn/\", \n      \"./.hg/\", \n      \"./.bzr/\"\n    ],\n    \"excluded_files\": [\n      \"yarn.lock\", \n      \"pnpm-lock.yaml\", \n      \"npm-shrinkwrap.json\", \n      \"poetry.lock\",\n      \"Pipfile.lock\", \n      \"requirements.txt.lock\", \n      \"Cargo.lock\", \n      \"composer.lock\",\n      \".lock\", \n      \".DS_Store\", \n      \"Thumbs.db\", \n      \"desktop.ini\", \n      \"*.lnk\", \n      \".env\", \n      \".env.*\", \n      \"*.env\", \n      \"*.cfg\", \n      \"*.ini\", \n      \".flaskenv\", \n      \".gitignore\", \n      \".gitattributes\", \n      \".gitmodules\", \n      \".github\", \n      \".gitlab-ci.yml\", \n      \".prettierrc\", \n      \".eslintrc\", \n      \".eslintignore\", \n      \".stylelintrc\", \n      \".editorconfig\", \n      \".jshintrc\", \n      \".pylintrc\", \n      \".flake8\", \n      \"mypy.ini\", \n      \"pyproject.toml\", \n      \"tsconfig.json\", \n      \"webpack.config.js\", \n      \"babel.config.js\", \n      \"rollup.config.js\", \n      \"jest.config.js\", \n      \"karma.conf.js\", \n      \"vite.config.js\", \n      \"next.config.js\", \n      \"*.min.js\", \n      \"*.min.css\", \n      \"*.bundle.js\", \n      \"*.bundle.css\", \n      \"*.map\", \n      \"*.gz\", \n      \"*.zip\", \n      \"*.tar\", \n      \"*.tgz\", \n      \"*.rar\", \n      \"*.7z\", \n      \"*.iso\", \n      \"*.dmg\", \n      \"*.img\", \n      \"*.msix\", \n      \"*.appx\", \n      \"*.appxbundle\", \n      \"*.xap\", \n      \"*.ipa\", \n      \"*.deb\", \n      \"*.rpm\", \n      \"*.msi\", \n      \"*.exe\", \n      \"*.dll\", \n      \"*.so\", \n      \"*.dylib\", \n      \"*.o\", \n      \"*.obj\", \n      \"*.jar\", \n      \"*.war\", \n      \"*.ear\", \n      \"*.jsm\", \n      \"*.class\", \n      \"*.pyc\", \n      \"*.pyd\", \n      \"*.pyo\", \n      \"__pycache__\", \n      \"*.a\", \n      \"*.lib\", \n      \"*.lo\", \n      \"*.la\", \n      \"*.slo\", \n      \"*.dSYM\",\n      \"*.egg\", \n      \"*.egg-info\", \n      \"*.dist-info\", \n      \"*.eggs\", \n      \"node_modules\",\n      \"bower_components\", \n      \"jspm_packages\", \n      \"lib-cov\", \n      \"coverage\", \n      \"htmlcov\", \n      \".nyc_output\", \n      \".tox\", \n      \"dist\", \n      \"build\", \n      \"bld\", \n      \"out\", \n      \"bin\", \n      \"target\",\n      \"packages/*/dist\", \n      \"packages/*/build\", \n      \".output\"\n    ]\n  },\n  \"repository\": {\n    \"max_size_mb\": 50000\n  }\n}\n"
  },
  {
    "path": "api/config.py",
    "content": "import os\nimport json\nimport logging\nimport re\nfrom pathlib import Path\nfrom typing import List, Union, Dict, Any\n\nlogger = logging.getLogger(__name__)\n\nfrom api.openai_client import OpenAIClient\nfrom api.openrouter_client import OpenRouterClient\nfrom api.bedrock_client import BedrockClient\nfrom api.google_embedder_client import GoogleEmbedderClient\nfrom api.azureai_client import AzureAIClient\nfrom api.dashscope_client import DashscopeClient\nfrom adalflow import GoogleGenAIClient, OllamaClient\n\n# Get API keys from environment variables\nOPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')\nGOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')\nOPENROUTER_API_KEY = os.environ.get('OPENROUTER_API_KEY')\nAWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')\nAWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')\nAWS_SESSION_TOKEN = os.environ.get('AWS_SESSION_TOKEN')\nAWS_REGION = os.environ.get('AWS_REGION')\nAWS_ROLE_ARN = os.environ.get('AWS_ROLE_ARN')\n\n# Set keys in environment (in case they're needed elsewhere in the code)\nif OPENAI_API_KEY:\n    os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY\nif GOOGLE_API_KEY:\n    os.environ[\"GOOGLE_API_KEY\"] = GOOGLE_API_KEY\nif OPENROUTER_API_KEY:\n    os.environ[\"OPENROUTER_API_KEY\"] = OPENROUTER_API_KEY\nif AWS_ACCESS_KEY_ID:\n    os.environ[\"AWS_ACCESS_KEY_ID\"] = AWS_ACCESS_KEY_ID\nif AWS_SECRET_ACCESS_KEY:\n    os.environ[\"AWS_SECRET_ACCESS_KEY\"] = AWS_SECRET_ACCESS_KEY\nif AWS_SESSION_TOKEN:\n    os.environ[\"AWS_SESSION_TOKEN\"] = AWS_SESSION_TOKEN\nif AWS_REGION:\n    os.environ[\"AWS_REGION\"] = AWS_REGION\nif AWS_ROLE_ARN:\n    os.environ[\"AWS_ROLE_ARN\"] = AWS_ROLE_ARN\n\n# Wiki authentication settings\nraw_auth_mode = os.environ.get('DEEPWIKI_AUTH_MODE', 'False')\nWIKI_AUTH_MODE = raw_auth_mode.lower() in ['true', '1', 't']\nWIKI_AUTH_CODE = os.environ.get('DEEPWIKI_AUTH_CODE', '')\n\n# Embedder settings\nEMBEDDER_TYPE = os.environ.get('DEEPWIKI_EMBEDDER_TYPE', 'openai').lower()\n\n# Get configuration directory from environment variable, or use default if not set\nCONFIG_DIR = os.environ.get('DEEPWIKI_CONFIG_DIR', None)\n\n# Client class mapping\nCLIENT_CLASSES = {\n    \"GoogleGenAIClient\": GoogleGenAIClient,\n    \"GoogleEmbedderClient\": GoogleEmbedderClient,\n    \"OpenAIClient\": OpenAIClient,\n    \"OpenRouterClient\": OpenRouterClient,\n    \"OllamaClient\": OllamaClient,\n    \"BedrockClient\": BedrockClient,\n    \"AzureAIClient\": AzureAIClient,\n    \"DashscopeClient\": DashscopeClient\n}\n\ndef replace_env_placeholders(config: Union[Dict[str, Any], List[Any], str, Any]) -> Union[Dict[str, Any], List[Any], str, Any]:\n    \"\"\"\n    Recursively replace placeholders like \"${ENV_VAR}\" in string values\n    within a nested configuration structure (dicts, lists, strings)\n    with environment variable values. Logs a warning if a placeholder is not found.\n    \"\"\"\n    pattern = re.compile(r\"\\$\\{([A-Z0-9_]+)\\}\")\n\n    def replacer(match: re.Match[str]) -> str:\n        env_var_name = match.group(1)\n        original_placeholder = match.group(0)\n        env_var_value = os.environ.get(env_var_name)\n        if env_var_value is None:\n            logger.warning(\n                f\"Environment variable placeholder '{original_placeholder}' was not found in the environment. \"\n                f\"The placeholder string will be used as is.\"\n            )\n            return original_placeholder\n        return env_var_value\n\n    if isinstance(config, dict):\n        return {k: replace_env_placeholders(v) for k, v in config.items()}\n    elif isinstance(config, list):\n        return [replace_env_placeholders(item) for item in config]\n    elif isinstance(config, str):\n        return pattern.sub(replacer, config)\n    else:\n        # Handles numbers, booleans, None, etc.\n        return config\n\n# Load JSON configuration file\ndef load_json_config(filename):\n    try:\n        # If environment variable is set, use the directory specified by it\n        if CONFIG_DIR:\n            config_path = Path(CONFIG_DIR) / filename\n        else:\n            # Otherwise use default directory\n            config_path = Path(__file__).parent / \"config\" / filename\n\n        logger.info(f\"Loading configuration from {config_path}\")\n\n        if not config_path.exists():\n            logger.warning(f\"Configuration file {config_path} does not exist\")\n            return {}\n\n        with open(config_path, 'r', encoding='utf-8') as f:\n            config = json.load(f)\n            config = replace_env_placeholders(config)\n            return config\n    except Exception as e:\n        logger.error(f\"Error loading configuration file {filename}: {str(e)}\")\n        return {}\n\n# Load generator model configuration\ndef load_generator_config():\n    generator_config = load_json_config(\"generator.json\")\n\n    # Add client classes to each provider\n    if \"providers\" in generator_config:\n        for provider_id, provider_config in generator_config[\"providers\"].items():\n            # Try to set client class from client_class\n            if provider_config.get(\"client_class\") in CLIENT_CLASSES:\n                provider_config[\"model_client\"] = CLIENT_CLASSES[provider_config[\"client_class\"]]\n            # Fall back to default mapping based on provider_id\n            elif provider_id in [\"google\", \"openai\", \"openrouter\", \"ollama\", \"bedrock\", \"azure\", \"dashscope\"]:\n                default_map = {\n                    \"google\": GoogleGenAIClient,\n                    \"openai\": OpenAIClient,\n                    \"openrouter\": OpenRouterClient,\n                    \"ollama\": OllamaClient,\n                    \"bedrock\": BedrockClient,\n                    \"azure\": AzureAIClient,\n                    \"dashscope\": DashscopeClient\n                }\n                provider_config[\"model_client\"] = default_map[provider_id]\n            else:\n                logger.warning(f\"Unknown provider or client class: {provider_id}\")\n\n    return generator_config\n\n# Load embedder configuration\ndef load_embedder_config():\n    embedder_config = load_json_config(\"embedder.json\")\n\n    # Process client classes\n    for key in [\"embedder\", \"embedder_ollama\", \"embedder_google\", \"embedder_bedrock\"]:\n        if key in embedder_config and \"client_class\" in embedder_config[key]:\n            class_name = embedder_config[key][\"client_class\"]\n            if class_name in CLIENT_CLASSES:\n                embedder_config[key][\"model_client\"] = CLIENT_CLASSES[class_name]\n\n    return embedder_config\n\ndef get_embedder_config():\n    \"\"\"\n    Get the current embedder configuration based on DEEPWIKI_EMBEDDER_TYPE.\n\n    Returns:\n        dict: The embedder configuration with model_client resolved\n    \"\"\"\n    embedder_type = EMBEDDER_TYPE\n    if embedder_type == 'bedrock' and 'embedder_bedrock' in configs:\n        return configs.get(\"embedder_bedrock\", {})\n    elif embedder_type == 'google' and 'embedder_google' in configs:\n        return configs.get(\"embedder_google\", {})\n    elif embedder_type == 'ollama' and 'embedder_ollama' in configs:\n        return configs.get(\"embedder_ollama\", {})\n    else:\n        return configs.get(\"embedder\", {})\n\ndef is_ollama_embedder():\n    \"\"\"\n    Check if the current embedder configuration uses OllamaClient.\n\n    Returns:\n        bool: True if using OllamaClient, False otherwise\n    \"\"\"\n    embedder_config = get_embedder_config()\n    if not embedder_config:\n        return False\n\n    # Check if model_client is OllamaClient\n    model_client = embedder_config.get(\"model_client\")\n    if model_client:\n        return model_client.__name__ == \"OllamaClient\"\n\n    # Fallback: check client_class string\n    client_class = embedder_config.get(\"client_class\", \"\")\n    return client_class == \"OllamaClient\"\n\ndef is_google_embedder():\n    \"\"\"\n    Check if the current embedder configuration uses GoogleEmbedderClient.\n\n    Returns:\n        bool: True if using GoogleEmbedderClient, False otherwise\n    \"\"\"\n    embedder_config = get_embedder_config()\n    if not embedder_config:\n        return False\n\n    # Check if model_client is GoogleEmbedderClient\n    model_client = embedder_config.get(\"model_client\")\n    if model_client:\n        return model_client.__name__ == \"GoogleEmbedderClient\"\n\n    # Fallback: check client_class string\n    client_class = embedder_config.get(\"client_class\", \"\")\n    return client_class == \"GoogleEmbedderClient\"\n\ndef is_bedrock_embedder():\n    \"\"\"\n    Check if the current embedder configuration uses BedrockClient.\n\n    Returns:\n        bool: True if using BedrockClient, False otherwise\n    \"\"\"\n    embedder_config = get_embedder_config()\n    if not embedder_config:\n        return False\n\n    model_client = embedder_config.get(\"model_client\")\n    if model_client:\n        return model_client.__name__ == \"BedrockClient\"\n\n    client_class = embedder_config.get(\"client_class\", \"\")\n    return client_class == \"BedrockClient\"\n\ndef get_embedder_type():\n    \"\"\"\n    Get the current embedder type based on configuration.\n    \n    Returns:\n        str: 'bedrock', 'ollama', 'google', or 'openai' (default)\n    \"\"\"\n    if is_bedrock_embedder():\n        return 'bedrock'\n    elif is_ollama_embedder():\n        return 'ollama'\n    elif is_google_embedder():\n        return 'google'\n    else:\n        return 'openai'\n\n# Load repository and file filters configuration\ndef load_repo_config():\n    return load_json_config(\"repo.json\")\n\n# Load language configuration\ndef load_lang_config():\n    default_config = {\n        \"supported_languages\": {\n            \"en\": \"English\",\n            \"ja\": \"Japanese (日本語)\",\n            \"zh\": \"Mandarin Chinese (中文)\",\n            \"zh-tw\": \"Traditional Chinese (繁體中文)\",\n            \"es\": \"Spanish (Español)\",\n            \"kr\": \"Korean (한국어)\",\n            \"vi\": \"Vietnamese (Tiếng Việt)\",\n            \"pt-br\": \"Brazilian Portuguese (Português Brasileiro)\",\n            \"fr\": \"Français (French)\",\n            \"ru\": \"Русский (Russian)\"\n        },\n        \"default\": \"en\"\n    }\n\n    loaded_config = load_json_config(\"lang.json\") # Let load_json_config handle path and loading\n\n    if not loaded_config:\n        return default_config\n\n    if \"supported_languages\" not in loaded_config or \"default\" not in loaded_config:\n        logger.warning(\"Language configuration file 'lang.json' is malformed. Using default language configuration.\")\n        return default_config\n\n    return loaded_config\n\n# Default excluded directories and files\nDEFAULT_EXCLUDED_DIRS: List[str] = [\n    # Virtual environments and package managers\n    \"./.venv/\", \"./venv/\", \"./env/\", \"./virtualenv/\",\n    \"./node_modules/\", \"./bower_components/\", \"./jspm_packages/\",\n    # Version control\n    \"./.git/\", \"./.svn/\", \"./.hg/\", \"./.bzr/\",\n    # Cache and compiled files\n    \"./__pycache__/\", \"./.pytest_cache/\", \"./.mypy_cache/\", \"./.ruff_cache/\", \"./.coverage/\",\n    # Build and distribution\n    \"./dist/\", \"./build/\", \"./out/\", \"./target/\", \"./bin/\", \"./obj/\",\n    # Documentation\n    \"./docs/\", \"./_docs/\", \"./site-docs/\", \"./_site/\",\n    # IDE specific\n    \"./.idea/\", \"./.vscode/\", \"./.vs/\", \"./.eclipse/\", \"./.settings/\",\n    # Logs and temporary files\n    \"./logs/\", \"./log/\", \"./tmp/\", \"./temp/\",\n]\n\nDEFAULT_EXCLUDED_FILES: List[str] = [\n    \"yarn.lock\", \"pnpm-lock.yaml\", \"npm-shrinkwrap.json\", \"poetry.lock\",\n    \"Pipfile.lock\", \"requirements.txt.lock\", \"Cargo.lock\", \"composer.lock\",\n    \".lock\", \".DS_Store\", \"Thumbs.db\", \"desktop.ini\", \"*.lnk\", \".env\",\n    \".env.*\", \"*.env\", \"*.cfg\", \"*.ini\", \".flaskenv\", \".gitignore\",\n    \".gitattributes\", \".gitmodules\", \".github\", \".gitlab-ci.yml\",\n    \".prettierrc\", \".eslintrc\", \".eslintignore\", \".stylelintrc\",\n    \".editorconfig\", \".jshintrc\", \".pylintrc\", \".flake8\", \"mypy.ini\",\n    \"pyproject.toml\", \"tsconfig.json\", \"webpack.config.js\", \"babel.config.js\",\n    \"rollup.config.js\", \"jest.config.js\", \"karma.conf.js\", \"vite.config.js\",\n    \"next.config.js\", \"*.min.js\", \"*.min.css\", \"*.bundle.js\", \"*.bundle.css\",\n    \"*.map\", \"*.gz\", \"*.zip\", \"*.tar\", \"*.tgz\", \"*.rar\", \"*.7z\", \"*.iso\",\n    \"*.dmg\", \"*.img\", \"*.msix\", \"*.appx\", \"*.appxbundle\", \"*.xap\", \"*.ipa\",\n    \"*.deb\", \"*.rpm\", \"*.msi\", \"*.exe\", \"*.dll\", \"*.so\", \"*.dylib\", \"*.o\",\n    \"*.obj\", \"*.jar\", \"*.war\", \"*.ear\", \"*.jsm\", \"*.class\", \"*.pyc\", \"*.pyd\",\n    \"*.pyo\", \"__pycache__\", \"*.a\", \"*.lib\", \"*.lo\", \"*.la\", \"*.slo\", \"*.dSYM\",\n    \"*.egg\", \"*.egg-info\", \"*.dist-info\", \"*.eggs\", \"node_modules\",\n    \"bower_components\", \"jspm_packages\", \"lib-cov\", \"coverage\", \"htmlcov\",\n    \".nyc_output\", \".tox\", \"dist\", \"build\", \"bld\", \"out\", \"bin\", \"target\",\n    \"packages/*/dist\", \"packages/*/build\", \".output\"\n]\n\n# Initialize empty configuration\nconfigs = {}\n\n# Load all configuration files\ngenerator_config = load_generator_config()\nembedder_config = load_embedder_config()\nrepo_config = load_repo_config()\nlang_config = load_lang_config()\n\n# Update configuration\nif generator_config:\n    configs[\"default_provider\"] = generator_config.get(\"default_provider\", \"google\")\n    configs[\"providers\"] = generator_config.get(\"providers\", {})\n\n# Update embedder configuration\nif embedder_config:\n    for key in [\"embedder\", \"embedder_ollama\", \"embedder_google\", \"embedder_bedrock\", \"retriever\", \"text_splitter\"]:\n        if key in embedder_config:\n            configs[key] = embedder_config[key]\n\n# Update repository configuration\nif repo_config:\n    for key in [\"file_filters\", \"repository\"]:\n        if key in repo_config:\n            configs[key] = repo_config[key]\n\n# Update language configuration\nif lang_config:\n    configs[\"lang_config\"] = lang_config\n\n\ndef get_model_config(provider=\"google\", model=None):\n    \"\"\"\n    Get configuration for the specified provider and model\n\n    Parameters:\n        provider (str): Model provider ('google', 'openai', 'openrouter', 'ollama', 'bedrock')\n        model (str): Model name, or None to use default model\n\n    Returns:\n        dict: Configuration containing model_client, model and other parameters\n    \"\"\"\n    # Get provider configuration\n    if \"providers\" not in configs:\n        raise ValueError(\"Provider configuration not loaded\")\n\n    provider_config = configs[\"providers\"].get(provider)\n    if not provider_config:\n        raise ValueError(f\"Configuration for provider '{provider}' not found\")\n\n    model_client = provider_config.get(\"model_client\")\n    if not model_client:\n        raise ValueError(f\"Model client not specified for provider '{provider}'\")\n\n    # If model not provided, use default model for the provider\n    if not model:\n        model = provider_config.get(\"default_model\")\n        if not model:\n            raise ValueError(f\"No default model specified for provider '{provider}'\")\n\n    # Get model parameters (if present)\n    model_params = {}\n    if model in provider_config.get(\"models\", {}):\n        model_params = provider_config[\"models\"][model]\n    else:\n        default_model = provider_config.get(\"default_model\")\n        model_params = provider_config[\"models\"][default_model]\n\n    # Prepare base configuration\n    result = {\n        \"model_client\": model_client,\n    }\n\n    # Provider-specific adjustments\n    if provider == \"ollama\":\n        # Ollama uses a slightly different parameter structure\n        if \"options\" in model_params:\n            result[\"model_kwargs\"] = {\"model\": model, **model_params[\"options\"]}\n        else:\n            result[\"model_kwargs\"] = {\"model\": model}\n    else:\n        # Standard structure for other providers\n        result[\"model_kwargs\"] = {\"model\": model, **model_params}\n\n    return result\n"
  },
  {
    "path": "api/dashscope_client.py",
    "content": "\"\"\"Dashscope (Alibaba Cloud) ModelClient integration.\"\"\"\n\nimport os\nimport pickle\nfrom typing import (\n    Dict,\n    Optional,\n    Any,\n    Callable,\n    Generator,\n    Union,\n    Literal,\n    List,\n    Sequence,\n)\n\nimport logging\nimport backoff\nfrom copy import deepcopy\nfrom tqdm import tqdm\n\n# optional import\nfrom adalflow.utils.lazy_import import safe_import, OptionalPackages\n\nopenai = safe_import(OptionalPackages.OPENAI.value[0], OptionalPackages.OPENAI.value[1])\n\nfrom openai import OpenAI, AsyncOpenAI, Stream\nfrom openai import (\n    APITimeoutError,\n    InternalServerError,\n    RateLimitError,\n    UnprocessableEntityError,\n    BadRequestError,\n)\nfrom openai.types import (\n    Completion,\n    CreateEmbeddingResponse,\n)\nfrom openai.types.chat import ChatCompletionChunk, ChatCompletion\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import (\n    ModelType,\n    EmbedderOutput,\n    CompletionUsage,\n    GeneratorOutput,\n    Document,\n    Embedding,\n    EmbedderOutputType,\n    EmbedderInputType,\n)\nfrom adalflow.core.component import DataComponent\nfrom adalflow.core.embedder import (\n    BatchEmbedderOutputType,\n    BatchEmbedderInputType,\n)\nimport adalflow.core.functional as F\nfrom adalflow.components.model_client.utils import parse_embedding_response\n\nfrom api.logging_config import setup_logging\n\n# # Disable tqdm progress bars\n# os.environ[\"TQDM_DISABLE\"] = \"1\"\n\nsetup_logging()\nlog = logging.getLogger(__name__)\n\ndef get_first_message_content(completion: ChatCompletion) -> str:\n    \"\"\"When we only need the content of the first message.\"\"\"\n    log.info(f\"🔍 get_first_message_content called with: {type(completion)}\")\n    log.debug(f\"raw completion: {completion}\")\n    \n    try:\n        if hasattr(completion, 'choices') and len(completion.choices) > 0:\n            choice = completion.choices[0]\n            if hasattr(choice, 'message') and hasattr(choice.message, 'content'):\n                content = choice.message.content\n                log.info(f\"✅ Successfully extracted content: {type(content)}, length: {len(content) if content else 0}\")\n                return content\n            else:\n                log.error(\"❌ Choice doesn't have message.content\")\n                return str(completion)\n        else:\n            log.error(\"❌ Completion doesn't have choices\")\n            return str(completion)\n    except Exception as e:\n        log.error(f\"❌ Error in get_first_message_content: {e}\")\n        return str(completion)\n\n\ndef parse_stream_response(completion: ChatCompletionChunk) -> str:\n    \"\"\"Parse the response of the stream API.\"\"\"\n    return completion.choices[0].delta.content\n\n\ndef handle_streaming_response(generator: Stream[ChatCompletionChunk]):\n    \"\"\"Handle the streaming response.\"\"\"\n    for completion in generator:\n        log.debug(f\"Raw chunk completion: {completion}\")\n        parsed_content = parse_stream_response(completion)\n        yield parsed_content\n\n\nclass DashscopeClient(ModelClient):\n    \"\"\"A component wrapper for the Dashscope (Alibaba Cloud) API client.\n\n    Dashscope provides access to Alibaba Cloud's Qwen and other models through an OpenAI-compatible API.\n    \n    Args:\n        api_key (Optional[str], optional): Dashscope API key. Defaults to None.\n        workspace_id (Optional[str], optional): Dashscope workspace ID. Defaults to None.\n        base_url (str): The API base URL. Defaults to \"https://dashscope.aliyuncs.com/compatible-mode/v1\".\n        env_api_key_name (str): Environment variable name for the API key. Defaults to \"DASHSCOPE_API_KEY\".\n        env_workspace_id_name (str): Environment variable name for the workspace ID. Defaults to \"DASHSCOPE_WORKSPACE_ID\".\n\n    References:\n        - Dashscope API Documentation: https://help.aliyun.com/zh/dashscope/\n    \"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        workspace_id: Optional[str] = None,\n        chat_completion_parser: Callable[[Completion], Any] = None,\n        input_type: Literal[\"text\", \"messages\"] = \"text\",\n        base_url: Optional[str] = None,\n        env_base_url_name: str = \"DASHSCOPE_BASE_URL\",\n        env_api_key_name: str = \"DASHSCOPE_API_KEY\",\n        env_workspace_id_name: str = \"DASHSCOPE_WORKSPACE_ID\",\n    ):\n        super().__init__()\n        self._api_key = api_key\n        self._workspace_id = workspace_id\n        self._env_api_key_name = env_api_key_name\n        self._env_workspace_id_name = env_workspace_id_name\n        self._env_base_url_name = env_base_url_name\n        self.base_url = base_url or os.getenv(self._env_base_url_name, \"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n        self.sync_client = self.init_sync_client()\n        self.async_client = None\n        \n        # Force use of get_first_message_content to ensure string output\n        self.chat_completion_parser = get_first_message_content\n        self._input_type = input_type\n        self._api_kwargs = {}\n\n    def _prepare_client_config(self):\n        \"\"\"\n        Private helper method to prepare client configuration.\n        \n        Returns:\n            tuple: (api_key, workspace_id, base_url) for client initialization\n        \n        Raises:\n            ValueError: If API key is not provided\n        \"\"\"\n        api_key = self._api_key or os.getenv(self._env_api_key_name)\n        workspace_id = self._workspace_id or os.getenv(self._env_workspace_id_name)\n        \n        if not api_key:\n            raise ValueError(\n                f\"Environment variable {self._env_api_key_name} must be set\"\n            )\n        \n        if not workspace_id:\n            log.warning(f\"Environment variable {self._env_workspace_id_name} not set. Some features may not work properly.\")\n        \n        # For Dashscope, we need to include the workspace ID in the base URL if provided\n        base_url = self.base_url\n        if workspace_id:\n            # Add workspace ID to headers or URL as required by Dashscope\n            base_url = f\"{self.base_url.rstrip('/')}\"\n        \n        return api_key, workspace_id, base_url\n\n    def init_sync_client(self):\n        api_key, workspace_id, base_url = self._prepare_client_config()\n        \n        client = OpenAI(api_key=api_key, base_url=base_url)\n        \n        # Store workspace_id for later use in requests\n        if workspace_id:\n            client._workspace_id = workspace_id\n        \n        return client\n\n    def init_async_client(self):\n        api_key, workspace_id, base_url = self._prepare_client_config()\n        \n        client = AsyncOpenAI(api_key=api_key, base_url=base_url)\n        \n        # Store workspace_id for later use in requests\n        if workspace_id:\n            client._workspace_id = workspace_id\n        \n        return client\n\n    def parse_chat_completion(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> \"GeneratorOutput\":\n        \"\"\"Parse the completion response to a GeneratorOutput.\"\"\"\n        try:\n            # If the completion is already a GeneratorOutput, return it directly (prevent recursion)\n            if isinstance(completion, GeneratorOutput):\n                return completion\n            \n            # Check if it's a ChatCompletion object (non-streaming response)\n            if hasattr(completion, 'choices') and hasattr(completion, 'usage'):\n                # ALWAYS extract the string content directly\n                try:\n                    # Direct extraction of message content\n                    if (hasattr(completion, 'choices') and \n                        len(completion.choices) > 0 and \n                        hasattr(completion.choices[0], 'message') and \n                        hasattr(completion.choices[0].message, 'content')):\n                        \n                        content = completion.choices[0].message.content\n                        if isinstance(content, str):\n                            parsed_data = content\n                        else:\n                            parsed_data = str(content)\n                    else:\n                        # Fallback: convert entire completion to string\n                        parsed_data = str(completion)\n                        \n                except Exception as e:\n                    # Ultimate fallback\n                    parsed_data = str(completion)\n                \n                return GeneratorOutput(\n                    data=parsed_data,\n                    usage=CompletionUsage(\n                        completion_tokens=completion.usage.completion_tokens,\n                        prompt_tokens=completion.usage.prompt_tokens,\n                        total_tokens=completion.usage.total_tokens,\n                    ),\n                    raw_response=str(completion),\n                )\n            else:\n                # Handle streaming response - collect all content parts into a single string\n                content_parts = []\n                usage_info = None\n                for chunk in completion:\n                    if chunk.choices[0].delta.content:\n                        content_parts.append(chunk.choices[0].delta.content)\n                    # Try to get usage info from the last chunk\n                    if hasattr(chunk, 'usage') and chunk.usage:\n                        usage_info = chunk.usage\n                \n                # Join all content parts into a single string\n                full_content = ''.join(content_parts)\n                \n                # Create usage object\n                usage = None\n                if usage_info:\n                    usage = CompletionUsage(\n                        completion_tokens=usage_info.completion_tokens,\n                        prompt_tokens=usage_info.prompt_tokens,\n                        total_tokens=usage_info.total_tokens,\n                    )\n                \n                return GeneratorOutput(\n                    data=full_content,\n                    usage=usage,\n                    raw_response=\"streaming\"\n                )\n        except Exception as e:\n            log.error(f\"Error parsing completion: {e}\")\n            raise\n\n    def track_completion_usage(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> CompletionUsage:\n        \"\"\"Track the completion usage.\"\"\"\n        if isinstance(completion, ChatCompletion):\n            return CompletionUsage(\n                completion_tokens=completion.usage.completion_tokens,\n                prompt_tokens=completion.usage.prompt_tokens,\n                total_tokens=completion.usage.total_tokens,\n            )\n        else:\n            # For streaming, we can't track usage accurately\n            return CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0)\n\n    def parse_embedding_response(\n        self, response: CreateEmbeddingResponse\n    ) -> EmbedderOutput:\n        \"\"\"Parse the embedding response to a EmbedderOutput.\"\"\"\n        # Add detailed debugging\n        try:\n            result = parse_embedding_response(response)\n            if result.data:\n                log.info(f\"🔍 Number of embeddings: {len(result.data)}\")\n                if len(result.data) > 0:\n                    log.info(f\"🔍 First embedding length: {len(result.data[0].embedding) if hasattr(result.data[0], 'embedding') else 'N/A'}\")\n            else:\n                log.warning(f\"🔍 No embedding data found in result\")\n            return result\n        except Exception as e:\n            log.error(f\"🔍 Error parsing DashScope embedding response: {e}\")\n            log.error(f\"🔍 Raw response details: {repr(response)}\")\n            return EmbedderOutput(data=[], error=str(e), raw_response=response)\n\n    def convert_inputs_to_api_kwargs(\n        self,\n        input: Optional[Any] = None,\n        model_kwargs: Dict = {},\n        model_type: ModelType = ModelType.UNDEFINED,\n    ) -> Dict:\n        \"\"\"Convert inputs to API kwargs.\"\"\"\n        final_model_kwargs = model_kwargs.copy()\n        \n        if model_type == ModelType.LLM:\n            messages = []\n            if isinstance(input, str):\n                messages = [{\"role\": \"user\", \"content\": input}]\n            elif isinstance(input, list):\n                messages = input\n            else:\n                raise ValueError(f\"Unsupported input type: {type(input)}\")\n            \n            api_kwargs = {\n                \"messages\": messages,\n                **final_model_kwargs\n            }\n            \n            # Add workspace ID to headers if available\n            workspace_id = getattr(self.sync_client, '_workspace_id', None) or getattr(self.async_client, '_workspace_id', None)\n            if workspace_id:\n                # Dashscope may require workspace ID in headers\n                if 'extra_headers' not in api_kwargs:\n                    api_kwargs['extra_headers'] = {}\n                api_kwargs['extra_headers']['X-DashScope-WorkSpace'] = workspace_id\n            \n            return api_kwargs\n            \n        elif model_type == ModelType.EMBEDDER:\n            # Convert Documents to text strings for embedding\n            processed_input = input\n            if isinstance(input, list):\n                # Extract text from Document objects\n                processed_input = []\n                for item in input:\n                    if hasattr(item, 'text'):\n                        # It's a Document object, extract text\n                        processed_input.append(item.text)\n                    elif isinstance(item, str):\n                        # It's already a string\n                        processed_input.append(item)\n                    else:\n                        # Try to convert to string\n                        processed_input.append(str(item))\n            elif hasattr(input, 'text'):\n                # Single Document object\n                processed_input = input.text\n            elif isinstance(input, str):\n                # Single string\n                processed_input = input\n            else:\n                # Convert to string as fallback\n                processed_input = str(input)\n            \n            api_kwargs = {\n                \"input\": processed_input,\n                **final_model_kwargs\n            }\n            \n            # Add workspace ID to headers if available\n            workspace_id = getattr(self.sync_client, '_workspace_id', None) or getattr(self.async_client, '_workspace_id', None)\n            if workspace_id:\n                if 'extra_headers' not in api_kwargs:\n                    api_kwargs['extra_headers'] = {}\n                api_kwargs['extra_headers']['X-DashScope-WorkSpace'] = workspace_id\n            \n            return api_kwargs\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED):\n        \"\"\"Call the Dashscope API.\"\"\"\n        if model_type == ModelType.LLM:\n            if not api_kwargs.get(\"stream\", False):\n                # For non-streaming, enable_thinking must be false.\n                # Pass it via extra_body to avoid TypeError from openai client validation.\n                extra_body = api_kwargs.get(\"extra_body\", {})\n                extra_body[\"enable_thinking\"] = False\n                api_kwargs[\"extra_body\"] = extra_body\n\n            completion = self.sync_client.chat.completions.create(**api_kwargs)\n            \n            if api_kwargs.get(\"stream\", False):\n                return handle_streaming_response(completion)\n            else:\n                return self.parse_chat_completion(completion)\n        elif model_type == ModelType.EMBEDDER:\n            # Extract input texts from api_kwargs\n            texts = api_kwargs.get(\"input\", [])\n            \n            if not texts:\n                log.warning(\"😭 No input texts provided\")\n                return EmbedderOutput(data=[], error=\"No input texts provided\", raw_response=None)\n            \n            # Ensure texts is a list\n            if isinstance(texts, str):\n                texts = [texts]\n            \n            # Filter out empty or None texts - following HuggingFace client pattern\n            valid_texts = []\n            valid_indices = []\n            for i, text in enumerate(texts):\n                if text and isinstance(text, str) and text.strip():\n                    valid_texts.append(text)\n                    valid_indices.append(i)\n                else:\n                    log.warning(f\"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}\")\n            \n            if not valid_texts:\n                log.error(\"😭 No valid texts found after filtering\")\n                return EmbedderOutput(data=[], error=\"No valid texts found after filtering\", raw_response=None)\n            \n            if len(valid_texts) != len(texts):\n                filtered_count = len(texts) - len(valid_texts)\n                log.warning(f\"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts\")\n            \n            # Create modified api_kwargs with only valid texts\n            filtered_api_kwargs = api_kwargs.copy()\n            filtered_api_kwargs[\"input\"] = valid_texts\n            \n            log.info(f\"🔍 DashScope embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total\")\n            \n            try:\n                response = self.sync_client.embeddings.create(**filtered_api_kwargs)\n                log.info(f\"🔍 DashScope API call successful, response type: {type(response)}\")\n                result = self.parse_embedding_response(response)\n                \n                # If we filtered texts, we need to create embeddings for the original indices\n                if len(valid_texts) != len(texts):\n                    log.info(f\"🔍 Creating embeddings for {len(texts)} original positions\")\n                    \n                    # Get the correct embedding dimension from the first valid embedding\n                    embedding_dim = None  # Must be determined from a successful response\n                    if result.data and len(result.data) > 0 and hasattr(result.data[0], 'embedding'):\n                        embedding_dim = len(result.data[0].embedding)\n                        log.info(f\"🔍 Using embedding dimension: {embedding_dim}\")\n                    \n                    final_data = []\n                    valid_idx = 0\n                    for i in range(len(texts)):\n                        if i in valid_indices:\n                            # Use the embedding from valid texts\n                            final_data.append(result.data[valid_idx])\n                            valid_idx += 1\n                        else:\n                            # Create zero embedding for filtered texts with correct dimension\n                            log.warning(f\"🔍 Creating zero embedding for filtered text at index {i}\")\n                            final_data.append(Embedding(\n                                embedding=[0.0] * embedding_dim,  # Use correct embedding dimension\n                                index=i\n                            ))\n                    \n                    result = EmbedderOutput(\n                        data=final_data,\n                        error=None,\n                        raw_response=result.raw_response\n                    )\n                \n                return result\n                \n            except Exception as e:\n                log.error(f\"🔍 DashScope API call failed: {e}\")\n                return EmbedderOutput(data=[], error=str(e), raw_response=None)\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    async def acall(\n        self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED\n    ):\n        \"\"\"Async call to the Dashscope API.\"\"\"\n        if not self.async_client:\n            self.async_client = self.init_async_client()\n\n        if model_type == ModelType.LLM:\n            if not api_kwargs.get(\"stream\", False):\n                # For non-streaming, enable_thinking must be false.\n                extra_body = api_kwargs.get(\"extra_body\", {})\n                extra_body[\"enable_thinking\"] = False\n                api_kwargs[\"extra_body\"] = extra_body\n\n            completion = await self.async_client.chat.completions.create(**api_kwargs)\n\n            # For async calls with streaming enabled, wrap the AsyncStream\n            # into an async generator of plain text chunks so that callers\n            # can simply `async for text in response`.\n            if api_kwargs.get(\"stream\", False):\n\n                async def async_stream_generator():\n                    async for chunk in completion:\n                        log.debug(f\"Raw async chunk completion: {chunk}\")\n                        try:\n                            parsed_content = parse_stream_response(chunk)\n                        except Exception as e:\n                            log.error(f\"Error parsing async stream chunk: {e}\")\n                            parsed_content = None\n                        if parsed_content:\n                            yield parsed_content\n\n                return async_stream_generator()\n            else:\n                return self.parse_chat_completion(completion)\n        elif model_type == ModelType.EMBEDDER:\n            # Extract input texts from api_kwargs\n            texts = api_kwargs.get(\"input\", [])\n            \n            if not texts:\n                log.warning(\"😭 No input texts provided\")\n                return EmbedderOutput(data=[], error=\"No input texts provided\", raw_response=None)\n            \n            # Ensure texts is a list\n            if isinstance(texts, str):\n                texts = [texts]\n            \n            # Filter out empty or None texts - following HuggingFace client pattern\n            valid_texts = []\n            valid_indices = []\n            for i, text in enumerate(texts):\n                if text and isinstance(text, str) and text.strip():\n                    valid_texts.append(text)\n                    valid_indices.append(i)\n                else:\n                    log.warning(f\"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}\")\n            \n            if not valid_texts:\n                log.error(\"😭 No valid texts found after filtering\")\n                return EmbedderOutput(data=[], error=\"No valid texts found after filtering\", raw_response=None)\n            \n            if len(valid_texts) != len(texts):\n                filtered_count = len(texts) - len(valid_texts)\n                log.warning(f\"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts\")\n            \n            # Create modified api_kwargs with only valid texts\n            filtered_api_kwargs = api_kwargs.copy()\n            filtered_api_kwargs[\"input\"] = valid_texts\n            \n            log.info(f\"🔍 DashScope async embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total\")\n            \n            try:\n                response = await self.async_client.embeddings.create(**filtered_api_kwargs)\n                log.info(f\"🔍 DashScope async API call successful, response type: {type(response)}\")\n                result = self.parse_embedding_response(response)\n                \n                # If we filtered texts, we need to create embeddings for the original indices\n                if len(valid_texts) != len(texts):\n                    log.info(f\"🔍 Creating embeddings for {len(texts)} original positions\")\n                    \n                    # Get the correct embedding dimension from the first valid embedding\n                    embedding_dim = 256  # Default fallback based on config\n                    if result.data and len(result.data) > 0 and hasattr(result.data[0], 'embedding'):\n                        embedding_dim = len(result.data[0].embedding)\n                        log.info(f\"🔍 Using embedding dimension: {embedding_dim}\")\n                    \n                    final_data = []\n                    valid_idx = 0\n                    for i in range(len(texts)):\n                        if i in valid_indices:\n                            # Use the embedding from valid texts\n                            final_data.append(result.data[valid_idx])\n                            valid_idx += 1\n                        else:\n                            # Create zero embedding for filtered texts with correct dimension\n                            log.warning(f\"🔍 Creating zero embedding for filtered text at index {i}\")\n                            final_data.append(Embedding(\n                                embedding=[0.0] * embedding_dim,  # Use correct embedding dimension\n                                index=i\n                            ))\n                    \n                    result = EmbedderOutput(\n                        data=final_data,\n                        error=None,\n                        raw_response=result.raw_response\n                    )\n                \n                return result\n                \n            except Exception as e:\n                log.error(f\"🔍 DashScope async API call failed: {e}\")\n                return EmbedderOutput(data=[], error=str(e), raw_response=None)\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @classmethod\n    def from_dict(cls, data: Dict[str, Any]):\n        \"\"\"Create an instance from a dictionary.\"\"\"\n        return cls(**data)\n\n    def to_dict(self) -> Dict[str, Any]:\n        \"\"\"Convert to dictionary.\"\"\"\n        return {\n            \"api_key\": self._api_key,\n            \"workspace_id\": self._workspace_id,\n            \"base_url\": self.base_url,\n            \"input_type\": self._input_type,\n        }\n\n    def __getstate__(self):\n        \"\"\"\n        Customize serialization to exclude non-picklable client objects.\n        This method is called by pickle when saving the object's state.\n        \"\"\"\n        state = self.__dict__.copy()\n        # Remove the unpicklable client instances\n        if 'sync_client' in state:\n            del state['sync_client']\n        if 'async_client' in state:\n            del state['async_client']\n        return state\n\n    def __setstate__(self, state):\n        \"\"\"\n        Customize deserialization to re-create the client objects.\n        This method is called by pickle when loading the object's state.\n        \"\"\"\n        self.__dict__.update(state)\n        # Re-initialize the clients after unpickling\n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # It will be lazily initialized when acall is used\n\n\nclass DashScopeEmbedder(DataComponent):\n    r\"\"\"\n    A user-facing component that orchestrates an embedder model via the DashScope model client and output processors.\n\n    Args:\n        model_client (ModelClient): The DashScope model client to use for the embedder.\n        model_kwargs (Dict[str, Any], optional): The model kwargs to pass to the model client. Defaults to {}.\n        output_processors (Optional[Component], optional): The output processors after model call. Defaults to None.\n    \"\"\"\n\n    model_type: ModelType = ModelType.EMBEDDER\n    model_client: ModelClient\n    output_processors: Optional[DataComponent]\n\n    def __init__(\n        self,\n        *,\n        model_client: ModelClient,\n        model_kwargs: Dict[str, Any] = {},\n        output_processors: Optional[DataComponent] = None,\n    ) -> None:\n\n        super().__init__(model_kwargs=model_kwargs)\n        if not isinstance(model_kwargs, Dict):\n            raise TypeError(\n                f\"{type(self).__name__} requires a dictionary for model_kwargs, not a string\"\n            )\n        self.model_kwargs = model_kwargs.copy()\n\n        if not isinstance(model_client, ModelClient):\n            raise TypeError(\n                f\"{type(self).__name__} requires a ModelClient instance for model_client.\"\n            )\n        self.model_client = model_client\n        self.output_processors = output_processors\n\n    def call(\n        self,\n        input: EmbedderInputType,\n        model_kwargs: Optional[Dict] = {},\n    ) -> EmbedderOutputType:\n        log.debug(f\"Calling {self.__class__.__name__} with input: {input}\")\n        api_kwargs = self.model_client.convert_inputs_to_api_kwargs(\n            input=input,\n            model_kwargs=self._compose_model_kwargs(**model_kwargs),\n            model_type=self.model_type,\n        )\n        try:\n            output = self.model_client.call(\n                api_kwargs=api_kwargs, model_type=self.model_type\n            )\n        except Exception as e:\n            log.error(f\"🤡 Error calling the DashScope model: {e}\")\n            output = EmbedderOutput(error=str(e))\n        return output\n\n    async def acall(\n        self,\n        input: EmbedderInputType,\n        model_kwargs: Optional[Dict] = {},\n    ) -> EmbedderOutputType:\n        log.debug(f\"Calling {self.__class__.__name__} with input: {input}\")\n        api_kwargs = self.model_client.convert_inputs_to_api_kwargs(\n            input=input,\n            model_kwargs=self._compose_model_kwargs(**model_kwargs),\n            model_type=self.model_type,\n        )\n        output: EmbedderOutputType = None\n        try:\n            response = await self.model_client.acall(\n                api_kwargs=api_kwargs, model_type=self.model_type\n            )\n            output = self.model_client.parse_embedding_response(response)\n        except Exception as e:\n            log.error(f\"Error calling the DashScope model: {e}\")\n            output = EmbedderOutput(error=str(e))\n\n        output.input = [input] if isinstance(input, str) else input\n        log.debug(f\"Output from {self.__class__.__name__}: {output}\")\n        return output\n\n    def _compose_model_kwargs(self, **model_kwargs) -> Dict[str, object]:\n        return F.compose_model_kwargs(self.model_kwargs, model_kwargs)\n\n# Batch Embedding Components for DashScope\nclass DashScopeBatchEmbedder(DataComponent):\n    \"\"\"Batch embedder specifically designed for DashScope API\"\"\"\n\n    def __init__(self, embedder, batch_size: int = 100, embedding_cache_file_name: str = \"default\") -> None:\n        super().__init__(batch_size=batch_size)\n        self.embedder = embedder\n        self.batch_size = batch_size\n        if self.batch_size > 25:\n            log.warning(f\"DashScope batch embedder initialization, batch size: {self.batch_size}, note that DashScope batch embedding size cannot exceed 25, automatically set to 25\")\n            self.batch_size = 25\n        self.cache_path = f'./embedding_cache/{embedding_cache_file_name}_{self.embedder.__class__.__name__}_dashscope_embeddings.pkl'\n\n    def call(\n        self, input: BatchEmbedderInputType, model_kwargs: Optional[Dict] = {}, force_recreate: bool = False\n    ) -> BatchEmbedderOutputType:\n        \"\"\"\n        Batch call to DashScope embedder\n        \n        Args:\n            input: List of input texts\n            model_kwargs: Model parameters\n            force_recreate: Whether to force recreation\n            \n        Returns:\n            Batch embedding output\n        \"\"\"\n        # Check cache first\n        \n        if not force_recreate and os.path.exists(self.cache_path):\n            try:\n                with open(self.cache_path, 'rb') as f:\n                    embeddings = pickle.load(f)\n                    log.info(f\"Loaded cached DashScope embeddings from: {self.cache_path}\")\n                return embeddings\n            except Exception as e:\n                log.warning(f\"Failed to load cache file {self.cache_path}: {e}, proceeding with fresh embedding\")\n        \n        if isinstance(input, str):\n            input = [input]\n        \n        n = len(input)\n        embeddings: List[EmbedderOutput] = []\n        \n        log.info(f\"Starting DashScope batch embedding processing, total {n} texts, batch size: {self.batch_size}\")\n        \n        for i in tqdm(\n            range(0, n, self.batch_size),\n            desc=\"DashScope batch embedding\",\n            disable=False,\n        ):\n            batch_input = input[i : min(i + self.batch_size, n)]\n            \n            try:\n                # Use correct calling method: directly call embedder instance\n                batch_output = self.embedder(\n                    input=batch_input, model_kwargs=model_kwargs\n                )\n                embeddings.append(batch_output)\n                \n                # Validate batch output\n                if batch_output.error:\n                    log.error(f\"Batch {i//self.batch_size + 1} embedding failed: {batch_output.error}\")\n                elif batch_output.data:\n                    log.debug(f\"Batch {i//self.batch_size + 1} successfully generated {len(batch_output.data)} embedding vectors\")\n                else:\n                    log.warning(f\"Batch {i//self.batch_size + 1} returned no embedding data\")\n                    \n            except Exception as e:\n                log.error(f\"Batch {i//self.batch_size + 1} processing exception: {e}\")\n                # Create error embedding output\n                error_output = EmbedderOutput(\n                    data=[],\n                    error=str(e),\n                    raw_response=None\n                )\n                embeddings.append(error_output)\n        \n        log.info(f\"DashScope batch embedding completed, processed {len(embeddings)} batches\")\n        \n        # Save to cache\n        try:\n            if not os.path.exists('./embedding_cache'):\n                os.makedirs('./embedding_cache')\n            with open(self.cache_path, 'wb') as f:\n                pickle.dump(embeddings, f)\n                log.info(f\"Saved DashScope embeddings cache to: {self.cache_path}\")\n        except Exception as e:\n            log.warning(f\"Failed to save cache to {self.cache_path}: {e}\")\n        \n        return embeddings\n    \n    def __call__(self, input: BatchEmbedderInputType, model_kwargs: Optional[Dict] = {}, force_recreate: bool = False) -> BatchEmbedderOutputType:\n        \"\"\"\n        Call operator interface, delegates to call method\n        \"\"\"\n        return self.call(input=input, model_kwargs=model_kwargs, force_recreate=force_recreate)\n\n\nclass DashScopeToEmbeddings(DataComponent):\n    \"\"\"Component that converts document sequences to embedding vector sequences, specifically optimized for DashScope API\"\"\"\n\n    def __init__(self, embedder, batch_size: int = 100, force_recreate_db: bool = False, embedding_cache_file_name: str = \"default\") -> None:\n        super().__init__(batch_size=batch_size)\n        self.embedder = embedder\n        self.batch_size = batch_size\n        self.batch_embedder = DashScopeBatchEmbedder(embedder=embedder, batch_size=batch_size, embedding_cache_file_name=embedding_cache_file_name)\n        self.force_recreate_db = force_recreate_db\n\n    def __call__(self, input: List[Document]) -> List[Document]:\n        \"\"\"\n        Process list of documents, generating embedding vectors for each document\n        \n        Args:\n            input: List of input documents\n            \n        Returns:\n            List of documents containing embedding vectors\n        \"\"\"\n        output = deepcopy(input)\n        \n        # Convert to text list\n        embedder_input: List[str] = [chunk.text for chunk in output]\n        \n        log.info(f\"Starting to process embeddings for {len(embedder_input)} documents\")\n        \n        # Batch process embeddings\n        outputs: List[EmbedderOutput] = self.batch_embedder(\n            input=embedder_input, \n            force_recreate=self.force_recreate_db\n        )\n        \n        # Validate output\n        total_embeddings = 0\n        error_batches = 0\n        \n        for batch_output in outputs:\n            if batch_output.error:\n                error_batches += 1\n                log.error(f\"Found error batch: {batch_output.error}\")\n            elif batch_output.data:\n                total_embeddings += len(batch_output.data)\n            \n        log.info(f\"Embedding statistics: total {total_embeddings} valid embeddings, {error_batches} error batches\")\n        \n        # Assign embedding vectors back to documents\n        doc_idx = 0\n        for batch_idx, batch_output in tqdm(\n            enumerate(outputs), \n            desc=\"Assigning embedding vectors to documents\",\n            disable=False\n        ):\n            if batch_output.error:\n                # Create empty vectors for documents in error batches\n                batch_size_actual = min(self.batch_size, len(output) - doc_idx)\n                log.warning(f\"Creating empty vectors for {batch_size_actual} documents in batch {batch_idx}\")\n                \n                for i in range(batch_size_actual):\n                    if doc_idx < len(output):\n                        output[doc_idx].vector = []\n                        doc_idx += 1\n            else:\n                # Assign normal embedding vectors\n                for embedding in batch_output.data:\n                    if doc_idx < len(output):\n                        if hasattr(embedding, 'embedding'):\n                            output[doc_idx].vector = embedding.embedding\n                        else:\n                            log.warning(f\"Invalid embedding format for document {doc_idx}\")\n                            output[doc_idx].vector = []\n                        doc_idx += 1\n        \n        # Validate results\n        valid_count = 0\n        empty_count = 0\n        \n        for doc in output:\n            if hasattr(doc, 'vector') and doc.vector and len(doc.vector) > 0:\n                valid_count += 1\n            else:\n                empty_count += 1\n        \n        log.info(f\"Embedding results: {valid_count} valid vectors, {empty_count} empty vectors\")\n        \n        if valid_count == 0:\n            log.error(\"❌ All documents have empty embedding vectors!\")\n        elif empty_count > 0:\n            log.warning(f\"⚠️ Found {empty_count} empty embedding vectors\")\n        else:\n            log.info(\"✅ All documents successfully generated embedding vectors\")\n        \n        return output\n\n    def _extra_repr(self) -> str:\n        return f\"batch_size={self.batch_size}\" "
  },
  {
    "path": "api/data_pipeline.py",
    "content": "import adalflow as adal\nfrom adalflow.core.types import Document, List\nfrom adalflow.components.data_process import TextSplitter, ToEmbeddings\nimport os\nimport subprocess\nimport json\nimport tiktoken\nimport logging\nimport base64\nimport glob\nfrom adalflow.utils import get_adalflow_default_root_path\nfrom adalflow.core.db import LocalDB\nfrom api.config import configs, DEFAULT_EXCLUDED_DIRS, DEFAULT_EXCLUDED_FILES\nfrom api.ollama_patch import OllamaDocumentProcessor\nfrom urllib.parse import urlparse, urlunparse, quote\nimport requests\nfrom requests.exceptions import RequestException\n\nfrom api.tools.embedder import get_embedder\n\n# Configure logging\nlogger = logging.getLogger(__name__)\n\n# Maximum token limit for OpenAI embedding models\nMAX_EMBEDDING_TOKENS = 8192\n\ndef count_tokens(text: str, embedder_type: str = None, is_ollama_embedder: bool = None) -> int:\n    \"\"\"\n    Count the number of tokens in a text string using tiktoken.\n\n    Args:\n        text (str): The text to count tokens for.\n        embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama', 'bedrock').\n                                     If None, will be determined from configuration.\n        is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                           If None, will be determined from configuration.\n\n    Returns:\n        int: The number of tokens in the text.\n    \"\"\"\n    try:\n        # Handle backward compatibility\n        if embedder_type is None and is_ollama_embedder is not None:\n            embedder_type = 'ollama' if is_ollama_embedder else None\n        \n        # Determine embedder type if not specified\n        if embedder_type is None:\n            from api.config import get_embedder_type\n            embedder_type = get_embedder_type()\n\n        # Choose encoding based on embedder type\n        if embedder_type == 'ollama':\n            # Ollama typically uses cl100k_base encoding\n            encoding = tiktoken.get_encoding(\"cl100k_base\")\n        elif embedder_type == 'google':\n            # Google uses similar tokenization to GPT models for rough estimation\n            encoding = tiktoken.get_encoding(\"cl100k_base\")\n        elif embedder_type == 'bedrock':\n            # Bedrock embedding models vary; use a common GPT-like encoding for rough estimation\n            encoding = tiktoken.get_encoding(\"cl100k_base\")\n        else:  # OpenAI or default\n            # Use OpenAI embedding model encoding\n            encoding = tiktoken.encoding_for_model(\"text-embedding-3-small\")\n\n        return len(encoding.encode(text))\n    except Exception as e:\n        # Fallback to a simple approximation if tiktoken fails\n        logger.warning(f\"Error counting tokens with tiktoken: {e}\")\n        # Rough approximation: 4 characters per token\n        return len(text) // 4\n\ndef download_repo(repo_url: str, local_path: str, repo_type: str = None, access_token: str = None) -> str:\n    \"\"\"\n    Downloads a Git repository (GitHub, GitLab, or Bitbucket) to a specified local path.\n\n    Args:\n        repo_type(str): Type of repository\n        repo_url (str): The URL of the Git repository to clone.\n        local_path (str): The local directory where the repository will be cloned.\n        access_token (str, optional): Access token for private repositories.\n\n    Returns:\n        str: The output message from the `git` command.\n    \"\"\"\n    try:\n        # Check if Git is installed\n        logger.info(f\"Preparing to clone repository to {local_path}\")\n        subprocess.run(\n            [\"git\", \"--version\"],\n            check=True,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n        )\n\n        # Check if repository already exists\n        if os.path.exists(local_path) and os.listdir(local_path):\n            # Directory exists and is not empty\n            logger.warning(f\"Repository already exists at {local_path}. Using existing repository.\")\n            return f\"Using existing repository at {local_path}\"\n\n        # Ensure the local path exists\n        os.makedirs(local_path, exist_ok=True)\n\n        # Prepare the clone URL with access token if provided\n        clone_url = repo_url\n        if access_token:\n            parsed = urlparse(repo_url)\n            # URL-encode the token to handle special characters\n            encoded_token = quote(access_token, safe='')\n            # Determine the repository type and format the URL accordingly\n            if repo_type == \"github\":\n                # Format: https://{token}@{domain}/owner/repo.git\n                # Works for both github.com and enterprise GitHub domains\n                clone_url = urlunparse((parsed.scheme, f\"{encoded_token}@{parsed.netloc}\", parsed.path, '', '', ''))\n            elif repo_type == \"gitlab\":\n                # Format: https://oauth2:{token}@gitlab.com/owner/repo.git\n                clone_url = urlunparse((parsed.scheme, f\"oauth2:{encoded_token}@{parsed.netloc}\", parsed.path, '', '', ''))\n            elif repo_type == \"bitbucket\":\n                # Format: https://x-token-auth:{token}@bitbucket.org/owner/repo.git\n                clone_url = urlunparse((parsed.scheme, f\"x-token-auth:{encoded_token}@{parsed.netloc}\", parsed.path, '', '', ''))\n\n            logger.info(\"Using access token for authentication\")\n\n        # Clone the repository\n        logger.info(f\"Cloning repository from {repo_url} to {local_path}\")\n        # We use repo_url in the log to avoid exposing the token in logs\n        result = subprocess.run(\n            [\"git\", \"clone\", \"--depth=1\", \"--single-branch\", clone_url, local_path],\n            check=True,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n        )\n\n        logger.info(\"Repository cloned successfully\")\n        return result.stdout.decode(\"utf-8\")\n\n    except subprocess.CalledProcessError as e:\n        error_msg = e.stderr.decode('utf-8')\n        # Sanitize error message to remove any tokens (both raw and URL-encoded)\n        if access_token:\n            # Remove raw token\n            error_msg = error_msg.replace(access_token, \"***TOKEN***\")\n            # Also remove URL-encoded token to prevent leaking encoded version\n            encoded_token = quote(access_token, safe='')\n            error_msg = error_msg.replace(encoded_token, \"***TOKEN***\")\n        raise ValueError(f\"Error during cloning: {error_msg}\")\n    except Exception as e:\n        raise ValueError(f\"An unexpected error occurred: {str(e)}\")\n\n# Alias for backward compatibility\ndownload_github_repo = download_repo\n\ndef read_all_documents(path: str, embedder_type: str = None, is_ollama_embedder: bool = None, \n                      excluded_dirs: List[str] = None, excluded_files: List[str] = None,\n                      included_dirs: List[str] = None, included_files: List[str] = None):\n    \"\"\"\n    Recursively reads all documents in a directory and its subdirectories.\n\n    Args:\n        path (str): The root directory path.\n        embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama').\n                                     If None, will be determined from configuration.\n        is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                           If None, will be determined from configuration.\n        excluded_dirs (List[str], optional): List of directories to exclude from processing.\n            Overrides the default configuration if provided.\n        excluded_files (List[str], optional): List of file patterns to exclude from processing.\n            Overrides the default configuration if provided.\n        included_dirs (List[str], optional): List of directories to include exclusively.\n            When provided, only files in these directories will be processed.\n        included_files (List[str], optional): List of file patterns to include exclusively.\n            When provided, only files matching these patterns will be processed.\n\n    Returns:\n        list: A list of Document objects with metadata.\n    \"\"\"\n    # Handle backward compatibility\n    if embedder_type is None and is_ollama_embedder is not None:\n        embedder_type = 'ollama' if is_ollama_embedder else None\n    documents = []\n    # File extensions to look for, prioritizing code files\n    code_extensions = [\".py\", \".js\", \".ts\", \".java\", \".cpp\", \".c\", \".h\", \".hpp\", \".go\", \".rs\",\n                       \".jsx\", \".tsx\", \".html\", \".css\", \".php\", \".swift\", \".cs\"]\n    doc_extensions = [\".md\", \".txt\", \".rst\", \".json\", \".yaml\", \".yml\"]\n\n    # Determine filtering mode: inclusion or exclusion\n    use_inclusion_mode = (included_dirs is not None and len(included_dirs) > 0) or (included_files is not None and len(included_files) > 0)\n\n    if use_inclusion_mode:\n        # Inclusion mode: only process specified directories and files\n        final_included_dirs = set(included_dirs) if included_dirs else set()\n        final_included_files = set(included_files) if included_files else set()\n\n        logger.info(f\"Using inclusion mode\")\n        logger.info(f\"Included directories: {list(final_included_dirs)}\")\n        logger.info(f\"Included files: {list(final_included_files)}\")\n\n        # Convert to lists for processing\n        included_dirs = list(final_included_dirs)\n        included_files = list(final_included_files)\n        excluded_dirs = []\n        excluded_files = []\n    else:\n        # Exclusion mode: use default exclusions plus any additional ones\n        final_excluded_dirs = set(DEFAULT_EXCLUDED_DIRS)\n        final_excluded_files = set(DEFAULT_EXCLUDED_FILES)\n\n        # Add any additional excluded directories from config\n        if \"file_filters\" in configs and \"excluded_dirs\" in configs[\"file_filters\"]:\n            final_excluded_dirs.update(configs[\"file_filters\"][\"excluded_dirs\"])\n\n        # Add any additional excluded files from config\n        if \"file_filters\" in configs and \"excluded_files\" in configs[\"file_filters\"]:\n            final_excluded_files.update(configs[\"file_filters\"][\"excluded_files\"])\n\n        # Add any explicitly provided excluded directories and files\n        if excluded_dirs is not None:\n            final_excluded_dirs.update(excluded_dirs)\n\n        if excluded_files is not None:\n            final_excluded_files.update(excluded_files)\n\n        # Convert back to lists for compatibility\n        excluded_dirs = list(final_excluded_dirs)\n        excluded_files = list(final_excluded_files)\n        included_dirs = []\n        included_files = []\n\n        logger.info(f\"Using exclusion mode\")\n        logger.info(f\"Excluded directories: {excluded_dirs}\")\n        logger.info(f\"Excluded files: {excluded_files}\")\n\n    logger.info(f\"Reading documents from {path}\")\n\n    def should_process_file(file_path: str, use_inclusion: bool, included_dirs: List[str], included_files: List[str],\n                           excluded_dirs: List[str], excluded_files: List[str]) -> bool:\n        \"\"\"\n        Determine if a file should be processed based on inclusion/exclusion rules.\n\n        Args:\n            file_path (str): The file path to check\n            use_inclusion (bool): Whether to use inclusion mode\n            included_dirs (List[str]): List of directories to include\n            included_files (List[str]): List of files to include\n            excluded_dirs (List[str]): List of directories to exclude\n            excluded_files (List[str]): List of files to exclude\n\n        Returns:\n            bool: True if the file should be processed, False otherwise\n        \"\"\"\n        file_path_parts = os.path.normpath(file_path).split(os.sep)\n        file_name = os.path.basename(file_path)\n\n        if use_inclusion:\n            # Inclusion mode: file must be in included directories or match included files\n            is_included = False\n\n            # Check if file is in an included directory\n            if included_dirs:\n                for included in included_dirs:\n                    clean_included = included.strip(\"./\").rstrip(\"/\")\n                    if clean_included in file_path_parts:\n                        is_included = True\n                        break\n\n            # Check if file matches included file patterns\n            if not is_included and included_files:\n                for included_file in included_files:\n                    if file_name == included_file or file_name.endswith(included_file):\n                        is_included = True\n                        break\n\n            # If no inclusion rules are specified for a category, allow all files from that category\n            if not included_dirs and not included_files:\n                is_included = True\n            elif not included_dirs and included_files:\n                # Only file patterns specified, allow all directories\n                pass  # is_included is already set based on file patterns\n            elif included_dirs and not included_files:\n                # Only directory patterns specified, allow all files in included directories\n                pass  # is_included is already set based on directory patterns\n\n            return is_included\n        else:\n            # Exclusion mode: file must not be in excluded directories or match excluded files\n            is_excluded = False\n\n            # Check if file is in an excluded directory\n            for excluded in excluded_dirs:\n                clean_excluded = excluded.strip(\"./\").rstrip(\"/\")\n                if clean_excluded in file_path_parts:\n                    is_excluded = True\n                    break\n\n            # Check if file matches excluded file patterns\n            if not is_excluded:\n                for excluded_file in excluded_files:\n                    if file_name == excluded_file:\n                        is_excluded = True\n                        break\n\n            return not is_excluded\n\n    # Process code files first\n    for ext in code_extensions:\n        files = glob.glob(f\"{path}/**/*{ext}\", recursive=True)\n        for file_path in files:\n            # Check if file should be processed based on inclusion/exclusion rules\n            if not should_process_file(file_path, use_inclusion_mode, included_dirs, included_files, excluded_dirs, excluded_files):\n                continue\n\n            try:\n                with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                    content = f.read()\n                    relative_path = os.path.relpath(file_path, path)\n\n                    # Determine if this is an implementation file\n                    is_implementation = (\n                        not relative_path.startswith(\"test_\")\n                        and not relative_path.startswith(\"app_\")\n                        and \"test\" not in relative_path.lower()\n                    )\n\n                    # Check token count\n                    token_count = count_tokens(content, embedder_type)\n                    if token_count > MAX_EMBEDDING_TOKENS * 10:\n                        logger.warning(f\"Skipping large file {relative_path}: Token count ({token_count}) exceeds limit\")\n                        continue\n\n                    doc = Document(\n                        text=content,\n                        meta_data={\n                            \"file_path\": relative_path,\n                            \"type\": ext[1:],\n                            \"is_code\": True,\n                            \"is_implementation\": is_implementation,\n                            \"title\": relative_path,\n                            \"token_count\": token_count,\n                        },\n                    )\n                    documents.append(doc)\n            except Exception as e:\n                logger.error(f\"Error reading {file_path}: {e}\")\n\n    # Then process documentation files\n    for ext in doc_extensions:\n        files = glob.glob(f\"{path}/**/*{ext}\", recursive=True)\n        for file_path in files:\n            # Check if file should be processed based on inclusion/exclusion rules\n            if not should_process_file(file_path, use_inclusion_mode, included_dirs, included_files, excluded_dirs, excluded_files):\n                continue\n\n            try:\n                with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                    content = f.read()\n                    relative_path = os.path.relpath(file_path, path)\n\n                    # Check token count\n                    token_count = count_tokens(content, embedder_type)\n                    if token_count > MAX_EMBEDDING_TOKENS:\n                        logger.warning(f\"Skipping large file {relative_path}: Token count ({token_count}) exceeds limit\")\n                        continue\n\n                    doc = Document(\n                        text=content,\n                        meta_data={\n                            \"file_path\": relative_path,\n                            \"type\": ext[1:],\n                            \"is_code\": False,\n                            \"is_implementation\": False,\n                            \"title\": relative_path,\n                            \"token_count\": token_count,\n                        },\n                    )\n                    documents.append(doc)\n            except Exception as e:\n                logger.error(f\"Error reading {file_path}: {e}\")\n\n    logger.info(f\"Found {len(documents)} documents\")\n    return documents\n\ndef prepare_data_pipeline(embedder_type: str = None, is_ollama_embedder: bool = None):\n    \"\"\"\n    Creates and returns the data transformation pipeline.\n\n    Args:\n        embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama').\n                                     If None, will be determined from configuration.\n        is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                           If None, will be determined from configuration.\n\n    Returns:\n        adal.Sequential: The data transformation pipeline\n    \"\"\"\n    from api.config import get_embedder_config, get_embedder_type\n\n    # Handle backward compatibility\n    if embedder_type is None and is_ollama_embedder is not None:\n        embedder_type = 'ollama' if is_ollama_embedder else None\n    \n    # Determine embedder type if not specified\n    if embedder_type is None:\n        embedder_type = get_embedder_type()\n\n    splitter = TextSplitter(**configs[\"text_splitter\"])\n    embedder_config = get_embedder_config()\n\n    embedder = get_embedder(embedder_type=embedder_type)\n\n    # Choose appropriate processor based on embedder type\n    if embedder_type == 'ollama':\n        # Use Ollama document processor for single-document processing\n        embedder_transformer = OllamaDocumentProcessor(embedder=embedder)\n    else:\n        # Use batch processing for OpenAI and Google embedders\n        batch_size = embedder_config.get(\"batch_size\", 500)\n        embedder_transformer = ToEmbeddings(\n            embedder=embedder, batch_size=batch_size\n        )\n\n    data_transformer = adal.Sequential(\n        splitter, embedder_transformer\n    )  # sequential will chain together splitter and embedder\n    return data_transformer\n\ndef transform_documents_and_save_to_db(\n    documents: List[Document], db_path: str, embedder_type: str = None, is_ollama_embedder: bool = None\n) -> LocalDB:\n    \"\"\"\n    Transforms a list of documents and saves them to a local database.\n\n    Args:\n        documents (list): A list of `Document` objects.\n        db_path (str): The path to the local database file.\n        embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama').\n                                     If None, will be determined from configuration.\n        is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                           If None, will be determined from configuration.\n    \"\"\"\n    # Get the data transformer\n    data_transformer = prepare_data_pipeline(embedder_type, is_ollama_embedder)\n\n    # Save the documents to a local database\n    db = LocalDB()\n    db.register_transformer(transformer=data_transformer, key=\"split_and_embed\")\n    db.load(documents)\n    db.transform(key=\"split_and_embed\")\n    os.makedirs(os.path.dirname(db_path), exist_ok=True)\n    db.save_state(filepath=db_path)\n    return db\n\ndef get_github_file_content(repo_url: str, file_path: str, access_token: str = None) -> str:\n    \"\"\"\n    Retrieves the content of a file from a GitHub repository using the GitHub API.\n    Supports both public GitHub (github.com) and GitHub Enterprise (custom domains).\n    \n    Args:\n        repo_url (str): The URL of the GitHub repository \n                       (e.g., \"https://github.com/username/repo\" or \"https://github.company.com/username/repo\")\n        file_path (str): The path to the file within the repository (e.g., \"src/main.py\")\n        access_token (str, optional): GitHub personal access token for private repositories\n\n    Returns:\n        str: The content of the file as a string\n\n    Raises:\n        ValueError: If the file cannot be fetched or if the URL is not a valid GitHub URL\n    \"\"\"\n    try:\n        # Parse the repository URL to support both github.com and enterprise GitHub\n        parsed_url = urlparse(repo_url)\n        if not parsed_url.scheme or not parsed_url.netloc:\n            raise ValueError(\"Not a valid GitHub repository URL\")\n\n        # Check if it's a GitHub-like URL structure\n        path_parts = parsed_url.path.strip('/').split('/')\n        if len(path_parts) < 2:\n            raise ValueError(\"Invalid GitHub URL format - expected format: https://domain/owner/repo\")\n\n        owner = path_parts[-2]\n        repo = path_parts[-1].replace(\".git\", \"\")\n\n        # Determine the API base URL\n        if parsed_url.netloc == \"github.com\":\n            # Public GitHub\n            api_base = \"https://api.github.com\"\n        else:\n            # GitHub Enterprise - API is typically at https://domain/api/v3/\n            api_base = f\"{parsed_url.scheme}://{parsed_url.netloc}/api/v3\"\n        \n        # Use GitHub API to get file content\n        # The API endpoint for getting file content is: /repos/{owner}/{repo}/contents/{path}\n        api_url = f\"{api_base}/repos/{owner}/{repo}/contents/{file_path}\"\n\n        # Fetch file content from GitHub API\n        headers = {}\n        if access_token:\n            headers[\"Authorization\"] = f\"token {access_token}\"\n        logger.info(f\"Fetching file content from GitHub API: {api_url}\")\n        try:\n            response = requests.get(api_url, headers=headers)\n            response.raise_for_status()\n        except RequestException as e:\n            raise ValueError(f\"Error fetching file content: {e}\")\n        try:\n            content_data = response.json()\n        except json.JSONDecodeError:\n            raise ValueError(\"Invalid response from GitHub API\")\n\n        # Check if we got an error response\n        if \"message\" in content_data and \"documentation_url\" in content_data:\n            raise ValueError(f\"GitHub API error: {content_data['message']}\")\n\n        # GitHub API returns file content as base64 encoded string\n        if \"content\" in content_data and \"encoding\" in content_data:\n            if content_data[\"encoding\"] == \"base64\":\n                # The content might be split into lines, so join them first\n                content_base64 = content_data[\"content\"].replace(\"\\n\", \"\")\n                content = base64.b64decode(content_base64).decode(\"utf-8\")\n                return content\n            else:\n                raise ValueError(f\"Unexpected encoding: {content_data['encoding']}\")\n        else:\n            raise ValueError(\"File content not found in GitHub API response\")\n\n    except Exception as e:\n        raise ValueError(f\"Failed to get file content: {str(e)}\")\n\ndef get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = None) -> str:\n    \"\"\"\n    Retrieves the content of a file from a GitLab repository (cloud or self-hosted).\n\n    Args:\n        repo_url (str): The GitLab repo URL (e.g., \"https://gitlab.com/username/repo\" or \"http://localhost/group/project\")\n        file_path (str): File path within the repository (e.g., \"src/main.py\")\n        access_token (str, optional): GitLab personal access token\n\n    Returns:\n        str: File content\n\n    Raises:\n        ValueError: If anything fails\n    \"\"\"\n    try:\n        # Parse and validate the URL\n        parsed_url = urlparse(repo_url)\n        if not parsed_url.scheme or not parsed_url.netloc:\n            raise ValueError(\"Not a valid GitLab repository URL\")\n\n        gitlab_domain = f\"{parsed_url.scheme}://{parsed_url.netloc}\"\n        if parsed_url.port not in (None, 80, 443):\n            gitlab_domain += f\":{parsed_url.port}\"\n        path_parts = parsed_url.path.strip(\"/\").split(\"/\")\n        if len(path_parts) < 2:\n            raise ValueError(\"Invalid GitLab URL format — expected something like https://gitlab.domain.com/group/project\")\n\n        # Build project path and encode for API\n        project_path = \"/\".join(path_parts).replace(\".git\", \"\")\n        encoded_project_path = quote(project_path, safe='')\n\n        # Encode file path\n        encoded_file_path = quote(file_path, safe='')\n\n        # Try to get the default branch from the project info\n        default_branch = None\n        try:\n            project_info_url = f\"{gitlab_domain}/api/v4/projects/{encoded_project_path}\"\n            project_headers = {}\n            if access_token:\n                project_headers[\"PRIVATE-TOKEN\"] = access_token\n            \n            project_response = requests.get(project_info_url, headers=project_headers)\n            if project_response.status_code == 200:\n                project_data = project_response.json()\n                default_branch = project_data.get('default_branch', 'main')\n                logger.info(f\"Found default branch: {default_branch}\")\n            else:\n                logger.warning(f\"Could not fetch project info, using 'main' as default branch\")\n                default_branch = 'main'\n        except Exception as e:\n            logger.warning(f\"Error fetching project info: {e}, using 'main' as default branch\")\n            default_branch = 'main'\n\n        api_url = f\"{gitlab_domain}/api/v4/projects/{encoded_project_path}/repository/files/{encoded_file_path}/raw?ref={default_branch}\"\n        # Fetch file content from GitLab API\n        headers = {}\n        if access_token:\n            headers[\"PRIVATE-TOKEN\"] = access_token\n        logger.info(f\"Fetching file content from GitLab API: {api_url}\")\n        try:\n            response = requests.get(api_url, headers=headers)\n            response.raise_for_status()\n            content = response.text\n        except RequestException as e:\n            raise ValueError(f\"Error fetching file content: {e}\")\n\n        # Check for GitLab error response (JSON instead of raw file)\n        if content.startswith(\"{\") and '\"message\":' in content:\n            try:\n                error_data = json.loads(content)\n                if \"message\" in error_data:\n                    raise ValueError(f\"GitLab API error: {error_data['message']}\")\n            except json.JSONDecodeError:\n                pass\n\n        return content\n\n    except Exception as e:\n        raise ValueError(f\"Failed to get file content: {str(e)}\")\n\ndef get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str = None) -> str:\n    \"\"\"\n    Retrieves the content of a file from a Bitbucket repository using the Bitbucket API.\n\n    Args:\n        repo_url (str): The URL of the Bitbucket repository (e.g., \"https://bitbucket.org/username/repo\")\n        file_path (str): The path to the file within the repository (e.g., \"src/main.py\")\n        access_token (str, optional): Bitbucket personal access token for private repositories\n\n    Returns:\n        str: The content of the file as a string\n    \"\"\"\n    try:\n        # Extract owner and repo name from Bitbucket URL\n        if not (repo_url.startswith(\"https://bitbucket.org/\") or repo_url.startswith(\"http://bitbucket.org/\")):\n            raise ValueError(\"Not a valid Bitbucket repository URL\")\n\n        parts = repo_url.rstrip('/').split('/')\n        if len(parts) < 5:\n            raise ValueError(\"Invalid Bitbucket URL format\")\n\n        owner = parts[-2]\n        repo = parts[-1].replace(\".git\", \"\")\n\n        # Try to get the default branch from the repository info\n        default_branch = None\n        try:\n            repo_info_url = f\"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}\"\n            repo_headers = {}\n            if access_token:\n                repo_headers[\"Authorization\"] = f\"Bearer {access_token}\"\n            \n            repo_response = requests.get(repo_info_url, headers=repo_headers)\n            if repo_response.status_code == 200:\n                repo_data = repo_response.json()\n                default_branch = repo_data.get('mainbranch', {}).get('name', 'main')\n                logger.info(f\"Found default branch: {default_branch}\")\n            else:\n                logger.warning(f\"Could not fetch repository info, using 'main' as default branch\")\n                default_branch = 'main'\n        except Exception as e:\n            logger.warning(f\"Error fetching repository info: {e}, using 'main' as default branch\")\n            default_branch = 'main'\n\n        # Use Bitbucket API to get file content\n        # The API endpoint for getting file content is: /2.0/repositories/{owner}/{repo}/src/{branch}/{path}\n        api_url = f\"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}/src/{default_branch}/{file_path}\"\n\n        # Fetch file content from Bitbucket API\n        headers = {}\n        if access_token:\n            headers[\"Authorization\"] = f\"Bearer {access_token}\"\n        logger.info(f\"Fetching file content from Bitbucket API: {api_url}\")\n        try:\n            response = requests.get(api_url, headers=headers)\n            if response.status_code == 200:\n                content = response.text\n            elif response.status_code == 404:\n                raise ValueError(\"File not found on Bitbucket. Please check the file path and repository.\")\n            elif response.status_code == 401:\n                raise ValueError(\"Unauthorized access to Bitbucket. Please check your access token.\")\n            elif response.status_code == 403:\n                raise ValueError(\"Forbidden access to Bitbucket. You might not have permission to access this file.\")\n            elif response.status_code == 500:\n                raise ValueError(\"Internal server error on Bitbucket. Please try again later.\")\n            else:\n                response.raise_for_status()\n                content = response.text\n            return content\n        except RequestException as e:\n            raise ValueError(f\"Error fetching file content: {e}\")\n\n    except Exception as e:\n        raise ValueError(f\"Failed to get file content: {str(e)}\")\n\n\ndef get_file_content(repo_url: str, file_path: str, repo_type: str = None, access_token: str = None) -> str:\n    \"\"\"\n    Retrieves the content of a file from a Git repository (GitHub or GitLab).\n\n    Args:\n        repo_type (str): Type of repository\n        repo_url (str): The URL of the repository\n        file_path (str): The path to the file within the repository\n        access_token (str, optional): Access token for private repositories\n\n    Returns:\n        str: The content of the file as a string\n\n    Raises:\n        ValueError: If the file cannot be fetched or if the URL is not valid\n    \"\"\"\n    if repo_type == \"github\":\n        return get_github_file_content(repo_url, file_path, access_token)\n    elif repo_type == \"gitlab\":\n        return get_gitlab_file_content(repo_url, file_path, access_token)\n    elif repo_type == \"bitbucket\":\n        return get_bitbucket_file_content(repo_url, file_path, access_token)\n    else:\n        raise ValueError(\"Unsupported repository type. Only GitHub, GitLab, and Bitbucket are supported.\")\n\nclass DatabaseManager:\n    \"\"\"\n    Manages the creation, loading, transformation, and persistence of LocalDB instances.\n    \"\"\"\n\n    def __init__(self):\n        self.db = None\n        self.repo_url_or_path = None\n        self.repo_paths = None\n\n    def prepare_database(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None,\n                         embedder_type: str = None, is_ollama_embedder: bool = None,\n                         excluded_dirs: List[str] = None, excluded_files: List[str] = None,\n                         included_dirs: List[str] = None, included_files: List[str] = None) -> List[Document]:\n        \"\"\"\n        Create a new database from the repository.\n\n        Args:\n            repo_type(str): Type of repository\n            repo_url_or_path (str): The URL or local path of the repository\n            access_token (str, optional): Access token for private repositories\n            embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama').\n                                         If None, will be determined from configuration.\n            is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                               If None, will be determined from configuration.\n            excluded_dirs (List[str], optional): List of directories to exclude from processing\n            excluded_files (List[str], optional): List of file patterns to exclude from processing\n            included_dirs (List[str], optional): List of directories to include exclusively\n            included_files (List[str], optional): List of file patterns to include exclusively\n\n        Returns:\n            List[Document]: List of Document objects\n        \"\"\"\n        # Handle backward compatibility\n        if embedder_type is None and is_ollama_embedder is not None:\n            embedder_type = 'ollama' if is_ollama_embedder else None\n        \n        self.reset_database()\n        self._create_repo(repo_url_or_path, repo_type, access_token)\n        return self.prepare_db_index(embedder_type=embedder_type, excluded_dirs=excluded_dirs, excluded_files=excluded_files,\n                                   included_dirs=included_dirs, included_files=included_files)\n\n    def reset_database(self):\n        \"\"\"\n        Reset the database to its initial state.\n        \"\"\"\n        self.db = None\n        self.repo_url_or_path = None\n        self.repo_paths = None\n\n    def _extract_repo_name_from_url(self, repo_url_or_path: str, repo_type: str) -> str:\n        # Extract owner and repo name to create unique identifier\n        url_parts = repo_url_or_path.rstrip('/').split('/')\n\n        if repo_type in [\"github\", \"gitlab\", \"bitbucket\"] and len(url_parts) >= 5:\n            # GitHub URL format: https://github.com/owner/repo\n            # GitLab URL format: https://gitlab.com/owner/repo or https://gitlab.com/group/subgroup/repo\n            # Bitbucket URL format: https://bitbucket.org/owner/repo\n            owner = url_parts[-2]\n            repo = url_parts[-1].replace(\".git\", \"\")\n            repo_name = f\"{owner}_{repo}\"\n        else:\n            repo_name = url_parts[-1].replace(\".git\", \"\")\n        return repo_name\n\n    def _create_repo(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None) -> None:\n        \"\"\"\n        Download and prepare all paths.\n        Paths:\n        ~/.adalflow/repos/{owner}_{repo_name} (for url, local path will be the same)\n        ~/.adalflow/databases/{owner}_{repo_name}.pkl\n\n        Args:\n            repo_type(str): Type of repository\n            repo_url_or_path (str): The URL or local path of the repository\n            access_token (str, optional): Access token for private repositories\n        \"\"\"\n        logger.info(f\"Preparing repo storage for {repo_url_or_path}...\")\n\n        try:\n            # Strip whitespace to handle URLs with leading/trailing spaces\n            repo_url_or_path = repo_url_or_path.strip()\n            \n            root_path = get_adalflow_default_root_path()\n\n            os.makedirs(root_path, exist_ok=True)\n            # url\n            if repo_url_or_path.startswith(\"https://\") or repo_url_or_path.startswith(\"http://\"):\n                # Extract the repository name from the URL\n                repo_name = self._extract_repo_name_from_url(repo_url_or_path, repo_type)\n                logger.info(f\"Extracted repo name: {repo_name}\")\n\n                save_repo_dir = os.path.join(root_path, \"repos\", repo_name)\n\n                # Check if the repository directory already exists and is not empty\n                if not (os.path.exists(save_repo_dir) and os.listdir(save_repo_dir)):\n                    # Only download if the repository doesn't exist or is empty\n                    download_repo(repo_url_or_path, save_repo_dir, repo_type, access_token)\n                else:\n                    logger.info(f\"Repository already exists at {save_repo_dir}. Using existing repository.\")\n            else:  # local path\n                repo_name = os.path.basename(repo_url_or_path)\n                save_repo_dir = repo_url_or_path\n\n            save_db_file = os.path.join(root_path, \"databases\", f\"{repo_name}.pkl\")\n            os.makedirs(save_repo_dir, exist_ok=True)\n            os.makedirs(os.path.dirname(save_db_file), exist_ok=True)\n\n            self.repo_paths = {\n                \"save_repo_dir\": save_repo_dir,\n                \"save_db_file\": save_db_file,\n            }\n            self.repo_url_or_path = repo_url_or_path\n            logger.info(f\"Repo paths: {self.repo_paths}\")\n\n        except Exception as e:\n            logger.error(f\"Failed to create repository structure: {e}\")\n            raise\n\n    def prepare_db_index(self, embedder_type: str = None, is_ollama_embedder: bool = None, \n                        excluded_dirs: List[str] = None, excluded_files: List[str] = None,\n                        included_dirs: List[str] = None, included_files: List[str] = None) -> List[Document]:\n        \"\"\"\n        Prepare the indexed database for the repository.\n\n        Args:\n            embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama').\n                                         If None, will be determined from configuration.\n            is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead.\n                                               If None, will be determined from configuration.\n            excluded_dirs (List[str], optional): List of directories to exclude from processing\n            excluded_files (List[str], optional): List of file patterns to exclude from processing\n            included_dirs (List[str], optional): List of directories to include exclusively\n            included_files (List[str], optional): List of file patterns to include exclusively\n\n        Returns:\n            List[Document]: List of Document objects\n        \"\"\"\n        def _embedding_vector_length(doc: Document) -> int:\n            vector = getattr(doc, \"vector\", None)\n            if vector is None:\n                return 0\n            try:\n                if hasattr(vector, \"shape\"):\n                    if len(vector.shape) == 0:\n                        return 0\n                    return int(vector.shape[-1])\n                if hasattr(vector, \"__len__\"):\n                    return int(len(vector))\n            except Exception:\n                return 0\n            return 0\n\n        # Handle backward compatibility\n        if embedder_type is None and is_ollama_embedder is not None:\n            embedder_type = 'ollama' if is_ollama_embedder else None\n        # check the database\n        if self.repo_paths and os.path.exists(self.repo_paths[\"save_db_file\"]):\n            logger.info(\"Loading existing database...\")\n            try:\n                self.db = LocalDB.load_state(self.repo_paths[\"save_db_file\"])\n                documents = self.db.get_transformed_data(key=\"split_and_embed\")\n                if documents:\n                    lengths = [_embedding_vector_length(doc) for doc in documents]\n                    non_empty = sum(1 for n in lengths if n > 0)\n                    empty = len(lengths) - non_empty\n                    sample_sizes = sorted({n for n in lengths if n > 0})[:3]\n                    logger.info(\n                        \"Loaded %s documents from existing database (embeddings: %s non-empty, %s empty; sample_dims=%s)\",\n                        len(documents),\n                        non_empty,\n                        empty,\n                        sample_sizes,\n                    )\n\n                    if non_empty == 0:\n                        logger.warning(\n                            \"Existing database contains no usable embeddings. Rebuilding embeddings...\"\n                        )\n                    else:\n                        return documents\n            except Exception as e:\n                logger.error(f\"Error loading existing database: {e}\")\n                # Continue to create a new database\n\n        # prepare the database\n        logger.info(\"Creating new database...\")\n        documents = read_all_documents(\n            self.repo_paths[\"save_repo_dir\"],\n            embedder_type=embedder_type,\n            excluded_dirs=excluded_dirs,\n            excluded_files=excluded_files,\n            included_dirs=included_dirs,\n            included_files=included_files\n        )\n        self.db = transform_documents_and_save_to_db(\n            documents, self.repo_paths[\"save_db_file\"], embedder_type=embedder_type\n        )\n        logger.info(f\"Total documents: {len(documents)}\")\n        transformed_docs = self.db.get_transformed_data(key=\"split_and_embed\")\n        logger.info(f\"Total transformed documents: {len(transformed_docs)}\")\n        return transformed_docs\n\n    def prepare_retriever(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None):\n        \"\"\"\n        Prepare the retriever for a repository.\n        This is a compatibility method for the isolated API.\n\n        Args:\n            repo_type(str): Type of repository\n            repo_url_or_path (str): The URL or local path of the repository\n            access_token (str, optional): Access token for private repositories\n\n        Returns:\n            List[Document]: List of Document objects\n        \"\"\"\n        return self.prepare_database(repo_url_or_path, repo_type, access_token)\n"
  },
  {
    "path": "api/google_embedder_client.py",
    "content": "\"\"\"Google AI Embeddings ModelClient integration.\"\"\"\n\nimport os\nimport logging\nimport backoff\nfrom typing import Dict, Any, Optional, List, Sequence\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import ModelType, EmbedderOutput\n\ntry:\n    import google.generativeai as genai\n    from google.generativeai.types.text_types import EmbeddingDict, BatchEmbeddingDict\nexcept ImportError:\n    raise ImportError(\"google-generativeai is required. Install it with 'pip install google-generativeai'\")\n\nlog = logging.getLogger(__name__)\n\n\nclass GoogleEmbedderClient(ModelClient):\n    __doc__ = r\"\"\"A component wrapper for Google AI Embeddings API client.\n\n    This client provides access to Google's embedding models through the Google AI API.\n    It supports text embeddings for various tasks including semantic similarity,\n    retrieval, and classification.\n\n    Args:\n        api_key (Optional[str]): Google AI API key. Defaults to None.\n            If not provided, will use the GOOGLE_API_KEY environment variable.\n        env_api_key_name (str): Environment variable name for the API key.\n            Defaults to \"GOOGLE_API_KEY\".\n\n    Example:\n        ```python\n        from api.google_embedder_client import GoogleEmbedderClient\n        import adalflow as adal\n\n        client = GoogleEmbedderClient()\n        embedder = adal.Embedder(\n            model_client=client,\n            model_kwargs={\n                \"model\": \"gemini-embedding-001\",\n                \"task_type\": \"SEMANTIC_SIMILARITY\"\n            }\n        )\n        ```\n\n    References:\n        - Google AI Embeddings: https://ai.google.dev/gemini-api/docs/embeddings\n        - Available models: gemini-embedding-001\n    \"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        env_api_key_name: str = \"GOOGLE_API_KEY\",\n    ):\n        \"\"\"Initialize Google AI Embeddings client.\n        \n        Args:\n            api_key: Google AI API key. If not provided, uses environment variable.\n            env_api_key_name: Name of environment variable containing API key.\n        \"\"\"\n        super().__init__()\n        self._api_key = api_key\n        self._env_api_key_name = env_api_key_name\n        self._initialize_client()\n\n    def _initialize_client(self):\n        \"\"\"Initialize the Google AI client with API key.\"\"\"\n        api_key = self._api_key or os.getenv(self._env_api_key_name)\n        if not api_key:\n            raise ValueError(\n                f\"Environment variable {self._env_api_key_name} must be set\"\n            )\n        genai.configure(api_key=api_key)\n\n    def parse_embedding_response(self, response) -> EmbedderOutput:\n        \"\"\"Parse Google AI embedding response to EmbedderOutput format.\n        \n        Args:\n            response: Google AI embedding response (EmbeddingDict or BatchEmbeddingDict)\n            \n        Returns:\n            EmbedderOutput with parsed embeddings\n        \"\"\"\n        try:\n            from adalflow.core.types import Embedding\n            \n            embedding_data = []\n\n            def _extract_embedding_value(obj):\n                if obj is None:\n                    return None\n                if isinstance(obj, dict):\n                    if \"embedding\" in obj:\n                        return obj.get(\"embedding\")\n                    if \"embeddings\" in obj:\n                        return obj.get(\"embeddings\")\n                if hasattr(obj, \"embedding\"):\n                    return getattr(obj, \"embedding\")\n                if hasattr(obj, \"embeddings\"):\n                    return getattr(obj, \"embeddings\")\n                for method_name in (\"model_dump\", \"to_dict\", \"dict\"):\n                    if hasattr(obj, method_name):\n                        try:\n                            dumped = getattr(obj, method_name)()\n                            if isinstance(dumped, dict):\n                                if \"embedding\" in dumped:\n                                    return dumped.get(\"embedding\")\n                                if \"embeddings\" in dumped:\n                                    return dumped.get(\"embeddings\")\n                        except Exception:\n                            pass\n                return None\n            \n            embedding_value = _extract_embedding_value(response)\n            if embedding_value is None:\n                log.warning(\"Unexpected embedding response type/structure: %s\", type(response))\n                embedding_data = []\n            elif isinstance(embedding_value, list) and len(embedding_value) > 0:\n                if isinstance(embedding_value[0], (int, float)):\n                    embedding_data = [Embedding(embedding=embedding_value, index=0)]\n                elif isinstance(embedding_value[0], list):\n                    embedding_data = [\n                        Embedding(embedding=emb_list, index=i)\n                        for i, emb_list in enumerate(embedding_value)\n                        if isinstance(emb_list, list) and len(emb_list) > 0\n                    ]\n                else:\n                    extracted = []\n                    for item in embedding_value:\n                        item_emb = _extract_embedding_value(item)\n                        if isinstance(item_emb, list) and len(item_emb) > 0:\n                            extracted.append(item_emb)\n                    embedding_data = [\n                        Embedding(embedding=emb_list, index=i)\n                        for i, emb_list in enumerate(extracted)\n                    ]\n            else:\n                log.warning(\"Empty or invalid embedding data parsed from response\")\n                embedding_data = []\n\n            if embedding_data:\n                first_dim = len(embedding_data[0].embedding) if embedding_data[0].embedding is not None else 0\n                log.info(\"Parsed %s embedding(s) (dim=%s)\", len(embedding_data), first_dim)\n            \n            return EmbedderOutput(\n                data=embedding_data,\n                error=None,\n                raw_response=response\n            )\n        except Exception as e:\n            log.error(f\"Error parsing Google AI embedding response: {e}\")\n            return EmbedderOutput(\n                data=[],\n                error=str(e),\n                raw_response=response\n            )\n\n    def convert_inputs_to_api_kwargs(\n        self,\n        input: Optional[Any] = None,\n        model_kwargs: Dict = {},\n        model_type: ModelType = ModelType.UNDEFINED,\n    ) -> Dict:\n        \"\"\"Convert inputs to Google AI API format.\n        \n        Args:\n            input: Text input(s) to embed\n            model_kwargs: Model parameters including model name and task_type\n            model_type: Should be ModelType.EMBEDDER for this client\n            \n        Returns:\n            Dict: API kwargs for Google AI embedding call\n        \"\"\"\n        if model_type != ModelType.EMBEDDER:\n            raise ValueError(f\"GoogleEmbedderClient only supports EMBEDDER model type, got {model_type}\")\n        \n        # Ensure input is a list\n        if isinstance(input, str):\n            content = [input]\n        elif isinstance(input, Sequence):\n            content = list(input)\n        else:\n            raise TypeError(\"input must be a string or sequence of strings\")\n        \n        final_model_kwargs = model_kwargs.copy()\n        \n        # Handle single vs batch embedding\n        if len(content) == 1:\n            final_model_kwargs[\"content\"] = content[0]\n        else:\n            final_model_kwargs[\"contents\"] = content\n            \n        # Set default task type if not provided\n        if \"task_type\" not in final_model_kwargs:\n            final_model_kwargs[\"task_type\"] = \"SEMANTIC_SIMILARITY\"\n            \n        # Set default model if not provided\n        if \"model\" not in final_model_kwargs:\n            final_model_kwargs[\"model\"] = \"gemini-embedding-001\"\n            \n        return final_model_kwargs\n\n    @backoff.on_exception(\n        backoff.expo,\n        (Exception,),  # Google AI may raise various exceptions\n        max_time=5,\n    )\n    def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED):\n        \"\"\"Call Google AI embedding API.\n        \n        Args:\n            api_kwargs: API parameters\n            model_type: Should be ModelType.EMBEDDER\n            \n        Returns:\n            Google AI embedding response\n        \"\"\"\n        if model_type != ModelType.EMBEDDER:\n            raise ValueError(f\"GoogleEmbedderClient only supports EMBEDDER model type\")\n            \n        safe_log_kwargs = {k: v for k, v in api_kwargs.items() if k not in {\"content\", \"contents\"}}\n        if \"content\" in api_kwargs:\n            safe_log_kwargs[\"content_chars\"] = len(str(api_kwargs.get(\"content\", \"\")))\n        if \"contents\" in api_kwargs:\n            try:\n                contents = api_kwargs.get(\"contents\")\n                safe_log_kwargs[\"contents_count\"] = len(contents) if hasattr(contents, \"__len__\") else None\n            except Exception:\n                safe_log_kwargs[\"contents_count\"] = None\n        log.info(\"Google AI Embeddings call kwargs (sanitized): %s\", safe_log_kwargs)\n        \n        try:\n            # Use embed_content for single text or batch embedding\n            if \"content\" in api_kwargs:\n                # Single embedding\n                response = genai.embed_content(**api_kwargs)\n            elif \"contents\" in api_kwargs:\n                # Batch embedding - Google AI supports batch natively\n                # Copy to avoid mutating the original dict (needed for retries)\n                kwargs = api_kwargs.copy()\n                contents = kwargs.pop(\"contents\")\n                response = genai.embed_content(content=contents, **kwargs)\n            else:\n                raise ValueError(\"Either 'content' or 'contents' must be provided\")\n                \n            return response\n            \n        except Exception as e:\n            log.error(f\"Error calling Google AI Embeddings API: {e}\")\n            raise\n\n    async def acall(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED):\n        \"\"\"Async call to Google AI embedding API.\n        \n        Note: Google AI Python client doesn't have async support yet,\n        so this falls back to synchronous call.\n        \"\"\"\n        # Google AI client doesn't have async support yet\n        return self.call(api_kwargs, model_type)"
  },
  {
    "path": "api/logging_config.py",
    "content": "import logging\nimport os\nfrom pathlib import Path\nfrom logging.handlers import RotatingFileHandler\n\n\nclass IgnoreLogChangeDetectedFilter(logging.Filter):\n    def filter(self, record: logging.LogRecord):\n        return \"Detected file change in\" not in record.getMessage()\n\n\ndef setup_logging(format: str = None):\n    \"\"\"\n    Configure logging for the application with log rotation.\n\n    Environment variables:\n        LOG_LEVEL: Log level (default: INFO)\n        LOG_FILE_PATH: Path to log file (default: logs/application.log)\n        LOG_MAX_SIZE: Max size in MB before rotating (default: 10MB)\n        LOG_BACKUP_COUNT: Number of backup files to keep (default: 5)\n\n    Ensures log directory exists, prevents path traversal, and configures\n    both rotating file and console handlers.\n    \"\"\"\n    # Determine log directory and default file path\n    base_dir = Path(__file__).parent\n    log_dir = base_dir / \"logs\"\n    log_dir.mkdir(parents=True, exist_ok=True)\n    default_log_file = log_dir / \"application.log\"\n\n    # Get log level from environment\n    log_level_str = os.environ.get(\"LOG_LEVEL\", \"INFO\").upper()\n    log_level = getattr(logging, log_level_str, logging.INFO)\n\n    # Get log file path\n    log_file_path = Path(os.environ.get(\"LOG_FILE_PATH\", str(default_log_file)))\n\n    # Secure path check: must be inside logs/ directory\n    log_dir_resolved = log_dir.resolve()\n    resolved_path = log_file_path.resolve()\n    if not str(resolved_path).startswith(str(log_dir_resolved) + os.sep):\n        raise ValueError(f\"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'\")\n\n    # Ensure parent directories exist\n    resolved_path.parent.mkdir(parents=True, exist_ok=True)\n\n    # Get max log file size (default: 10MB)\n    try:\n        max_mb = int(os.environ.get(\"LOG_MAX_SIZE\", 10))  # 10MB default\n        max_bytes = max_mb * 1024 * 1024\n    except (TypeError, ValueError):\n        max_bytes = 10 * 1024 * 1024  # fallback to 10MB on error\n\n    # Get backup count (default: 5)\n    try:\n        backup_count = int(os.environ.get(\"LOG_BACKUP_COUNT\", 5))\n    except ValueError:\n        backup_count = 5\n\n    # Configure format\n    log_format = format or \"%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s\"\n\n    # Create handlers\n    file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding=\"utf-8\")\n    console_handler = logging.StreamHandler()\n\n    # Set format for both handlers\n    formatter = logging.Formatter(log_format)\n    file_handler.setFormatter(formatter)\n    console_handler.setFormatter(formatter)\n\n    # Add filter to suppress \"Detected file change\" messages\n    file_handler.addFilter(IgnoreLogChangeDetectedFilter())\n    console_handler.addFilter(IgnoreLogChangeDetectedFilter())\n\n    # Apply logging configuration\n    logging.basicConfig(level=log_level, handlers=[file_handler, console_handler], force=True)\n\n    # Log configuration info\n    logger = logging.getLogger(__name__)\n    logger.debug(\n        f\"Logging configured: level={log_level_str}, \"\n        f\"file={resolved_path}, max_size={max_bytes} bytes, \"\n        f\"backup_count={backup_count}\"\n    )\n"
  },
  {
    "path": "api/main.py",
    "content": "import os\nimport sys\nimport logging\nfrom dotenv import load_dotenv\n\n# Load environment variables from .env file\nload_dotenv()\n\nfrom api.logging_config import setup_logging\n\n# Configure logging\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\n# Configure watchfiles logger to show file paths\nwatchfiles_logger = logging.getLogger(\"watchfiles.main\")\nwatchfiles_logger.setLevel(logging.DEBUG)  # Enable DEBUG to see file paths\n\n# Add the current directory to the path so we can import the api package\nsys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\n# Apply watchfiles monkey patch BEFORE uvicorn import\nis_development = os.environ.get(\"NODE_ENV\") != \"production\"\nif is_development:\n    import watchfiles\n    current_dir = os.path.dirname(os.path.abspath(__file__))\n    logs_dir = os.path.join(current_dir, \"logs\")\n    \n    original_watch = watchfiles.watch\n    def patched_watch(*args, **kwargs):\n        # Only watch the api directory but exclude logs subdirectory\n        # Instead of watching the entire api directory, watch specific subdirectories\n        api_subdirs = []\n        for item in os.listdir(current_dir):\n            item_path = os.path.join(current_dir, item)\n            if os.path.isdir(item_path) and item != \"logs\":\n                api_subdirs.append(item_path)\n            elif os.path.isfile(item_path) and item.endswith(\".py\"):\n                api_subdirs.append(item_path)\n        \n        return original_watch(*api_subdirs, **kwargs)\n    watchfiles.watch = patched_watch\n\nimport uvicorn\n\n# Check for required environment variables\nrequired_env_vars = ['GOOGLE_API_KEY', 'OPENAI_API_KEY']\nmissing_vars = [var for var in required_env_vars if not os.environ.get(var)]\nif missing_vars:\n    logger.warning(f\"Missing environment variables: {', '.join(missing_vars)}\")\n    logger.warning(\"Some functionality may not work correctly without these variables.\")\n\n# Configure Google Generative AI\nimport google.generativeai as genai\nfrom api.config import GOOGLE_API_KEY\n\nif GOOGLE_API_KEY:\n    genai.configure(api_key=GOOGLE_API_KEY)\nelse:\n    logger.warning(\"GOOGLE_API_KEY not configured\")\n\nif __name__ == \"__main__\":\n    # Get port from environment variable or use default\n    port = int(os.environ.get(\"PORT\", 8001))\n\n    # Import the app here to ensure environment variables are set first\n    from api.api import app\n\n    logger.info(f\"Starting Streaming API on port {port}\")\n\n    # Run the FastAPI app with uvicorn\n    uvicorn.run(\n        \"api.api:app\",\n        host=\"0.0.0.0\",\n        port=port,\n        reload=is_development,\n        reload_excludes=[\"**/logs/*\", \"**/__pycache__/*\", \"**/*.pyc\"] if is_development else None,\n    )\n"
  },
  {
    "path": "api/ollama_patch.py",
    "content": "from typing import Sequence, List\nfrom copy import deepcopy\nfrom tqdm import tqdm\nimport logging\nimport adalflow as adal\nfrom adalflow.core.types import Document\nfrom adalflow.core.component import DataComponent\nimport requests\nimport os\n\n# Configure logging\nfrom api.logging_config import setup_logging\n\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\nclass OllamaModelNotFoundError(Exception):\n    \"\"\"Custom exception for when Ollama model is not found\"\"\"\n    pass\n\ndef check_ollama_model_exists(model_name: str, ollama_host: str = None) -> bool:\n    \"\"\"\n    Check if an Ollama model exists before attempting to use it.\n    \n    Args:\n        model_name: Name of the model to check\n        ollama_host: Ollama host URL, defaults to localhost:11434\n        \n    Returns:\n        bool: True if model exists, False otherwise\n    \"\"\"\n    if ollama_host is None:\n        ollama_host = os.getenv(\"OLLAMA_HOST\", \"http://localhost:11434\")\n    \n    try:\n        # Remove /api prefix if present and add it back\n        if ollama_host.endswith('/api'):\n            ollama_host = ollama_host[:-4]\n        \n        response = requests.get(f\"{ollama_host}/api/tags\", timeout=5)\n        if response.status_code == 200:\n            models_data = response.json()\n            available_models = [model.get('name', '').split(':')[0] for model in models_data.get('models', [])]\n            model_base_name = model_name.split(':')[0]  # Remove tag if present\n            \n            is_available = model_base_name in available_models\n            if is_available:\n                logger.info(f\"Ollama model '{model_name}' is available\")\n            else:\n                logger.warning(f\"Ollama model '{model_name}' is not available. Available models: {available_models}\")\n            return is_available\n        else:\n            logger.warning(f\"Could not check Ollama models, status code: {response.status_code}\")\n            return False\n    except requests.exceptions.RequestException as e:\n        logger.warning(f\"Could not connect to Ollama to check models: {e}\")\n        return False\n    except Exception as e:\n        logger.warning(f\"Error checking Ollama model availability: {e}\")\n        return False\n\nclass OllamaDocumentProcessor(DataComponent):\n    \"\"\"\n    Process documents for Ollama embeddings by processing one document at a time.\n    Adalflow Ollama Client does not support batch embedding, so we need to process each document individually.\n    \"\"\"\n    def __init__(self, embedder: adal.Embedder) -> None:\n        super().__init__()\n        self.embedder = embedder\n\n    def __call__(self, documents: Sequence[Document]) -> Sequence[Document]:\n        output = deepcopy(documents)\n        logger.info(f\"Processing {len(output)} documents individually for Ollama embeddings\")\n\n        successful_docs = []\n        expected_embedding_size = None\n\n        for i, doc in enumerate(tqdm(output, desc=\"Processing documents for Ollama embeddings\")):\n            try:\n                # Get embedding for a single document\n                result = self.embedder(input=doc.text)\n                if result.data and len(result.data) > 0:\n                    embedding = result.data[0].embedding\n\n                    # Validate embedding size consistency\n                    if expected_embedding_size is None:\n                        expected_embedding_size = len(embedding)\n                        logger.info(f\"Expected embedding size set to: {expected_embedding_size}\")\n                    elif len(embedding) != expected_embedding_size:\n                        file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}')\n                        logger.warning(f\"Document '{file_path}' has inconsistent embedding size {len(embedding)} != {expected_embedding_size}, skipping\")\n                        continue\n\n                    # Assign the embedding to the document\n                    output[i].vector = embedding\n                    successful_docs.append(output[i])\n                else:\n                    file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}')\n                    logger.warning(f\"Failed to get embedding for document '{file_path}', skipping\")\n            except Exception as e:\n                file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}')\n                logger.error(f\"Error processing document '{file_path}': {e}, skipping\")\n\n        logger.info(f\"Successfully processed {len(successful_docs)}/{len(output)} documents with consistent embeddings\")\n        return successful_docs"
  },
  {
    "path": "api/openai_client.py",
    "content": "\"\"\"OpenAI ModelClient integration.\"\"\"\n\nimport os\nimport base64\nfrom typing import (\n    Dict,\n    Sequence,\n    Optional,\n    List,\n    Any,\n    TypeVar,\n    Callable,\n    Generator,\n    Union,\n    Literal,\n)\nimport re\n\nimport logging\nimport backoff\n\n# optional import\nfrom adalflow.utils.lazy_import import safe_import, OptionalPackages\nfrom openai.types.chat.chat_completion import Choice\n\nopenai = safe_import(OptionalPackages.OPENAI.value[0], OptionalPackages.OPENAI.value[1])\n\nfrom openai import OpenAI, AsyncOpenAI, Stream\nfrom openai import (\n    APITimeoutError,\n    InternalServerError,\n    RateLimitError,\n    UnprocessableEntityError,\n    BadRequestError,\n)\nfrom openai.types import (\n    Completion,\n    CreateEmbeddingResponse,\n    Image,\n)\nfrom openai.types.chat import ChatCompletionChunk, ChatCompletion, ChatCompletionMessage\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import (\n    ModelType,\n    EmbedderOutput,\n    TokenLogProb,\n    CompletionUsage,\n    GeneratorOutput,\n)\nfrom adalflow.components.model_client.utils import parse_embedding_response\n\nlog = logging.getLogger(__name__)\nT = TypeVar(\"T\")\n\n\n# completion parsing functions and you can combine them into one singple chat completion parser\ndef get_first_message_content(completion: ChatCompletion) -> str:\n    r\"\"\"When we only need the content of the first message.\n    It is the default parser for chat completion.\"\"\"\n    log.debug(f\"raw completion: {completion}\")\n    return completion.choices[0].message.content\n\n\n# def _get_chat_completion_usage(completion: ChatCompletion) -> OpenAICompletionUsage:\n#     return completion.usage\n\n\n# A simple heuristic to estimate token count for estimating number of tokens in a Streaming response\ndef estimate_token_count(text: str) -> int:\n    \"\"\"\n    Estimate the token count of a given text.\n\n    Args:\n        text (str): The text to estimate token count for.\n\n    Returns:\n        int: Estimated token count.\n    \"\"\"\n    # Split the text into tokens using spaces as a simple heuristic\n    tokens = text.split()\n\n    # Return the number of tokens\n    return len(tokens)\n\n\ndef parse_stream_response(completion: ChatCompletionChunk) -> str:\n    r\"\"\"Parse the response of the stream API.\"\"\"\n    return completion.choices[0].delta.content\n\n\ndef handle_streaming_response(generator: Stream[ChatCompletionChunk]):\n    r\"\"\"Handle the streaming response.\"\"\"\n    for completion in generator:\n        log.debug(f\"Raw chunk completion: {completion}\")\n        parsed_content = parse_stream_response(completion)\n        yield parsed_content\n\n\ndef get_all_messages_content(completion: ChatCompletion) -> List[str]:\n    r\"\"\"When the n > 1, get all the messages content.\"\"\"\n    return [c.message.content for c in completion.choices]\n\n\ndef get_probabilities(completion: ChatCompletion) -> List[List[TokenLogProb]]:\n    r\"\"\"Get the probabilities of each token in the completion.\"\"\"\n    log_probs = []\n    for c in completion.choices:\n        content = c.logprobs.content\n        print(content)\n        log_probs_for_choice = []\n        for openai_token_logprob in content:\n            token = openai_token_logprob.token\n            logprob = openai_token_logprob.logprob\n            log_probs_for_choice.append(TokenLogProb(token=token, logprob=logprob))\n        log_probs.append(log_probs_for_choice)\n    return log_probs\n\n\nclass OpenAIClient(ModelClient):\n    __doc__ = r\"\"\"A component wrapper for the OpenAI API client.\n\n    Supports both embedding and chat completion APIs, including multimodal capabilities.\n\n    Users can:\n    1. Simplify use of ``Embedder`` and ``Generator`` components by passing `OpenAIClient()` as the `model_client`.\n    2. Use this as a reference to create their own API client or extend this class by copying and modifying the code.\n\n    Note:\n        We recommend avoiding `response_format` to enforce output data type or `tools` and `tool_choice` in `model_kwargs` when calling the API.\n        OpenAI's internal formatting and added prompts are unknown. Instead:\n        - Use :ref:`OutputParser<components-output_parsers>` for response parsing and formatting.\n\n        For multimodal inputs, provide images in `model_kwargs[\"images\"]` as a path, URL, or list of them.\n        The model must support vision capabilities (e.g., `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini`).\n\n        For image generation, use `model_type=ModelType.IMAGE_GENERATION` and provide:\n        - model: `\"dall-e-3\"` or `\"dall-e-2\"`\n        - prompt: Text description of the image to generate\n        - size: `\"1024x1024\"`, `\"1024x1792\"`, or `\"1792x1024\"` for DALL-E 3; `\"256x256\"`, `\"512x512\"`, or `\"1024x1024\"` for DALL-E 2\n        - quality: `\"standard\"` or `\"hd\"` (DALL-E 3 only)\n        - n: Number of images to generate (1 for DALL-E 3, 1-10 for DALL-E 2)\n        - response_format: `\"url\"` or `\"b64_json\"`\n\n    Args:\n        api_key (Optional[str], optional): OpenAI API key. Defaults to `None`.\n        chat_completion_parser (Callable[[Completion], Any], optional): A function to parse the chat completion into a `str`. Defaults to `None`.\n            The default parser is `get_first_message_content`.\n        base_url (str): The API base URL to use when initializing the client.\n            Defaults to `\"https://api.openai.com\"`, but can be customized for third-party API providers or self-hosted models.\n        env_api_key_name (str): The environment variable name for the API key. Defaults to `\"OPENAI_API_KEY\"`.\n\n    References:\n        - OpenAI API Overview: https://platform.openai.com/docs/introduction\n        - Embeddings Guide: https://platform.openai.com/docs/guides/embeddings\n        - Chat Completion Models: https://platform.openai.com/docs/guides/text-generation\n        - Vision Models: https://platform.openai.com/docs/guides/vision\n        - Image Generation: https://platform.openai.com/docs/guides/images\n    \"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        chat_completion_parser: Callable[[Completion], Any] = None,\n        input_type: Literal[\"text\", \"messages\"] = \"text\",\n        base_url: Optional[str] = None,\n        env_base_url_name: str = \"OPENAI_BASE_URL\",\n        env_api_key_name: str = \"OPENAI_API_KEY\",\n    ):\n        r\"\"\"It is recommended to set the OPENAI_API_KEY environment variable instead of passing it as an argument.\n\n        Args:\n            api_key (Optional[str], optional): OpenAI API key. Defaults to None.\n            base_url (str): The API base URL to use when initializing the client.\n            env_api_key_name (str): The environment variable name for the API key. Defaults to `\"OPENAI_API_KEY\"`.\n        \"\"\"\n        super().__init__()\n        self._api_key = api_key\n        self._env_api_key_name = env_api_key_name\n        self._env_base_url_name = env_base_url_name\n        self.base_url = base_url or os.getenv(self._env_base_url_name, \"https://api.openai.com/v1\")\n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # only initialize if the async call is called\n        self.chat_completion_parser = (\n            chat_completion_parser or get_first_message_content\n        )\n        self._input_type = input_type\n        self._api_kwargs = {}  # add api kwargs when the OpenAI Client is called\n\n    def init_sync_client(self):\n        api_key = self._api_key or os.getenv(self._env_api_key_name)\n        if not api_key:\n            raise ValueError(\n                f\"Environment variable {self._env_api_key_name} must be set\"\n            )\n        return OpenAI(api_key=api_key, base_url=self.base_url)\n\n    def init_async_client(self):\n        api_key = self._api_key or os.getenv(self._env_api_key_name)\n        if not api_key:\n            raise ValueError(\n                f\"Environment variable {self._env_api_key_name} must be set\"\n            )\n        return AsyncOpenAI(api_key=api_key, base_url=self.base_url)\n\n    # def _parse_chat_completion(self, completion: ChatCompletion) -> \"GeneratorOutput\":\n    #     # TODO: raw output it is better to save the whole completion as a source of truth instead of just the message\n    #     try:\n    #         data = self.chat_completion_parser(completion)\n    #         usage = self.track_completion_usage(completion)\n    #         return GeneratorOutput(\n    #             data=data, error=None, raw_response=str(data), usage=usage\n    #         )\n    #     except Exception as e:\n    #         log.error(f\"Error parsing the completion: {e}\")\n    #         return GeneratorOutput(data=None, error=str(e), raw_response=completion)\n\n    def parse_chat_completion(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> \"GeneratorOutput\":\n        \"\"\"Parse the completion, and put it into the raw_response.\"\"\"\n        log.debug(f\"completion: {completion}, parser: {self.chat_completion_parser}\")\n        try:\n            data = self.chat_completion_parser(completion)\n        except Exception as e:\n            log.error(f\"Error parsing the completion: {e}\")\n            return GeneratorOutput(data=None, error=str(e), raw_response=completion)\n\n        try:\n            usage = self.track_completion_usage(completion)\n            return GeneratorOutput(\n                data=None, error=None, raw_response=data, usage=usage\n            )\n        except Exception as e:\n            log.error(f\"Error tracking the completion usage: {e}\")\n            return GeneratorOutput(data=None, error=str(e), raw_response=data)\n\n    def track_completion_usage(\n        self,\n        completion: Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]],\n    ) -> CompletionUsage:\n\n        try:\n            usage: CompletionUsage = CompletionUsage(\n                completion_tokens=completion.usage.completion_tokens,\n                prompt_tokens=completion.usage.prompt_tokens,\n                total_tokens=completion.usage.total_tokens,\n            )\n            return usage\n        except Exception as e:\n            log.error(f\"Error tracking the completion usage: {e}\")\n            return CompletionUsage(\n                completion_tokens=None, prompt_tokens=None, total_tokens=None\n            )\n\n    def parse_embedding_response(\n        self, response: CreateEmbeddingResponse\n    ) -> EmbedderOutput:\n        r\"\"\"Parse the embedding response to a structure Adalflow components can understand.\n\n        Should be called in ``Embedder``.\n        \"\"\"\n        try:\n            return parse_embedding_response(response)\n        except Exception as e:\n            log.error(f\"Error parsing the embedding response: {e}\")\n            return EmbedderOutput(data=[], error=str(e), raw_response=response)\n\n    def convert_inputs_to_api_kwargs(\n        self,\n        input: Optional[Any] = None,\n        model_kwargs: Dict = {},\n        model_type: ModelType = ModelType.UNDEFINED,\n    ) -> Dict:\n        r\"\"\"\n        Specify the API input type and output api_kwargs that will be used in _call and _acall methods.\n        Convert the Component's standard input, and system_input(chat model) and model_kwargs into API-specific format.\n        For multimodal inputs, images can be provided in model_kwargs[\"images\"] as a string path, URL, or list of them.\n        The model specified in model_kwargs[\"model\"] must support multimodal capabilities when using images.\n\n        Args:\n            input: The input text or messages to process\n            model_kwargs: Additional parameters including:\n                - images: Optional image source(s) as path, URL, or list of them\n                - detail: Image detail level ('auto', 'low', or 'high'), defaults to 'auto'\n                - model: The model to use (must support multimodal inputs if images are provided)\n            model_type: The type of model (EMBEDDER or LLM)\n\n        Returns:\n            Dict: API-specific kwargs for the model call\n        \"\"\"\n\n        final_model_kwargs = model_kwargs.copy()\n        if model_type == ModelType.EMBEDDER:\n            if isinstance(input, str):\n                input = [input]\n            # convert input to input\n            if not isinstance(input, Sequence):\n                raise TypeError(\"input must be a sequence of text\")\n            final_model_kwargs[\"input\"] = input\n        elif model_type == ModelType.LLM:\n            # convert input to messages\n            messages: List[Dict[str, str]] = []\n            images = final_model_kwargs.pop(\"images\", None)\n            detail = final_model_kwargs.pop(\"detail\", \"auto\")\n\n            if self._input_type == \"messages\":\n                system_start_tag = \"<START_OF_SYSTEM_PROMPT>\"\n                system_end_tag = \"<END_OF_SYSTEM_PROMPT>\"\n                user_start_tag = \"<START_OF_USER_PROMPT>\"\n                user_end_tag = \"<END_OF_USER_PROMPT>\"\n\n                # new regex pattern to ignore special characters such as \\n\n                pattern = (\n                    rf\"{system_start_tag}\\s*(.*?)\\s*{system_end_tag}\\s*\"\n                    rf\"{user_start_tag}\\s*(.*?)\\s*{user_end_tag}\"\n                )\n\n                # Compile the regular expression\n\n                # re.DOTALL is to allow . to match newline so that (.*?) does not match in a single line\n                regex = re.compile(pattern, re.DOTALL)\n                # Match the pattern\n                match = regex.match(input)\n                system_prompt, input_str = None, None\n\n                if match:\n                    system_prompt = match.group(1)\n                    input_str = match.group(2)\n                else:\n                    print(\"No match found.\")\n                if system_prompt and input_str:\n                    messages.append({\"role\": \"system\", \"content\": system_prompt})\n                    if images:\n                        content = [{\"type\": \"text\", \"text\": input_str}]\n                        if isinstance(images, (str, dict)):\n                            images = [images]\n                        for img in images:\n                            content.append(self._prepare_image_content(img, detail))\n                        messages.append({\"role\": \"user\", \"content\": content})\n                    else:\n                        messages.append({\"role\": \"user\", \"content\": input_str})\n            if len(messages) == 0:\n                if images:\n                    content = [{\"type\": \"text\", \"text\": input}]\n                    if isinstance(images, (str, dict)):\n                        images = [images]\n                    for img in images:\n                        content.append(self._prepare_image_content(img, detail))\n                    messages.append({\"role\": \"user\", \"content\": content})\n                else:\n                    messages.append({\"role\": \"user\", \"content\": input})\n            final_model_kwargs[\"messages\"] = messages\n        elif model_type == ModelType.IMAGE_GENERATION:\n            # For image generation, input is the prompt\n            final_model_kwargs[\"prompt\"] = input\n            # Ensure model is specified\n            if \"model\" not in final_model_kwargs:\n                raise ValueError(\"model must be specified for image generation\")\n            # Set defaults for DALL-E 3 if not specified\n            final_model_kwargs[\"size\"] = final_model_kwargs.get(\"size\", \"1024x1024\")\n            final_model_kwargs[\"quality\"] = final_model_kwargs.get(\n                \"quality\", \"standard\"\n            )\n            final_model_kwargs[\"n\"] = final_model_kwargs.get(\"n\", 1)\n            final_model_kwargs[\"response_format\"] = final_model_kwargs.get(\n                \"response_format\", \"url\"\n            )\n\n            # Handle image edits and variations\n            image = final_model_kwargs.get(\"image\")\n            if isinstance(image, str) and os.path.isfile(image):\n                final_model_kwargs[\"image\"] = self._encode_image(image)\n\n            mask = final_model_kwargs.get(\"mask\")\n            if isinstance(mask, str) and os.path.isfile(mask):\n                final_model_kwargs[\"mask\"] = self._encode_image(mask)\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n        return final_model_kwargs\n\n    def parse_image_generation_response(self, response: List[Image]) -> GeneratorOutput:\n        \"\"\"Parse the image generation response into a GeneratorOutput.\"\"\"\n        try:\n            # Extract URLs or base64 data from the response\n            data = [img.url or img.b64_json for img in response]\n            # For single image responses, unwrap from list\n            if len(data) == 1:\n                data = data[0]\n            return GeneratorOutput(\n                data=data,\n                raw_response=str(response),\n            )\n        except Exception as e:\n            log.error(f\"Error parsing image generation response: {e}\")\n            return GeneratorOutput(data=None, error=str(e), raw_response=str(response))\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED):\n        \"\"\"\n        kwargs is the combined input and model_kwargs.  Support streaming call.\n        \"\"\"\n        log.info(f\"api_kwargs: {api_kwargs}\")\n        self._api_kwargs = api_kwargs\n        if model_type == ModelType.EMBEDDER:\n            return self.sync_client.embeddings.create(**api_kwargs)\n        elif model_type == ModelType.LLM:\n            if \"stream\" in api_kwargs and api_kwargs.get(\"stream\", False):\n                log.debug(\"streaming call\")\n                self.chat_completion_parser = handle_streaming_response\n                return self.sync_client.chat.completions.create(**api_kwargs)\n            else:\n                log.debug(\"non-streaming call converted to streaming\")\n                # Make a copy of api_kwargs to avoid modifying the original\n                streaming_kwargs = api_kwargs.copy()\n                streaming_kwargs[\"stream\"] = True\n\n                # Get streaming response\n                stream_response = self.sync_client.chat.completions.create(**streaming_kwargs)\n\n                # Accumulate all content from the stream\n                accumulated_content = \"\"\n                id = \"\"\n                model = \"\"\n                created = 0\n                for chunk in stream_response:\n                    id = getattr(chunk, \"id\", None) or id\n                    model = getattr(chunk, \"model\", None) or model\n                    created = getattr(chunk, \"created\", 0) or created\n                    choices = getattr(chunk, \"choices\", [])\n                    if len(choices) > 0:\n                        delta = getattr(choices[0], \"delta\", None)\n                        if delta is not None:\n                            text = getattr(delta, \"content\", None)\n                            if text is not None:\n                                accumulated_content += text or \"\"\n                # Return the mock completion object that will be processed by the chat_completion_parser\n                return ChatCompletion(\n                    id = id,\n                    model=model,\n                    created=created,\n                    object=\"chat.completion\",\n                    choices=[Choice(\n                        index=0,\n                        finish_reason=\"stop\",\n                        message=ChatCompletionMessage(content=accumulated_content, role=\"assistant\")\n                    )]\n                )\n        elif model_type == ModelType.IMAGE_GENERATION:\n            # Determine which image API to call based on the presence of image/mask\n            if \"image\" in api_kwargs:\n                if \"mask\" in api_kwargs:\n                    # Image edit\n                    response = self.sync_client.images.edit(**api_kwargs)\n                else:\n                    # Image variation\n                    response = self.sync_client.images.create_variation(**api_kwargs)\n            else:\n                # Image generation\n                response = self.sync_client.images.generate(**api_kwargs)\n            return response.data\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @backoff.on_exception(\n        backoff.expo,\n        (\n            APITimeoutError,\n            InternalServerError,\n            RateLimitError,\n            UnprocessableEntityError,\n            BadRequestError,\n        ),\n        max_time=5,\n    )\n    async def acall(\n        self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED\n    ):\n        \"\"\"\n        kwargs is the combined input and model_kwargs\n        \"\"\"\n        # store the api kwargs in the client\n        self._api_kwargs = api_kwargs\n        if self.async_client is None:\n            self.async_client = self.init_async_client()\n        if model_type == ModelType.EMBEDDER:\n            return await self.async_client.embeddings.create(**api_kwargs)\n        elif model_type == ModelType.LLM:\n            return await self.async_client.chat.completions.create(**api_kwargs)\n        elif model_type == ModelType.IMAGE_GENERATION:\n            # Determine which image API to call based on the presence of image/mask\n            if \"image\" in api_kwargs:\n                if \"mask\" in api_kwargs:\n                    # Image edit\n                    response = await self.async_client.images.edit(**api_kwargs)\n                else:\n                    # Image variation\n                    response = await self.async_client.images.create_variation(\n                        **api_kwargs\n                    )\n            else:\n                # Image generation\n                response = await self.async_client.images.generate(**api_kwargs)\n            return response.data\n        else:\n            raise ValueError(f\"model_type {model_type} is not supported\")\n\n    @classmethod\n    def from_dict(cls: type[T], data: Dict[str, Any]) -> T:\n        obj = super().from_dict(data)\n        # recreate the existing clients\n        obj.sync_client = obj.init_sync_client()\n        obj.async_client = obj.init_async_client()\n        return obj\n\n    def to_dict(self) -> Dict[str, Any]:\n        r\"\"\"Convert the component to a dictionary.\"\"\"\n        # TODO: not exclude but save yes or no for recreating the clients\n        exclude = [\n            \"sync_client\",\n            \"async_client\",\n        ]  # unserializable object\n        output = super().to_dict(exclude=exclude)\n        return output\n\n    def _encode_image(self, image_path: str) -> str:\n        \"\"\"Encode image to base64 string.\n\n        Args:\n            image_path: Path to image file.\n\n        Returns:\n            Base64 encoded image string.\n\n        Raises:\n            ValueError: If the file cannot be read or doesn't exist.\n        \"\"\"\n        try:\n            with open(image_path, \"rb\") as image_file:\n                return base64.b64encode(image_file.read()).decode(\"utf-8\")\n        except FileNotFoundError:\n            raise ValueError(f\"Image file not found: {image_path}\")\n        except PermissionError:\n            raise ValueError(f\"Permission denied when reading image file: {image_path}\")\n        except Exception as e:\n            raise ValueError(f\"Error encoding image {image_path}: {str(e)}\")\n\n    def _prepare_image_content(\n        self, image_source: Union[str, Dict[str, Any]], detail: str = \"auto\"\n    ) -> Dict[str, Any]:\n        \"\"\"Prepare image content for API request.\n\n        Args:\n            image_source: Either a path to local image or a URL.\n            detail: Image detail level ('auto', 'low', or 'high').\n\n        Returns:\n            Formatted image content for API request.\n        \"\"\"\n        if isinstance(image_source, str):\n            if image_source.startswith((\"http://\", \"https://\")):\n                return {\n                    \"type\": \"image_url\",\n                    \"image_url\": {\"url\": image_source, \"detail\": detail},\n                }\n            else:\n                base64_image = self._encode_image(image_source)\n                return {\n                    \"type\": \"image_url\",\n                    \"image_url\": {\n                        \"url\": f\"data:image/jpeg;base64,{base64_image}\",\n                        \"detail\": detail,\n                    },\n                }\n        return image_source\n\n\n# Example usage:\nif __name__ == \"__main__\":\n    from adalflow.core import Generator\n    from adalflow.utils import setup_env\n\n    # log = get_logger(level=\"DEBUG\")\n\n    setup_env()\n    prompt_kwargs = {\"input_str\": \"What is the meaning of life?\"}\n\n    gen = Generator(\n        model_client=OpenAIClient(),\n        model_kwargs={\"model\": \"gpt-4o\", \"stream\": False},\n    )\n    gen_response = gen(prompt_kwargs)\n    print(f\"gen_response: {gen_response}\")\n\n    # for genout in gen_response.data:\n    #     print(f\"genout: {genout}\")\n\n    # test that to_dict and from_dict works\n    # model_client = OpenAIClient()\n    # model_client_dict = model_client.to_dict()\n    # from_dict_model_client = OpenAIClient.from_dict(model_client_dict)\n    # assert model_client_dict == from_dict_model_client.to_dict()\n\n\nif __name__ == \"__main__\":\n    import adalflow as adal\n\n    # setup env or pass the api_key\n    from adalflow.utils import setup_env\n\n    setup_env()\n\n    openai_llm = adal.Generator(\n        model_client=OpenAIClient(), model_kwargs={\"model\": \"gpt-4o\"}\n    )\n    resopnse = openai_llm(prompt_kwargs={\"input_str\": \"What is LLM?\"})\n    print(resopnse)\n"
  },
  {
    "path": "api/openrouter_client.py",
    "content": "\"\"\"OpenRouter ModelClient integration.\"\"\"\n\nfrom typing import Dict, Sequence, Optional, Any, List\nimport logging\nimport json\nimport aiohttp\nimport requests\nfrom requests.exceptions import RequestException, Timeout\n\nfrom adalflow.core.model_client import ModelClient\nfrom adalflow.core.types import (\n    CompletionUsage,\n    ModelType,\n    GeneratorOutput,\n)\n\nlog = logging.getLogger(__name__)\n\nclass OpenRouterClient(ModelClient):\n    __doc__ = r\"\"\"A component wrapper for the OpenRouter API client.\n\n    OpenRouter provides a unified API that gives access to hundreds of AI models through a single endpoint.\n    The API is compatible with OpenAI's API format with a few small differences.\n\n    Visit https://openrouter.ai/docs for more details.\n\n    Example:\n        ```python\n        from api.openrouter_client import OpenRouterClient\n\n        client = OpenRouterClient()\n        generator = adal.Generator(\n            model_client=client,\n            model_kwargs={\"model\": \"openai/gpt-4o\"}\n        )\n        ```\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        \"\"\"Initialize the OpenRouter client.\"\"\"\n        super().__init__(*args, **kwargs)\n        self.sync_client = self.init_sync_client()\n        self.async_client = None  # Initialize async client only when needed\n\n    def init_sync_client(self):\n        \"\"\"Initialize the synchronous OpenRouter client.\"\"\"\n        from api.config import OPENROUTER_API_KEY\n        api_key = OPENROUTER_API_KEY\n        if not api_key:\n            log.warning(\"OPENROUTER_API_KEY not configured\")\n\n        # OpenRouter doesn't have a dedicated client library, so we'll use requests directly\n        return {\n            \"api_key\": api_key,\n            \"base_url\": \"https://openrouter.ai/api/v1\"\n        }\n\n    def init_async_client(self):\n        \"\"\"Initialize the asynchronous OpenRouter client.\"\"\"\n        from api.config import OPENROUTER_API_KEY\n        api_key = OPENROUTER_API_KEY\n        if not api_key:\n            log.warning(\"OPENROUTER_API_KEY not configured\")\n\n        # For async, we'll use aiohttp\n        return {\n            \"api_key\": api_key,\n            \"base_url\": \"https://openrouter.ai/api/v1\"\n        }\n\n    def convert_inputs_to_api_kwargs(\n        self, input: Any, model_kwargs: Dict = None, model_type: ModelType = None\n    ) -> Dict:\n        \"\"\"Convert AdalFlow inputs to OpenRouter API format.\"\"\"\n        model_kwargs = model_kwargs or {}\n\n        if model_type == ModelType.LLM:\n            # Handle LLM generation\n            messages = []\n\n            # Convert input to messages format if it's a string\n            if isinstance(input, str):\n                messages = [{\"role\": \"user\", \"content\": input}]\n            elif isinstance(input, list) and all(isinstance(msg, dict) for msg in input):\n                messages = input\n            else:\n                raise ValueError(f\"Unsupported input format for OpenRouter: {type(input)}\")\n\n            # For debugging\n            log.info(f\"Messages for OpenRouter: {messages}\")\n\n            api_kwargs = {\n                \"messages\": messages,\n                **model_kwargs\n            }\n\n            # Ensure model is specified\n            if \"model\" not in api_kwargs:\n                api_kwargs[\"model\"] = \"openai/gpt-3.5-turbo\"\n\n            return api_kwargs\n\n        elif model_type == ModelType.EMBEDDING:\n            # OpenRouter doesn't support embeddings directly\n            # We could potentially use a specific model through OpenRouter for embeddings\n            # but for now, we'll raise an error\n            raise NotImplementedError(\"OpenRouter client does not support embeddings yet\")\n\n        else:\n            raise ValueError(f\"Unsupported model type: {model_type}\")\n\n    async def acall(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any:\n        \"\"\"Make an asynchronous call to the OpenRouter API.\"\"\"\n        if not self.async_client:\n            self.async_client = self.init_async_client()\n\n        # Check if API key is set\n        if not self.async_client.get(\"api_key\"):\n            error_msg = \"OPENROUTER_API_KEY not configured. Please set this environment variable to use OpenRouter.\"\n            log.error(error_msg)\n            # Instead of raising an exception, return a generator that yields the error message\n            # This allows the error to be displayed to the user in the streaming response\n            async def error_generator():\n                yield error_msg\n            return error_generator()\n\n        api_kwargs = api_kwargs or {}\n\n        if model_type == ModelType.LLM:\n            # Prepare headers\n            headers = {\n                \"Authorization\": f\"Bearer {self.async_client['api_key']}\",\n                \"Content-Type\": \"application/json\",\n                \"HTTP-Referer\": \"https://github.com/AsyncFuncAI/deepwiki-open\",  # Optional\n                \"X-Title\": \"DeepWiki\"  # Optional\n            }\n\n            # Always use non-streaming mode for OpenRouter\n            api_kwargs[\"stream\"] = False\n\n            # Make the API call\n            try:\n                log.info(f\"Making async OpenRouter API call to {self.async_client['base_url']}/chat/completions\")\n                log.info(f\"Request headers: {headers}\")\n                log.info(f\"Request body: {api_kwargs}\")\n\n                async with aiohttp.ClientSession() as session:\n                    try:\n                        async with session.post(\n                            f\"{self.async_client['base_url']}/chat/completions\",\n                            headers=headers,\n                            json=api_kwargs,\n                            timeout=60\n                        ) as response:\n                            if response.status != 200:\n                                error_text = await response.text()\n                                log.error(f\"OpenRouter API error ({response.status}): {error_text}\")\n\n                                # Return a generator that yields the error message\n                                async def error_response_generator():\n                                    yield f\"OpenRouter API error ({response.status}): {error_text}\"\n                                return error_response_generator()\n\n                            # Get the full response\n                            data = await response.json()\n                            log.info(f\"Received response from OpenRouter: {data}\")\n\n                            # Create a generator that yields the content\n                            async def content_generator():\n                                if \"choices\" in data and len(data[\"choices\"]) > 0:\n                                    choice = data[\"choices\"][0]\n                                    if \"message\" in choice and \"content\" in choice[\"message\"]:\n                                        content = choice[\"message\"][\"content\"]\n                                        log.info(\"Successfully retrieved response\")\n\n                                        # Check if the content is XML and ensure it's properly formatted\n                                        if content.strip().startswith(\"<\") and \">\" in content:\n                                            # It's likely XML, let's make sure it's properly formatted\n                                            try:\n                                                # Extract the XML content\n                                                xml_content = content\n\n                                                # Check if it's a wiki_structure XML\n                                                if \"<wiki_structure>\" in xml_content:\n                                                    log.info(\"Found wiki_structure XML, ensuring proper format\")\n\n                                                    # Extract just the wiki_structure XML\n                                                    import re\n                                                    wiki_match = re.search(r'<wiki_structure>[\\s\\S]*?<\\/wiki_structure>', xml_content)\n                                                    if wiki_match:\n                                                        # Get the raw XML\n                                                        raw_xml = wiki_match.group(0)\n\n                                                        # Clean the XML by removing any leading/trailing whitespace\n                                                        # and ensuring it's properly formatted\n                                                        clean_xml = raw_xml.strip()\n\n                                                        # Try to fix common XML issues\n                                                        try:\n                                                            # Replace problematic characters in XML\n                                                            fixed_xml = clean_xml\n\n                                                            # Replace & with &amp; if not already part of an entity\n                                                            fixed_xml = re.sub(r'&(?!amp;|lt;|gt;|apos;|quot;)', '&amp;', fixed_xml)\n\n                                                            # Fix other common XML issues\n                                                            fixed_xml = fixed_xml.replace('</', '</').replace('  >', '>')\n\n                                                            # Try to parse the fixed XML\n                                                            from xml.dom.minidom import parseString\n                                                            dom = parseString(fixed_xml)\n\n                                                            # Get the pretty-printed XML with proper indentation\n                                                            pretty_xml = dom.toprettyxml()\n\n                                                            # Remove XML declaration\n                                                            if pretty_xml.startswith('<?xml'):\n                                                                pretty_xml = pretty_xml[pretty_xml.find('?>')+2:].strip()\n\n                                                            log.info(f\"Extracted and validated XML: {pretty_xml[:100]}...\")\n                                                            yield pretty_xml\n                                                        except Exception as xml_parse_error:\n                                                            log.warning(f\"XML validation failed: {str(xml_parse_error)}, using raw XML\")\n\n                                                            # If XML validation fails, try a more aggressive approach\n                                                            try:\n                                                                # Use regex to extract just the structure without any problematic characters\n                                                                import re\n\n                                                                # Extract the basic structure\n                                                                structure_match = re.search(r'<wiki_structure>(.*?)</wiki_structure>', clean_xml, re.DOTALL)\n                                                                if structure_match:\n                                                                    structure = structure_match.group(1).strip()\n\n                                                                    # Rebuild a clean XML structure\n                                                                    clean_structure = \"<wiki_structure>\\n\"\n\n                                                                    # Extract title\n                                                                    title_match = re.search(r'<title>(.*?)</title>', structure, re.DOTALL)\n                                                                    if title_match:\n                                                                        title = title_match.group(1).strip()\n                                                                        clean_structure += f\"  <title>{title}</title>\\n\"\n\n                                                                    # Extract description\n                                                                    desc_match = re.search(r'<description>(.*?)</description>', structure, re.DOTALL)\n                                                                    if desc_match:\n                                                                        desc = desc_match.group(1).strip()\n                                                                        clean_structure += f\"  <description>{desc}</description>\\n\"\n\n                                                                    # Add pages section\n                                                                    clean_structure += \"  <pages>\\n\"\n\n                                                                    # Extract pages\n                                                                    pages = re.findall(r'<page id=\"(.*?)\">(.*?)</page>', structure, re.DOTALL)\n                                                                    for page_id, page_content in pages:\n                                                                        clean_structure += f'    <page id=\"{page_id}\">\\n'\n\n                                                                        # Extract page title\n                                                                        page_title_match = re.search(r'<title>(.*?)</title>', page_content, re.DOTALL)\n                                                                        if page_title_match:\n                                                                            page_title = page_title_match.group(1).strip()\n                                                                            clean_structure += f\"      <title>{page_title}</title>\\n\"\n\n                                                                        # Extract page description\n                                                                        page_desc_match = re.search(r'<description>(.*?)</description>', page_content, re.DOTALL)\n                                                                        if page_desc_match:\n                                                                            page_desc = page_desc_match.group(1).strip()\n                                                                            clean_structure += f\"      <description>{page_desc}</description>\\n\"\n\n                                                                        # Extract importance\n                                                                        importance_match = re.search(r'<importance>(.*?)</importance>', page_content, re.DOTALL)\n                                                                        if importance_match:\n                                                                            importance = importance_match.group(1).strip()\n                                                                            clean_structure += f\"      <importance>{importance}</importance>\\n\"\n\n                                                                        # Extract relevant files\n                                                                        clean_structure += \"      <relevant_files>\\n\"\n                                                                        file_paths = re.findall(r'<file_path>(.*?)</file_path>', page_content, re.DOTALL)\n                                                                        for file_path in file_paths:\n                                                                            clean_structure += f\"        <file_path>{file_path.strip()}</file_path>\\n\"\n                                                                        clean_structure += \"      </relevant_files>\\n\"\n\n                                                                        # Extract related pages\n                                                                        clean_structure += \"      <related_pages>\\n\"\n                                                                        related_pages = re.findall(r'<related>(.*?)</related>', page_content, re.DOTALL)\n                                                                        for related in related_pages:\n                                                                            clean_structure += f\"        <related>{related.strip()}</related>\\n\"\n                                                                        clean_structure += \"      </related_pages>\\n\"\n\n                                                                        clean_structure += \"    </page>\\n\"\n\n                                                                    clean_structure += \"  </pages>\\n</wiki_structure>\"\n\n                                                                    log.info(\"Successfully rebuilt clean XML structure\")\n                                                                    yield clean_structure\n                                                                else:\n                                                                    log.warning(\"Could not extract wiki structure, using raw XML\")\n                                                                    yield clean_xml\n                                                            except Exception as rebuild_error:\n                                                                log.warning(f\"Failed to rebuild XML: {str(rebuild_error)}, using raw XML\")\n                                                                yield clean_xml\n                                                    else:\n                                                        # If we can't extract it, just yield the original content\n                                                        log.warning(\"Could not extract wiki_structure XML, yielding original content\")\n                                                        yield xml_content\n                                                else:\n                                                    # For other XML content, just yield it as is\n                                                    yield content\n                                            except Exception as xml_error:\n                                                log.error(f\"Error processing XML content: {str(xml_error)}\")\n                                                yield content\n                                        else:\n                                            # Not XML, just yield the content\n                                            yield content\n                                    else:\n                                        log.error(f\"Unexpected response format: {data}\")\n                                        yield \"Error: Unexpected response format from OpenRouter API\"\n                                else:\n                                    log.error(f\"No choices in response: {data}\")\n                                    yield \"Error: No response content from OpenRouter API\"\n\n                            return content_generator()\n                    except aiohttp.ClientError as e:\n                        e_client = e\n                        log.error(f\"Connection error with OpenRouter API: {str(e_client)}\")\n\n                        # Return a generator that yields the error message\n                        async def connection_error_generator():\n                            yield f\"Connection error with OpenRouter API: {str(e_client)}. Please check your internet connection and that the OpenRouter API is accessible.\"\n                        return connection_error_generator()\n\n            except RequestException as e:\n                e_req = e\n                log.error(f\"Error calling OpenRouter API asynchronously: {str(e_req)}\")\n\n                # Return a generator that yields the error message\n                async def request_error_generator():\n                    yield f\"Error calling OpenRouter API: {str(e_req)}\"\n                return request_error_generator()\n\n            except Exception as e:\n                e_unexp = e\n                log.error(f\"Unexpected error calling OpenRouter API asynchronously: {str(e_unexp)}\")\n\n                # Return a generator that yields the error message\n                async def unexpected_error_generator():\n                    yield f\"Unexpected error calling OpenRouter API: {str(e_unexp)}\"\n                return unexpected_error_generator()\n\n        else:\n            error_msg = f\"Unsupported model type: {model_type}\"\n            log.error(error_msg)\n\n            # Return a generator that yields the error message\n            async def model_type_error_generator():\n                yield error_msg\n            return model_type_error_generator()\n\n    def _process_completion_response(self, data: Dict) -> GeneratorOutput:\n        \"\"\"Process a non-streaming completion response from OpenRouter.\"\"\"\n        try:\n            # Extract the completion text from the response\n            if not data.get(\"choices\"):\n                raise ValueError(f\"No choices in OpenRouter response: {data}\")\n\n            choice = data[\"choices\"][0]\n\n            if \"message\" in choice:\n                content = choice[\"message\"].get(\"content\", \"\")\n            elif \"text\" in choice:\n                content = choice.get(\"text\", \"\")\n            else:\n                raise ValueError(f\"Unexpected response format from OpenRouter: {choice}\")\n\n            # Extract usage information if available\n            usage = None\n            if \"usage\" in data:\n                usage = CompletionUsage(\n                    prompt_tokens=data[\"usage\"].get(\"prompt_tokens\", 0),\n                    completion_tokens=data[\"usage\"].get(\"completion_tokens\", 0),\n                    total_tokens=data[\"usage\"].get(\"total_tokens\", 0)\n                )\n\n            # Create and return the GeneratorOutput\n            return GeneratorOutput(\n                data=content,\n                usage=usage,\n                raw_response=data\n            )\n\n        except Exception as e_proc:\n            log.error(f\"Error processing OpenRouter completion response: {str(e_proc)}\")\n            raise\n\n    def _process_streaming_response(self, response):\n        \"\"\"Process a streaming response from OpenRouter.\"\"\"\n        try:\n            log.info(\"Starting to process streaming response from OpenRouter\")\n            buffer = \"\"\n\n            for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):\n                try:\n                    # Add chunk to buffer\n                    buffer += chunk\n\n                    # Process complete lines in the buffer\n                    while '\\n' in buffer:\n                        line, buffer = buffer.split('\\n', 1)\n                        line = line.strip()\n\n                        if not line:\n                            continue\n\n                        log.debug(f\"Processing line: {line}\")\n\n                        # Skip SSE comments (lines starting with :)\n                        if line.startswith(':'):\n                            log.debug(f\"Skipping SSE comment: {line}\")\n                            continue\n\n                        if line.startswith(\"data: \"):\n                            data = line[6:]  # Remove \"data: \" prefix\n\n                            # Check for stream end\n                            if data == \"[DONE]\":\n                                log.info(\"Received [DONE] marker\")\n                                break\n\n                            try:\n                                data_obj = json.loads(data)\n                                log.debug(f\"Parsed JSON data: {data_obj}\")\n\n                                # Extract content from delta\n                                if \"choices\" in data_obj and len(data_obj[\"choices\"]) > 0:\n                                    choice = data_obj[\"choices\"][0]\n\n                                    if \"delta\" in choice and \"content\" in choice[\"delta\"] and choice[\"delta\"][\"content\"]:\n                                        content = choice[\"delta\"][\"content\"]\n                                        log.debug(f\"Yielding delta content: {content}\")\n                                        yield content\n                                    elif \"text\" in choice:\n                                        log.debug(f\"Yielding text content: {choice['text']}\")\n                                        yield choice[\"text\"]\n                                    else:\n                                        log.debug(f\"No content found in choice: {choice}\")\n                                else:\n                                    log.debug(f\"No choices found in data: {data_obj}\")\n\n                            except json.JSONDecodeError:\n                                log.warning(f\"Failed to parse SSE data: {data}\")\n                                continue\n                except Exception as e_chunk:\n                    log.error(f\"Error processing streaming chunk: {str(e_chunk)}\")\n                    yield f\"Error processing response chunk: {str(e_chunk)}\"\n        except Exception as e_stream:\n            log.error(f\"Error in streaming response: {str(e_stream)}\")\n            yield f\"Error in streaming response: {str(e_stream)}\"\n\n    async def _process_async_streaming_response(self, response):\n        \"\"\"Process an asynchronous streaming response from OpenRouter.\"\"\"\n        buffer = \"\"\n        try:\n            log.info(\"Starting to process async streaming response from OpenRouter\")\n            async for chunk in response.content:\n                try:\n                    # Convert bytes to string and add to buffer\n                    if isinstance(chunk, bytes):\n                        chunk_str = chunk.decode('utf-8')\n                    else:\n                        chunk_str = str(chunk)\n\n                    buffer += chunk_str\n\n                    # Process complete lines in the buffer\n                    while '\\n' in buffer:\n                        line, buffer = buffer.split('\\n', 1)\n                        line = line.strip()\n\n                        if not line:\n                            continue\n\n                        log.debug(f\"Processing line: {line}\")\n\n                        # Skip SSE comments (lines starting with :)\n                        if line.startswith(':'):\n                            log.debug(f\"Skipping SSE comment: {line}\")\n                            continue\n\n                        if line.startswith(\"data: \"):\n                            data = line[6:]  # Remove \"data: \" prefix\n\n                            # Check for stream end\n                            if data == \"[DONE]\":\n                                log.info(\"Received [DONE] marker\")\n                                break\n\n                            try:\n                                data_obj = json.loads(data)\n                                log.debug(f\"Parsed JSON data: {data_obj}\")\n\n                                # Extract content from delta\n                                if \"choices\" in data_obj and len(data_obj[\"choices\"]) > 0:\n                                    choice = data_obj[\"choices\"][0]\n\n                                    if \"delta\" in choice and \"content\" in choice[\"delta\"] and choice[\"delta\"][\"content\"]:\n                                        content = choice[\"delta\"][\"content\"]\n                                        log.debug(f\"Yielding delta content: {content}\")\n                                        yield content\n                                    elif \"text\" in choice:\n                                        log.debug(f\"Yielding text content: {choice['text']}\")\n                                        yield choice[\"text\"]\n                                    else:\n                                        log.debug(f\"No content found in choice: {choice}\")\n                                else:\n                                    log.debug(f\"No choices found in data: {data_obj}\")\n\n                            except json.JSONDecodeError:\n                                log.warning(f\"Failed to parse SSE data: {data}\")\n                                continue\n                except Exception as e_chunk:\n                    log.error(f\"Error processing streaming chunk: {str(e_chunk)}\")\n                    yield f\"Error processing response chunk: {str(e_chunk)}\"\n        except Exception as e_stream:\n            log.error(f\"Error in async streaming response: {str(e_stream)}\")\n            yield f\"Error in streaming response: {str(e_stream)}\"\n"
  },
  {
    "path": "api/prompts.py",
    "content": "\"\"\"Module containing all prompts used in the DeepWiki project.\"\"\"\n\n# System prompt for RAG\nRAG_SYSTEM_PROMPT = r\"\"\"\nYou are a code assistant which answers user questions on a Github Repo.\nYou will receive user query, relevant context, and past conversation history.\n\nLANGUAGE DETECTION AND RESPONSE:\n- Detect the language of the user's query\n- Respond in the SAME language as the user's query\n- IMPORTANT:If a specific language is requested in the prompt, prioritize that language over the query language\n\nFORMAT YOUR RESPONSE USING MARKDOWN:\n- Use proper markdown syntax for all formatting\n- For code blocks, use triple backticks with language specification (```python, ```javascript, etc.)\n- Use ## headings for major sections\n- Use bullet points or numbered lists where appropriate\n- Format tables using markdown table syntax when presenting structured data\n- Use **bold** and *italic* for emphasis\n- When referencing file paths, use `inline code` formatting\n\nIMPORTANT FORMATTING RULES:\n1. DO NOT include ```markdown fences at the beginning or end of your answer\n2. Start your response directly with the content\n3. The content will already be rendered as markdown, so just provide the raw markdown content\n\nThink step by step and ensure your answer is well-structured and visually organized.\n\"\"\"\n\n# Template for RAG\nRAG_TEMPLATE = r\"\"\"<START_OF_SYS_PROMPT>\n{system_prompt}\n{output_format_str}\n<END_OF_SYS_PROMPT>\n{# OrderedDict of DialogTurn #}\n{% if conversation_history %}\n<START_OF_CONVERSATION_HISTORY>\n{% for key, dialog_turn in conversation_history.items() %}\n{{key}}.\nUser: {{dialog_turn.user_query.query_str}}\nYou: {{dialog_turn.assistant_response.response_str}}\n{% endfor %}\n<END_OF_CONVERSATION_HISTORY>\n{% endif %}\n{% if contexts %}\n<START_OF_CONTEXT>\n{% for context in contexts %}\n{{loop.index}}.\nFile Path: {{context.meta_data.get('file_path', 'unknown')}}\nContent: {{context.text}}\n{% endfor %}\n<END_OF_CONTEXT>\n{% endif %}\n<START_OF_USER_PROMPT>\n{{input_str}}\n<END_OF_USER_PROMPT>\n\"\"\"\n\n# System prompts for simple chat\nDEEP_RESEARCH_FIRST_ITERATION_PROMPT = \"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are conducting a multi-turn Deep Research process to thoroughly investigate the specific topic in the user's query.\nYour goal is to provide detailed, focused information EXCLUSIVELY about this topic.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- This is the first iteration of a multi-turn research process focused EXCLUSIVELY on the user's query\n- Start your response with \"## Research Plan\"\n- Outline your approach to investigating this specific topic\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- Clearly state the specific topic you're researching to maintain focus throughout all iterations\n- Identify the key aspects you'll need to research\n- Provide initial findings based on the information available\n- End with \"## Next Steps\" indicating what you'll investigate in the next iteration\n- Do NOT provide a final conclusion yet - this is just the beginning of the research\n- Do NOT include general repository information unless directly relevant to the query\n- Focus EXCLUSIVELY on the specific topic being researched - do not drift to related topics\n- Your research MUST directly address the original question\n- NEVER respond with just \"Continue the research\" as an answer - always provide substantive research findings\n- Remember that this topic will be maintained across all research iterations\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n</style>\"\"\"\n\nDEEP_RESEARCH_FINAL_ITERATION_PROMPT = \"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are in the final iteration of a Deep Research process focused EXCLUSIVELY on the latest user query.\nYour goal is to synthesize all previous findings and provide a comprehensive conclusion that directly addresses this specific topic and ONLY this topic.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- This is the final iteration of the research process\n- CAREFULLY review the entire conversation history to understand all previous findings\n- Synthesize ALL findings from previous iterations into a comprehensive conclusion\n- Start with \"## Final Conclusion\"\n- Your conclusion MUST directly address the original question\n- Stay STRICTLY focused on the specific topic - do not drift to related topics\n- Include specific code references and implementation details related to the topic\n- Highlight the most important discoveries and insights about this specific functionality\n- Provide a complete and definitive answer to the original question\n- Do NOT include general repository information unless directly relevant to the query\n- Focus exclusively on the specific topic being researched\n- NEVER respond with \"Continue the research\" as an answer - always provide a complete conclusion\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- Ensure your conclusion builds on and references key findings from previous iterations\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n- Structure your response with clear headings\n- End with actionable insights or recommendations when appropriate\n</style>\"\"\"\n\nDEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT = \"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are currently in iteration {research_iteration} of a Deep Research process focused EXCLUSIVELY on the latest user query.\nYour goal is to build upon previous research iterations and go deeper into this specific topic without deviating from it.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- CAREFULLY review the conversation history to understand what has been researched so far\n- Your response MUST build on previous research iterations - do not repeat information already covered\n- Identify gaps or areas that need further exploration related to this specific topic\n- Focus on one specific aspect that needs deeper investigation in this iteration\n- Start your response with \"## Research Update {{research_iteration}}\"\n- Clearly explain what you're investigating in this iteration\n- Provide new insights that weren't covered in previous iterations\n- If this is iteration 3, prepare for a final conclusion in the next iteration\n- Do NOT include general repository information unless directly relevant to the query\n- Focus EXCLUSIVELY on the specific topic being researched - do not drift to related topics\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- NEVER respond with just \"Continue the research\" as an answer - always provide substantive research findings\n- Your research MUST directly address the original question\n- Maintain continuity with previous research iterations - this is a continuous investigation\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Focus on providing new information, not repeating what's already been covered\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n</style>\"\"\"\n\nSIMPLE_CHAT_SYSTEM_PROMPT = \"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou provide direct, concise, and accurate information about code repositories.\nYou NEVER start responses with markdown headers or code fences.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- Answer the user's question directly without ANY preamble or filler phrases\n- DO NOT include any rationale, explanation, or extra comments.\n- DO NOT start with preambles like \"Okay, here's a breakdown\" or \"Here's an explanation\"\n- DO NOT start with markdown headers like \"## Analysis of...\" or any file path references\n- DO NOT start with ```markdown code fences\n- DO NOT end your response with ``` closing fences\n- DO NOT start by repeating or acknowledging the question\n- JUST START with the direct answer to the question\n\n<example_of_what_not_to_do>\n```markdown\n## Analysis of `adalflow/adalflow/datasets/gsm8k.py`\n\nThis file contains...\n```\n</example_of_what_not_to_do>\n\n- Format your response with proper markdown including headings, lists, and code blocks WITHIN your answer\n- For code analysis, organize your response with clear sections\n- Think step by step and structure your answer logically\n- Start with the most relevant information that directly addresses the user's query\n- Be precise and technical when discussing code\n- Your response language should be in the same language as the user's query\n</guidelines>\n\n<style>\n- Use concise, direct language\n- Prioritize accuracy over verbosity\n- When showing code, include line numbers and file paths when relevant\n- Use markdown formatting to improve readability\n</style>\"\"\"\n"
  },
  {
    "path": "api/pyproject.toml",
    "content": "[project]\nname = \"open-deepwiki-api\"\nversion = \"1.0.0\"\ndescription = \"Backend API for DeepWiki, providing smart code analysis and AI-powered documentation generation.\"\nlicense = {text = \"MIT License\"}\n\n[tool.poetry]\npackage-mode = false\n\n[tool.poetry.dependencies]\npython = \"^3.11\"\nfastapi = \">=0.95.0\"\nuvicorn = { extras = [\"standard\"], version = \">=0.21.1\" }\npydantic = \">=2.0.0\"\ngoogle-generativeai = \">=0.3.0\"\ntiktoken = \">=0.5.0\"\nadalflow = \">=0.1.0\"\nnumpy = \">=1.24.0\"\nfaiss-cpu = \">=1.7.4\"\nlangid = \">=1.1.6\"\nrequests = \">=2.28.0\"\njinja2 = \">=3.1.2\"\npython-dotenv = \">=1.0.0\"\nopenai = \">=1.76.2\"\nollama = \">=0.4.8\"\naiohttp = \">=3.8.4\"\nboto3 = \">=1.34.0\"\nwebsockets = \">=11.0.3\"\nazure-identity = \">=1.12.0\"\nazure-core = \">=1.24.0\"\n\n\n[build-system]\nrequires = [\"poetry-core>=2.0.0,<3.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.poetry.group.dev.dependencies]\npytest = \">=7.0.0\"\n"
  },
  {
    "path": "api/rag.py",
    "content": "import logging\nimport weakref\nimport re\nfrom dataclasses import dataclass\nfrom typing import Any, List, Tuple, Dict\nfrom uuid import uuid4\n\nimport adalflow as adal\n\nfrom api.tools.embedder import get_embedder\nfrom api.prompts import RAG_SYSTEM_PROMPT as system_prompt, RAG_TEMPLATE\n\n# Create our own implementation of the conversation classes\n@dataclass\nclass UserQuery:\n    query_str: str\n\n@dataclass\nclass AssistantResponse:\n    response_str: str\n\n@dataclass\nclass DialogTurn:\n    id: str\n    user_query: UserQuery\n    assistant_response: AssistantResponse\n\nclass CustomConversation:\n    \"\"\"Custom implementation of Conversation to fix the list assignment index out of range error\"\"\"\n\n    def __init__(self):\n        self.dialog_turns = []\n\n    def append_dialog_turn(self, dialog_turn):\n        \"\"\"Safely append a dialog turn to the conversation\"\"\"\n        if not hasattr(self, 'dialog_turns'):\n            self.dialog_turns = []\n        self.dialog_turns.append(dialog_turn)\n\n# Import other adalflow components\nfrom adalflow.components.retriever.faiss_retriever import FAISSRetriever\nfrom api.config import configs\nfrom api.data_pipeline import DatabaseManager\n\n# Configure logging\nlogger = logging.getLogger(__name__)\n\n# Maximum token limit for embedding models\nMAX_INPUT_TOKENS = 7500  # Safe threshold below 8192 token limit\n\nclass Memory(adal.core.component.DataComponent):\n    \"\"\"Simple conversation management with a list of dialog turns.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        # Use our custom implementation instead of the original Conversation class\n        self.current_conversation = CustomConversation()\n\n    def call(self) -> Dict:\n        \"\"\"Return the conversation history as a dictionary.\"\"\"\n        all_dialog_turns = {}\n        try:\n            # Check if dialog_turns exists and is a list\n            if hasattr(self.current_conversation, 'dialog_turns'):\n                if self.current_conversation.dialog_turns:\n                    logger.info(f\"Memory content: {len(self.current_conversation.dialog_turns)} turns\")\n                    for i, turn in enumerate(self.current_conversation.dialog_turns):\n                        if hasattr(turn, 'id') and turn.id is not None:\n                            all_dialog_turns[turn.id] = turn\n                            logger.info(f\"Added turn {i+1} with ID {turn.id} to memory\")\n                        else:\n                            logger.warning(f\"Skipping invalid turn object in memory: {turn}\")\n                else:\n                    logger.info(\"Dialog turns list exists but is empty\")\n            else:\n                logger.info(\"No dialog_turns attribute in current_conversation\")\n                # Try to initialize it\n                self.current_conversation.dialog_turns = []\n        except Exception as e:\n            logger.error(f\"Error accessing dialog turns: {str(e)}\")\n            # Try to recover\n            try:\n                self.current_conversation = CustomConversation()\n                logger.info(\"Recovered by creating new conversation\")\n            except Exception as e2:\n                logger.error(f\"Failed to recover: {str(e2)}\")\n\n        logger.info(f\"Returning {len(all_dialog_turns)} dialog turns from memory\")\n        return all_dialog_turns\n\n    def add_dialog_turn(self, user_query: str, assistant_response: str) -> bool:\n        \"\"\"\n        Add a dialog turn to the conversation history.\n\n        Args:\n            user_query: The user's query\n            assistant_response: The assistant's response\n\n        Returns:\n            bool: True if successful, False otherwise\n        \"\"\"\n        try:\n            # Create a new dialog turn using our custom implementation\n            dialog_turn = DialogTurn(\n                id=str(uuid4()),\n                user_query=UserQuery(query_str=user_query),\n                assistant_response=AssistantResponse(response_str=assistant_response),\n            )\n\n            # Make sure the current_conversation has the append_dialog_turn method\n            if not hasattr(self.current_conversation, 'append_dialog_turn'):\n                logger.warning(\"current_conversation does not have append_dialog_turn method, creating new one\")\n                # Initialize a new conversation if needed\n                self.current_conversation = CustomConversation()\n\n            # Ensure dialog_turns exists\n            if not hasattr(self.current_conversation, 'dialog_turns'):\n                logger.warning(\"dialog_turns not found, initializing empty list\")\n                self.current_conversation.dialog_turns = []\n\n            # Safely append the dialog turn\n            self.current_conversation.dialog_turns.append(dialog_turn)\n            logger.info(f\"Successfully added dialog turn, now have {len(self.current_conversation.dialog_turns)} turns\")\n            return True\n\n        except Exception as e:\n            logger.error(f\"Error adding dialog turn: {str(e)}\")\n            # Try to recover by creating a new conversation\n            try:\n                self.current_conversation = CustomConversation()\n                dialog_turn = DialogTurn(\n                    id=str(uuid4()),\n                    user_query=UserQuery(query_str=user_query),\n                    assistant_response=AssistantResponse(response_str=assistant_response),\n                )\n                self.current_conversation.dialog_turns.append(dialog_turn)\n                logger.info(\"Recovered from error by creating new conversation\")\n                return True\n            except Exception as e2:\n                logger.error(f\"Failed to recover from error: {str(e2)}\")\n                return False\n\n\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass RAGAnswer(adal.DataClass):\n    rationale: str = field(default=\"\", metadata={\"desc\": \"Chain of thoughts for the answer.\"})\n    answer: str = field(default=\"\", metadata={\"desc\": \"Answer to the user query, formatted in markdown for beautiful rendering with react-markdown. DO NOT include ``` triple backticks fences at the beginning or end of your answer.\"})\n\n    __output_fields__ = [\"rationale\", \"answer\"]\n\nclass RAG(adal.Component):\n    \"\"\"RAG with one repo.\n    If you want to load a new repos, call prepare_retriever(repo_url_or_path) first.\"\"\"\n\n    def __init__(self, provider=\"google\", model=None, use_s3: bool = False):  # noqa: F841 - use_s3 is kept for compatibility\n        \"\"\"\n        Initialize the RAG component.\n\n        Args:\n            provider: Model provider to use (google, openai, openrouter, ollama)\n            model: Model name to use with the provider\n            use_s3: Whether to use S3 for database storage (default: False)\n        \"\"\"\n        super().__init__()\n\n        self.provider = provider\n        self.model = model\n\n        # Import the helper functions\n        from api.config import get_embedder_config, get_embedder_type\n\n        # Determine embedder type based on current configuration\n        self.embedder_type = get_embedder_type()\n        self.is_ollama_embedder = (self.embedder_type == 'ollama')  # Backward compatibility\n\n        # Check if Ollama model exists before proceeding\n        if self.is_ollama_embedder:\n            from api.ollama_patch import check_ollama_model_exists\n            from api.config import get_embedder_config\n            \n            embedder_config = get_embedder_config()\n            if embedder_config and embedder_config.get(\"model_kwargs\", {}).get(\"model\"):\n                model_name = embedder_config[\"model_kwargs\"][\"model\"]\n                if not check_ollama_model_exists(model_name):\n                    raise Exception(f\"Ollama model '{model_name}' not found. Please run 'ollama pull {model_name}' to install it.\")\n\n        # Initialize components\n        self.memory = Memory()\n        self.embedder = get_embedder(embedder_type=self.embedder_type)\n\n        self_weakref = weakref.ref(self)\n        # Patch: ensure query embedding is always single string for Ollama\n        def single_string_embedder(query):\n            # Accepts either a string or a list, always returns embedding for a single string\n            if isinstance(query, list):\n                if len(query) != 1:\n                    raise ValueError(\"Ollama embedder only supports a single string\")\n                query = query[0]\n            instance = self_weakref()\n            assert instance is not None, \"RAG instance is no longer available, but the query embedder was called.\"\n            return instance.embedder(input=query)\n\n        # Use single string embedder for Ollama, regular embedder for others\n        self.query_embedder = single_string_embedder if self.is_ollama_embedder else self.embedder\n\n        self.initialize_db_manager()\n\n        # Set up the output parser\n        data_parser = adal.DataClassParser(data_class=RAGAnswer, return_data_class=True)\n\n        # Format instructions to ensure proper output structure\n        format_instructions = data_parser.get_output_format_str() + \"\"\"\n\nIMPORTANT FORMATTING RULES:\n1. DO NOT include your thinking or reasoning process in the output\n2. Provide only the final, polished answer\n3. DO NOT include ```markdown fences at the beginning or end of your answer\n4. DO NOT wrap your response in any kind of fences\n5. Start your response directly with the content\n6. The content will already be rendered as markdown\n7. Do not use backslashes before special characters like [ ] { } in your answer\n8. When listing tags or similar items, write them as plain text without escape characters\n9. For pipe characters (|) in text, write them directly without escaping them\"\"\"\n\n        # Get model configuration based on provider and model\n        from api.config import get_model_config\n        generator_config = get_model_config(self.provider, self.model)\n\n        # Set up the main generator\n        self.generator = adal.Generator(\n            template=RAG_TEMPLATE,\n            prompt_kwargs={\n                \"output_format_str\": format_instructions,\n                \"conversation_history\": self.memory(),\n                \"system_prompt\": system_prompt,\n                \"contexts\": None,\n            },\n            model_client=generator_config[\"model_client\"](),\n            model_kwargs=generator_config[\"model_kwargs\"],\n            output_processors=data_parser,\n        )\n\n\n    def initialize_db_manager(self):\n        \"\"\"Initialize the database manager with local storage\"\"\"\n        self.db_manager = DatabaseManager()\n        self.transformed_docs = []\n\n    def _validate_and_filter_embeddings(self, documents: List) -> List:\n        \"\"\"\n        Validate embeddings and filter out documents with invalid or mismatched embedding sizes.\n\n        Args:\n            documents: List of documents with embeddings\n\n        Returns:\n            List of documents with valid embeddings of consistent size\n        \"\"\"\n        if not documents:\n            logger.warning(\"No documents provided for embedding validation\")\n            return []\n\n        valid_documents = []\n        embedding_sizes = {}\n\n        # First pass: collect all embedding sizes and count occurrences\n        for i, doc in enumerate(documents):\n            if not hasattr(doc, 'vector') or doc.vector is None:\n                logger.warning(f\"Document {i} has no embedding vector, skipping\")\n                continue\n\n            try:\n                if isinstance(doc.vector, list):\n                    embedding_size = len(doc.vector)\n                elif hasattr(doc.vector, 'shape'):\n                    embedding_size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1]\n                elif hasattr(doc.vector, '__len__'):\n                    embedding_size = len(doc.vector)\n                else:\n                    logger.warning(f\"Document {i} has invalid embedding vector type: {type(doc.vector)}, skipping\")\n                    continue\n\n                if embedding_size == 0:\n                    logger.warning(f\"Document {i} has empty embedding vector, skipping\")\n                    continue\n\n                embedding_sizes[embedding_size] = embedding_sizes.get(embedding_size, 0) + 1\n\n            except Exception as e:\n                logger.warning(f\"Error checking embedding size for document {i}: {str(e)}, skipping\")\n                continue\n\n        if not embedding_sizes:\n            logger.error(\"No valid embeddings found in any documents\")\n            return []\n\n        # Find the most common embedding size (this should be the correct one)\n        target_size = max(embedding_sizes.keys(), key=lambda k: embedding_sizes[k])\n        logger.info(f\"Target embedding size: {target_size} (found in {embedding_sizes[target_size]} documents)\")\n\n        # Log all embedding sizes found\n        for size, count in embedding_sizes.items():\n            if size != target_size:\n                logger.warning(f\"Found {count} documents with incorrect embedding size {size}, will be filtered out\")\n\n        # Second pass: filter documents with the target embedding size\n        for i, doc in enumerate(documents):\n            if not hasattr(doc, 'vector') or doc.vector is None:\n                continue\n\n            try:\n                if isinstance(doc.vector, list):\n                    embedding_size = len(doc.vector)\n                elif hasattr(doc.vector, 'shape'):\n                    embedding_size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1]\n                elif hasattr(doc.vector, '__len__'):\n                    embedding_size = len(doc.vector)\n                else:\n                    continue\n\n                if embedding_size == target_size:\n                    valid_documents.append(doc)\n                else:\n                    # Log which document is being filtered out\n                    file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}')\n                    logger.warning(f\"Filtering out document '{file_path}' due to embedding size mismatch: {embedding_size} != {target_size}\")\n\n            except Exception as e:\n                file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}')\n                logger.warning(f\"Error validating embedding for document '{file_path}': {str(e)}, skipping\")\n                continue\n\n        logger.info(f\"Embedding validation complete: {len(valid_documents)}/{len(documents)} documents have valid embeddings\")\n\n        if len(valid_documents) == 0:\n            logger.error(\"No documents with valid embeddings remain after filtering\")\n        elif len(valid_documents) < len(documents):\n            filtered_count = len(documents) - len(valid_documents)\n            logger.warning(f\"Filtered out {filtered_count} documents due to embedding issues\")\n\n        return valid_documents\n\n    def prepare_retriever(self, repo_url_or_path: str, type: str = \"github\", access_token: str = None,\n                      excluded_dirs: List[str] = None, excluded_files: List[str] = None,\n                      included_dirs: List[str] = None, included_files: List[str] = None):\n        \"\"\"\n        Prepare the retriever for a repository.\n        Will load database from local storage if available.\n\n        Args:\n            repo_url_or_path: URL or local path to the repository\n            access_token: Optional access token for private repositories\n            excluded_dirs: Optional list of directories to exclude from processing\n            excluded_files: Optional list of file patterns to exclude from processing\n            included_dirs: Optional list of directories to include exclusively\n            included_files: Optional list of file patterns to include exclusively\n        \"\"\"\n        self.initialize_db_manager()\n        self.repo_url_or_path = repo_url_or_path\n        self.transformed_docs = self.db_manager.prepare_database(\n            repo_url_or_path,\n            type,\n            access_token,\n            embedder_type=self.embedder_type,\n            excluded_dirs=excluded_dirs,\n            excluded_files=excluded_files,\n            included_dirs=included_dirs,\n            included_files=included_files\n        )\n        logger.info(f\"Loaded {len(self.transformed_docs)} documents for retrieval\")\n\n        # Validate and filter embeddings to ensure consistent sizes\n        self.transformed_docs = self._validate_and_filter_embeddings(self.transformed_docs)\n\n        if not self.transformed_docs:\n            raise ValueError(\"No valid documents with embeddings found. Cannot create retriever.\")\n\n        logger.info(f\"Using {len(self.transformed_docs)} documents with valid embeddings for retrieval\")\n\n        try:\n            # Use the appropriate embedder for retrieval\n            retrieve_embedder = self.query_embedder if self.is_ollama_embedder else self.embedder\n            self.retriever = FAISSRetriever(\n                **configs[\"retriever\"],\n                embedder=retrieve_embedder,\n                documents=self.transformed_docs,\n                document_map_func=lambda doc: doc.vector,\n            )\n            logger.info(\"FAISS retriever created successfully\")\n        except Exception as e:\n            logger.error(f\"Error creating FAISS retriever: {str(e)}\")\n            # Try to provide more specific error information\n            if \"All embeddings should be of the same size\" in str(e):\n                logger.error(\"Embedding size validation failed. This suggests there are still inconsistent embedding sizes.\")\n                # Log embedding sizes for debugging\n                sizes = []\n                for i, doc in enumerate(self.transformed_docs[:10]):  # Check first 10 docs\n                    if hasattr(doc, 'vector') and doc.vector is not None:\n                        try:\n                            if isinstance(doc.vector, list):\n                                size = len(doc.vector)\n                            elif hasattr(doc.vector, 'shape'):\n                                size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1]\n                            elif hasattr(doc.vector, '__len__'):\n                                size = len(doc.vector)\n                            else:\n                                size = \"unknown\"\n                            sizes.append(f\"doc_{i}: {size}\")\n                        except Exception:\n                            sizes.append(f\"doc_{i}: error\")\n                logger.error(f\"Sample embedding sizes: {', '.join(sizes)}\")\n            raise\n\n    def call(self, query: str, language: str = \"en\") -> Tuple[List]:\n        \"\"\"\n        Process a query using RAG.\n\n        Args:\n            query: The user's query\n\n        Returns:\n            Tuple of (RAGAnswer, retrieved_documents)\n        \"\"\"\n        try:\n            retrieved_documents = self.retriever(query)\n\n            # Fill in the documents\n            retrieved_documents[0].documents = [\n                self.transformed_docs[doc_index]\n                for doc_index in retrieved_documents[0].doc_indices\n            ]\n\n            return retrieved_documents\n\n        except Exception as e:\n            logger.error(f\"Error in RAG call: {str(e)}\")\n\n            # Create error response\n            error_response = RAGAnswer(\n                rationale=\"Error occurred while processing the query.\",\n                answer=f\"I apologize, but I encountered an error while processing your question. Please try again or rephrase your question.\"\n            )\n            return error_response, []\n"
  },
  {
    "path": "api/simple_chat.py",
    "content": "import logging\nimport os\nfrom typing import List, Optional\nfrom urllib.parse import unquote\n\nimport google.generativeai as genai\nfrom adalflow.components.model_client.ollama_client import OllamaClient\nfrom adalflow.core.types import ModelType\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseModel, Field\n\nfrom api.config import get_model_config, configs, OPENROUTER_API_KEY, OPENAI_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY\nfrom api.data_pipeline import count_tokens, get_file_content\nfrom api.openai_client import OpenAIClient\nfrom api.openrouter_client import OpenRouterClient\nfrom api.bedrock_client import BedrockClient\nfrom api.azureai_client import AzureAIClient\nfrom api.dashscope_client import DashscopeClient\nfrom api.rag import RAG\nfrom api.prompts import (\n    DEEP_RESEARCH_FIRST_ITERATION_PROMPT,\n    DEEP_RESEARCH_FINAL_ITERATION_PROMPT,\n    DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT,\n    SIMPLE_CHAT_SYSTEM_PROMPT\n)\n\n# Configure logging\nfrom api.logging_config import setup_logging\n\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\n\n# Initialize FastAPI app\napp = FastAPI(\n    title=\"Simple Chat API\",\n    description=\"Simplified API for streaming chat completions\"\n)\n\n# Configure CORS\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],  # Allows all origins\n    allow_credentials=True,\n    allow_methods=[\"*\"],  # Allows all methods\n    allow_headers=[\"*\"],  # Allows all headers\n)\n\n# Models for the API\nclass ChatMessage(BaseModel):\n    role: str  # 'user' or 'assistant'\n    content: str\n\nclass ChatCompletionRequest(BaseModel):\n    \"\"\"\n    Model for requesting a chat completion.\n    \"\"\"\n    repo_url: str = Field(..., description=\"URL of the repository to query\")\n    messages: List[ChatMessage] = Field(..., description=\"List of chat messages\")\n    filePath: Optional[str] = Field(None, description=\"Optional path to a file in the repository to include in the prompt\")\n    token: Optional[str] = Field(None, description=\"Personal access token for private repositories\")\n    type: Optional[str] = Field(\"github\", description=\"Type of repository (e.g., 'github', 'gitlab', 'bitbucket')\")\n\n    # model parameters\n    provider: str = Field(\"google\", description=\"Model provider (google, openai, openrouter, ollama, bedrock, azure, dashscope)\")\n    model: Optional[str] = Field(None, description=\"Model name for the specified provider\")\n\n    language: Optional[str] = Field(\"en\", description=\"Language for content generation (e.g., 'en', 'ja', 'zh', 'es', 'kr', 'vi')\")\n    excluded_dirs: Optional[str] = Field(None, description=\"Comma-separated list of directories to exclude from processing\")\n    excluded_files: Optional[str] = Field(None, description=\"Comma-separated list of file patterns to exclude from processing\")\n    included_dirs: Optional[str] = Field(None, description=\"Comma-separated list of directories to include exclusively\")\n    included_files: Optional[str] = Field(None, description=\"Comma-separated list of file patterns to include exclusively\")\n\n@app.post(\"/chat/completions/stream\")\nasync def chat_completions_stream(request: ChatCompletionRequest):\n    \"\"\"Stream a chat completion response directly using Google Generative AI\"\"\"\n    try:\n        # Check if request contains very large input\n        input_too_large = False\n        if request.messages and len(request.messages) > 0:\n            last_message = request.messages[-1]\n            if hasattr(last_message, 'content') and last_message.content:\n                tokens = count_tokens(last_message.content, request.provider == \"ollama\")\n                logger.info(f\"Request size: {tokens} tokens\")\n                if tokens > 8000:\n                    logger.warning(f\"Request exceeds recommended token limit ({tokens} > 7500)\")\n                    input_too_large = True\n\n        # Create a new RAG instance for this request\n        try:\n            request_rag = RAG(provider=request.provider, model=request.model)\n\n            # Extract custom file filter parameters if provided\n            excluded_dirs = None\n            excluded_files = None\n            included_dirs = None\n            included_files = None\n\n            if request.excluded_dirs:\n                excluded_dirs = [unquote(dir_path) for dir_path in request.excluded_dirs.split('\\n') if dir_path.strip()]\n                logger.info(f\"Using custom excluded directories: {excluded_dirs}\")\n            if request.excluded_files:\n                excluded_files = [unquote(file_pattern) for file_pattern in request.excluded_files.split('\\n') if file_pattern.strip()]\n                logger.info(f\"Using custom excluded files: {excluded_files}\")\n            if request.included_dirs:\n                included_dirs = [unquote(dir_path) for dir_path in request.included_dirs.split('\\n') if dir_path.strip()]\n                logger.info(f\"Using custom included directories: {included_dirs}\")\n            if request.included_files:\n                included_files = [unquote(file_pattern) for file_pattern in request.included_files.split('\\n') if file_pattern.strip()]\n                logger.info(f\"Using custom included files: {included_files}\")\n\n            request_rag.prepare_retriever(request.repo_url, request.type, request.token, excluded_dirs, excluded_files, included_dirs, included_files)\n            logger.info(f\"Retriever prepared for {request.repo_url}\")\n        except ValueError as e:\n            if \"No valid documents with embeddings found\" in str(e):\n                logger.error(f\"No valid embeddings found: {str(e)}\")\n                raise HTTPException(status_code=500, detail=\"No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content.\")\n            else:\n                logger.error(f\"ValueError preparing retriever: {str(e)}\")\n                raise HTTPException(status_code=500, detail=f\"Error preparing retriever: {str(e)}\")\n        except Exception as e:\n            logger.error(f\"Error preparing retriever: {str(e)}\")\n            # Check for specific embedding-related errors\n            if \"All embeddings should be of the same size\" in str(e):\n                raise HTTPException(status_code=500, detail=\"Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again.\")\n            else:\n                raise HTTPException(status_code=500, detail=f\"Error preparing retriever: {str(e)}\")\n\n        # Validate request\n        if not request.messages or len(request.messages) == 0:\n            raise HTTPException(status_code=400, detail=\"No messages provided\")\n\n        last_message = request.messages[-1]\n        if last_message.role != \"user\":\n            raise HTTPException(status_code=400, detail=\"Last message must be from the user\")\n\n        # Process previous messages to build conversation history\n        for i in range(0, len(request.messages) - 1, 2):\n            if i + 1 < len(request.messages):\n                user_msg = request.messages[i]\n                assistant_msg = request.messages[i + 1]\n\n                if user_msg.role == \"user\" and assistant_msg.role == \"assistant\":\n                    request_rag.memory.add_dialog_turn(\n                        user_query=user_msg.content,\n                        assistant_response=assistant_msg.content\n                    )\n\n        # Check if this is a Deep Research request\n        is_deep_research = False\n        research_iteration = 1\n\n        # Process messages to detect Deep Research requests\n        for msg in request.messages:\n            if hasattr(msg, 'content') and msg.content and \"[DEEP RESEARCH]\" in msg.content:\n                is_deep_research = True\n                # Only remove the tag from the last message\n                if msg == request.messages[-1]:\n                    # Remove the Deep Research tag\n                    msg.content = msg.content.replace(\"[DEEP RESEARCH]\", \"\").strip()\n\n        # Count research iterations if this is a Deep Research request\n        if is_deep_research:\n            research_iteration = sum(1 for msg in request.messages if msg.role == 'assistant') + 1\n            logger.info(f\"Deep Research request detected - iteration {research_iteration}\")\n\n            # Check if this is a continuation request\n            if \"continue\" in last_message.content.lower() and \"research\" in last_message.content.lower():\n                # Find the original topic from the first user message\n                original_topic = None\n                for msg in request.messages:\n                    if msg.role == \"user\" and \"continue\" not in msg.content.lower():\n                        original_topic = msg.content.replace(\"[DEEP RESEARCH]\", \"\").strip()\n                        logger.info(f\"Found original research topic: {original_topic}\")\n                        break\n\n                if original_topic:\n                    # Replace the continuation message with the original topic\n                    last_message.content = original_topic\n                    logger.info(f\"Using original topic for research: {original_topic}\")\n\n        # Get the query from the last message\n        query = last_message.content\n\n        # Only retrieve documents if input is not too large\n        context_text = \"\"\n        retrieved_documents = None\n\n        if not input_too_large:\n            try:\n                # If filePath exists, modify the query for RAG to focus on the file\n                rag_query = query\n                if request.filePath:\n                    # Use the file path to get relevant context about the file\n                    rag_query = f\"Contexts related to {request.filePath}\"\n                    logger.info(f\"Modified RAG query to focus on file: {request.filePath}\")\n\n                # Try to perform RAG retrieval\n                try:\n                    # This will use the actual RAG implementation\n                    retrieved_documents = request_rag(rag_query, language=request.language)\n\n                    if retrieved_documents and retrieved_documents[0].documents:\n                        # Format context for the prompt in a more structured way\n                        documents = retrieved_documents[0].documents\n                        logger.info(f\"Retrieved {len(documents)} documents\")\n\n                        # Group documents by file path\n                        docs_by_file = {}\n                        for doc in documents:\n                            file_path = doc.meta_data.get('file_path', 'unknown')\n                            if file_path not in docs_by_file:\n                                docs_by_file[file_path] = []\n                            docs_by_file[file_path].append(doc)\n\n                        # Format context text with file path grouping\n                        context_parts = []\n                        for file_path, docs in docs_by_file.items():\n                            # Add file header with metadata\n                            header = f\"## File Path: {file_path}\\n\\n\"\n                            # Add document content\n                            content = \"\\n\\n\".join([doc.text for doc in docs])\n\n                            context_parts.append(f\"{header}{content}\")\n\n                        # Join all parts with clear separation\n                        context_text = \"\\n\\n\" + \"-\" * 10 + \"\\n\\n\".join(context_parts)\n                    else:\n                        logger.warning(\"No documents retrieved from RAG\")\n                except Exception as e:\n                    logger.error(f\"Error in RAG retrieval: {str(e)}\")\n                    # Continue without RAG if there's an error\n\n            except Exception as e:\n                logger.error(f\"Error retrieving documents: {str(e)}\")\n                context_text = \"\"\n\n        # Get repository information\n        repo_url = request.repo_url\n        repo_name = repo_url.split(\"/\")[-1] if \"/\" in repo_url else repo_url\n\n        # Determine repository type\n        repo_type = request.type\n\n        # Get language information\n        language_code = request.language or configs[\"lang_config\"][\"default\"]\n        supported_langs = configs[\"lang_config\"][\"supported_languages\"]\n        language_name = supported_langs.get(language_code, \"English\")\n\n        # Create system prompt\n        if is_deep_research:\n            # Check if this is the first iteration\n            is_first_iteration = research_iteration == 1\n\n            # Check if this is the final iteration\n            is_final_iteration = research_iteration >= 5\n\n            if is_first_iteration:\n                system_prompt = DEEP_RESEARCH_FIRST_ITERATION_PROMPT.format(\n                    repo_type=repo_type,\n                    repo_url=repo_url,\n                    repo_name=repo_name,\n                    language_name=language_name\n                )\n            elif is_final_iteration:\n                system_prompt = DEEP_RESEARCH_FINAL_ITERATION_PROMPT.format(\n                    repo_type=repo_type,\n                    repo_url=repo_url,\n                    repo_name=repo_name,\n                    research_iteration=research_iteration,\n                    language_name=language_name\n                )\n            else:\n                system_prompt = DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT.format(\n                    repo_type=repo_type,\n                    repo_url=repo_url,\n                    repo_name=repo_name,\n                    research_iteration=research_iteration,\n                    language_name=language_name\n                )\n        else:\n            system_prompt = SIMPLE_CHAT_SYSTEM_PROMPT.format(\n                repo_type=repo_type,\n                repo_url=repo_url,\n                repo_name=repo_name,\n                language_name=language_name\n            )\n\n        # Fetch file content if provided\n        file_content = \"\"\n        if request.filePath:\n            try:\n                file_content = get_file_content(request.repo_url, request.filePath, request.type, request.token)\n                logger.info(f\"Successfully retrieved content for file: {request.filePath}\")\n            except Exception as e:\n                logger.error(f\"Error retrieving file content: {str(e)}\")\n                # Continue without file content if there's an error\n\n        # Format conversation history\n        conversation_history = \"\"\n        for turn_id, turn in request_rag.memory().items():\n            if not isinstance(turn_id, int) and hasattr(turn, 'user_query') and hasattr(turn, 'assistant_response'):\n                conversation_history += f\"<turn>\\n<user>{turn.user_query.query_str}</user>\\n<assistant>{turn.assistant_response.response_str}</assistant>\\n</turn>\\n\"\n\n        # Create the prompt with context\n        prompt = f\"/no_think {system_prompt}\\n\\n\"\n\n        if conversation_history:\n            prompt += f\"<conversation_history>\\n{conversation_history}</conversation_history>\\n\\n\"\n\n        # Check if filePath is provided and fetch file content if it exists\n        if file_content:\n            # Add file content to the prompt after conversation history\n            prompt += f\"<currentFileContent path=\\\"{request.filePath}\\\">\\n{file_content}\\n</currentFileContent>\\n\\n\"\n\n        # Only include context if it's not empty\n        CONTEXT_START = \"<START_OF_CONTEXT>\"\n        CONTEXT_END = \"<END_OF_CONTEXT>\"\n        if context_text.strip():\n            prompt += f\"{CONTEXT_START}\\n{context_text}\\n{CONTEXT_END}\\n\\n\"\n        else:\n            # Add a note that we're skipping RAG due to size constraints or because it's the isolated API\n            logger.info(\"No context available from RAG\")\n            prompt += \"<note>Answering without retrieval augmentation.</note>\\n\\n\"\n\n        prompt += f\"<query>\\n{query}\\n</query>\\n\\nAssistant: \"\n\n        model_config = get_model_config(request.provider, request.model)[\"model_kwargs\"]\n\n        if request.provider == \"ollama\":\n            prompt += \" /no_think\"\n\n            model = OllamaClient()\n            model_kwargs = {\n                \"model\": model_config[\"model\"],\n                \"stream\": True,\n                \"options\": {\n                    \"temperature\": model_config[\"temperature\"],\n                    \"top_p\": model_config[\"top_p\"],\n                    \"num_ctx\": model_config[\"num_ctx\"]\n                }\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"openrouter\":\n            logger.info(f\"Using OpenRouter with model: {request.model}\")\n\n            # Check if OpenRouter API key is set\n            if not OPENROUTER_API_KEY:\n                logger.warning(\"OPENROUTER_API_KEY not configured, but continuing with request\")\n                # We'll let the OpenRouterClient handle this and return a friendly error message\n\n            model = OpenRouterClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"]\n            }\n            # Only add top_p if it exists in the model config\n            if \"top_p\" in model_config:\n                model_kwargs[\"top_p\"] = model_config[\"top_p\"]\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"openai\":\n            logger.info(f\"Using Openai protocol with model: {request.model}\")\n\n            # Check if an API key is set for Openai\n            if not OPENAI_API_KEY:\n                logger.warning(\"OPENAI_API_KEY not configured, but continuing with request\")\n                # We'll let the OpenAIClient handle this and return an error message\n\n            # Initialize Openai client\n            model = OpenAIClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"]\n            }\n            # Only add top_p if it exists in the model config\n            if \"top_p\" in model_config:\n                model_kwargs[\"top_p\"] = model_config[\"top_p\"]\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"bedrock\":\n            logger.info(f\"Using AWS Bedrock with model: {request.model}\")\n\n            # Check if AWS credentials are set\n            if not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY:\n                logger.warning(\"AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY not configured, but continuing with request\")\n                # We'll let the BedrockClient handle this and return an error message\n\n            # Initialize Bedrock client\n            model = BedrockClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"temperature\": model_config[\"temperature\"],\n                \"top_p\": model_config[\"top_p\"]\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"azure\":\n            logger.info(f\"Using Azure AI with model: {request.model}\")\n\n            # Initialize Azure AI client\n            model = AzureAIClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"],\n                \"top_p\": model_config[\"top_p\"]\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"dashscope\":\n            logger.info(f\"Using Dashscope with model: {request.model}\")\n\n            model = DashscopeClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"],\n                \"top_p\": model_config[\"top_p\"],\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM,\n            )\n        else:\n            # Initialize Google Generative AI model (default provider)\n            model = genai.GenerativeModel(\n                model_name=model_config[\"model\"],\n                generation_config={\n                    \"temperature\": model_config[\"temperature\"],\n                    \"top_p\": model_config[\"top_p\"],\n                    \"top_k\": model_config[\"top_k\"],\n                },\n            )\n\n        # Create a streaming response\n        async def response_stream():\n            try:\n                if request.provider == \"ollama\":\n                    # Get the response and handle it properly using the previously created api_kwargs\n                    response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                    # Handle streaming response from Ollama\n                    async for chunk in response:\n                        text = getattr(chunk, 'response', None) or getattr(chunk, 'text', None) or str(chunk)\n                        if text and not text.startswith('model=') and not text.startswith('created_at='):\n                            text = text.replace('<think>', '').replace('</think>', '')\n                            yield text\n                elif request.provider == \"openrouter\":\n                    try:\n                        # Get the response and handle it properly using the previously created api_kwargs\n                        logger.info(\"Making OpenRouter API call\")\n                        response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                        # Handle streaming response from OpenRouter\n                        async for chunk in response:\n                            yield chunk\n                    except Exception as e_openrouter:\n                        logger.error(f\"Error with OpenRouter API: {str(e_openrouter)}\")\n                        yield f\"\\nError with OpenRouter API: {str(e_openrouter)}\\n\\nPlease check that you have set the OPENROUTER_API_KEY environment variable with a valid API key.\"\n                elif request.provider == \"openai\":\n                    try:\n                        # Get the response and handle it properly using the previously created api_kwargs\n                        logger.info(\"Making Openai API call\")\n                        response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                        # Handle streaming response from Openai\n                        async for chunk in response:\n                           choices = getattr(chunk, \"choices\", [])\n                           if len(choices) > 0:\n                               delta = getattr(choices[0], \"delta\", None)\n                               if delta is not None:\n                                    text = getattr(delta, \"content\", None)\n                                    if text is not None:\n                                        yield text\n                    except Exception as e_openai:\n                        logger.error(f\"Error with Openai API: {str(e_openai)}\")\n                        yield f\"\\nError with Openai API: {str(e_openai)}\\n\\nPlease check that you have set the OPENAI_API_KEY environment variable with a valid API key.\"\n                elif request.provider == \"bedrock\":\n                    try:\n                        # Get the response and handle it properly using the previously created api_kwargs\n                        logger.info(\"Making AWS Bedrock API call\")\n                        response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                        # Handle response from Bedrock (not streaming yet)\n                        if isinstance(response, str):\n                            yield response\n                        else:\n                            # Try to extract text from the response\n                            yield str(response)\n                    except Exception as e_bedrock:\n                        logger.error(f\"Error with AWS Bedrock API: {str(e_bedrock)}\")\n                        yield f\"\\nError with AWS Bedrock API: {str(e_bedrock)}\\n\\nPlease check that you have set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables with valid credentials.\"\n                elif request.provider == \"azure\":\n                    try:\n                        # Get the response and handle it properly using the previously created api_kwargs\n                        logger.info(\"Making Azure AI API call\")\n                        response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                        # Handle streaming response from Azure AI\n                        async for chunk in response:\n                            choices = getattr(chunk, \"choices\", [])\n                            if len(choices) > 0:\n                                delta = getattr(choices[0], \"delta\", None)\n                                if delta is not None:\n                                    text = getattr(delta, \"content\", None)\n                                    if text is not None:\n                                        yield text\n                    except Exception as e_azure:\n                        logger.error(f\"Error with Azure AI API: {str(e_azure)}\")\n                        yield f\"\\nError with Azure AI API: {str(e_azure)}\\n\\nPlease check that you have set the AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_VERSION environment variables with valid values.\"\n                elif request.provider == \"dashscope\":\n                    try:\n                        logger.info(\"Making Dashscope API call\")\n                        response = await model.acall(\n                            api_kwargs=api_kwargs, model_type=ModelType.LLM\n                        )\n                        # DashscopeClient.acall with stream=True returns an async\n                        # generator of text chunks\n                        async for text in response:\n                            if text:\n                                yield text\n                    except Exception as e_dashscope:\n                        logger.error(f\"Error with Dashscope API: {str(e_dashscope)}\")\n                        yield (\n                            f\"\\nError with Dashscope API: {str(e_dashscope)}\\n\\n\"\n                            \"Please check that you have set the DASHSCOPE_API_KEY (and optionally \"\n                            \"DASHSCOPE_WORKSPACE_ID) environment variables with valid values.\"\n                        )\n                else:\n                    # Google Generative AI (default provider)\n                    response = model.generate_content(prompt, stream=True)\n                    for chunk in response:\n                        if hasattr(chunk, \"text\"):\n                            yield chunk.text\n\n            except Exception as e_outer:\n                logger.error(f\"Error in streaming response: {str(e_outer)}\")\n                error_message = str(e_outer)\n\n                # Check for token limit errors\n                if \"maximum context length\" in error_message or \"token limit\" in error_message or \"too many tokens\" in error_message:\n                    # If we hit a token limit error, try again without context\n                    logger.warning(\"Token limit exceeded, retrying without context\")\n                    try:\n                        # Create a simplified prompt without context\n                        simplified_prompt = f\"/no_think {system_prompt}\\n\\n\"\n                        if conversation_history:\n                            simplified_prompt += f\"<conversation_history>\\n{conversation_history}</conversation_history>\\n\\n\"\n\n                        # Include file content in the fallback prompt if it was retrieved\n                        if request.filePath and file_content:\n                            simplified_prompt += f\"<currentFileContent path=\\\"{request.filePath}\\\">\\n{file_content}\\n</currentFileContent>\\n\\n\"\n\n                        simplified_prompt += \"<note>Answering without retrieval augmentation due to input size constraints.</note>\\n\\n\"\n                        simplified_prompt += f\"<query>\\n{query}\\n</query>\\n\\nAssistant: \"\n\n                        if request.provider == \"ollama\":\n                            simplified_prompt += \" /no_think\"\n\n                            # Create new api_kwargs with the simplified prompt\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM\n                            )\n\n                            # Get the response using the simplified prompt\n                            fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                            # Handle streaming fallback_response from Ollama\n                            async for chunk in fallback_response:\n                                text = getattr(chunk, 'response', None) or getattr(chunk, 'text', None) or str(chunk)\n                                if text and not text.startswith('model=') and not text.startswith('created_at='):\n                                    text = text.replace('<think>', '').replace('</think>', '')\n                                    yield text\n                        elif request.provider == \"openrouter\":\n                            try:\n                                # Create new api_kwargs with the simplified prompt\n                                fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                    input=simplified_prompt,\n                                    model_kwargs=model_kwargs,\n                                    model_type=ModelType.LLM\n                                )\n\n                                # Get the response using the simplified prompt\n                                logger.info(\"Making fallback OpenRouter API call\")\n                                fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                                # Handle streaming fallback_response from OpenRouter\n                                async for chunk in fallback_response:\n                                    yield chunk\n                            except Exception as e_fallback:\n                                logger.error(f\"Error with OpenRouter API fallback: {str(e_fallback)}\")\n                                yield f\"\\nError with OpenRouter API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the OPENROUTER_API_KEY environment variable with a valid API key.\"\n                        elif request.provider == \"openai\":\n                            try:\n                                # Create new api_kwargs with the simplified prompt\n                                fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                    input=simplified_prompt,\n                                    model_kwargs=model_kwargs,\n                                    model_type=ModelType.LLM\n                                )\n\n                                # Get the response using the simplified prompt\n                                logger.info(\"Making fallback Openai API call\")\n                                fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                                # Handle streaming fallback_response from Openai\n                                async for chunk in fallback_response:\n                                    text = chunk if isinstance(chunk, str) else getattr(chunk, 'text', str(chunk))\n                                    yield text\n                            except Exception as e_fallback:\n                                logger.error(f\"Error with Openai API fallback: {str(e_fallback)}\")\n                                yield f\"\\nError with Openai API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the OPENAI_API_KEY environment variable with a valid API key.\"\n                        elif request.provider == \"bedrock\":\n                            try:\n                                # Create new api_kwargs with the simplified prompt\n                                fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                    input=simplified_prompt,\n                                    model_kwargs=model_kwargs,\n                                    model_type=ModelType.LLM\n                                )\n\n                                # Get the response using the simplified prompt\n                                logger.info(\"Making fallback AWS Bedrock API call\")\n                                fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                                # Handle response from Bedrock\n                                if isinstance(fallback_response, str):\n                                    yield fallback_response\n                                else:\n                                    # Try to extract text from the response\n                                    yield str(fallback_response)\n                            except Exception as e_fallback:\n                                logger.error(f\"Error with AWS Bedrock API fallback: {str(e_fallback)}\")\n                                yield f\"\\nError with AWS Bedrock API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables with valid credentials.\"\n                        elif request.provider == \"azure\":\n                            try:\n                                # Create new api_kwargs with the simplified prompt\n                                fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                    input=simplified_prompt,\n                                    model_kwargs=model_kwargs,\n                                    model_type=ModelType.LLM\n                                )\n\n                                # Get the response using the simplified prompt\n                                logger.info(\"Making fallback Azure AI API call\")\n                                fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                                # Handle streaming fallback response from Azure AI\n                                async for chunk in fallback_response:\n                                    choices = getattr(chunk, \"choices\", [])\n                                    if len(choices) > 0:\n                                        delta = getattr(choices[0], \"delta\", None)\n                                        if delta is not None:\n                                            text = getattr(delta, \"content\", None)\n                                            if text is not None:\n                                                yield text\n                            except Exception as e_fallback:\n                                logger.error(f\"Error with Azure AI API fallback: {str(e_fallback)}\")\n                                yield f\"\\nError with Azure AI API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_VERSION environment variables with valid values.\"\n                        elif request.provider == \"dashscope\":\n                            try:\n                                # Create new api_kwargs with the simplified prompt\n                                fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                    input=simplified_prompt,\n                                    model_kwargs=model_kwargs,\n                                    model_type=ModelType.LLM,\n                                )\n\n                                logger.info(\"Making fallback Dashscope API call\")\n                                fallback_response = await model.acall(\n                                    api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM\n                                )\n\n                                # DashscopeClient.acall (stream=True) returns an async\n                                # generator of text chunks\n                                async for text in fallback_response:\n                                    if text:\n                                        yield text\n                            except Exception as e_fallback:\n                                logger.error(\n                                    f\"Error with Dashscope API fallback: {str(e_fallback)}\"\n                                )\n                                yield (\n                                    f\"\\nError with Dashscope API fallback: {str(e_fallback)}\\n\\n\"\n                                    \"Please check that you have set the DASHSCOPE_API_KEY (and optionally \"\n                                    \"DASHSCOPE_WORKSPACE_ID) environment variables with valid values.\"\n                                )\n                        else:\n                            # Google Generative AI fallback (default provider)\n                            model_config = get_model_config(request.provider, request.model)\n                            fallback_model = genai.GenerativeModel(\n                                model_name=model_config[\"model_kwargs\"][\"model\"],\n                                generation_config={\n                                    \"temperature\": model_config[\"model_kwargs\"].get(\"temperature\", 0.7),\n                                    \"top_p\": model_config[\"model_kwargs\"].get(\"top_p\", 0.8),\n                                    \"top_k\": model_config[\"model_kwargs\"].get(\"top_k\", 40),\n                                },\n                            )\n\n                            fallback_response = fallback_model.generate_content(\n                                simplified_prompt, stream=True\n                            )\n                            for chunk in fallback_response:\n                                if hasattr(chunk, \"text\"):\n                                    yield chunk.text\n                    except Exception as e2:\n                        logger.error(f\"Error in fallback streaming response: {str(e2)}\")\n                        yield f\"\\nI apologize, but your request is too large for me to process. Please try a shorter query or break it into smaller parts.\"\n                else:\n                    # For other errors, return the error message\n                    yield f\"\\nError: {error_message}\"\n\n        # Return streaming response\n        return StreamingResponse(response_stream(), media_type=\"text/event-stream\")\n\n    except HTTPException:\n        raise\n    except Exception as e_handler:\n        error_msg = f\"Error in streaming chat completion: {str(e_handler)}\"\n        logger.error(error_msg)\n        raise HTTPException(status_code=500, detail=error_msg)\n\n@app.get(\"/\")\nasync def root():\n    \"\"\"Root endpoint to check if the API is running\"\"\"\n    return {\"status\": \"API is running\", \"message\": \"Navigate to /docs for API documentation\"}\n"
  },
  {
    "path": "api/tools/embedder.py",
    "content": "import adalflow as adal\n\nfrom api.config import configs, get_embedder_type\n\n\ndef get_embedder(is_local_ollama: bool = False, use_google_embedder: bool = False, embedder_type: str = None) -> adal.Embedder:\n    \"\"\"Get embedder based on configuration or parameters.\n    \n    Args:\n        is_local_ollama: Legacy parameter for Ollama embedder\n        use_google_embedder: Legacy parameter for Google embedder  \n        embedder_type: Direct specification of embedder type ('ollama', 'google', 'bedrock', 'openai')\n    \n    Returns:\n        adal.Embedder: Configured embedder instance\n    \"\"\"\n    # Determine which embedder config to use\n    if embedder_type:\n        if embedder_type == 'ollama':\n            embedder_config = configs[\"embedder_ollama\"]\n        elif embedder_type == 'google':\n            embedder_config = configs[\"embedder_google\"]\n        elif embedder_type == 'bedrock':\n            embedder_config = configs[\"embedder_bedrock\"]\n        else:  # default to openai\n            embedder_config = configs[\"embedder\"]\n    elif is_local_ollama:\n        embedder_config = configs[\"embedder_ollama\"]\n    elif use_google_embedder:\n        embedder_config = configs[\"embedder_google\"]\n    else:\n        # Auto-detect based on current configuration\n        current_type = get_embedder_type()\n        if current_type == 'bedrock':\n            embedder_config = configs[\"embedder_bedrock\"]\n        elif current_type == 'ollama':\n            embedder_config = configs[\"embedder_ollama\"]\n        elif current_type == 'google':\n            embedder_config = configs[\"embedder_google\"]\n        else:\n            embedder_config = configs[\"embedder\"]\n\n    # --- Initialize Embedder ---\n    model_client_class = embedder_config[\"model_client\"]\n    if \"initialize_kwargs\" in embedder_config:\n        model_client = model_client_class(**embedder_config[\"initialize_kwargs\"])\n    else:\n        model_client = model_client_class()\n    \n    # Create embedder with basic parameters\n    embedder_kwargs = {\"model_client\": model_client, \"model_kwargs\": embedder_config[\"model_kwargs\"]}\n    \n    embedder = adal.Embedder(**embedder_kwargs)\n    \n    # Set batch_size as an attribute if available (not a constructor parameter)\n    if \"batch_size\" in embedder_config:\n        embedder.batch_size = embedder_config[\"batch_size\"]\n    return embedder\n"
  },
  {
    "path": "api/websocket_wiki.py",
    "content": "import logging\nimport os\nfrom typing import List, Optional, Dict, Any\nfrom urllib.parse import unquote\n\nimport google.generativeai as genai\nfrom adalflow.components.model_client.ollama_client import OllamaClient\nfrom adalflow.core.types import ModelType\nfrom fastapi import WebSocket, WebSocketDisconnect, HTTPException\nfrom pydantic import BaseModel, Field\n\nfrom api.config import (\n    get_model_config,\n    configs,\n    OPENROUTER_API_KEY,\n    OPENAI_API_KEY,\n    AWS_ACCESS_KEY_ID,\n    AWS_SECRET_ACCESS_KEY,\n)\nfrom api.data_pipeline import count_tokens, get_file_content\nfrom api.bedrock_client import BedrockClient\nfrom api.openai_client import OpenAIClient\nfrom api.openrouter_client import OpenRouterClient\nfrom api.azureai_client import AzureAIClient\nfrom api.dashscope_client import DashscopeClient\nfrom api.rag import RAG\n\n# Configure logging\nfrom api.logging_config import setup_logging\n\nsetup_logging()\nlogger = logging.getLogger(__name__)\n\n\n# Models for the API\nclass ChatMessage(BaseModel):\n    role: str  # 'user' or 'assistant'\n    content: str\n\nclass ChatCompletionRequest(BaseModel):\n    \"\"\"\n    Model for requesting a chat completion.\n    \"\"\"\n    repo_url: str = Field(..., description=\"URL of the repository to query\")\n    messages: List[ChatMessage] = Field(..., description=\"List of chat messages\")\n    filePath: Optional[str] = Field(None, description=\"Optional path to a file in the repository to include in the prompt\")\n    token: Optional[str] = Field(None, description=\"Personal access token for private repositories\")\n    type: Optional[str] = Field(\"github\", description=\"Type of repository (e.g., 'github', 'gitlab', 'bitbucket')\")\n\n    # model parameters\n    provider: str = Field(\n        \"google\",\n        description=\"Model provider (google, openai, openrouter, ollama, bedrock, azure, dashscope)\",\n    )\n    model: Optional[str] = Field(None, description=\"Model name for the specified provider\")\n\n    language: Optional[str] = Field(\"en\", description=\"Language for content generation (e.g., 'en', 'ja', 'zh', 'es', 'kr', 'vi')\")\n    excluded_dirs: Optional[str] = Field(None, description=\"Comma-separated list of directories to exclude from processing\")\n    excluded_files: Optional[str] = Field(None, description=\"Comma-separated list of file patterns to exclude from processing\")\n    included_dirs: Optional[str] = Field(None, description=\"Comma-separated list of directories to include exclusively\")\n    included_files: Optional[str] = Field(None, description=\"Comma-separated list of file patterns to include exclusively\")\n\nasync def handle_websocket_chat(websocket: WebSocket):\n    \"\"\"\n    Handle WebSocket connection for chat completions.\n    This replaces the HTTP streaming endpoint with a WebSocket connection.\n    \"\"\"\n    await websocket.accept()\n\n    try:\n        # Receive and parse the request data\n        request_data = await websocket.receive_json()\n        request = ChatCompletionRequest(**request_data)\n\n        # Check if request contains very large input\n        input_too_large = False\n        if request.messages and len(request.messages) > 0:\n            last_message = request.messages[-1]\n            if hasattr(last_message, 'content') and last_message.content:\n                tokens = count_tokens(last_message.content, request.provider == \"ollama\")\n                logger.info(f\"Request size: {tokens} tokens\")\n                if tokens > 8000:\n                    logger.warning(f\"Request exceeds recommended token limit ({tokens} > 7500)\")\n                    input_too_large = True\n\n        # Create a new RAG instance for this request\n        try:\n            request_rag = RAG(provider=request.provider, model=request.model)\n\n            # Extract custom file filter parameters if provided\n            excluded_dirs = None\n            excluded_files = None\n            included_dirs = None\n            included_files = None\n\n            if request.excluded_dirs:\n                excluded_dirs = [unquote(dir_path) for dir_path in request.excluded_dirs.split('\\n') if dir_path.strip()]\n                logger.info(f\"Using custom excluded directories: {excluded_dirs}\")\n            if request.excluded_files:\n                excluded_files = [unquote(file_pattern) for file_pattern in request.excluded_files.split('\\n') if file_pattern.strip()]\n                logger.info(f\"Using custom excluded files: {excluded_files}\")\n            if request.included_dirs:\n                included_dirs = [unquote(dir_path) for dir_path in request.included_dirs.split('\\n') if dir_path.strip()]\n                logger.info(f\"Using custom included directories: {included_dirs}\")\n            if request.included_files:\n                included_files = [unquote(file_pattern) for file_pattern in request.included_files.split('\\n') if file_pattern.strip()]\n                logger.info(f\"Using custom included files: {included_files}\")\n\n            request_rag.prepare_retriever(request.repo_url, request.type, request.token, excluded_dirs, excluded_files, included_dirs, included_files)\n            logger.info(f\"Retriever prepared for {request.repo_url}\")\n        except ValueError as e:\n            if \"No valid documents with embeddings found\" in str(e):\n                logger.error(f\"No valid embeddings found: {str(e)}\")\n                await websocket.send_text(\"Error: No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content.\")\n                await websocket.close()\n                return\n            else:\n                logger.error(f\"ValueError preparing retriever: {str(e)}\")\n                await websocket.send_text(f\"Error preparing retriever: {str(e)}\")\n                await websocket.close()\n                return\n        except Exception as e:\n            logger.error(f\"Error preparing retriever: {str(e)}\")\n            # Check for specific embedding-related errors\n            if \"All embeddings should be of the same size\" in str(e):\n                await websocket.send_text(\"Error: Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again.\")\n            else:\n                await websocket.send_text(f\"Error preparing retriever: {str(e)}\")\n            await websocket.close()\n            return\n\n        # Validate request\n        if not request.messages or len(request.messages) == 0:\n            await websocket.send_text(\"Error: No messages provided\")\n            await websocket.close()\n            return\n\n        last_message = request.messages[-1]\n        if last_message.role != \"user\":\n            await websocket.send_text(\"Error: Last message must be from the user\")\n            await websocket.close()\n            return\n\n        # Process previous messages to build conversation history\n        for i in range(0, len(request.messages) - 1, 2):\n            if i + 1 < len(request.messages):\n                user_msg = request.messages[i]\n                assistant_msg = request.messages[i + 1]\n\n                if user_msg.role == \"user\" and assistant_msg.role == \"assistant\":\n                    request_rag.memory.add_dialog_turn(\n                        user_query=user_msg.content,\n                        assistant_response=assistant_msg.content\n                    )\n\n        # Check if this is a Deep Research request\n        is_deep_research = False\n        research_iteration = 1\n\n        # Process messages to detect Deep Research requests\n        for msg in request.messages:\n            if hasattr(msg, 'content') and msg.content and \"[DEEP RESEARCH]\" in msg.content:\n                is_deep_research = True\n                # Only remove the tag from the last message\n                if msg == request.messages[-1]:\n                    # Remove the Deep Research tag\n                    msg.content = msg.content.replace(\"[DEEP RESEARCH]\", \"\").strip()\n\n        # Count research iterations if this is a Deep Research request\n        if is_deep_research:\n            research_iteration = sum(1 for msg in request.messages if msg.role == 'assistant') + 1\n            logger.info(f\"Deep Research request detected - iteration {research_iteration}\")\n\n            # Check if this is a continuation request\n            if \"continue\" in last_message.content.lower() and \"research\" in last_message.content.lower():\n                # Find the original topic from the first user message\n                original_topic = None\n                for msg in request.messages:\n                    if msg.role == \"user\" and \"continue\" not in msg.content.lower():\n                        original_topic = msg.content.replace(\"[DEEP RESEARCH]\", \"\").strip()\n                        logger.info(f\"Found original research topic: {original_topic}\")\n                        break\n\n                if original_topic:\n                    # Replace the continuation message with the original topic\n                    last_message.content = original_topic\n                    logger.info(f\"Using original topic for research: {original_topic}\")\n\n        # Get the query from the last message\n        query = last_message.content\n\n        # Only retrieve documents if input is not too large\n        context_text = \"\"\n        retrieved_documents = None\n\n        if not input_too_large:\n            try:\n                # If filePath exists, modify the query for RAG to focus on the file\n                rag_query = query\n                if request.filePath:\n                    # Use the file path to get relevant context about the file\n                    rag_query = f\"Contexts related to {request.filePath}\"\n                    logger.info(f\"Modified RAG query to focus on file: {request.filePath}\")\n\n                # Try to perform RAG retrieval\n                try:\n                    # This will use the actual RAG implementation\n                    retrieved_documents = request_rag(rag_query, language=request.language)\n\n                    if retrieved_documents and retrieved_documents[0].documents:\n                        # Format context for the prompt in a more structured way\n                        documents = retrieved_documents[0].documents\n                        logger.info(f\"Retrieved {len(documents)} documents\")\n\n                        # Group documents by file path\n                        docs_by_file = {}\n                        for doc in documents:\n                            file_path = doc.meta_data.get('file_path', 'unknown')\n                            if file_path not in docs_by_file:\n                                docs_by_file[file_path] = []\n                            docs_by_file[file_path].append(doc)\n\n                        # Format context text with file path grouping\n                        context_parts = []\n                        for file_path, docs in docs_by_file.items():\n                            # Add file header with metadata\n                            header = f\"## File Path: {file_path}\\n\\n\"\n                            # Add document content\n                            content = \"\\n\\n\".join([doc.text for doc in docs])\n\n                            context_parts.append(f\"{header}{content}\")\n\n                        # Join all parts with clear separation\n                        context_text = \"\\n\\n\" + \"-\" * 10 + \"\\n\\n\".join(context_parts)\n                    else:\n                        logger.warning(\"No documents retrieved from RAG\")\n                except Exception as e:\n                    logger.error(f\"Error in RAG retrieval: {str(e)}\")\n                    # Continue without RAG if there's an error\n\n            except Exception as e:\n                logger.error(f\"Error retrieving documents: {str(e)}\")\n                context_text = \"\"\n\n        # Get repository information\n        repo_url = request.repo_url\n        repo_name = repo_url.split(\"/\")[-1] if \"/\" in repo_url else repo_url\n\n        # Determine repository type\n        repo_type = request.type\n\n        # Get language information\n        language_code = request.language or configs[\"lang_config\"][\"default\"]\n        supported_langs = configs[\"lang_config\"][\"supported_languages\"]\n        language_name = supported_langs.get(language_code, \"English\")\n\n        # Create system prompt\n        if is_deep_research:\n            # Check if this is the first iteration\n            is_first_iteration = research_iteration == 1\n\n            # Check if this is the final iteration\n            is_final_iteration = research_iteration >= 5\n\n            if is_first_iteration:\n                system_prompt = f\"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are conducting a multi-turn Deep Research process to thoroughly investigate the specific topic in the user's query.\nYour goal is to provide detailed, focused information EXCLUSIVELY about this topic.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- This is the first iteration of a multi-turn research process focused EXCLUSIVELY on the user's query\n- Start your response with \"## Research Plan\"\n- Outline your approach to investigating this specific topic\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- Clearly state the specific topic you're researching to maintain focus throughout all iterations\n- Identify the key aspects you'll need to research\n- Provide initial findings based on the information available\n- End with \"## Next Steps\" indicating what you'll investigate in the next iteration\n- Do NOT provide a final conclusion yet - this is just the beginning of the research\n- Do NOT include general repository information unless directly relevant to the query\n- Focus EXCLUSIVELY on the specific topic being researched - do not drift to related topics\n- Your research MUST directly address the original question\n- NEVER respond with just \"Continue the research\" as an answer - always provide substantive research findings\n- Remember that this topic will be maintained across all research iterations\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n</style>\"\"\"\n            elif is_final_iteration:\n                system_prompt = f\"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are in the final iteration of a Deep Research process focused EXCLUSIVELY on the latest user query.\nYour goal is to synthesize all previous findings and provide a comprehensive conclusion that directly addresses this specific topic and ONLY this topic.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- This is the final iteration of the research process\n- CAREFULLY review the entire conversation history to understand all previous findings\n- Synthesize ALL findings from previous iterations into a comprehensive conclusion\n- Start with \"## Final Conclusion\"\n- Your conclusion MUST directly address the original question\n- Stay STRICTLY focused on the specific topic - do not drift to related topics\n- Include specific code references and implementation details related to the topic\n- Highlight the most important discoveries and insights about this specific functionality\n- Provide a complete and definitive answer to the original question\n- Do NOT include general repository information unless directly relevant to the query\n- Focus exclusively on the specific topic being researched\n- NEVER respond with \"Continue the research\" as an answer - always provide a complete conclusion\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- Ensure your conclusion builds on and references key findings from previous iterations\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n- Structure your response with clear headings\n- End with actionable insights or recommendations when appropriate\n</style>\"\"\"\n            else:\n                system_prompt = f\"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou are currently in iteration {research_iteration} of a Deep Research process focused EXCLUSIVELY on the latest user query.\nYour goal is to build upon previous research iterations and go deeper into this specific topic without deviating from it.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- CAREFULLY review the conversation history to understand what has been researched so far\n- Your response MUST build on previous research iterations - do not repeat information already covered\n- Identify gaps or areas that need further exploration related to this specific topic\n- Focus on one specific aspect that needs deeper investigation in this iteration\n- Start your response with \"## Research Update {research_iteration}\"\n- Clearly explain what you're investigating in this iteration\n- Provide new insights that weren't covered in previous iterations\n- If this is iteration 3, prepare for a final conclusion in the next iteration\n- Do NOT include general repository information unless directly relevant to the query\n- Focus EXCLUSIVELY on the specific topic being researched - do not drift to related topics\n- If the topic is about a specific file or feature (like \"Dockerfile\"), focus ONLY on that file or feature\n- NEVER respond with just \"Continue the research\" as an answer - always provide substantive research findings\n- Your research MUST directly address the original question\n- Maintain continuity with previous research iterations - this is a continuous investigation\n</guidelines>\n\n<style>\n- Be concise but thorough\n- Focus on providing new information, not repeating what's already been covered\n- Use markdown formatting to improve readability\n- Cite specific files and code sections when relevant\n</style>\"\"\"\n        else:\n            system_prompt = f\"\"\"<role>\nYou are an expert code analyst examining the {repo_type} repository: {repo_url} ({repo_name}).\nYou provide direct, concise, and accurate information about code repositories.\nYou NEVER start responses with markdown headers or code fences.\nIMPORTANT:You MUST respond in {language_name} language.\n</role>\n\n<guidelines>\n- Answer the user's question directly without ANY preamble or filler phrases\n- DO NOT include any rationale, explanation, or extra comments.\n- Strictly base answers ONLY on existing code or documents\n- DO NOT speculate or invent citations.\n- DO NOT start with preambles like \"Okay, here's a breakdown\" or \"Here's an explanation\"\n- DO NOT start with markdown headers like \"## Analysis of...\" or any file path references\n- DO NOT start with ```markdown code fences\n- DO NOT end your response with ``` closing fences\n- DO NOT start by repeating or acknowledging the question\n- JUST START with the direct answer to the question\n\n<example_of_what_not_to_do>\n```markdown\n## Analysis of `adalflow/adalflow/datasets/gsm8k.py`\n\nThis file contains...\n```\n</example_of_what_not_to_do>\n\n- Format your response with proper markdown including headings, lists, and code blocks WITHIN your answer\n- For code analysis, organize your response with clear sections\n- Think step by step and structure your answer logically\n- Start with the most relevant information that directly addresses the user's query\n- Be precise and technical when discussing code\n- Your response language should be in the same language as the user's query\n</guidelines>\n\n<style>\n- Use concise, direct language\n- Prioritize accuracy over verbosity\n- When showing code, include line numbers and file paths when relevant\n- Use markdown formatting to improve readability\n</style>\"\"\"\n\n        # Fetch file content if provided\n        file_content = \"\"\n        if request.filePath:\n            try:\n                file_content = get_file_content(request.repo_url, request.filePath, request.type, request.token)\n                logger.info(f\"Successfully retrieved content for file: {request.filePath}\")\n            except Exception as e:\n                logger.error(f\"Error retrieving file content: {str(e)}\")\n                # Continue without file content if there's an error\n\n        # Format conversation history\n        conversation_history = \"\"\n        for turn_id, turn in request_rag.memory().items():\n            if not isinstance(turn_id, int) and hasattr(turn, 'user_query') and hasattr(turn, 'assistant_response'):\n                conversation_history += f\"<turn>\\n<user>{turn.user_query.query_str}</user>\\n<assistant>{turn.assistant_response.response_str}</assistant>\\n</turn>\\n\"\n\n        # Create the prompt with context\n        prompt = f\"/no_think {system_prompt}\\n\\n\"\n\n        if conversation_history:\n            prompt += f\"<conversation_history>\\n{conversation_history}</conversation_history>\\n\\n\"\n\n        # Check if filePath is provided and fetch file content if it exists\n        if file_content:\n            # Add file content to the prompt after conversation history\n            prompt += f\"<currentFileContent path=\\\"{request.filePath}\\\">\\n{file_content}\\n</currentFileContent>\\n\\n\"\n\n        # Only include context if it's not empty\n        CONTEXT_START = \"<START_OF_CONTEXT>\"\n        CONTEXT_END = \"<END_OF_CONTEXT>\"\n        if context_text.strip():\n            prompt += f\"{CONTEXT_START}\\n{context_text}\\n{CONTEXT_END}\\n\\n\"\n        else:\n            # Add a note that we're skipping RAG due to size constraints or because it's the isolated API\n            logger.info(\"No context available from RAG\")\n            prompt += \"<note>Answering without retrieval augmentation.</note>\\n\\n\"\n\n        prompt += f\"<query>\\n{query}\\n</query>\\n\\nAssistant: \"\n\n        model_config = get_model_config(request.provider, request.model)[\"model_kwargs\"]\n\n        if request.provider == \"ollama\":\n            prompt += \" /no_think\"\n\n            model = OllamaClient()\n            model_kwargs = {\n                \"model\": model_config[\"model\"],\n                \"stream\": True,\n                \"options\": {\n                    \"temperature\": model_config[\"temperature\"],\n                    \"top_p\": model_config[\"top_p\"],\n                    \"num_ctx\": model_config[\"num_ctx\"]\n                }\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"openrouter\":\n            logger.info(f\"Using OpenRouter with model: {request.model}\")\n\n            # Check if OpenRouter API key is set\n            if not OPENROUTER_API_KEY:\n                logger.warning(\"OPENROUTER_API_KEY not configured, but continuing with request\")\n                # We'll let the OpenRouterClient handle this and return a friendly error message\n\n            model = OpenRouterClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"]\n            }\n            # Only add top_p if it exists in the model config\n            if \"top_p\" in model_config:\n                model_kwargs[\"top_p\"] = model_config[\"top_p\"]\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"openai\":\n            logger.info(f\"Using Openai protocol with model: {request.model}\")\n\n            # Check if an API key is set for Openai\n            if not OPENAI_API_KEY:\n                logger.warning(\"OPENAI_API_KEY not configured, but continuing with request\")\n                # We'll let the OpenAIClient handle this and return an error message\n\n            # Initialize Openai client\n            model = OpenAIClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"]\n            }\n            # Only add top_p if it exists in the model config\n            if \"top_p\" in model_config:\n                model_kwargs[\"top_p\"] = model_config[\"top_p\"]\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"bedrock\":\n            logger.info(f\"Using AWS Bedrock with model: {request.model}\")\n\n            if not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY:\n                logger.warning(\n                    \"AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY not configured, but continuing with request\")\n\n            model = BedrockClient()\n            model_kwargs = {\n                \"model\": request.model,\n            }\n\n            for key in [\"temperature\", \"top_p\"]:\n                if key in model_config:\n                    model_kwargs[key] = model_config[key]\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"azure\":\n            logger.info(f\"Using Azure AI with model: {request.model}\")\n\n            # Initialize Azure AI client\n            model = AzureAIClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"],\n                \"top_p\": model_config[\"top_p\"]\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        elif request.provider == \"dashscope\":\n            logger.info(f\"Using Dashscope with model: {request.model}\")\n\n            # Initialize Dashscope client\n            model = DashscopeClient()\n            model_kwargs = {\n                \"model\": request.model,\n                \"stream\": True,\n                \"temperature\": model_config[\"temperature\"],\n                \"top_p\": model_config[\"top_p\"]\n            }\n\n            api_kwargs = model.convert_inputs_to_api_kwargs(\n                input=prompt,\n                model_kwargs=model_kwargs,\n                model_type=ModelType.LLM\n            )\n        else:\n            # Initialize Google Generative AI model\n            model = genai.GenerativeModel(\n                model_name=model_config[\"model\"],\n                generation_config={\n                    \"temperature\": model_config[\"temperature\"],\n                    \"top_p\": model_config[\"top_p\"],\n                    \"top_k\": model_config[\"top_k\"]\n                }\n            )\n\n        # Process the response based on the provider\n        try:\n            if request.provider == \"ollama\":\n                # Get the response and handle it properly using the previously created api_kwargs\n                response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                # Handle streaming response from Ollama\n                async for chunk in response:\n                    text = None\n                    if isinstance(chunk, dict):\n                        text = chunk.get(\"message\", {}).get(\"content\") if isinstance(chunk.get(\"message\"), dict) else chunk.get(\"message\")\n                    else:\n                        message = getattr(chunk, \"message\", None)\n                        if message is not None:\n                            if isinstance(message, dict):\n                                text = message.get(\"content\")\n                            else:\n                                text = getattr(message, \"content\", None)\n\n                    if not text:\n                        text = getattr(chunk, 'response', None) or getattr(chunk, 'text', None)\n\n                    if not text and hasattr(chunk, \"__dict__\"):\n                        message = chunk.__dict__.get(\"message\")\n                        if isinstance(message, dict):\n                            text = message.get(\"content\")\n\n                    if isinstance(text, str) and text and not text.startswith('model=') and not text.startswith('created_at='):\n                        clean_text = text.replace('<think>', '').replace('</think>', '')\n                        await websocket.send_text(clean_text)\n                # Explicitly close the WebSocket connection after the response is complete\n                await websocket.close()\n            elif request.provider == \"openrouter\":\n                try:\n                    # Get the response and handle it properly using the previously created api_kwargs\n                    logger.info(\"Making OpenRouter API call\")\n                    response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                    # Handle streaming response from OpenRouter\n                    async for chunk in response:\n                        await websocket.send_text(chunk)\n                    # Explicitly close the WebSocket connection after the response is complete\n                    await websocket.close()\n                except Exception as e_openrouter:\n                    logger.error(f\"Error with OpenRouter API: {str(e_openrouter)}\")\n                    error_msg = f\"\\nError with OpenRouter API: {str(e_openrouter)}\\n\\nPlease check that you have set the OPENROUTER_API_KEY environment variable with a valid API key.\"\n                    await websocket.send_text(error_msg)\n                    # Close the WebSocket connection after sending the error message\n                    await websocket.close()\n            elif request.provider == \"openai\":\n                try:\n                    # Get the response and handle it properly using the previously created api_kwargs\n                    logger.info(\"Making Openai API call\")\n                    response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                    # Handle streaming response from Openai\n                    async for chunk in response:\n                        choices = getattr(chunk, \"choices\", [])\n                        if len(choices) > 0:\n                            delta = getattr(choices[0], \"delta\", None)\n                            if delta is not None:\n                                text = getattr(delta, \"content\", None)\n                                if text is not None:\n                                    await websocket.send_text(text)\n                    # Explicitly close the WebSocket connection after the response is complete\n                    await websocket.close()\n                except Exception as e_openai:\n                    logger.error(f\"Error with Openai API: {str(e_openai)}\")\n                    error_msg = f\"\\nError with Openai API: {str(e_openai)}\\n\\nPlease check that you have set the OPENAI_API_KEY environment variable with a valid API key.\"\n                    await websocket.send_text(error_msg)\n                    # Close the WebSocket connection after sending the error message\n                    await websocket.close()\n            elif request.provider == \"bedrock\":\n                try:\n                    logger.info(\"Making AWS Bedrock API call\")\n                    response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                    if isinstance(response, str):\n                        await websocket.send_text(response)\n                    else:\n                        await websocket.send_text(str(response))\n                    await websocket.close()\n                except Exception as e_bedrock:\n                    logger.error(f\"Error with AWS Bedrock API: {str(e_bedrock)}\")\n                    error_msg = (\n                        f\"\\nError with AWS Bedrock API: {str(e_bedrock)}\\n\\n\"\n                        \"Please check that you have set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY \"\n                        \"environment variables with valid credentials.\"\n                    )\n                    await websocket.send_text(error_msg)\n                    await websocket.close()\n            elif request.provider == \"azure\":\n                try:\n                    # Get the response and handle it properly using the previously created api_kwargs\n                    logger.info(\"Making Azure AI API call\")\n                    response = await model.acall(api_kwargs=api_kwargs, model_type=ModelType.LLM)\n                    # Handle streaming response from Azure AI\n                    async for chunk in response:\n                        choices = getattr(chunk, \"choices\", [])\n                        if len(choices) > 0:\n                            delta = getattr(choices[0], \"delta\", None)\n                            if delta is not None:\n                                text = getattr(delta, \"content\", None)\n                                if text is not None:\n                                    await websocket.send_text(text)\n                    # Explicitly close the WebSocket connection after the response is complete\n                    await websocket.close()\n                except Exception as e_azure:\n                    logger.error(f\"Error with Azure AI API: {str(e_azure)}\")\n                    error_msg = f\"\\nError with Azure AI API: {str(e_azure)}\\n\\nPlease check that you have set the AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_VERSION environment variables with valid values.\"\n                    await websocket.send_text(error_msg)\n                    # Close the WebSocket connection after sending the error message\n                    await websocket.close()\n            elif request.provider == \"dashscope\":\n                try:\n                    # Get the response and handle it properly using the previously created api_kwargs\n                    logger.info(\"Making Dashscope API call\")\n                    response = await model.acall(\n                        api_kwargs=api_kwargs, model_type=ModelType.LLM\n                    )\n                    # DashscopeClient.acall with stream=True returns an async\n                    # generator of plain text chunks\n                    async for text in response:\n                        if text:\n                            await websocket.send_text(text)\n                    # Explicitly close the WebSocket connection after the response is complete\n                    await websocket.close()\n                except Exception as e_dashscope:\n                    logger.error(f\"Error with Dashscope API: {str(e_dashscope)}\")\n                    error_msg = (\n                        f\"\\nError with Dashscope API: {str(e_dashscope)}\\n\\n\"\n                        \"Please check that you have set the DASHSCOPE_API_KEY (and optionally \"\n                        \"DASHSCOPE_WORKSPACE_ID) environment variables with valid values.\"\n                    )\n                    await websocket.send_text(error_msg)\n                    # Close the WebSocket connection after sending the error message\n                    await websocket.close()\n            else:\n                # Google Generative AI (default provider)\n                response = model.generate_content(prompt, stream=True)\n                for chunk in response:\n                    if hasattr(chunk, 'text'):\n                        await websocket.send_text(chunk.text)\n                await websocket.close()\n\n        except Exception as e_outer:\n            logger.error(f\"Error in streaming response: {str(e_outer)}\")\n            error_message = str(e_outer)\n\n            # Check for token limit errors\n            if \"maximum context length\" in error_message or \"token limit\" in error_message or \"too many tokens\" in error_message:\n                # If we hit a token limit error, try again without context\n                logger.warning(\"Token limit exceeded, retrying without context\")\n                try:\n                    # Create a simplified prompt without context\n                    simplified_prompt = f\"/no_think {system_prompt}\\n\\n\"\n                    if conversation_history:\n                        simplified_prompt += f\"<conversation_history>\\n{conversation_history}</conversation_history>\\n\\n\"\n\n                    # Include file content in the fallback prompt if it was retrieved\n                    if request.filePath and file_content:\n                        simplified_prompt += f\"<currentFileContent path=\\\"{request.filePath}\\\">\\n{file_content}\\n</currentFileContent>\\n\\n\"\n\n                    simplified_prompt += \"<note>Answering without retrieval augmentation due to input size constraints.</note>\\n\\n\"\n                    simplified_prompt += f\"<query>\\n{query}\\n</query>\\n\\nAssistant: \"\n\n                    if request.provider == \"ollama\":\n                        simplified_prompt += \" /no_think\"\n\n                        # Create new api_kwargs with the simplified prompt\n                        fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                            input=simplified_prompt,\n                            model_kwargs=model_kwargs,\n                            model_type=ModelType.LLM\n                        )\n\n                        # Get the response using the simplified prompt\n                        fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                        # Handle streaming fallback_response from Ollama\n                        async for chunk in fallback_response:\n                            text = getattr(chunk, 'response', None) or getattr(chunk, 'text', None) or str(chunk)\n                            if text and not text.startswith('model=') and not text.startswith('created_at='):\n                                text = text.replace('<think>', '').replace('</think>', '')\n                                await websocket.send_text(text)\n                    elif request.provider == \"openrouter\":\n                        try:\n                            # Create new api_kwargs with the simplified prompt\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM\n                            )\n\n                            # Get the response using the simplified prompt\n                            logger.info(\"Making fallback OpenRouter API call\")\n                            fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                            # Handle streaming fallback_response from OpenRouter\n                            async for chunk in fallback_response:\n                                await websocket.send_text(chunk)\n                        except Exception as e_fallback:\n                            logger.error(f\"Error with OpenRouter API fallback: {str(e_fallback)}\")\n                            error_msg = f\"\\nError with OpenRouter API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the OPENROUTER_API_KEY environment variable with a valid API key.\"\n                            await websocket.send_text(error_msg)\n                    elif request.provider == \"openai\":\n                        try:\n                            # Create new api_kwargs with the simplified prompt\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM\n                            )\n\n                            # Get the response using the simplified prompt\n                            logger.info(\"Making fallback Openai API call\")\n                            fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                            # Handle streaming fallback_response from Openai\n                            async for chunk in fallback_response:\n                                text = chunk if isinstance(chunk, str) else getattr(chunk, 'text', str(chunk))\n                                await websocket.send_text(text)\n                        except Exception as e_fallback:\n                            logger.error(f\"Error with Openai API fallback: {str(e_fallback)}\")\n                            error_msg = f\"\\nError with Openai API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the OPENAI_API_KEY environment variable with a valid API key.\"\n                            await websocket.send_text(error_msg)\n                    elif request.provider == \"bedrock\":\n                        try:\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM,\n                            )\n\n                            logger.info(\"Making fallback AWS Bedrock API call\")\n                            fallback_response = await model.acall(\n                                api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM\n                            )\n\n                            if isinstance(fallback_response, str):\n                                await websocket.send_text(fallback_response)\n                            else:\n                                await websocket.send_text(str(fallback_response))\n                        except Exception as e_fallback:\n                            logger.error(\n                                f\"Error with AWS Bedrock API fallback: {str(e_fallback)}\"\n                            )\n                            error_msg = (\n                                f\"\\nError with AWS Bedrock API fallback: {str(e_fallback)}\\n\\n\"\n                                \"Please check that you have set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY \"\n                                \"environment variables with valid credentials.\"\n                            )\n                            await websocket.send_text(error_msg)\n                    elif request.provider == \"azure\":\n                        try:\n                            # Create new api_kwargs with the simplified prompt\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM\n                            )\n\n                            # Get the response using the simplified prompt\n                            logger.info(\"Making fallback Azure AI API call\")\n                            fallback_response = await model.acall(api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM)\n\n                            # Handle streaming fallback response from Azure AI\n                            async for chunk in fallback_response:\n                                choices = getattr(chunk, \"choices\", [])\n                                if len(choices) > 0:\n                                    delta = getattr(choices[0], \"delta\", None)\n                                    if delta is not None:\n                                        text = getattr(delta, \"content\", None)\n                                        if text is not None:\n                                            await websocket.send_text(text)\n                        except Exception as e_fallback:\n                            logger.error(f\"Error with Azure AI API fallback: {str(e_fallback)}\")\n                            error_msg = f\"\\nError with Azure AI API fallback: {str(e_fallback)}\\n\\nPlease check that you have set the AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_VERSION environment variables with valid values.\"\n                            await websocket.send_text(error_msg)\n                    elif request.provider == \"dashscope\":\n                        try:\n                            # Create new api_kwargs with the simplified prompt\n                            fallback_api_kwargs = model.convert_inputs_to_api_kwargs(\n                                input=simplified_prompt,\n                                model_kwargs=model_kwargs,\n                                model_type=ModelType.LLM,\n                            )\n\n                            logger.info(\"Making fallback Dashscope API call\")\n                            fallback_response = await model.acall(\n                                api_kwargs=fallback_api_kwargs, model_type=ModelType.LLM\n                            )\n\n                            # DashscopeClient.acall (stream=True) returns an async\n                            # generator of text chunks\n                            async for text in fallback_response:\n                                if text:\n                                    await websocket.send_text(text)\n                        except Exception as e_fallback:\n                            logger.error(\n                                f\"Error with Dashscope API fallback: {str(e_fallback)}\"\n                            )\n                            error_msg = (\n                                f\"\\nError with Dashscope API fallback: {str(e_fallback)}\\n\\n\"\n                                \"Please check that you have set the DASHSCOPE_API_KEY (and optionally \"\n                                \"DASHSCOPE_WORKSPACE_ID) environment variables with valid values.\"\n                            )\n                            await websocket.send_text(error_msg)\n                    else:\n                        # Google Generative AI fallback (default provider)\n                        model_config = get_model_config(request.provider, request.model)\n                        fallback_model = genai.GenerativeModel(\n                            model_name=model_config[\"model_kwargs\"][\"model\"],\n                            generation_config={\n                                \"temperature\": model_config[\"model_kwargs\"].get(\"temperature\", 0.7),\n                                \"top_p\": model_config[\"model_kwargs\"].get(\"top_p\", 0.8),\n                                \"top_k\": model_config[\"model_kwargs\"].get(\"top_k\", 40),\n                            },\n                        )\n\n                        fallback_response = fallback_model.generate_content(\n                            simplified_prompt, stream=True\n                        )\n                        for chunk in fallback_response:\n                            if hasattr(chunk, \"text\"):\n                                await websocket.send_text(chunk.text)\n                except Exception as e2:\n                    logger.error(f\"Error in fallback streaming response: {str(e2)}\")\n                    await websocket.send_text(f\"\\nI apologize, but your request is too large for me to process. Please try a shorter query or break it into smaller parts.\")\n                    # Close the WebSocket connection after sending the error message\n                    await websocket.close()\n            else:\n                # For other errors, return the error message\n                await websocket.send_text(f\"\\nError: {error_message}\")\n                # Close the WebSocket connection after sending the error message\n                await websocket.close()\n\n    except WebSocketDisconnect:\n        logger.info(\"WebSocket disconnected\")\n    except Exception as e:\n        logger.error(f\"Error in WebSocket handler: {str(e)}\")\n        try:\n            await websocket.send_text(f\"Error: {str(e)}\")\n            await websocket.close()\n        except Exception:\n            pass\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  deepwiki:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"${PORT:-8001}:${PORT:-8001}\"  # API port\n      - \"3000:3000\"  # Next.js port\n    env_file:\n      - .env\n    environment:\n      - PORT=${PORT:-8001}\n      - NODE_ENV=production\n      - SERVER_BASE_URL=http://localhost:${PORT:-8001}\n      - LOG_LEVEL=${LOG_LEVEL:-INFO}\n      - LOG_FILE_PATH=${LOG_FILE_PATH:-api/logs/application.log}\n    volumes:\n      - ~/.adalflow:/root/.adalflow      # Persist repository and embedding data\n      - ./api/logs:/app/api/logs          # Persist log files across container restarts\n    # Resource limits for docker-compose up (not Swarm mode)\n    mem_limit: 6g\n    mem_reservation: 2g\n    # Health check configuration\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:${PORT:-8001}/health\"]\n      interval: 60s\n      timeout: 10s\n      retries: 3\n      start_period: 30s\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n  output: 'standalone',\n  // Optimize build for Docker\n  experimental: {\n    optimizePackageImports: ['@mermaid-js/mermaid', 'react-syntax-highlighter'],\n  },\n  // Reduce memory usage during build\n  webpack: (config, { isServer }) => {\n    if (!isServer) {\n      config.resolve.fallback = {\n        ...config.resolve.fallback,\n        fs: false,\n      };\n    }\n    // Optimize bundle size\n    config.optimization = {\n      ...config.optimization,\n      splitChunks: {\n        chunks: 'all',\n        cacheGroups: {\n          vendor: {\n            test: /[\\\\/]node_modules[\\\\/]/,\n            name: 'vendors',\n            chunks: 'all',\n          },\n        },\n      },\n    };\n    return config;\n  },\n  async rewrites() {\n    return [\n      {\n        source: '/api/wiki_cache/:path*',\n        destination: `${TARGET_SERVER_BASE_URL}/api/wiki_cache/:path*`,\n      },\n      {\n        source: '/export/wiki/:path*',\n        destination: `${TARGET_SERVER_BASE_URL}/export/wiki/:path*`,\n      },\n      {\n        source: '/api/wiki_cache',\n        destination: `${TARGET_SERVER_BASE_URL}/api/wiki_cache`,\n      },\n      {\n        source: '/local_repo/structure',\n        destination: `${TARGET_SERVER_BASE_URL}/local_repo/structure`,\n      },\n      {\n        source: '/api/auth/status',\n        destination: `${TARGET_SERVER_BASE_URL}/auth/status`,\n      },\n      {\n        source: '/api/auth/validate',\n        destination: `${TARGET_SERVER_BASE_URL}/auth/validate`,\n      },\n      {\n        source: '/api/lang/config',\n        destination: `${TARGET_SERVER_BASE_URL}/lang/config`,\n      },\n    ];\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"deepwiki-open\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack --port 3000\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"mermaid\": \"^11.4.1\",\n    \"next\": \"15.3.1\",\n    \"next-intl\": \"^4.1.0\",\n    \"next-themes\": \"^0.4.6\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-icons\": \"^5.5.0\",\n    \"react-markdown\": \"^10.1.0\",\n    \"react-syntax-highlighter\": \"^15.6.1\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"svg-pan-zoom\": \"^3.6.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.3.1\",\n    \"tailwindcss\": \"^4\",\n    \"typescript\": \"^5\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "pytest.ini",
    "content": "[tool:pytest]\ntestpaths = test\npython_files = test_*.py *_test.py\npython_classes = Test*\npython_functions = test_*\naddopts = \n    -v\n    --strict-markers\n    --disable-warnings\n    --tb=short\nmarkers =\n    unit: Unit tests\n    integration: Integration tests\n    slow: Slow tests that take more than a few seconds\n    network: Tests that require network access\n"
  },
  {
    "path": "run.sh",
    "content": "uv run -m api.main"
  },
  {
    "path": "src/app/[owner]/[repo]/page.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\n'use client';\n\nimport Ask from '@/components/Ask';\nimport Markdown from '@/components/Markdown';\nimport ModelSelectionModal from '@/components/ModelSelectionModal';\nimport ThemeToggle from '@/components/theme-toggle';\nimport WikiTreeView from '@/components/WikiTreeView';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport { RepoInfo } from '@/types/repoinfo';\nimport getRepoUrl from '@/utils/getRepoUrl';\nimport { extractUrlDomain, extractUrlPath } from '@/utils/urlDecoder';\nimport Link from 'next/link';\nimport { useParams, useSearchParams } from 'next/navigation';\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { FaBitbucket, FaBookOpen, FaComments, FaDownload, FaExclamationTriangle, FaFileExport, FaFolder, FaGithub, FaGitlab, FaHome, FaSync, FaTimes } from 'react-icons/fa';\n// Define the WikiSection and WikiStructure types directly in this file\n// since the imported types don't have the sections and rootSections properties\ninterface WikiSection {\n  id: string;\n  title: string;\n  pages: string[];\n  subsections?: string[];\n}\n\ninterface WikiPage {\n  id: string;\n  title: string;\n  content: string;\n  filePaths: string[];\n  importance: 'high' | 'medium' | 'low';\n  relatedPages: string[];\n  parentId?: string;\n  isSection?: boolean;\n  children?: string[];\n}\n\ninterface WikiStructure {\n  id: string;\n  title: string;\n  description: string;\n  pages: WikiPage[];\n  sections: WikiSection[];\n  rootSections: string[];\n}\n\n// Add CSS styles for wiki with Japanese aesthetic\nconst wikiStyles = `\n  .prose code {\n    @apply bg-[var(--background)]/70 px-1.5 py-0.5 rounded font-mono text-xs border border-[var(--border-color)];\n  }\n\n  .prose pre {\n    @apply bg-[var(--background)]/80 text-[var(--foreground)] rounded-md p-4 overflow-x-auto border border-[var(--border-color)] shadow-sm;\n  }\n\n  .prose h1, .prose h2, .prose h3, .prose h4 {\n    @apply font-serif text-[var(--foreground)];\n  }\n\n  .prose p {\n    @apply text-[var(--foreground)] leading-relaxed;\n  }\n\n  .prose a {\n    @apply text-[var(--accent-primary)] hover:text-[var(--highlight)] transition-colors no-underline border-b border-[var(--border-color)] hover:border-[var(--accent-primary)];\n  }\n\n  .prose blockquote {\n    @apply border-l-4 border-[var(--accent-primary)]/30 bg-[var(--background)]/30 pl-4 py-1 italic;\n  }\n\n  .prose ul, .prose ol {\n    @apply text-[var(--foreground)];\n  }\n\n  .prose table {\n    @apply border-collapse border border-[var(--border-color)];\n  }\n\n  .prose th {\n    @apply bg-[var(--background)]/70 text-[var(--foreground)] p-2 border border-[var(--border-color)];\n  }\n\n  .prose td {\n    @apply p-2 border border-[var(--border-color)];\n  }\n`;\n\n// Helper function to generate cache key for localStorage\nconst getCacheKey = (owner: string, repo: string, repoType: string, language: string, isComprehensive: boolean = true): string => {\n  return `deepwiki_cache_${repoType}_${owner}_${repo}_${language}_${isComprehensive ? 'comprehensive' : 'concise'}`;\n};\n\n// Helper function to add tokens and other parameters to request body\nconst addTokensToRequestBody = (\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  requestBody: Record<string, any>,\n  token: string,\n  repoType: string,\n  provider: string = '',\n  model: string = '',\n  isCustomModel: boolean = false,\n  customModel: string = '',\n  language: string = 'en',\n  excludedDirs?: string,\n  excludedFiles?: string,\n  includedDirs?: string,\n  includedFiles?: string\n): void => {\n  if (token !== '') {\n    requestBody.token = token;\n  }\n\n  // Add provider-based model selection parameters\n  requestBody.provider = provider;\n  requestBody.model = model;\n  if (isCustomModel && customModel) {\n    requestBody.custom_model = customModel;\n  }\n\n  requestBody.language = language;\n\n  // Add file filter parameters if provided\n  if (excludedDirs) {\n    requestBody.excluded_dirs = excludedDirs;\n  }\n  if (excludedFiles) {\n    requestBody.excluded_files = excludedFiles;\n  }\n  if (includedDirs) {\n    requestBody.included_dirs = includedDirs;\n  }\n  if (includedFiles) {\n    requestBody.included_files = includedFiles;\n  }\n\n};\n\nconst createGithubHeaders = (githubToken: string): HeadersInit => {\n  const headers: HeadersInit = {\n    'Accept': 'application/vnd.github.v3+json'\n  };\n\n  if (githubToken) {\n    headers['Authorization'] = `Bearer ${githubToken}`;\n  }\n\n  return headers;\n};\n\nconst createGitlabHeaders = (gitlabToken: string): HeadersInit => {\n  const headers: HeadersInit = {\n    'Content-Type': 'application/json',\n  };\n\n  if (gitlabToken) {\n    headers['PRIVATE-TOKEN'] = gitlabToken;\n  }\n\n  return headers;\n};\n\nconst createBitbucketHeaders = (bitbucketToken: string): HeadersInit => {\n  const headers: HeadersInit = {\n    'Content-Type': 'application/json',\n  };\n\n  if (bitbucketToken) {\n    headers['Authorization'] = `Bearer ${bitbucketToken}`;\n  }\n\n  return headers;\n};\n\n\nexport default function RepoWikiPage() {\n  // Get route parameters and search params\n  const params = useParams();\n  const searchParams = useSearchParams();\n\n  // Extract owner and repo from route params\n  const owner = params.owner as string;\n  const repo = params.repo as string;\n\n  // Extract tokens from search params\n  const token = searchParams.get('token') || '';\n  const localPath = searchParams.get('local_path') ? decodeURIComponent(searchParams.get('local_path') || '') : undefined;\n  const repoUrl = searchParams.get('repo_url') ? decodeURIComponent(searchParams.get('repo_url') || '') : undefined;\n  const providerParam = searchParams.get('provider') || '';\n  const modelParam = searchParams.get('model') || '';\n  const isCustomModelParam = searchParams.get('is_custom_model') === 'true';\n  const customModelParam = searchParams.get('custom_model') || '';\n  const language = searchParams.get('language') || 'en';\n  const repoHost = (() => {\n    if (!repoUrl) return '';\n    try {\n      return new URL(repoUrl).hostname.toLowerCase();\n    } catch (e) {\n      console.warn(`Invalid repoUrl provided: ${repoUrl}`);\n      return '';\n    }\n  })();\n  const repoType = repoHost?.includes('bitbucket')\n    ? 'bitbucket'\n    : repoHost?.includes('gitlab')\n      ? 'gitlab'\n      : repoHost?.includes('github')\n        ? 'github'\n        : searchParams.get('type') || 'github';\n\n  // Import language context for translations\n  const { messages } = useLanguage();\n\n  // Initialize repo info\n  const repoInfo = useMemo<RepoInfo>(() => ({\n    owner,\n    repo,\n    type: repoType,\n    token: token || null,\n    localPath: localPath || null,\n    repoUrl: repoUrl || null\n  }), [owner, repo, repoType, localPath, repoUrl, token]);\n\n  // State variables\n  const [isLoading, setIsLoading] = useState(true);\n  const [loadingMessage, setLoadingMessage] = useState<string | undefined>(\n    messages.loading?.initializing || 'Initializing wiki generation...'\n  );\n  const [error, setError] = useState<string | null>(null);\n  const [wikiStructure, setWikiStructure] = useState<WikiStructure | undefined>();\n  const [currentPageId, setCurrentPageId] = useState<string | undefined>();\n  const [generatedPages, setGeneratedPages] = useState<Record<string, WikiPage>>({});\n  const [pagesInProgress, setPagesInProgress] = useState(new Set<string>());\n  const [isExporting, setIsExporting] = useState(false);\n  const [exportError, setExportError] = useState<string | null>(null);\n  const [originalMarkdown, setOriginalMarkdown] = useState<Record<string, string>>({});\n  const [requestInProgress, setRequestInProgress] = useState(false);\n  const [currentToken, setCurrentToken] = useState(token); // Track current effective token\n  const [effectiveRepoInfo, setEffectiveRepoInfo] = useState(repoInfo); // Track effective repo info with cached data\n  const [embeddingError, setEmbeddingError] = useState(false);\n\n  // Model selection state variables\n  const [selectedProviderState, setSelectedProviderState] = useState(providerParam);\n  const [selectedModelState, setSelectedModelState] = useState(modelParam);\n  const [isCustomSelectedModelState, setIsCustomSelectedModelState] = useState(isCustomModelParam);\n  const [customSelectedModelState, setCustomSelectedModelState] = useState(customModelParam);\n  const [showModelOptions, setShowModelOptions] = useState(false); // Controls whether to show model options\n  const excludedDirs = searchParams.get('excluded_dirs') || '';\n  const excludedFiles = searchParams.get('excluded_files') || '';\n  const [modelExcludedDirs, setModelExcludedDirs] = useState(excludedDirs);\n  const [modelExcludedFiles, setModelExcludedFiles] = useState(excludedFiles);\n  const includedDirs = searchParams.get('included_dirs') || '';\n  const includedFiles = searchParams.get('included_files') || '';\n  const [modelIncludedDirs, setModelIncludedDirs] = useState(includedDirs);\n  const [modelIncludedFiles, setModelIncludedFiles] = useState(includedFiles);\n\n\n  // Wiki type state - default to comprehensive view\n  const isComprehensiveParam = searchParams.get('comprehensive') !== 'false';\n  const [isComprehensiveView, setIsComprehensiveView] = useState(isComprehensiveParam);\n  // Using useRef for activeContentRequests to maintain a single instance across renders\n  // This map tracks which pages are currently being processed to prevent duplicate requests\n  // Note: In a multi-threaded environment, additional synchronization would be needed,\n  // but in React's single-threaded model, this is safe as long as we set the flag before any async operations\n  const activeContentRequests = useRef(new Map<string, boolean>()).current;\n  const [structureRequestInProgress, setStructureRequestInProgress] = useState(false);\n  // Create a flag to track if data was loaded from cache to prevent immediate re-save\n  const cacheLoadedSuccessfully = useRef(false);\n\n  // Create a flag to ensure the effect only runs once\n  const effectRan = React.useRef(false);\n\n  // State for Ask modal\n  const [isAskModalOpen, setIsAskModalOpen] = useState(false);\n  const askComponentRef = useRef<{ clearConversation: () => void } | null>(null);\n\n  // Authentication state\n  const [authRequired, setAuthRequired] = useState<boolean>(false);\n  const [authCode, setAuthCode] = useState<string>('');\n  const [isAuthLoading, setIsAuthLoading] = useState<boolean>(true);\n\n  // Default branch state\n  const [defaultBranch, setDefaultBranch] = useState<string>('main');\n\n  // Helper function to generate proper repository file URLs\n  const generateFileUrl = useCallback((filePath: string): string => {\n    if (effectiveRepoInfo.type === 'local') {\n      // For local repositories, we can't generate web URLs\n      return filePath;\n    }\n\n    const repoUrl = effectiveRepoInfo.repoUrl;\n    if (!repoUrl) {\n      return filePath;\n    }\n\n    try {\n      const url = new URL(repoUrl);\n      const hostname = url.hostname;\n      \n      if (hostname === 'github.com' || hostname.includes('github')) {\n        // GitHub URL format: https://github.com/owner/repo/blob/branch/path\n        return `${repoUrl}/blob/${defaultBranch}/${filePath}`;\n      } else if (hostname === 'gitlab.com' || hostname.includes('gitlab')) {\n        // GitLab URL format: https://gitlab.com/owner/repo/-/blob/branch/path\n        return `${repoUrl}/-/blob/${defaultBranch}/${filePath}`;\n      } else if (hostname === 'bitbucket.org' || hostname.includes('bitbucket')) {\n        // Bitbucket URL format: https://bitbucket.org/owner/repo/src/branch/path\n        return `${repoUrl}/src/${defaultBranch}/${filePath}`;\n      }\n    } catch (error) {\n      console.warn('Error generating file URL:', error);\n    }\n\n    // Fallback to just the file path\n    return filePath;\n  }, [effectiveRepoInfo, defaultBranch]);\n\n  // Memoize repo info to avoid triggering updates in callbacks\n\n  // Add useEffect to handle scroll reset\n  useEffect(() => {\n    // Scroll to top when currentPageId changes\n    const wikiContent = document.getElementById('wiki-content');\n    if (wikiContent) {\n      wikiContent.scrollTo({ top: 0, behavior: 'smooth' });\n    }\n  }, [currentPageId]);\n\n  // close the modal when escape is pressed\n  useEffect(() => {\n    const handleEsc = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        setIsAskModalOpen(false);\n      }\n    };\n\n    if (isAskModalOpen) {\n      window.addEventListener('keydown', handleEsc);\n    }\n\n    // Cleanup on unmount or when modal closes\n    return () => {\n      window.removeEventListener('keydown', handleEsc);\n    };\n  }, [isAskModalOpen]);\n\n  // Fetch authentication status on component mount\n  useEffect(() => {\n    const fetchAuthStatus = async () => {\n      try {\n        setIsAuthLoading(true);\n        const response = await fetch('/api/auth/status');\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n        const data = await response.json();\n        setAuthRequired(data.auth_required);\n      } catch (err) {\n        console.error(\"Failed to fetch auth status:\", err);\n        // Assuming auth is required if fetch fails to avoid blocking UI for safety\n        setAuthRequired(true);\n      } finally {\n        setIsAuthLoading(false);\n      }\n    };\n\n    fetchAuthStatus();\n  }, []);\n\n  // Generate content for a wiki page\n  const generatePageContent = useCallback(async (page: WikiPage, owner: string, repo: string) => {\n    return new Promise<void>(async (resolve) => {\n      try {\n        // Skip if content already exists\n        if (generatedPages[page.id]?.content) {\n          resolve();\n          return;\n        }\n\n        // Skip if this page is already being processed\n        // Use a synchronized pattern to avoid race conditions\n        if (activeContentRequests.get(page.id)) {\n          console.log(`Page ${page.id} (${page.title}) is already being processed, skipping duplicate call`);\n          resolve();\n          return;\n        }\n\n        // Mark this page as being processed immediately to prevent race conditions\n        // This ensures that if multiple calls happen nearly simultaneously, only one proceeds\n        activeContentRequests.set(page.id, true);\n\n        // Validate repo info\n        if (!owner || !repo) {\n          throw new Error('Invalid repository information. Owner and repo name are required.');\n        }\n\n        // Mark page as in progress\n        setPagesInProgress(prev => new Set(prev).add(page.id));\n        // Don't set loading message for individual pages during queue processing\n\n        const filePaths = page.filePaths;\n\n        // Store the initially generated content BEFORE rendering/potential modification\n        setGeneratedPages(prev => ({\n          ...prev,\n          [page.id]: { ...page, content: 'Loading...' } // Placeholder\n        }));\n        setOriginalMarkdown(prev => ({ ...prev, [page.id]: '' })); // Clear previous original\n\n        // Make API call to generate page content\n        console.log(`Starting content generation for page: ${page.title}`);\n\n        // Get repository URL\n        const repoUrl = getRepoUrl(effectiveRepoInfo);\n\n        // Create the prompt content - simplified to avoid message dialogs\n const promptContent =\n`You are an expert technical writer and software architect.\nYour task is to generate a comprehensive and accurate technical wiki page in Markdown format about a specific feature, system, or module within a given software project.\n\nYou will be given:\n1. The \"[WIKI_PAGE_TOPIC]\" for the page you need to create.\n2. A list of \"[RELEVANT_SOURCE_FILES]\" from the project that you MUST use as the sole basis for the content. You have access to the full content of these files. You MUST use AT LEAST 5 relevant source files for comprehensive coverage - if fewer are provided, search for additional related files in the codebase.\n\nCRITICAL STARTING INSTRUCTION:\nThe very first thing on the page MUST be a \\`<details>\\` block listing ALL the \\`[RELEVANT_SOURCE_FILES]\\` you used to generate the content. There MUST be AT LEAST 5 source files listed - if fewer were provided, you MUST find additional related files to include.\nFormat it exactly like this:\n<details>\n<summary>Relevant source files</summary>\n\nRemember, do not provide any acknowledgements, disclaimers, apologies, or any other preface before the \\`<details>\\` block. JUST START with the \\`<details>\\` block.\nThe following files were used as context for generating this wiki page:\n\n${filePaths.map(path => `- [${path}](${generateFileUrl(path)})`).join('\\n')}\n<!-- Add additional relevant files if fewer than 5 were provided -->\n</details>\n\nImmediately after the \\`<details>\\` block, the main title of the page should be a H1 Markdown heading: \\`# ${page.title}\\`.\n\nBased ONLY on the content of the \\`[RELEVANT_SOURCE_FILES]\\`:\n\n1.  **Introduction:** Start with a concise introduction (1-2 paragraphs) explaining the purpose, scope, and high-level overview of \"${page.title}\" within the context of the overall project. If relevant, and if information is available in the provided files, link to other potential wiki pages using the format \\`[Link Text](#page-anchor-or-id)\\`.\n\n2.  **Detailed Sections:** Break down \"${page.title}\" into logical sections using H2 (\\`##\\`) and H3 (\\`###\\`) Markdown headings. For each section:\n    *   Explain the architecture, components, data flow, or logic relevant to the section's focus, as evidenced in the source files.\n    *   Identify key functions, classes, data structures, API endpoints, or configuration elements pertinent to that section.\n\n3.  **Mermaid Diagrams:**\n    *   EXTENSIVELY use Mermaid diagrams (e.g., \\`flowchart TD\\`, \\`sequenceDiagram\\`, \\`classDiagram\\`, \\`erDiagram\\`, \\`graph TD\\`) to visually represent architectures, flows, relationships, and schemas found in the source files.\n    *   Ensure diagrams are accurate and directly derived from information in the \\`[RELEVANT_SOURCE_FILES]\\`.\n    *   Provide a brief explanation before or after each diagram to give context.\n    *   CRITICAL: All diagrams MUST follow strict vertical orientation:\n       - Use \"graph TD\" (top-down) directive for flow diagrams\n       - NEVER use \"graph LR\" (left-right)\n       - Maximum node width should be 3-4 words\n       - For sequence diagrams:\n         - Start with \"sequenceDiagram\" directive on its own line\n         - Define ALL participants at the beginning using \"participant\" keyword\n         - Optionally specify participant types: actor, boundary, control, entity, database, collections, queue\n         - Use descriptive but concise participant names, or use aliases: \"participant A as Alice\"\n         - Use the correct Mermaid arrow syntax (8 types available):\n           - -> solid line without arrow (rarely used)\n           - --> dotted line without arrow (rarely used)\n           - ->> solid line with arrowhead (most common for requests/calls)\n           - -->> dotted line with arrowhead (most common for responses/returns)\n           - ->x solid line with X at end (failed/error message)\n           - -->x dotted line with X at end (failed/error response)\n           - -) solid line with open arrow (async message, fire-and-forget)\n           - --) dotted line with open arrow (async response)\n           - Examples: A->>B: Request, B-->>A: Response, A->xB: Error, A-)B: Async event\n         - Use +/- suffix for activation boxes: A->>+B: Start (activates B), B-->>-A: End (deactivates B)\n         - Group related participants using \"box\": box GroupName ... end\n         - Use structural elements for complex flows:\n           - loop LoopText ... end (for iterations)\n           - alt ConditionText ... else ... end (for conditionals)\n           - opt OptionalText ... end (for optional flows)\n           - par ParallelText ... and ... end (for parallel actions)\n           - critical CriticalText ... option ... end (for critical regions)\n           - break BreakText ... end (for breaking flows/exceptions)\n         - Add notes for clarification: \"Note over A,B: Description\", \"Note right of A: Detail\"\n         - Use autonumber directive to add sequence numbers to messages\n         - NEVER use flowchart-style labels like A--|label|-->B. Always use a colon for labels: A->>B: My Label\n\n4.  **Tables:**\n    *   Use Markdown tables to summarize information such as:\n        *   Key features or components and their descriptions.\n        *   API endpoint parameters, types, and descriptions.\n        *   Configuration options, their types, and default values.\n        *   Data model fields, types, constraints, and descriptions.\n\n5.  **Code Snippets (ENTIRELY OPTIONAL):**\n    *   Include short, relevant code snippets (e.g., Python, Java, JavaScript, SQL, JSON, YAML) directly from the \\`[RELEVANT_SOURCE_FILES]\\` to illustrate key implementation details, data structures, or configurations.\n    *   Ensure snippets are well-formatted within Markdown code blocks with appropriate language identifiers.\n\n6.  **Source Citations (EXTREMELY IMPORTANT):**\n    *   For EVERY piece of significant information, explanation, diagram, table entry, or code snippet, you MUST cite the specific source file(s) and relevant line numbers from which the information was derived.\n    *   Place citations at the end of the paragraph, under the diagram/table, or after the code snippet.\n    *   Use the exact format: \\`Sources: [filename.ext:start_line-end_line]()\\` for a range, or \\`Sources: [filename.ext:line_number]()\\` for a single line. Multiple files can be cited: \\`Sources: [file1.ext:1-10](), [file2.ext:5](), [dir/file3.ext]()\\` (if the whole file is relevant and line numbers are not applicable or too broad).\n    *   If an entire section is overwhelmingly based on one or two files, you can cite them under the section heading in addition to more specific citations within the section.\n    *   IMPORTANT: You MUST cite AT LEAST 5 different source files throughout the wiki page to ensure comprehensive coverage.\n\n7.  **Technical Accuracy:** All information must be derived SOLELY from the \\`[RELEVANT_SOURCE_FILES]\\`. Do not infer, invent, or use external knowledge about similar systems or common practices unless it's directly supported by the provided code. If information is not present in the provided files, do not include it or explicitly state its absence if crucial to the topic.\n\n8.  **Clarity and Conciseness:** Use clear, professional, and concise technical language suitable for other developers working on or learning about the project. Avoid unnecessary jargon, but use correct technical terms where appropriate.\n\n9.  **Conclusion/Summary:** End with a brief summary paragraph if appropriate for \"${page.title}\", reiterating the key aspects covered and their significance within the project.\n\nIMPORTANT: Generate the content in ${language === 'en' ? 'English' :\n            language === 'ja' ? 'Japanese (日本語)' :\n            language === 'zh' ? 'Mandarin Chinese (中文)' :\n            language === 'zh-tw' ? 'Traditional Chinese (繁體中文)' :\n            language === 'es' ? 'Spanish (Español)' :\n            language === 'kr' ? 'Korean (한국어)' :\n            language === 'vi' ? 'Vietnamese (Tiếng Việt)' : \n            language === \"pt-br\" ? \"Brazilian Portuguese (Português Brasileiro)\" :\n            language === \"fr\" ? \"Français (French)\" :\n            language === \"ru\" ? \"Русский (Russian)\" :\n            'English'} language.\n\nRemember:\n- Ground every claim in the provided source files.\n- Prioritize accuracy and direct representation of the code's functionality and structure.\n- Structure the document logically for easy understanding by other developers.\n`;\n\n        // Prepare request body\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const requestBody: Record<string, any> = {\n          repo_url: repoUrl,\n          type: effectiveRepoInfo.type,\n          messages: [{\n            role: 'user',\n            content: promptContent\n          }]\n        };\n\n        // Add tokens if available\n        addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles);\n\n        // Use WebSocket for communication\n        let content = '';\n\n        try {\n          // Create WebSocket URL from the server base URL\n          const serverBaseUrl = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n          const wsBaseUrl = serverBaseUrl.replace(/^http/, 'ws')? serverBaseUrl.replace(/^https/, 'wss'): serverBaseUrl.replace(/^http/, 'ws');\n          const wsUrl = `${wsBaseUrl}/ws/chat`;\n\n          // Create a new WebSocket connection\n          const ws = new WebSocket(wsUrl);\n\n          // Create a promise that resolves when the WebSocket connection is complete\n          await new Promise<void>((resolve, reject) => {\n            // Set up event handlers\n            ws.onopen = () => {\n              console.log(`WebSocket connection established for page: ${page.title}`);\n              // Send the request as JSON\n              ws.send(JSON.stringify(requestBody));\n              resolve();\n            };\n\n            ws.onerror = (error) => {\n              console.error('WebSocket error:', error);\n              reject(new Error('WebSocket connection failed'));\n            };\n\n            // If the connection doesn't open within 5 seconds, fall back to HTTP\n            const timeout = setTimeout(() => {\n              reject(new Error('WebSocket connection timeout'));\n            }, 5000);\n\n            // Clear the timeout if the connection opens successfully\n            ws.onopen = () => {\n              clearTimeout(timeout);\n              console.log(`WebSocket connection established for page: ${page.title}`);\n              // Send the request as JSON\n              ws.send(JSON.stringify(requestBody));\n              resolve();\n            };\n          });\n\n          // Create a promise that resolves when the WebSocket response is complete\n          await new Promise<void>((resolve, reject) => {\n            // Handle incoming messages\n            ws.onmessage = (event) => {\n              content += event.data;\n            };\n\n            // Handle WebSocket close\n            ws.onclose = () => {\n              console.log(`WebSocket connection closed for page: ${page.title}`);\n              resolve();\n            };\n\n            // Handle WebSocket errors\n            ws.onerror = (error) => {\n              console.error('WebSocket error during message reception:', error);\n              reject(new Error('WebSocket error during message reception'));\n            };\n          });\n        } catch (wsError) {\n          console.error('WebSocket error, falling back to HTTP:', wsError);\n\n          // Fall back to HTTP if WebSocket fails\n          const response = await fetch(`/api/chat/stream`, {\n            method: 'POST',\n            headers: {\n              'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(requestBody)\n          });\n\n          if (!response.ok) {\n            const errorText = await response.text().catch(() => 'No error details available');\n            console.error(`API error (${response.status}): ${errorText}`);\n            throw new Error(`Error generating page content: ${response.status} - ${response.statusText}`);\n          }\n\n          // Process the response\n          content = '';\n          const reader = response.body?.getReader();\n          const decoder = new TextDecoder();\n\n          if (!reader) {\n            throw new Error('Failed to get response reader');\n          }\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) break;\n              content += decoder.decode(value, { stream: true });\n            }\n            // Ensure final decoding\n            content += decoder.decode();\n          } catch (readError) {\n            console.error('Error reading stream:', readError);\n            throw new Error('Error processing response stream');\n          }\n        }\n\n        // Clean up markdown delimiters\n        content = content.replace(/^```markdown\\s*/i, '').replace(/```\\s*$/i, '');\n\n        console.log(`Received content for ${page.title}, length: ${content.length} characters`);\n\n        // Store the FINAL generated content\n        const updatedPage = { ...page, content };\n        setGeneratedPages(prev => ({ ...prev, [page.id]: updatedPage }));\n        // Store this as the original for potential mermaid retries\n        setOriginalMarkdown(prev => ({ ...prev, [page.id]: content }));\n\n        resolve();\n      } catch (err) {\n        console.error(`Error generating content for page ${page.id}:`, err);\n        const errorMessage = err instanceof Error ? err.message : 'Unknown error';\n        // Update page state to show error\n        setGeneratedPages(prev => ({\n          ...prev,\n          [page.id]: { ...page, content: `Error generating content: ${errorMessage}` }\n        }));\n        setError(`Failed to generate content for ${page.title}.`);\n        resolve(); // Resolve even on error to unblock queue\n      } finally {\n        // Clear the processing flag for this page\n        // This must happen in the finally block to ensure the flag is cleared\n        // even if an error occurs during processing\n        activeContentRequests.delete(page.id);\n\n        // Mark page as done\n        setPagesInProgress(prev => {\n          const next = new Set(prev);\n          next.delete(page.id);\n          return next;\n        });\n        setLoadingMessage(undefined); // Clear specific loading message\n      }\n    });\n  }, [generatedPages, currentToken, effectiveRepoInfo, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, language, activeContentRequests, generateFileUrl]);\n\n  // Determine the wiki structure from repository data\n  const determineWikiStructure = useCallback(async (fileTree: string, readme: string, owner: string, repo: string) => {\n    if (!owner || !repo) {\n      setError('Invalid repository information. Owner and repo name are required.');\n      setIsLoading(false);\n      setEmbeddingError(false); // Reset embedding error state\n      return;\n    }\n\n    // Skip if structure request is already in progress\n    if (structureRequestInProgress) {\n      console.log('Wiki structure determination already in progress, skipping duplicate call');\n      return;\n    }\n\n    try {\n      setStructureRequestInProgress(true);\n      setLoadingMessage(messages.loading?.determiningStructure || 'Determining wiki structure...');\n\n      // Get repository URL\n      const repoUrl = getRepoUrl(effectiveRepoInfo);\n\n      // Prepare request body\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const requestBody: Record<string, any> = {\n        repo_url: repoUrl,\n        type: effectiveRepoInfo.type,\n        messages: [{\n          role: 'user',\ncontent: `Analyze this GitHub repository ${owner}/${repo} and create a wiki structure for it.\n\n1. The complete file tree of the project:\n<file_tree>\n${fileTree}\n</file_tree>\n\n2. The README file of the project:\n<readme>\n${readme}\n</readme>\n\nI want to create a wiki for this repository. Determine the most logical structure for a wiki based on the repository's content.\n\nIMPORTANT: The wiki content will be generated in ${language === 'en' ? 'English' :\n            language === 'ja' ? 'Japanese (日本語)' :\n            language === 'zh' ? 'Mandarin Chinese (中文)' :\n            language === 'zh-tw' ? 'Traditional Chinese (繁體中文)' :\n            language === 'es' ? 'Spanish (Español)' :\n            language === 'kr' ? 'Korean (한国語)' :\n            language === 'vi' ? 'Vietnamese (Tiếng Việt)' :\n            language === \"pt-br\" ? \"Brazilian Portuguese (Português Brasileiro)\" :\n            language === \"fr\" ? \"Français (French)\" :\n            language === \"ru\" ? \"Русский (Russian)\" :\n            'English'} language.\n\nWhen designing the wiki structure, include pages that would benefit from visual diagrams, such as:\n- Architecture overviews\n- Data flow descriptions\n- Component relationships\n- Process workflows\n- State machines\n- Class hierarchies\n\n${isComprehensiveView ? `\nCreate a structured wiki with the following main sections:\n- Overview (general information about the project)\n- System Architecture (how the system is designed)\n- Core Features (key functionality)\n- Data Management/Flow: If applicable, how data is stored, processed, accessed, and managed (e.g., database schema, data pipelines, state management).\n- Frontend Components (UI elements, if applicable.)\n- Backend Systems (server-side components)\n- Model Integration (AI model connections)\n- Deployment/Infrastructure (how to deploy, what's the infrastructure like)\n- Extensibility and Customization: If the project architecture supports it, explain how to extend or customize its functionality (e.g., plugins, theming, custom modules, hooks).\n\nEach section should contain relevant pages. For example, the \"Frontend Components\" section might include pages for \"Home Page\", \"Repository Wiki Page\", \"Ask Component\", etc.\n\nReturn your analysis in the following XML format:\n\n<wiki_structure>\n  <title>[Overall title for the wiki]</title>\n  <description>[Brief description of the repository]</description>\n  <sections>\n    <section id=\"section-1\">\n      <title>[Section title]</title>\n      <pages>\n        <page_ref>page-1</page_ref>\n        <page_ref>page-2</page_ref>\n      </pages>\n      <subsections>\n        <section_ref>section-2</section_ref>\n      </subsections>\n    </section>\n    <!-- More sections as needed -->\n  </sections>\n  <pages>\n    <page id=\"page-1\">\n      <title>[Page title]</title>\n      <description>[Brief description of what this page will cover]</description>\n      <importance>high|medium|low</importance>\n      <relevant_files>\n        <file_path>[Path to a relevant file]</file_path>\n        <!-- More file paths as needed -->\n      </relevant_files>\n      <related_pages>\n        <related>page-2</related>\n        <!-- More related page IDs as needed -->\n      </related_pages>\n      <parent_section>section-1</parent_section>\n    </page>\n    <!-- More pages as needed -->\n  </pages>\n</wiki_structure>\n` : `\nReturn your analysis in the following XML format:\n\n<wiki_structure>\n  <title>[Overall title for the wiki]</title>\n  <description>[Brief description of the repository]</description>\n  <pages>\n    <page id=\"page-1\">\n      <title>[Page title]</title>\n      <description>[Brief description of what this page will cover]</description>\n      <importance>high|medium|low</importance>\n      <relevant_files>\n        <file_path>[Path to a relevant file]</file_path>\n        <!-- More file paths as needed -->\n      </relevant_files>\n      <related_pages>\n        <related>page-2</related>\n        <!-- More related page IDs as needed -->\n      </related_pages>\n    </page>\n    <!-- More pages as needed -->\n  </pages>\n</wiki_structure>\n`}\n\nIMPORTANT FORMATTING INSTRUCTIONS:\n- Return ONLY the valid XML structure specified above\n- DO NOT wrap the XML in markdown code blocks (no \\`\\`\\` or \\`\\`\\`xml)\n- DO NOT include any explanation text before or after the XML\n- Ensure the XML is properly formatted and valid\n- Start directly with <wiki_structure> and end with </wiki_structure>\n\nIMPORTANT:\n1. Create ${isComprehensiveView ? '8-12' : '4-6'} pages that would make a ${isComprehensiveView ? 'comprehensive' : 'concise'} wiki for this repository\n2. Each page should focus on a specific aspect of the codebase (e.g., architecture, key features, setup)\n3. The relevant_files should be actual files from the repository that would be used to generate that page\n4. Return ONLY valid XML with the structure specified above, with no markdown code block delimiters`\n        }]\n      };\n\n      // Add tokens if available\n      addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles);\n\n      // Use WebSocket for communication\n      let responseText = '';\n\n      try {\n        // Create WebSocket URL from the server base URL\n        const serverBaseUrl = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n        const wsBaseUrl = serverBaseUrl.replace(/^http/, 'ws')? serverBaseUrl.replace(/^https/, 'wss'): serverBaseUrl.replace(/^http/, 'ws');\n        const wsUrl = `${wsBaseUrl}/ws/chat`;\n\n        // Create a new WebSocket connection\n        const ws = new WebSocket(wsUrl);\n\n        // Create a promise that resolves when the WebSocket connection is complete\n        await new Promise<void>((resolve, reject) => {\n          // Set up event handlers\n          ws.onopen = () => {\n            console.log('WebSocket connection established for wiki structure');\n            // Send the request as JSON\n            ws.send(JSON.stringify(requestBody));\n            resolve();\n          };\n\n          ws.onerror = (error) => {\n            console.error('WebSocket error:', error);\n            reject(new Error('WebSocket connection failed'));\n          };\n\n          // If the connection doesn't open within 5 seconds, fall back to HTTP\n          const timeout = setTimeout(() => {\n            reject(new Error('WebSocket connection timeout'));\n          }, 5000);\n\n          // Clear the timeout if the connection opens successfully\n          ws.onopen = () => {\n            clearTimeout(timeout);\n            console.log('WebSocket connection established for wiki structure');\n            // Send the request as JSON\n            ws.send(JSON.stringify(requestBody));\n            resolve();\n          };\n        });\n\n        // Create a promise that resolves when the WebSocket response is complete\n        await new Promise<void>((resolve, reject) => {\n          // Handle incoming messages\n          ws.onmessage = (event) => {\n            responseText += event.data;\n          };\n\n          // Handle WebSocket close\n          ws.onclose = () => {\n            console.log('WebSocket connection closed for wiki structure');\n            resolve();\n          };\n\n          // Handle WebSocket errors\n          ws.onerror = (error) => {\n            console.error('WebSocket error during message reception:', error);\n            reject(new Error('WebSocket error during message reception'));\n          };\n        });\n      } catch (wsError) {\n        console.error('WebSocket error, falling back to HTTP:', wsError);\n\n        // Fall back to HTTP if WebSocket fails\n        const response = await fetch(`/api/chat/stream`, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(requestBody)\n        });\n\n        if (!response.ok) {\n          throw new Error(`Error determining wiki structure: ${response.status}`);\n        }\n\n        // Process the response\n        responseText = '';\n        const reader = response.body?.getReader();\n        const decoder = new TextDecoder();\n\n        if (!reader) {\n          throw new Error('Failed to get response reader');\n        }\n\n        while (true) {\n          const { done, value } = await reader.read();\n          if (done) break;\n          responseText += decoder.decode(value, { stream: true });\n        }\n      }\n\n      if(responseText.includes('Error preparing retriever: Environment variable OPENAI_API_KEY must be set')) {\n         setEmbeddingError(true);\n         throw new Error('OPENAI_API_KEY environment variable is not set. Please configure your OpenAI API key.');\n       }\n\n       if(responseText.includes('Ollama model') && responseText.includes('not found')) {\n         setEmbeddingError(true);\n         throw new Error('The specified Ollama embedding model was not found. Please ensure the model is installed locally or select a different embedding model in the configuration.');\n       }\n\n        // Clean up markdown delimiters\n      responseText = responseText.replace(/^```(?:xml)?\\s*/i, '').replace(/```\\s*$/i, '');\n\n      // Extract wiki structure from response\n      const xmlMatch = responseText.match(/<wiki_structure>[\\s\\S]*?<\\/wiki_structure>/m);\n      if (!xmlMatch) {\n        throw new Error('No valid XML found in response');\n      }\n\n      let xmlText = xmlMatch[0];\n      xmlText = xmlText.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '');\n      // Try parsing with DOMParser\n      const parser = new DOMParser();\n      const xmlDoc = parser.parseFromString(xmlText, \"text/xml\");\n\n      // Check for parsing errors\n      const parseError = xmlDoc.querySelector('parsererror');\n      if (parseError) {\n        // Log the first few elements to see what was parsed\n        const elements = xmlDoc.querySelectorAll('*');\n        if (elements.length > 0) {\n          console.log('First 5 element names:',\n            Array.from(elements).slice(0, 5).map(el => el.nodeName).join(', '));\n        }\n\n        // We'll continue anyway since the XML might still be usable\n      }\n\n      // Extract wiki structure\n      let title = '';\n      let description = '';\n      let pages: WikiPage[] = [];\n\n      // Try using DOM parsing first\n      const titleEl = xmlDoc.querySelector('title');\n      const descriptionEl = xmlDoc.querySelector('description');\n      const pagesEls = xmlDoc.querySelectorAll('page');\n\n      title = titleEl ? titleEl.textContent || '' : '';\n      description = descriptionEl ? descriptionEl.textContent || '' : '';\n\n      // Parse pages using DOM\n      pages = [];\n\n      if (parseError && (!pagesEls || pagesEls.length === 0)) {\n        console.warn('DOM parsing failed, trying regex fallback');\n      }\n\n      pagesEls.forEach(pageEl => {\n        const id = pageEl.getAttribute('id') || `page-${pages.length + 1}`;\n        const titleEl = pageEl.querySelector('title');\n        const importanceEl = pageEl.querySelector('importance');\n        const filePathEls = pageEl.querySelectorAll('file_path');\n        const relatedEls = pageEl.querySelectorAll('related');\n\n        const title = titleEl ? titleEl.textContent || '' : '';\n        const importance = importanceEl ?\n          (importanceEl.textContent === 'high' ? 'high' :\n            importanceEl.textContent === 'medium' ? 'medium' : 'low') : 'medium';\n\n        const filePaths: string[] = [];\n        filePathEls.forEach(el => {\n          if (el.textContent) filePaths.push(el.textContent);\n        });\n\n        const relatedPages: string[] = [];\n        relatedEls.forEach(el => {\n          if (el.textContent) relatedPages.push(el.textContent);\n        });\n\n        pages.push({\n          id,\n          title,\n          content: '', // Will be generated later\n          filePaths,\n          importance,\n          relatedPages\n        });\n      });\n\n      // Extract sections if they exist in the XML\n      const sections: WikiSection[] = [];\n      const rootSections: string[] = [];\n\n      // Try to parse sections if we're in comprehensive view\n      if (isComprehensiveView) {\n        const sectionsEls = xmlDoc.querySelectorAll('section');\n\n        if (sectionsEls && sectionsEls.length > 0) {\n          // Process sections\n          sectionsEls.forEach(sectionEl => {\n            const id = sectionEl.getAttribute('id') || `section-${sections.length + 1}`;\n            const titleEl = sectionEl.querySelector('title');\n            const pageRefEls = sectionEl.querySelectorAll('page_ref');\n            const sectionRefEls = sectionEl.querySelectorAll('section_ref');\n\n            const title = titleEl ? titleEl.textContent || '' : '';\n            const pages: string[] = [];\n            const subsections: string[] = [];\n\n            pageRefEls.forEach(el => {\n              if (el.textContent) pages.push(el.textContent);\n            });\n\n            sectionRefEls.forEach(el => {\n              if (el.textContent) subsections.push(el.textContent);\n            });\n\n            sections.push({\n              id,\n              title,\n              pages,\n              subsections: subsections.length > 0 ? subsections : undefined\n            });\n\n            // Check if this is a root section (not referenced by any other section)\n            let isReferenced = false;\n            sectionsEls.forEach(otherSection => {\n              const otherSectionRefs = otherSection.querySelectorAll('section_ref');\n              otherSectionRefs.forEach(ref => {\n                if (ref.textContent === id) {\n                  isReferenced = true;\n                }\n              });\n            });\n\n            if (!isReferenced) {\n              rootSections.push(id);\n            }\n          });\n        }\n      }\n\n      // Create wiki structure\n      const wikiStructure: WikiStructure = {\n        id: 'wiki',\n        title,\n        description,\n        pages,\n        sections,\n        rootSections\n      };\n\n      setWikiStructure(wikiStructure);\n      setCurrentPageId(pages.length > 0 ? pages[0].id : undefined);\n\n      // Start generating content for all pages with controlled concurrency\n      if (pages.length > 0) {\n        // Mark all pages as in progress\n        const initialInProgress = new Set(pages.map(p => p.id));\n        setPagesInProgress(initialInProgress);\n\n        console.log(`Starting generation for ${pages.length} pages with controlled concurrency`);\n\n        // Maximum concurrent requests\n        const MAX_CONCURRENT = 1;\n\n        // Create a queue of pages\n        const queue = [...pages];\n        let activeRequests = 0;\n\n        // Function to process next items in queue\n        const processQueue = () => {\n          // Process as many items as we can up to our concurrency limit\n          while (queue.length > 0 && activeRequests < MAX_CONCURRENT) {\n            const page = queue.shift();\n            if (page) {\n              activeRequests++;\n              console.log(`Starting page ${page.title} (${activeRequests} active, ${queue.length} remaining)`);\n\n              // Start generating content for this page\n              generatePageContent(page, owner, repo)\n                .finally(() => {\n                  // When done (success or error), decrement active count and process more\n                  activeRequests--;\n                  console.log(`Finished page ${page.title} (${activeRequests} active, ${queue.length} remaining)`);\n\n                  // Check if all work is done (queue empty and no active requests)\n                  if (queue.length === 0 && activeRequests === 0) {\n                    console.log(\"All page generation tasks completed.\");\n                    setIsLoading(false);\n                    setLoadingMessage(undefined);\n                  } else {\n                    // Only process more if there are items remaining and we're under capacity\n                    if (queue.length > 0 && activeRequests < MAX_CONCURRENT) {\n                      processQueue();\n                    }\n                  }\n                });\n            }\n          }\n\n          // Additional check: If the queue started empty or becomes empty and no requests were started/active\n          if (queue.length === 0 && activeRequests === 0 && pages.length > 0 && pagesInProgress.size === 0) {\n            // This handles the case where the queue might finish before the finally blocks fully update activeRequests\n            // or if the initial queue was processed very quickly\n            console.log(\"Queue empty and no active requests after loop, ensuring loading is false.\");\n            setIsLoading(false);\n            setLoadingMessage(undefined);\n          } else if (pages.length === 0) {\n            // Handle case where there were no pages to begin with\n            setIsLoading(false);\n            setLoadingMessage(undefined);\n          }\n        };\n\n        // Start processing the queue\n        processQueue();\n      } else {\n        // Set loading to false if there were no pages found\n        setIsLoading(false);\n        setLoadingMessage(undefined);\n      }\n\n    } catch (error) {\n      console.error('Error determining wiki structure:', error);\n      setIsLoading(false);\n      setError(error instanceof Error ? error.message : 'An unknown error occurred');\n      setLoadingMessage(undefined);\n    } finally {\n      setStructureRequestInProgress(false);\n    }\n  }, [generatePageContent, currentToken, effectiveRepoInfo, pagesInProgress.size, structureRequestInProgress, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, language, messages.loading, isComprehensiveView]);\n\n  // Fetch repository structure using GitHub or GitLab API\n  const fetchRepositoryStructure = useCallback(async () => {\n    // If a request is already in progress, don't start another one\n    if (requestInProgress) {\n      console.log('Repository fetch already in progress, skipping duplicate call');\n      return;\n    }\n\n    // Reset previous state\n    setWikiStructure(undefined);\n    setCurrentPageId(undefined);\n    setGeneratedPages({});\n    setPagesInProgress(new Set());\n    setError(null);\n    setEmbeddingError(false); // Reset embedding error state\n\n    try {\n      // Set the request in progress flag\n      setRequestInProgress(true);\n\n      // Update loading state\n      setIsLoading(true);\n      setLoadingMessage(messages.loading?.fetchingStructure || 'Fetching repository structure...');\n\n      let fileTreeData = '';\n      let readmeContent = '';\n\n      if (effectiveRepoInfo.type === 'local' && effectiveRepoInfo.localPath) {\n        try {\n          const response = await fetch(`/local_repo/structure?path=${encodeURIComponent(effectiveRepoInfo.localPath)}`);\n\n          if (!response.ok) {\n            const errorData = await response.text();\n            throw new Error(`Local repository API error (${response.status}): ${errorData}`);\n          }\n\n          const data = await response.json();\n          fileTreeData = data.file_tree;\n          readmeContent = data.readme;\n          // For local repos, we can't determine the actual branch, so use 'main' as default\n          setDefaultBranch('main');\n        } catch (err) {\n          throw err;\n        }\n      } else if (effectiveRepoInfo.type === 'github') {\n        // GitHub API approach\n        // Try to get the tree data for common branch names\n        let treeData = null;\n        let apiErrorDetails = '';\n\n        // Determine the GitHub API base URL based on the repository URL\n        const getGithubApiUrl = (repoUrl: string | null): string => {\n          if (!repoUrl) {\n            return 'https://api.github.com'; // Default to public GitHub\n          }\n          \n          try {\n            const url = new URL(repoUrl);\n            const hostname = url.hostname;\n            \n            // If it's the public GitHub, use the standard API URL\n            if (hostname === 'github.com') {\n              return 'https://api.github.com';\n            }\n            \n            // For GitHub Enterprise, use the enterprise API URL format\n            // GitHub Enterprise API URL format: https://github.company.com/api/v3\n            return `${url.protocol}//${hostname}/api/v3`;\n          } catch {\n            return 'https://api.github.com'; // Fallback to public GitHub if URL parsing fails\n          }\n        };\n\n        const githubApiBaseUrl = getGithubApiUrl(effectiveRepoInfo.repoUrl);\n        // First, try to get the default branch from the repository info\n        let defaultBranchLocal = null;\n        try {\n          const repoInfoResponse = await fetch(`${githubApiBaseUrl}/repos/${owner}/${repo}`, {\n            headers: createGithubHeaders(currentToken)\n          });\n          \n          if (repoInfoResponse.ok) {\n            const repoData = await repoInfoResponse.json();\n            defaultBranchLocal = repoData.default_branch;\n            console.log(`Found default branch: ${defaultBranchLocal}`);\n            // Store the default branch in state\n            setDefaultBranch(defaultBranchLocal || 'main');\n          }\n        } catch (err) {\n          console.warn('Could not fetch repository info for default branch:', err);\n        }\n\n        // Create list of branches to try, prioritizing the actual default branch\n        const branchesToTry = defaultBranchLocal \n          ? [defaultBranchLocal, 'main', 'master'].filter((branch, index, arr) => arr.indexOf(branch) === index)\n          : ['main', 'master'];\n\n        for (const branch of branchesToTry) {\n          const apiUrl = `${githubApiBaseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;\n          const headers = createGithubHeaders(currentToken);\n\n          console.log(`Fetching repository structure from branch: ${branch}`);\n          try {\n            const response = await fetch(apiUrl, {\n              headers\n            });\n\n            if (response.ok) {\n              treeData = await response.json();\n              console.log('Successfully fetched repository structure');\n              break;\n            } else {\n              const errorData = await response.text();\n              apiErrorDetails = `Status: ${response.status}, Response: ${errorData}`;\n              console.error(`Error fetching repository structure: ${apiErrorDetails}`);\n            }\n          } catch (err) {\n            console.error(`Network error fetching branch ${branch}:`, err);\n          }\n        }\n\n        if (!treeData || !treeData.tree) {\n          if (apiErrorDetails) {\n            throw new Error(`Could not fetch repository structure. API Error: ${apiErrorDetails}`);\n          } else {\n            throw new Error('Could not fetch repository structure. Repository might not exist, be empty or private.');\n          }\n        }\n\n        // Convert tree data to a string representation\n        fileTreeData = treeData.tree\n          .filter((item: { type: string; path: string }) => item.type === 'blob')\n          .map((item: { type: string; path: string }) => item.path)\n          .join('\\n');\n\n        // Try to fetch README.md content\n        try {\n          const headers = createGithubHeaders(currentToken);\n\n          const readmeResponse = await fetch(`${githubApiBaseUrl}/repos/${owner}/${repo}/readme`, {\n            headers\n          });\n\n          if (readmeResponse.ok) {\n            const readmeData = await readmeResponse.json();\n            readmeContent = atob(readmeData.content);\n          } else {\n            console.warn(`Could not fetch README.md, status: ${readmeResponse.status}`);\n          }\n        } catch (err) {\n          console.warn('Could not fetch README.md, continuing with empty README', err);\n        }\n      }\n      else if (effectiveRepoInfo.type === 'gitlab') {\n        // GitLab API approach\n        const projectPath = extractUrlPath(effectiveRepoInfo.repoUrl ?? '')?.replace(/\\.git$/, '') || `${owner}/${repo}`;\n        const projectDomain = extractUrlDomain(effectiveRepoInfo.repoUrl ?? \"https://gitlab.com\");\n        const encodedProjectPath = encodeURIComponent(projectPath);\n\n        const headers = createGitlabHeaders(currentToken);\n\n        /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n        const filesData: any[] = [];\n\n        try {\n          // Step 1: Get project info to determine default branch\n          let projectInfoUrl: string;\n          let defaultBranchLocal = 'main'; // fallback\n          try {\n            const validatedUrl = new URL(projectDomain ?? ''); // Validate domain\n            projectInfoUrl = `${validatedUrl.origin}/api/v4/projects/${encodedProjectPath}`;\n          } catch (err) {\n            throw new Error(`Invalid project domain URL: ${projectDomain}`);\n          }\n          const projectInfoRes = await fetch(projectInfoUrl, { headers });\n\n          if (!projectInfoRes.ok) {\n            const errorData = await projectInfoRes.text();\n            throw new Error(`GitLab project info error: Status ${projectInfoRes.status}, Response: ${errorData}`);\n          }\n\n          const projectInfo = await projectInfoRes.json();\n          defaultBranchLocal = projectInfo.default_branch || 'main';\n          console.log(`Found GitLab default branch: ${defaultBranchLocal}`);\n          // Store the default branch in state\n          setDefaultBranch(defaultBranchLocal);\n\n          // Step 2: Paginate to fetch full file tree\n          let page = 1;\n          let morePages = true;\n          \n          while (morePages) {\n            const apiUrl = `${projectInfoUrl}/repository/tree?recursive=true&per_page=100&page=${page}`;\n            const response = await fetch(apiUrl, { headers });\n\n            if (!response.ok) {\n                const errorData = await response.text();\n              throw new Error(`Error fetching GitLab repository structure (page ${page}): ${errorData}`);\n            }\n\n            const pageData = await response.json();\n            filesData.push(...pageData);\n\n            const nextPage = response.headers.get('x-next-page');\n            morePages = !!nextPage;\n            page = nextPage ? parseInt(nextPage, 10) : page + 1;\n        }\n\n          if (!Array.isArray(filesData) || filesData.length === 0) {\n            throw new Error('Could not fetch repository structure. Repository might be empty or inaccessible.');\n        }\n\n          // Step 3: Format file paths\n        fileTreeData = filesData\n          .filter((item: { type: string; path: string }) => item.type === 'blob')\n          .map((item: { type: string; path: string }) => item.path)\n          .join('\\n');\n\n          // Step 4: Try to fetch README.md content\n          const readmeUrl = `${projectInfoUrl}/repository/files/README.md/raw`;\n            try {\n            const readmeResponse = await fetch(readmeUrl, { headers });\n              if (readmeResponse.ok) {\n                readmeContent = await readmeResponse.text();\n                console.log('Successfully fetched GitLab README.md');\n              } else {\n              console.warn(`Could not fetch GitLab README.md status: ${readmeResponse.status}`);\n              }\n            } catch (err) {\n            console.warn(`Error fetching GitLab README.md:`, err);\n            }\n        } catch (err) {\n          console.error(\"Error during GitLab repository tree retrieval:\", err);\n          throw err;\n        }\n      }\n      else if (effectiveRepoInfo.type === 'bitbucket') {\n        // Bitbucket API approach\n        const repoPath = extractUrlPath(effectiveRepoInfo.repoUrl ?? '') ?? `${owner}/${repo}`;\n        const encodedRepoPath = encodeURIComponent(repoPath);\n\n        // Try to get the file tree for common branch names\n        let filesData = null;\n        let apiErrorDetails = '';\n        let defaultBranchLocal = '';\n        const headers = createBitbucketHeaders(currentToken);\n\n        // First get project info to determine default branch\n        const projectInfoUrl = `https://api.bitbucket.org/2.0/repositories/${encodedRepoPath}`;\n        try {\n          const response = await fetch(projectInfoUrl, { headers });\n\n          const responseText = await response.text();\n\n          if (response.ok) {\n            const projectData = JSON.parse(responseText);\n            defaultBranchLocal = projectData.mainbranch.name;\n            // Store the default branch in state\n            setDefaultBranch(defaultBranchLocal);\n\n            const apiUrl = `https://api.bitbucket.org/2.0/repositories/${encodedRepoPath}/src/${defaultBranchLocal}/?recursive=true&per_page=100`;\n            try {\n              const response = await fetch(apiUrl, {\n                headers\n              });\n\n              const structureResponseText = await response.text();\n\n              if (response.ok) {\n                filesData = JSON.parse(structureResponseText);\n              } else {\n                const errorData = structureResponseText;\n                apiErrorDetails = `Status: ${response.status}, Response: ${errorData}`;\n              }\n            } catch (err) {\n              console.error(`Network error fetching Bitbucket branch ${defaultBranchLocal}:`, err);\n            }\n          } else {\n            const errorData = responseText;\n            apiErrorDetails = `Status: ${response.status}, Response: ${errorData}`;\n          }\n        } catch (err) {\n          console.error(\"Network error fetching Bitbucket project info:\", err);\n        }\n\n        if (!filesData || !Array.isArray(filesData.values) || filesData.values.length === 0) {\n          if (apiErrorDetails) {\n            throw new Error(`Could not fetch repository structure. Bitbucket API Error: ${apiErrorDetails}`);\n          } else {\n            throw new Error('Could not fetch repository structure. Repository might not exist, be empty or private.');\n          }\n        }\n\n        // Convert files data to a string representation\n        fileTreeData = filesData.values\n          .filter((item: { type: string; path: string }) => item.type === 'commit_file')\n          .map((item: { type: string; path: string }) => item.path)\n          .join('\\n');\n\n        // Try to fetch README.md content\n        try {\n          const headers = createBitbucketHeaders(currentToken);\n\n          const readmeResponse = await fetch(`https://api.bitbucket.org/2.0/repositories/${encodedRepoPath}/src/${defaultBranchLocal}/README.md`, {\n            headers\n          });\n\n          if (readmeResponse.ok) {\n            readmeContent = await readmeResponse.text();\n          } else {\n            console.warn(`Could not fetch Bitbucket README.md, status: ${readmeResponse.status}`);\n          }\n        } catch (err) {\n          console.warn('Could not fetch Bitbucket README.md, continuing with empty README', err);\n        }\n      }\n\n      // Now determine the wiki structure\n      await determineWikiStructure(fileTreeData, readmeContent, owner, repo);\n\n    } catch (error) {\n      console.error('Error fetching repository structure:', error);\n      setIsLoading(false);\n      setError(error instanceof Error ? error.message : 'An unknown error occurred');\n      setLoadingMessage(undefined);\n    } finally {\n      // Reset the request in progress flag\n      setRequestInProgress(false);\n    }\n  }, [owner, repo, determineWikiStructure, currentToken, effectiveRepoInfo, requestInProgress, messages.loading]);\n\n  // Function to export wiki content\n  const exportWiki = useCallback(async (format: 'markdown' | 'json') => {\n    if (!wikiStructure || Object.keys(generatedPages).length === 0) {\n      setExportError('No wiki content to export');\n      return;\n    }\n\n    try {\n      setIsExporting(true);\n      setExportError(null);\n      setLoadingMessage(`${language === 'ja' ? 'Wikiを' : 'Exporting wiki as '} ${format} ${language === 'ja' ? 'としてエクスポート中...' : '...'}`);\n\n      // Prepare the pages for export\n      const pagesToExport = wikiStructure.pages.map(page => {\n        // Use the generated content if available, otherwise use an empty string\n        const content = generatedPages[page.id]?.content || 'Content not generated';\n        return {\n          ...page,\n          content\n        };\n      });\n\n      // Get repository URL\n      const repoUrl = getRepoUrl(effectiveRepoInfo);\n\n      // Make API call to export wiki\n      const response = await fetch(`/export/wiki`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          repo_url: repoUrl,\n          type: effectiveRepoInfo.type,\n          pages: pagesToExport,\n          format\n        })\n      });\n\n      if (!response.ok) {\n        const errorText = await response.text().catch(() => 'No error details available');\n        throw new Error(`Error exporting wiki: ${response.status} - ${errorText}`);\n      }\n\n      // Get the filename from the Content-Disposition header if available\n      const contentDisposition = response.headers.get('Content-Disposition');\n      let filename = `${effectiveRepoInfo.repo}_wiki.${format === 'markdown' ? 'md' : 'json'}`;\n\n      if (contentDisposition) {\n        const filenameMatch = contentDisposition.match(/filename=(.+)/);\n        if (filenameMatch && filenameMatch[1]) {\n          filename = filenameMatch[1].replace(/\"/g, '');\n        }\n      }\n\n      // Convert the response to a blob and download it\n      const blob = await response.blob();\n      const url = window.URL.createObjectURL(blob);\n      const a = document.createElement('a');\n      a.href = url;\n      a.download = filename;\n      document.body.appendChild(a);\n      a.click();\n      window.URL.revokeObjectURL(url);\n      document.body.removeChild(a);\n\n    } catch (err) {\n      console.error('Error exporting wiki:', err);\n      const errorMessage = err instanceof Error ? err.message : 'Unknown error during export';\n      setExportError(errorMessage);\n    } finally {\n      setIsExporting(false);\n      setLoadingMessage(undefined);\n    }\n  }, [wikiStructure, generatedPages, effectiveRepoInfo, language]);\n\n  // No longer needed as we use the modal directly\n\n  const confirmRefresh = useCallback(async (newToken?: string) => {\n    setShowModelOptions(false);\n    setLoadingMessage(messages.loading?.clearingCache || 'Clearing server cache...');\n    setIsLoading(true); // Show loading indicator immediately\n\n    try {\n      const params = new URLSearchParams({\n        owner: effectiveRepoInfo.owner,\n        repo: effectiveRepoInfo.repo,\n        repo_type: effectiveRepoInfo.type,\n        language: language,\n        provider: selectedProviderState,\n        model: selectedModelState,\n        is_custom_model: isCustomSelectedModelState.toString(),\n        custom_model: customSelectedModelState,\n        comprehensive: isComprehensiveView.toString(),\n        authorization_code: authCode,\n      });\n\n      // Add file filters configuration\n      if (modelExcludedDirs) {\n        params.append('excluded_dirs', modelExcludedDirs);\n      }\n      if (modelExcludedFiles) {\n        params.append('excluded_files', modelExcludedFiles);\n      }\n\n      if(authRequired && !authCode) {\n        setIsLoading(false);\n        console.error(\"Authorization code is required\");\n        setError('Authorization code is required');\n        return;\n      }\n\n      const response = await fetch(`/api/wiki_cache?${params.toString()}`, {\n        method: 'DELETE',\n        headers: {\n          'Accept': 'application/json',\n        }\n      });\n\n      if (response.ok) {\n        console.log('Server-side wiki cache cleared successfully.');\n        // Optionally, show a success message for cache clearing if desired\n        // setLoadingMessage('Cache cleared. Refreshing wiki...');\n      } else {\n        const errorText = await response.text();\n        console.warn(`Failed to clear server-side wiki cache (status: ${response.status}): ${errorText}. Proceeding with refresh anyway.`);\n        // Optionally, inform the user about the cache clear failure but that refresh will still attempt\n        // setError(\\`Cache clear failed: ${errorText}. Trying to refresh...\\`);\n        if(response.status == 401) {\n          setIsLoading(false);\n          setLoadingMessage(undefined);\n          setError('Failed to validate the authorization code');\n          console.error('Failed to validate the authorization code')\n          return;\n        }\n      }\n    } catch (err) {\n      console.warn('Error calling DELETE /api/wiki_cache:', err);\n      setIsLoading(false);\n      setEmbeddingError(false); // Reset embedding error state\n      // Optionally, inform the user about the cache clear error\n      // setError(\\`Error clearing cache: ${err instanceof Error ? err.message : String(err)}. Trying to refresh...\\`);\n      throw err;\n    }\n\n    // Update token if provided\n    if (newToken) {\n      // Update current token state\n      setCurrentToken(newToken);\n      // Update the URL parameters to include the new token\n      const currentUrl = new URL(window.location.href);\n      currentUrl.searchParams.set('token', newToken);\n      window.history.replaceState({}, '', currentUrl.toString());\n    }\n\n    // Proceed with the rest of the refresh logic\n    console.log('Refreshing wiki. Server cache will be overwritten upon new generation if not cleared.');\n\n    // Clear the localStorage cache (if any remnants or if it was used before this change)\n    const localStorageCacheKey = getCacheKey(effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, language, isComprehensiveView);\n    localStorage.removeItem(localStorageCacheKey);\n\n    // Reset cache loaded flag\n    cacheLoadedSuccessfully.current = false;\n    effectRan.current = false; // Allow the main data loading useEffect to run again\n\n    // Reset all state\n    setWikiStructure(undefined);\n    setCurrentPageId(undefined);\n    setGeneratedPages({});\n    setPagesInProgress(new Set());\n    setError(null);\n    setEmbeddingError(false); // Reset embedding error state\n    setIsLoading(true); // Set loading state for refresh\n    setLoadingMessage(messages.loading?.initializing || 'Initializing wiki generation...');\n\n    // Clear any in-progress requests for page content\n    activeContentRequests.clear();\n    // Reset flags related to request processing if they are component-wide\n    setStructureRequestInProgress(false); // Assuming this flag should be reset\n    setRequestInProgress(false); // Assuming this flag should be reset\n\n    // Explicitly trigger the data loading process again by re-invoking what the main useEffect does.\n    // This will first attempt to load from (now hopefully non-existent or soon-to-be-overwritten) server cache,\n    // then proceed to fetchRepositoryStructure if needed.\n    // To ensure fetchRepositoryStructure is called if cache is somehow still there or to force a full refresh:\n    // One option is to directly call fetchRepositoryStructure() if force refresh means bypassing cache check.\n    // For now, we rely on the standard loadData flow initiated by resetting effectRan and dependencies.\n    // This will re-trigger the main data loading useEffect.\n    // No direct call to fetchRepositoryStructure here, let the useEffect handle it based on effectRan.current = false.\n  }, [effectiveRepoInfo, language, messages.loading, activeContentRequests, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, isComprehensiveView, authCode, authRequired]);\n\n  // Start wiki generation when component mounts\n  useEffect(() => {\n    if (effectRan.current === false) {\n      effectRan.current = true; // Set to true immediately to prevent re-entry due to StrictMode\n\n      const loadData = async () => {\n        // Try loading from server-side cache first\n        setLoadingMessage(messages.loading?.fetchingCache || 'Checking for cached wiki...');\n        try {\n          const params = new URLSearchParams({\n            owner: effectiveRepoInfo.owner,\n            repo: effectiveRepoInfo.repo,\n            repo_type: effectiveRepoInfo.type,\n            language: language,\n            comprehensive: isComprehensiveView.toString(),\n          });\n          const response = await fetch(`/api/wiki_cache?${params.toString()}`);\n\n          if (response.ok) {\n            const cachedData = await response.json(); // Returns null if no cache\n            if (cachedData && cachedData.wiki_structure && cachedData.generated_pages && Object.keys(cachedData.generated_pages).length > 0) {\n              console.log('Using server-cached wiki data');\n              if(cachedData.model) {\n                setSelectedModelState(cachedData.model);\n              }\n              if(cachedData.provider) {\n                setSelectedProviderState(cachedData.provider);\n              }\n\n              // Update repoInfo\n              if(cachedData.repo) {\n                setEffectiveRepoInfo(cachedData.repo);\n              } else if (cachedData.repo_url && !effectiveRepoInfo.repoUrl) {\n                const updatedRepoInfo = { ...effectiveRepoInfo, repoUrl: cachedData.repo_url };\n                setEffectiveRepoInfo(updatedRepoInfo); // Update effective repo info state\n                console.log('Using cached repo_url:', cachedData.repo_url);\n              }\n\n              // Ensure the cached structure has sections and rootSections\n              const cachedStructure = {\n                ...cachedData.wiki_structure,\n                sections: cachedData.wiki_structure.sections || [],\n                rootSections: cachedData.wiki_structure.rootSections || []\n              };\n\n              // If sections or rootSections are missing, create intelligent ones based on page titles\n              if (!cachedStructure.sections.length || !cachedStructure.rootSections.length) {\n                const pages = cachedStructure.pages;\n                const sections: WikiSection[] = [];\n                const rootSections: string[] = [];\n\n                // Group pages by common prefixes or categories\n                const pageClusters = new Map<string, WikiPage[]>();\n\n                // Define common categories that might appear in page titles\n                const categories = [\n                  { id: 'overview', title: 'Overview', keywords: ['overview', 'introduction', 'about'] },\n                  { id: 'architecture', title: 'Architecture', keywords: ['architecture', 'structure', 'design', 'system'] },\n                  { id: 'features', title: 'Core Features', keywords: ['feature', 'functionality', 'core'] },\n                  { id: 'components', title: 'Components', keywords: ['component', 'module', 'widget'] },\n                  { id: 'api', title: 'API', keywords: ['api', 'endpoint', 'service', 'server'] },\n                  { id: 'data', title: 'Data Flow', keywords: ['data', 'flow', 'pipeline', 'storage'] },\n                  { id: 'models', title: 'Models', keywords: ['model', 'ai', 'ml', 'integration'] },\n                  { id: 'ui', title: 'User Interface', keywords: ['ui', 'interface', 'frontend', 'page'] },\n                  { id: 'setup', title: 'Setup & Configuration', keywords: ['setup', 'config', 'installation', 'deploy'] }\n                ];\n\n                // Initialize clusters with empty arrays\n                categories.forEach(category => {\n                  pageClusters.set(category.id, []);\n                });\n\n                // Add an \"Other\" category for pages that don't match any category\n                pageClusters.set('other', []);\n\n                // Assign pages to categories based on title keywords\n                pages.forEach((page: WikiPage) => {\n                  const title = page.title.toLowerCase();\n                  let assigned = false;\n\n                  // Try to find a matching category\n                  for (const category of categories) {\n                    if (category.keywords.some(keyword => title.includes(keyword))) {\n                      pageClusters.get(category.id)?.push(page);\n                      assigned = true;\n                      break;\n                    }\n                  }\n\n                  // If no category matched, put in \"Other\"\n                  if (!assigned) {\n                    pageClusters.get('other')?.push(page);\n                  }\n                });\n\n                // Create sections for non-empty categories\n                for (const [categoryId, categoryPages] of pageClusters.entries()) {\n                  if (categoryPages.length > 0) {\n                    const category = categories.find(c => c.id === categoryId) ||\n                                    { id: categoryId, title: categoryId === 'other' ? 'Other' : categoryId.charAt(0).toUpperCase() + categoryId.slice(1) };\n\n                    const sectionId = `section-${categoryId}`;\n                    sections.push({\n                      id: sectionId,\n                      title: category.title,\n                      pages: categoryPages.map((p: WikiPage) => p.id)\n                    });\n                    rootSections.push(sectionId);\n\n                    // Update page parentId\n                    categoryPages.forEach((page: WikiPage) => {\n                      page.parentId = sectionId;\n                    });\n                  }\n                }\n\n                // If we still have no sections (unlikely), fall back to importance-based grouping\n                if (sections.length === 0) {\n                  const highImportancePages = pages.filter((p: WikiPage) => p.importance === 'high').map((p: WikiPage) => p.id);\n                  const mediumImportancePages = pages.filter((p: WikiPage) => p.importance === 'medium').map((p: WikiPage) => p.id);\n                  const lowImportancePages = pages.filter((p: WikiPage) => p.importance === 'low').map((p: WikiPage) => p.id);\n\n                  if (highImportancePages.length > 0) {\n                    sections.push({\n                      id: 'section-high',\n                      title: 'Core Components',\n                      pages: highImportancePages\n                    });\n                    rootSections.push('section-high');\n                  }\n\n                  if (mediumImportancePages.length > 0) {\n                    sections.push({\n                      id: 'section-medium',\n                      title: 'Key Features',\n                      pages: mediumImportancePages\n                    });\n                    rootSections.push('section-medium');\n                  }\n\n                  if (lowImportancePages.length > 0) {\n                    sections.push({\n                      id: 'section-low',\n                      title: 'Additional Information',\n                      pages: lowImportancePages\n                    });\n                    rootSections.push('section-low');\n                  }\n                }\n\n                cachedStructure.sections = sections;\n                cachedStructure.rootSections = rootSections;\n              }\n\n              setWikiStructure(cachedStructure);\n              setGeneratedPages(cachedData.generated_pages);\n              setCurrentPageId(cachedStructure.pages.length > 0 ? cachedStructure.pages[0].id : undefined);\n              setIsLoading(false);\n              setEmbeddingError(false); \n              setLoadingMessage(undefined);\n              cacheLoadedSuccessfully.current = true;\n              return; // Exit if cache is successfully loaded\n            } else {\n              console.log('No valid wiki data in server cache or cache is empty.');\n            }\n          } else {\n            // Log error but proceed to fetch structure, as cache is optional\n            console.error('Error fetching wiki cache from server:', response.status, await response.text());\n          }\n        } catch (error) {\n          console.error('Error loading from server cache:', error);\n          // Proceed to fetch structure if cache loading fails\n        }\n\n        // If we reached here, either there was no cache, it was invalid, or an error occurred\n        // Proceed to fetch repository structure\n        fetchRepositoryStructure();\n      };\n\n      loadData();\n\n    } else {\n      console.log('Skipping duplicate repository fetch/cache check');\n    }\n\n    // Clean up function for this effect is not strictly necessary for loadData,\n    // but keeping the main unmount cleanup in the other useEffect\n  }, [effectiveRepoInfo, effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, language, fetchRepositoryStructure, messages.loading?.fetchingCache, isComprehensiveView]);\n\n  // Save wiki to server-side cache when generation is complete\n  useEffect(() => {\n    const saveCache = async () => {\n      if (!isLoading &&\n          !error &&\n          wikiStructure &&\n          Object.keys(generatedPages).length > 0 &&\n          Object.keys(generatedPages).length >= wikiStructure.pages.length &&\n          !cacheLoadedSuccessfully.current) {\n\n        const allPagesHaveContent = wikiStructure.pages.every(page =>\n          generatedPages[page.id] && generatedPages[page.id].content && generatedPages[page.id].content !== 'Loading...');\n\n        if (allPagesHaveContent) {\n          console.log('Attempting to save wiki data to server cache via Next.js proxy');\n\n          try {\n            // Make sure wikiStructure has sections and rootSections\n            const structureToCache = {\n              ...wikiStructure,\n              sections: wikiStructure.sections || [],\n              rootSections: wikiStructure.rootSections || []\n            };\n            const dataToCache = {\n              repo: effectiveRepoInfo,\n              language: language,\n              comprehensive: isComprehensiveView,\n              wiki_structure: structureToCache,\n              generated_pages: generatedPages,\n              provider: selectedProviderState,\n              model: selectedModelState\n            };\n            const response = await fetch(`/api/wiki_cache`, {\n              method: 'POST',\n              headers: {\n                'Content-Type': 'application/json',\n              },\n              body: JSON.stringify(dataToCache),\n            });\n\n            if (response.ok) {\n              console.log('Wiki data successfully saved to server cache');\n            } else {\n              console.error('Error saving wiki data to server cache:', response.status, await response.text());\n            }\n          } catch (error) {\n            console.error('Error saving to server cache:', error);\n          }\n        }\n      }\n    };\n\n    saveCache();\n  }, [isLoading, error, wikiStructure, generatedPages, effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, effectiveRepoInfo.repoUrl, repoUrl, language, isComprehensiveView]);\n\n  const handlePageSelect = (pageId: string) => {\n    if (currentPageId != pageId) {\n      setCurrentPageId(pageId)\n    }\n  };\n\n  const [isModelSelectionModalOpen, setIsModelSelectionModalOpen] = useState(false);\n\n  return (\n    <div className=\"h-screen paper-texture p-4 md:p-8 flex flex-col\">\n      <style>{wikiStyles}</style>\n\n      <header className=\"max-w-[90%] xl:max-w-[1400px] mx-auto mb-8 h-fit w-full\">\n        <div className=\"flex flex-col md:flex-row md:items-center md:justify-between gap-4\">\n          <div className=\"flex items-center gap-4\">\n            <Link href=\"/\" className=\"text-[var(--accent-primary)] hover:text-[var(--highlight)] flex items-center gap-1.5 transition-colors border-b border-[var(--border-color)] hover:border-[var(--accent-primary)] pb-0.5\">\n              <FaHome /> {messages.repoPage?.home || 'Home'}\n            </Link>\n          </div>\n        </div>\n      </header>\n\n      <main className=\"flex-1 max-w-[90%] xl:max-w-[1400px] mx-auto overflow-y-auto\">\n        {isLoading ? (\n          <div className=\"flex flex-col items-center justify-center p-8 bg-[var(--card-bg)] rounded-lg shadow-custom card-japanese\">\n            <div className=\"relative mb-6\">\n              <div className=\"absolute -inset-4 bg-[var(--accent-primary)]/10 rounded-full blur-md animate-pulse\"></div>\n              <div className=\"relative flex items-center justify-center\">\n                <div className=\"w-3 h-3 bg-[var(--accent-primary)]/70 rounded-full animate-pulse\"></div>\n                <div className=\"w-3 h-3 bg-[var(--accent-primary)]/70 rounded-full animate-pulse delay-75 mx-2\"></div>\n                <div className=\"w-3 h-3 bg-[var(--accent-primary)]/70 rounded-full animate-pulse delay-150\"></div>\n              </div>\n            </div>\n            <p className=\"text-[var(--foreground)] text-center mb-3 font-serif\">\n              {loadingMessage || messages.common?.loading || 'Loading...'}\n              {isExporting && (messages.loading?.preparingDownload || ' Please wait while we prepare your download...')}\n            </p>\n\n            {/* Progress bar for page generation */}\n            {wikiStructure && (\n              <div className=\"w-full max-w-md mt-3\">\n                <div className=\"bg-[var(--background)]/50 rounded-full h-2 mb-3 overflow-hidden border border-[var(--border-color)]\">\n                  <div\n                    className=\"bg-[var(--accent-primary)] h-2 rounded-full transition-all duration-300 ease-in-out\"\n                    style={{\n                      width: `${Math.max(5, 100 * (wikiStructure.pages.length - pagesInProgress.size) / wikiStructure.pages.length)}%`\n                    }}\n                  />\n                </div>\n                <p className=\"text-xs text-[var(--muted)] text-center\">\n                  {language === 'ja'\n                    ? `${wikiStructure.pages.length}ページ中${wikiStructure.pages.length - pagesInProgress.size}ページ完了`\n                    : messages.repoPage?.pagesCompleted\n                        ? messages.repoPage.pagesCompleted\n                            .replace('{completed}', (wikiStructure.pages.length - pagesInProgress.size).toString())\n                            .replace('{total}', wikiStructure.pages.length.toString())\n                        : `${wikiStructure.pages.length - pagesInProgress.size} of ${wikiStructure.pages.length} pages completed`}\n                </p>\n\n                {/* Show list of in-progress pages */}\n                {pagesInProgress.size > 0 && (\n                  <div className=\"mt-4 text-xs\">\n                    <p className=\"text-[var(--muted)] mb-2\">\n                      {messages.repoPage?.currentlyProcessing || 'Currently processing:'}\n                    </p>\n                    <ul className=\"text-[var(--foreground)] space-y-1\">\n                      {Array.from(pagesInProgress).slice(0, 3).map(pageId => {\n                        const page = wikiStructure.pages.find(p => p.id === pageId);\n                        return page ? <li key={pageId} className=\"truncate border-l-2 border-[var(--accent-primary)]/30 pl-2\">{page.title}</li> : null;\n                      })}\n                      {pagesInProgress.size > 3 && (\n                        <li className=\"text-[var(--muted)]\">\n                          {language === 'ja'\n                            ? `...他に${pagesInProgress.size - 3}ページ`\n                            : messages.repoPage?.andMorePages\n                                ? messages.repoPage.andMorePages.replace('{count}', (pagesInProgress.size - 3).toString())\n                                : `...and ${pagesInProgress.size - 3} more`}\n                        </li>\n                      )}\n                    </ul>\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n        ) : error ? (\n          <div className=\"bg-[var(--highlight)]/5 border border-[var(--highlight)]/30 rounded-lg p-5 mb-4 shadow-sm\">\n            <div className=\"flex items-center text-[var(--highlight)] mb-3\">\n              <FaExclamationTriangle className=\"mr-2\" />\n              <span className=\"font-bold font-serif\">{messages.repoPage?.errorTitle || messages.common?.error || 'Error'}</span>\n            </div>\n            <p className=\"text-[var(--foreground)] text-sm mb-3\">{error}</p>\n            <p className=\"text-[var(--muted)] text-xs\">\n              {embeddingError ? (\n                messages.repoPage?.embeddingErrorDefault || 'This error is related to the document embedding system used for analyzing your repository. Please verify your embedding model configuration, API keys, and try again. If the issue persists, consider switching to a different embedding provider in the model settings.'\n              ) : (\n                messages.repoPage?.errorMessageDefault || 'Please check that your repository exists and is public. Valid formats are \"owner/repo\", \"https://github.com/owner/repo\", \"https://gitlab.com/owner/repo\", \"https://bitbucket.org/owner/repo\", or local folder paths like \"C:\\\\path\\\\to\\\\folder\" or \"/path/to/folder\".'\n              )}\n            </p>\n            <div className=\"mt-5\">\n              <Link\n                href=\"/\"\n                className=\"btn-japanese px-5 py-2 inline-flex items-center gap-1.5\"\n              >\n                <FaHome className=\"text-sm\" />\n                {messages.repoPage?.backToHome || 'Back to Home'}\n              </Link>\n            </div>\n          </div>\n        ) : wikiStructure ? (\n          <div className=\"h-full overflow-y-auto flex flex-col lg:flex-row gap-4 w-full overflow-hidden bg-[var(--card-bg)] rounded-lg shadow-custom card-japanese\">\n            {/* Wiki Navigation */}\n            <div className=\"h-full w-full lg:w-[280px] xl:w-[320px] flex-shrink-0 bg-[var(--background)]/50 rounded-lg rounded-r-none p-5 border-b lg:border-b-0 lg:border-r border-[var(--border-color)] overflow-y-auto\">\n              <h3 className=\"text-lg font-bold text-[var(--foreground)] mb-3 font-serif\">{wikiStructure.title}</h3>\n              <p className=\"text-[var(--muted)] text-sm mb-5 leading-relaxed\">{wikiStructure.description}</p>\n\n              {/* Display repository info */}\n              <div className=\"text-xs text-[var(--muted)] mb-5 flex items-center\">\n                {effectiveRepoInfo.type === 'local' ? (\n                  <div className=\"flex items-center\">\n                    <FaFolder className=\"mr-2\" />\n                    <span className=\"break-all\">{effectiveRepoInfo.localPath}</span>\n                  </div>\n                ) : (\n                  <>\n                    {effectiveRepoInfo.type === 'github' ? (\n                      <FaGithub className=\"mr-2\" />\n                    ) : effectiveRepoInfo.type === 'gitlab' ? (\n                      <FaGitlab className=\"mr-2\" />\n                    ) : (\n                      <FaBitbucket className=\"mr-2\" />\n                    )}\n                    <a\n                      href={effectiveRepoInfo.repoUrl ?? ''}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"hover:text-[var(--accent-primary)] transition-colors border-b border-[var(--border-color)] hover:border-[var(--accent-primary)]\"\n                    >\n                      {effectiveRepoInfo.owner}/{effectiveRepoInfo.repo}\n                    </a>\n                  </>\n                )}\n              </div>\n\n              {/* Wiki Type Indicator */}\n              <div className=\"mb-3 flex items-center text-xs text-[var(--muted)]\">\n                <span className=\"mr-2\">Wiki Type:</span>\n                <span className={`px-2 py-0.5 rounded-full ${isComprehensiveView\n                  ? 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] border border-[var(--accent-primary)]/30'\n                  : 'bg-[var(--background)] text-[var(--foreground)] border border-[var(--border-color)]'}`}>\n                  {isComprehensiveView\n                    ? (messages.form?.comprehensive || 'Comprehensive')\n                    : (messages.form?.concise || 'Concise')}\n                </span>\n              </div>\n\n              {/* Refresh Wiki button */}\n              <div className=\"mb-5\">\n                <button\n                  onClick={() => setIsModelSelectionModalOpen(true)}\n                  disabled={isLoading}\n                  className=\"flex items-center w-full text-xs px-3 py-2 bg-[var(--background)] text-[var(--foreground)] rounded-md hover:bg-[var(--background)]/80 disabled:opacity-50 disabled:cursor-not-allowed border border-[var(--border-color)] transition-colors hover:cursor-pointer\"\n                >\n                  <FaSync className={`mr-2 ${isLoading ? 'animate-spin' : ''}`} />\n                  {messages.repoPage?.refreshWiki || 'Refresh Wiki'}\n                </button>\n              </div>\n\n              {/* Export buttons */}\n              {Object.keys(generatedPages).length > 0 && (\n                <div className=\"mb-5\">\n                  <h4 className=\"text-sm font-semibold text-[var(--foreground)] mb-3 font-serif\">\n                    {messages.repoPage?.exportWiki || 'Export Wiki'}\n                  </h4>\n                  <div className=\"flex flex-col gap-2\">\n                    <button\n                      onClick={() => exportWiki('markdown')}\n                      disabled={isExporting}\n                      className=\"btn-japanese flex items-center text-xs px-3 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed\"\n                    >\n                      <FaDownload className=\"mr-2\" />\n                      {messages.repoPage?.exportAsMarkdown || 'Export as Markdown'}\n                    </button>\n                    <button\n                      onClick={() => exportWiki('json')}\n                      disabled={isExporting}\n                      className=\"flex items-center text-xs px-3 py-2 bg-[var(--background)] text-[var(--foreground)] rounded-md hover:bg-[var(--background)]/80 disabled:opacity-50 disabled:cursor-not-allowed border border-[var(--border-color)] transition-colors\"\n                    >\n                      <FaFileExport className=\"mr-2\" />\n                      {messages.repoPage?.exportAsJson || 'Export as JSON'}\n                    </button>\n                  </div>\n                  {exportError && (\n                    <div className=\"mt-2 text-xs text-[var(--highlight)]\">\n                      {exportError}\n                    </div>\n                  )}\n                </div>\n              )}\n\n              <h4 className=\"text-md font-semibold text-[var(--foreground)] mb-3 font-serif\">\n                {messages.repoPage?.pages || 'Pages'}\n              </h4>\n              <WikiTreeView\n                wikiStructure={wikiStructure}\n                currentPageId={currentPageId}\n                onPageSelect={handlePageSelect}\n                messages={messages.repoPage}\n              />\n            </div>\n\n            {/* Wiki Content */}\n            <div id=\"wiki-content\" className=\"w-full flex-grow p-6 lg:p-8 overflow-y-auto\">\n              {currentPageId && generatedPages[currentPageId] ? (\n                <div className=\"max-w-[900px] xl:max-w-[1000px] mx-auto\">\n                  <h3 className=\"text-xl font-bold text-[var(--foreground)] mb-4 break-words font-serif\">\n                    {generatedPages[currentPageId].title}\n                  </h3>\n\n\n\n                  <div className=\"prose prose-sm md:prose-base lg:prose-lg max-w-none\">\n                    <Markdown\n                      content={generatedPages[currentPageId].content}\n                    />\n                  </div>\n\n                  {generatedPages[currentPageId].relatedPages.length > 0 && (\n                    <div className=\"mt-8 pt-4 border-t border-[var(--border-color)]\">\n                      <h4 className=\"text-sm font-semibold text-[var(--muted)] mb-3\">\n                        {messages.repoPage?.relatedPages || 'Related Pages:'}\n                      </h4>\n                      <div className=\"flex flex-wrap gap-2\">\n                        {generatedPages[currentPageId].relatedPages.map(relatedId => {\n                          const relatedPage = wikiStructure.pages.find(p => p.id === relatedId);\n                          return relatedPage ? (\n                            <button\n                              key={relatedId}\n                              className=\"bg-[var(--accent-primary)]/10 hover:bg-[var(--accent-primary)]/20 text-xs text-[var(--accent-primary)] px-3 py-1.5 rounded-md transition-colors truncate max-w-full border border-[var(--accent-primary)]/20\"\n                              onClick={() => handlePageSelect(relatedId)}\n                            >\n                              {relatedPage.title}\n                            </button>\n                          ) : null;\n                        })}\n                      </div>\n                    </div>\n                  )}\n                </div>\n              ) : (\n                <div className=\"flex flex-col items-center justify-center p-8 text-[var(--muted)] h-full\">\n                  <div className=\"relative mb-4\">\n                    <div className=\"absolute -inset-2 bg-[var(--accent-primary)]/5 rounded-full blur-md\"></div>\n                    <FaBookOpen className=\"text-4xl relative z-10\" />\n                  </div>\n                  <p className=\"font-serif\">\n                    {messages.repoPage?.selectPagePrompt || 'Select a page from the navigation to view its content'}\n                  </p>\n                </div>\n              )}\n            </div>\n          </div>\n        ) : null}\n      </main>\n\n      <footer className=\"max-w-[90%] xl:max-w-[1400px] mx-auto mt-8 flex flex-col gap-4 w-full\">\n        <div className=\"flex justify-between items-center gap-4 text-center text-[var(--muted)] text-sm h-fit w-full bg-[var(--card-bg)] rounded-lg p-3 shadow-sm border border-[var(--border-color)]\">\n          <p className=\"flex-1 font-serif\">\n            {messages.footer?.copyright || 'DeepWiki - Generate Wiki from GitHub/Gitlab/Bitbucket repositories'}\n          </p>\n          <ThemeToggle />\n        </div>\n      </footer>\n\n      {/* Floating Chat Button */}\n      {!isLoading && wikiStructure && (\n        <button\n          onClick={() => setIsAskModalOpen(true)}\n          className=\"fixed bottom-6 right-6 w-14 h-14 rounded-full bg-[var(--accent-primary)] text-white shadow-lg flex items-center justify-center hover:bg-[var(--accent-primary)]/90 transition-all z-50\"\n          aria-label={messages.ask?.title || 'Ask about this repository'}\n        >\n          <FaComments className=\"text-xl\" />\n        </button>\n      )}\n\n      {/* Ask Modal - Always render but conditionally show/hide */}\n      <div className={`fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 transition-opacity duration-300 ${isAskModalOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>\n        <div className=\"bg-[var(--card-bg)] rounded-lg shadow-xl w-full max-w-3xl max-h-[80vh] flex flex-col\">\n          <div className=\"flex items-center justify-end p-3 absolute top-0 right-0 z-10\">\n            <button\n              onClick={() => {\n                // Just close the modal without clearing the conversation\n                setIsAskModalOpen(false);\n              }}\n              className=\"text-[var(--muted)] hover:text-[var(--foreground)] transition-colors bg-[var(--card-bg)]/80 rounded-full p-2\"\n              aria-label=\"Close\"\n            >\n              <FaTimes className=\"text-xl\" />\n            </button>\n          </div>\n          <div className=\"flex-1 overflow-y-auto p-4\">\n            <Ask\n              repoInfo={effectiveRepoInfo}\n              provider={selectedProviderState}\n              model={selectedModelState}\n              isCustomModel={isCustomSelectedModelState}\n              customModel={customSelectedModelState}\n              language={language}\n              onRef={(ref) => (askComponentRef.current = ref)}\n            />\n          </div>\n        </div>\n      </div>\n\n      <ModelSelectionModal\n        isOpen={isModelSelectionModalOpen}\n        onClose={() => setIsModelSelectionModalOpen(false)}\n        provider={selectedProviderState}\n        setProvider={setSelectedProviderState}\n        model={selectedModelState}\n        setModel={setSelectedModelState}\n        isCustomModel={isCustomSelectedModelState}\n        setIsCustomModel={setIsCustomSelectedModelState}\n        customModel={customSelectedModelState}\n        setCustomModel={setCustomSelectedModelState}\n        isComprehensiveView={isComprehensiveView}\n        setIsComprehensiveView={setIsComprehensiveView}\n        showFileFilters={true}\n        excludedDirs={modelExcludedDirs}\n        setExcludedDirs={setModelExcludedDirs}\n        excludedFiles={modelExcludedFiles}\n        setExcludedFiles={setModelExcludedFiles}\n        includedDirs={modelIncludedDirs}\n        setIncludedDirs={setModelIncludedDirs}\n        includedFiles={modelIncludedFiles}\n        setIncludedFiles={setModelIncludedFiles}\n        onApply={confirmRefresh}\n        showWikiType={true}\n        showTokenInput={effectiveRepoInfo.type !== 'local' && !currentToken} // Show token input if not local and no current token\n        repositoryType={effectiveRepoInfo.type as 'github' | 'gitlab' | 'bitbucket'}\n        authRequired={authRequired}\n        authCode={authCode}\n        setAuthCode={setAuthCode}\n        isAuthLoading={isAuthLoading}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/[owner]/[repo]/slides/page.tsx",
    "content": "'use client';\n\nimport React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';\nimport { useParams, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { FaArrowLeft, FaSync, FaDownload, FaArrowRight, FaArrowUp, FaTimes } from 'react-icons/fa';\nimport ThemeToggle from '@/components/theme-toggle';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport { RepoInfo } from '@/types/repoinfo';\nimport getRepoUrl from '@/utils/getRepoUrl';\n\n// Helper function to add tokens and other parameters to request body\nconst addTokensToRequestBody = (\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  requestBody: Record<string, any>,\n  token: string,\n  repoType: string,\n  provider: string = '',\n  model: string = '',\n  isCustomModel: boolean = false,\n  customModel: string = '',\n  language: string = 'en',\n) => {\n  if (token !== '') {\n    requestBody.token = token;\n  }\n\n  // Add provider-based model selection parameters\n  requestBody.provider = provider;\n  requestBody.model = model;\n  if (isCustomModel && customModel) {\n    requestBody.custom_model = customModel;\n  }\n\n  requestBody.language = language;\n};\n\ninterface Slide {\n  id: string;\n  title: string;\n  content: string;\n  html: string;\n}\n\nexport default function SlidesPage() {\n  // Get route parameters and search params\n  const params = useParams();\n  const searchParams = useSearchParams();\n\n  // Extract owner and repo from route params\n  const owner = params.owner as string;\n  const repo = params.repo as string;\n\n  // Extract tokens from search params\n  const token = searchParams.get('token') || '';\n  const repoType = searchParams.get('type') || 'github';\n  const localPath = searchParams.get('local_path') ? decodeURIComponent(searchParams.get('local_path') || '') : undefined;\n  const repoUrl = searchParams.get('repo_url') ? decodeURIComponent(searchParams.get('repo_url') || '') : undefined;\n  const providerParam = searchParams.get('provider') || '';\n  const modelParam = searchParams.get('model') || '';\n  const isCustomModelParam = searchParams.get('is_custom_model') === 'true';\n  const customModelParam = searchParams.get('custom_model') || '';\n  const language = searchParams.get('language') || 'en';\n\n  // Import language context for translations\n  const { messages } = useLanguage();\n\n  // Initialize repo info with useMemo to prevent unnecessary re-renders\n  const repoInfo = useMemo<RepoInfo>(() => ({\n    owner,\n    repo,\n    type: repoType,\n    token: token || null,\n    localPath: localPath || null,\n    repoUrl: repoUrl || null\n  }), [owner, repo, repoType, token, localPath, repoUrl]);\n\n  // State variables\n  const [isLoading, setIsLoading] = useState(false);\n  const [loadingMessage, setLoadingMessage] = useState<string | undefined>(\n    messages.loading?.initializing || 'Initializing slides generation...'\n  );\n  const [error, setError] = useState<string | null>(null);\n  const [slides, setSlides] = useState<Slide[]>([]);\n  const [currentSlideIndex, setCurrentSlideIndex] = useState(0);\n  const [isExporting, setIsExporting] = useState(false);\n  const [exportError, setExportError] = useState<string | null>(null);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n\n  // Define a type for the wiki content\n  interface WikiPage {\n    id: string;\n    title: string;\n    content: string;\n    importance: string;\n    filePaths: string[];\n    relatedPages: string[];\n  }\n\n  interface WikiSection {\n    id: string;\n    title: string;\n    pages: string[];\n    subsections: string[];\n  }\n\n  interface WikiStructure {\n    description: string;\n    pages: WikiPage[];\n    sections: WikiSection[];\n    rootSections: string[];\n  }\n\n  interface WikiCacheData {\n    wiki_structure: WikiStructure;\n    generated_pages: Record<string, WikiPage>;\n  }\n\n  const [cachedWikiContent, setCachedWikiContent] = useState<WikiCacheData | null>(null);\n\n  // Function to fetch cached wiki content\n  const fetchCachedWikiContent = useCallback(async () => {\n    try {\n      const params = new URLSearchParams({\n        owner: repoInfo.owner,\n        repo: repoInfo.repo,\n        repo_type: repoInfo.type,\n        language: language,\n      });\n      const response = await fetch(`/api/wiki_cache?${params.toString()}`);\n\n      if (response.ok) {\n        const cachedData = await response.json();\n        if (cachedData && cachedData.wiki_structure && cachedData.generated_pages &&\n            Object.keys(cachedData.generated_pages).length > 0) {\n          console.log('Successfully fetched cached wiki data for slides generation');\n          setCachedWikiContent(cachedData);\n          return cachedData;\n        } else {\n          console.log('No valid wiki data in server cache or cache is empty.');\n          return null;\n        }\n      } else {\n        console.error('Error fetching wiki cache from server:', response.status);\n        return null;\n      }\n    } catch (error) {\n      console.error('Error loading from server cache:', error);\n      return null;\n    }\n  }, [repoInfo.owner, repoInfo.repo, repoInfo.type, language]);\n\n  // Generate slides content\n  const generateSlidesContent = useCallback(async () => {\n    if (isLoading) return;\n\n    setIsLoading(true);\n    setError(null);\n    // Clear previous content\n    setSlides([]);\n    setCurrentSlideIndex(0);\n    setLoadingMessage(messages.loading?.generatingSlides || 'Generating slides...');\n\n    try {\n      // Get repository URL\n      const repoUrl = getRepoUrl(repoInfo);\n\n      // Fetch cached wiki content if not already available\n      let wikiData = cachedWikiContent;\n      if (!wikiData) {\n        wikiData = await fetchCachedWikiContent();\n      }\n\n      // We'll just pass the entire wiki data to the LLM without complex processing\n      let wikiContent = '';\n\n      if (wikiData && wikiData.wiki_structure && wikiData.generated_pages) {\n        // Add the wiki structure description\n        wikiContent += `## Project Overview\\n${wikiData.wiki_structure.description || ''}\\n\\n`;\n\n        // Add all wiki pages content\n        const pages = wikiData.wiki_structure.pages || [];\n        const generatedPages = wikiData.generated_pages || {};\n\n        // Limit the total content to avoid token limits\n        let totalContentLength = 0;\n        const maxContentLength = 30000; // Approximate limit to avoid token issues\n\n        // First add high importance pages\n        const highImportancePages = pages.filter(page => page.importance === 'high');\n        for (const page of highImportancePages) {\n          if (generatedPages[page.id] && generatedPages[page.id].content) {\n            const content = `## ${page.title}\\n${generatedPages[page.id].content}\\n\\n`;\n            wikiContent += content;\n            totalContentLength += content.length;\n\n            if (totalContentLength > maxContentLength) break;\n          }\n        }\n\n        // Then add other pages if we still have space\n        if (totalContentLength < maxContentLength) {\n          for (const page of pages) {\n            // Skip high importance pages we've already added\n            if (page.importance === 'high') continue;\n\n            if (generatedPages[page.id] && generatedPages[page.id].content) {\n              const content = `## ${page.title}\\n${generatedPages[page.id].content}\\n\\n`;\n\n              // Check if adding this content would exceed our limit\n              if (totalContentLength + content.length > maxContentLength) {\n                // If it would exceed, just add a summary\n                const summaryMatch = generatedPages[page.id].content.match(/# .*?\\n\\n(.*?)(\\n\\n|$)/);\n                const summary = summaryMatch ? summaryMatch[1].trim() : 'No summary available';\n                const summaryContent = `## ${page.title}\\n${summary}\\n\\n`;\n\n                wikiContent += summaryContent;\n                totalContentLength += summaryContent.length;\n              } else {\n                // Otherwise add the full content\n                wikiContent += content;\n                totalContentLength += content.length;\n              }\n\n              if (totalContentLength > maxContentLength) break;\n            }\n          }\n        }\n      }\n\n      // First, get a plan for the slides\n      const planRequestBody: Record<string, unknown> = {\n        repo_url: repoUrl,\n        type: repoInfo.type,\n        messages: [{\n          role: 'user',\n          content: `Create an engaging outline for a high-quality marketing slide presentation about the ${owner}/${repo} repository.\n\nBased on this wiki content:\n${wikiContent}\n\nI need a numbered list of 7-8 creative slide titles with brief descriptions for a professional marketing presentation. Think of this as a pitch deck that would impress potential users or investors.\n\nFocus on:\n- Compelling value propositions\n- Unique selling points\n- Impressive features and capabilities\n- Real-world applications and benefits\n- Visually interesting concepts that can be represented creatively\n\nFor example, instead of generic titles like \"Introduction\" or \"Features\", use more engaging titles like:\n1. \"Revolutionizing Development with ${repo}\"\n2. \"Unlock Powerful Capabilities with Our Innovative Architecture\"\n3. \"How ${repo} Transforms Your Workflow\"\n\nGive me the numbered list with brief descriptions for each slide. Be creative but professional.`\n        }]\n      };\n\n      // Add tokens if available\n      addTokensToRequestBody(planRequestBody, token, repoInfo.type, providerParam, modelParam, isCustomModelParam, customModelParam, language);\n\n      // Use WebSocket for communication\n      let planContent = '';\n\n      try {\n        // Create WebSocket URL from the server base URL\n        const serverBaseUrl = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n        const wsBaseUrl = serverBaseUrl.replace(/^http/, 'ws')? serverBaseUrl.replace(/^https/, 'wss'): serverBaseUrl.replace(/^http/, 'ws');\n        const wsUrl = `${wsBaseUrl}/ws/chat`;\n\n        // Create a new WebSocket connection\n        const ws = new WebSocket(wsUrl);\n\n        // Create a single promise that handles the entire WebSocket lifecycle\n        await new Promise<void>((resolve, reject) => {\n          let isResolved = false;\n\n          // If the connection doesn't open or complete within 10 seconds, fall back to HTTP\n          const timeout = setTimeout(() => {\n            if (!isResolved) {\n              isResolved = true;\n              // Try to close the WebSocket if it's still open\n              if (ws.readyState === WebSocket.OPEN) {\n                ws.close();\n              }\n              reject(new Error('WebSocket connection timeout'));\n            }\n          }, 10000);\n\n          // Set up event handlers\n          ws.onopen = () => {\n            console.log('WebSocket connection established for slide plan');\n            // Send the request as JSON\n            ws.send(JSON.stringify(planRequestBody));\n            // Don't resolve here, wait for the complete response\n          };\n\n          ws.onmessage = (event) => {\n            const chunk = event.data;\n            planContent += chunk;\n          };\n\n          ws.onclose = () => {\n            clearTimeout(timeout);\n            console.log('WebSocket connection closed for slide plan');\n            if (!isResolved) {\n              isResolved = true;\n              resolve();\n            }\n          };\n\n          ws.onerror = (error) => {\n            console.error('WebSocket error:', error);\n            if (!isResolved) {\n              isResolved = true;\n              reject(new Error('WebSocket connection failed'));\n            }\n          };\n        });\n      } catch (wsError) {\n        console.error('WebSocket error, falling back to HTTP:', wsError);\n\n        // Fall back to HTTP if WebSocket fails\n        const planResponse = await fetch(`/api/chat/stream`, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(planRequestBody)\n        });\n\n        if (!planResponse.ok) {\n          throw new Error(`Error generating slide plan: ${planResponse.status}`);\n        }\n\n        // Process the plan response\n        planContent = '';\n        const planReader = planResponse.body?.getReader();\n        const planDecoder = new TextDecoder();\n\n        if (!planReader) {\n          throw new Error('Failed to get plan response reader');\n        }\n\n        try {\n          while (true) {\n            const { done, value } = await planReader.read();\n            if (done) break;\n            const chunk = planDecoder.decode(value, { stream: true });\n            planContent += chunk;\n          }\n          // Ensure final decoding\n          const finalChunk = planDecoder.decode();\n          planContent += finalChunk;\n        } catch (readError) {\n          console.error('Error reading plan stream:', readError);\n          throw new Error('Error processing plan response stream');\n        }\n      }\n\n      // Log the plan content for debugging\n      console.log(\"Received slide plan:\", planContent);\n\n      // Try multiple regex patterns to extract slide plan\n      let slideMatches: RegExpExecArray[] = [];\n\n      // Pattern 1: Standard numbered list with periods (1. Title: Description)\n      const pattern1 = /\\d+\\.\\s+(.*?)(?=\\n\\d+\\.|\\n*$)/g;\n      let match;\n      while ((match = pattern1.exec(planContent)) !== null) {\n        slideMatches.push(match);\n      }\n\n      // Pattern 2: Numbered list with parentheses (1) Title: Description\n      if (slideMatches.length === 0) {\n        const pattern2 = /\\d+\\)\\s+(.*?)(?=\\n\\d+\\)|\\n*$)/g;\n        while ((match = pattern2.exec(planContent)) !== null) {\n          slideMatches.push(match);\n        }\n      }\n\n      // Pattern 3: Look for lines with \"Slide\" followed by number\n      if (slideMatches.length === 0) {\n        const pattern3 = /Slide\\s+\\d+\\s*:?\\s*(.*?)(?=\\nSlide|\\n*$)/gi;\n        while ((match = pattern3.exec(planContent)) !== null) {\n          slideMatches.push(match);\n        }\n      }\n\n      // Pattern 4: Look for any lines with a title that might be a slide\n      if (slideMatches.length === 0) {\n        const pattern4 = /^([^:\\n]+)(?::\\s*(.*?))?$/gm;\n        while ((match = pattern4.exec(planContent)) !== null) {\n          // Filter out very short lines or lines that look like instructions\n          if (match[1].length > 3 && !match[1].toLowerCase().includes(\"please\") && !match[1].toLowerCase().includes(\"here\")) {\n            slideMatches.push(match);\n          }\n        }\n      }\n\n      // If we still don't have matches, create some default slides\n      if (slideMatches.length === 0) {\n        console.warn(\"Could not extract slide plan from response, using default slides\");\n\n        // Create default slides\n        const defaultSlides = [\n          `Title Slide: Introduction to ${repo}`,\n          `Overview: Key features and purpose of ${repo}`,\n          `Architecture: System components and structure`,\n          `Features: Main capabilities and functionalities`,\n          `Implementation: How it works and technical details`,\n          `Use Cases: How to use ${repo} effectively`,\n          `Conclusion: Summary and next steps`\n        ];\n\n        // Convert to match format\n        slideMatches = defaultSlides.map((slide, index) => {\n          const mockMatch = [\"\", slide] as unknown as RegExpExecArray;\n          mockMatch.index = index;\n          mockMatch.input = slide;\n          return mockMatch;\n        });\n      }\n\n      console.log(`Found ${slideMatches.length} slides in the plan`);\n\n\n      // Now generate each slide one by one\n      const generatedSlides: Slide[] = [];\n      let slideCounter = 1;\n\n      for (const slideMatch of slideMatches) {\n        const slideTitle = slideMatch[1].split(':')[0].trim();\n        const slideDescription = slideMatch[1].includes(':') ? slideMatch[1].split(':')[1].trim() : '';\n\n        setLoadingMessage(`Generating slide ${slideCounter} of ${slideMatches.length}: ${slideTitle}`);\n\n        // Create a request for this specific slide\n        const slideRequestBody: Record<string, unknown> = {\n          repo_url: repoUrl,\n          type: repoInfo.type,\n          messages: [{\n            role: 'user',\n            content: `Create a single HTML slide about the ${owner}/${repo} repository with the title \"${slideTitle}\".\n\nThis is slide ${slideCounter} of ${slideMatches.length} in the presentation.\n${slideDescription ? `The slide should cover: ${slideDescription}` : ''}\n\nUse the following wiki content as reference:\n${wikiContent}\n\nI need ONLY the HTML for this slide. The slide should maintain a consistent dark theme with gradients and professional styling, but BE CREATIVE with the content and layout.\n\nIMPORTANT LAYOUT REQUIREMENTS:\n1. The slide MUST be designed for a 16:9 HORIZONTAL layout (landscape orientation)\n2. All content MUST fit within the visible area without requiring scrolling\n3. Text must be properly sized and positioned for readability in a presentation context\n4. Content should be well-structured with clear visual hierarchy\n5. Use grid or flexbox layouts to ensure proper horizontal organization of content\n6. Limit text content to what can be comfortably read from a distance\n\nMARKETING QUALITY:\nCreate a genuinely high-quality marketing slide that would impress potential users or investors. Use compelling language, impactful visuals, and professional marketing techniques. Think of this as a slide for a professional pitch deck or product showcase.\n\nYou can use:\n- Two or three-column layouts for better horizontal space utilization\n- Engaging marketing copy with concise bullet points (no more than 4-5 per slide)\n- Visual metaphors and analogies positioned to the side of text content\n- Charts, diagrams, or code snippets when relevant (positioned appropriately)\n- Icons from Font Awesome (already included)\n- Creative use of gradients, shadows, and visual elements\n\nThe slide should maintain the dark theme aesthetic but can be uniquely designed. Use creative HTML/CSS to make the slide visually impressive while ensuring all content fits properly in the horizontal layout.\n\nHere's a basic structure to build upon (but feel free to be creative):\n\n<div class=\"slide\">\n    <div class=\"code-pattern\"></div>\n    <div class=\"accent-glow\"></div>\n\n    <div class=\"content\">\n        <!-- Use horizontal layout structures -->\n        <div class=\"slide-header\">\n            <h1 class=\"main-title\">${slideTitle}</h1>\n        </div>\n\n        <div class=\"slide-body\">\n            <!-- Consider using flex or grid layout here -->\n            <div class=\"left-column\">\n                <!-- Main points or text content -->\n            </div>\n            <div class=\"right-column\">\n                <!-- Visual elements, diagrams, or supporting content -->\n            </div>\n        </div>\n    </div>\n</div>\n<style>\n    /* Base styling with horizontal layout focus */\n    .slide {\n        width: 100%;\n        height: 100%;\n        background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);\n        color: #e6edf3;\n        display: flex;\n        flex-direction: column;\n        overflow: hidden;\n    }\n    .content {\n        display: flex;\n        flex-direction: column;\n        height: 100%;\n        padding: 40px 60px;\n        z-index: 2;\n    }\n    .slide-header {\n        margin-bottom: 30px;\n    }\n    .slide-body {\n        display: flex;\n        flex: 1;\n        gap: 40px;\n    }\n    .left-column, .right-column {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n    }\n</style>\n\nPlease return ONLY the HTML with no markdown formatting or code blocks. Just the raw HTML for the slide.`\n          }]\n        };\n\n        // Add tokens if available\n        addTokensToRequestBody(slideRequestBody, token, repoInfo.type, providerParam, modelParam, isCustomModelParam, customModelParam, language);\n\n        // Use WebSocket for communication\n        let slideContent = '';\n\n        try {\n          // Create WebSocket URL from the server base URL\n          const serverBaseUrl = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n          const wsBaseUrl = serverBaseUrl.replace(/^http/, 'ws')? serverBaseUrl.replace(/^https/, 'wss'): serverBaseUrl.replace(/^http/, 'ws');\n          const wsUrl = `${wsBaseUrl}/ws/chat`;\n\n          // Create a new WebSocket connection\n          const ws = new WebSocket(wsUrl);\n\n          // Create a single promise that handles the entire WebSocket lifecycle\n          await new Promise<void>((resolve, reject) => {\n            let isResolved = false;\n\n            // If the connection doesn't open or complete within 10 seconds, fall back to HTTP\n            const timeout = setTimeout(() => {\n              if (!isResolved) {\n                isResolved = true;\n                // Try to close the WebSocket if it's still open\n                if (ws.readyState === WebSocket.OPEN) {\n                  ws.close();\n                }\n                reject(new Error('WebSocket connection timeout'));\n              }\n            }, 10000);\n\n            // Set up event handlers\n            ws.onopen = () => {\n              console.log(`WebSocket connection established for slide ${slideCounter}`);\n              // Send the request as JSON\n              ws.send(JSON.stringify(slideRequestBody));\n              // Don't resolve here, wait for the complete response\n            };\n\n            ws.onmessage = (event) => {\n              const chunk = event.data;\n              slideContent += chunk;\n            };\n\n            ws.onclose = () => {\n              clearTimeout(timeout);\n              console.log(`WebSocket connection closed for slide ${slideCounter}`);\n              if (!isResolved) {\n                isResolved = true;\n                resolve();\n              }\n            };\n\n            ws.onerror = (error) => {\n              console.error('WebSocket error:', error);\n              if (!isResolved) {\n                isResolved = true;\n                reject(new Error('WebSocket connection failed'));\n              }\n            };\n          });\n        } catch (wsError) {\n          console.error('WebSocket error, falling back to HTTP:', wsError);\n\n          // Fall back to HTTP if WebSocket fails\n          const slideResponse = await fetch(`/api/chat/stream`, {\n            method: 'POST',\n            headers: {\n              'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(slideRequestBody)\n          });\n\n          if (!slideResponse.ok) {\n            throw new Error(`Error generating slide ${slideCounter}: ${slideResponse.status}`);\n          }\n\n          // Process the slide response\n          slideContent = '';\n          const slideReader = slideResponse.body?.getReader();\n          const slideDecoder = new TextDecoder();\n\n          if (!slideReader) {\n            throw new Error(`Failed to get reader for slide ${slideCounter}`);\n          }\n\n          try {\n            while (true) {\n              const { done, value } = await slideReader.read();\n              if (done) break;\n              const chunk = slideDecoder.decode(value, { stream: true });\n              slideContent += chunk;\n            }\n            // Ensure final decoding\n            const finalChunk = slideDecoder.decode();\n            slideContent += finalChunk;\n          } catch (readError) {\n            console.error(`Error reading slide ${slideCounter} stream:`, readError);\n            throw new Error(`Error processing slide ${slideCounter} response stream`);\n          }\n        }\n\n        // Extract HTML content - look for content between HTML tags or code blocks\n        let slideHtml = '';\n\n        console.log(`Processing slide ${slideCounter} response`);\n\n        // Try to extract from code blocks if present\n        const codeBlockMatch = slideContent.match(/```(?:html)?\\s*([\\s\\S]*?)\\s*```/);\n        if (codeBlockMatch) {\n          slideHtml = codeBlockMatch[1];\n          console.log(\"Extracted HTML from code block\");\n        }\n        // Try to extract content between <div class=\"slide\"> and closing </div>\n        else if (slideContent.includes('<div class=\"slide\"')) {\n          const divMatch = slideContent.match(/<div class=\"slide\"[\\s\\S]*?<\\/div>\\s*<\\/div>/);\n          if (divMatch) {\n            slideHtml = divMatch[0];\n            console.log(\"Extracted HTML from div tags\");\n          }\n        }\n        // Try to extract any HTML-like content\n        else if (slideContent.includes('<') && slideContent.includes('>')) {\n          const htmlTagMatch = slideContent.match(/<[\\s\\S]*?>/);\n          if (htmlTagMatch) {\n            // Find the first HTML tag\n            const firstTag = htmlTagMatch[0].match(/<([a-z][a-z0-9]*)/i);\n            if (firstTag && firstTag[1]) {\n              const tagName = firstTag[1];\n              // Try to extract everything from this opening tag to its closing tag\n              const fullTagRegex = new RegExp(`<${tagName}[\\\\s\\\\S]*?<\\\\/${tagName}>`, 'i');\n              const fullTagMatch = slideContent.match(fullTagRegex);\n              if (fullTagMatch) {\n                slideHtml = fullTagMatch[0];\n                console.log(`Extracted HTML using tag matching for ${tagName}`);\n              }\n            }\n          }\n        }\n\n        // If we still don't have HTML, use the raw content\n        if (!slideHtml) {\n          console.log(\"Using raw content as HTML\");\n          slideHtml = slideContent;\n        }\n\n        // Add default styling if not present\n        if (!slideHtml.includes('<style>') && !slideHtml.includes('<link rel=\"stylesheet\"')) {\n          slideHtml = `\n<div class=\"slide\">\n    <div class=\"code-pattern\"></div>\n    <div class=\"accent-glow\"></div>\n\n    <div class=\"content\">\n        <div class=\"slide-header\">\n            <h1 class=\"main-title\">${slideTitle}</h1>\n        </div>\n\n        <div class=\"slide-body\">\n            <div class=\"left-column\">\n                <div class=\"slide-content\">\n                    ${slideHtml}\n                </div>\n            </div>\n            <div class=\"right-column\">\n                <!-- The AI will likely provide content for both columns, but if not, this ensures proper layout -->\n                <div class=\"visual-content\">\n                    <i class=\"fas fa-code fa-5x\" style=\"opacity: 0.3; color: #58a6ff; margin: 2rem auto; display: block; text-align: center;\"></i>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n<style>\n    /* Base slide styling - optimized for horizontal layout */\n    .slide {\n        width: 100%;\n        height: 100%;\n        position: relative;\n        overflow: hidden;\n        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n        color: #e6edf3;\n        background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);\n        display: flex;\n        flex-direction: column;\n    }\n\n    /* Optional decorative elements that can be used or overridden */\n    .code-pattern {\n        position: absolute;\n        width: 100%;\n        height: 100%;\n        background-image: url(\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%2330363d' fill-opacity='0.15'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\");\n        opacity: 0.2;\n        z-index: 0;\n    }\n\n    .accent-glow {\n        position: absolute;\n        width: 600px;\n        height: 600px;\n        border-radius: 50%;\n        background: radial-gradient(circle, rgba(88, 166, 255, 0.1) 0%, rgba(88, 166, 255, 0) 70%);\n        top: -200px;\n        right: -100px;\n        z-index: 1;\n    }\n\n    /* Content container - optimized for horizontal layout */\n    .content {\n        z-index: 2;\n        position: relative;\n        height: 100%;\n        padding: 40px 60px;\n        display: flex;\n        flex-direction: column;\n    }\n\n    /* Slide structure for better horizontal organization */\n    .slide-header {\n        margin-bottom: 30px;\n    }\n\n    .slide-body {\n        display: flex;\n        flex: 1;\n        gap: 40px;\n        align-items: flex-start;\n    }\n\n    .left-column, .right-column {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n    }\n\n    /* Default title styling - can be overridden */\n    .main-title {\n        font-size: 3.5rem;\n        font-weight: 700;\n        background: linear-gradient(135deg, #58a6ff 0%, #8957e5 100%);\n        -webkit-background-clip: text;\n        background-clip: text;\n        -webkit-text-fill-color: transparent;\n        line-height: 1.1;\n        margin-bottom: 10px;\n    }\n\n    /* Default content styling - optimized for readability */\n    .slide-content {\n        font-size: 1.5rem;\n        color: #e6edf3;\n        line-height: 1.5;\n        display: flex;\n        flex-direction: column;\n    }\n\n    /* Ensure bullet points are properly spaced and aligned */\n    .slide-content ul, .slide-content ol {\n        margin: 0.5em 0;\n        padding-left: 1.5em;\n    }\n\n    .slide-content li {\n        margin-bottom: 0.5em;\n    }\n\n    /* Ensure code snippets don't overflow */\n    .slide-content pre, .slide-content code {\n        max-width: 100%;\n        overflow-x: auto;\n        white-space: pre-wrap;\n        font-size: 1.2rem;\n    }\n\n    /* Additional utility classes for creative layouts */\n    .flex-row { display: flex; flex-direction: row; }\n    .flex-col { display: flex; flex-direction: column; }\n    .items-center { align-items: center; }\n    .justify-center { justify-content: center; }\n    .justify-between { justify-content: space-between; }\n    .text-center { text-align: center; }\n    .text-right { text-align: right; }\n    .w-full { width: 100%; }\n    .h-full { height: 100%; }\n    .relative { position: relative; }\n    .absolute { position: absolute; }\n\n    /* Accent colors for creative use */\n    .text-accent-blue { color: #58a6ff; }\n    .text-accent-purple { color: #8957e5; }\n    .text-accent-green { color: #3fb950; }\n    .text-accent-orange { color: #f0883e; }\n    .bg-accent-blue { background-color: rgba(88, 166, 255, 0.2); }\n    .bg-accent-purple { background-color: rgba(137, 87, 229, 0.2); }\n</style>\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css\">\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.css\">\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/mermaid@10.0.0/dist/mermaid.min.js\"></script>\n<script>\n  // Initialize Mermaid for diagrams if present\n  document.addEventListener('DOMContentLoaded', function() {\n    if (typeof mermaid !== 'undefined') {\n      mermaid.initialize({\n        theme: 'dark',\n        securityLevel: 'loose',\n        startOnLoad: true\n      });\n    }\n\n    // Initialize any Chart.js charts if present\n    if (typeof Chart !== 'undefined') {\n      // Charts will be initialized by their own script tags\n    }\n  });\n</script>\n          `;\n        }\n\n        // Create the slide object\n        const slide: Slide = {\n          id: `slide-${slideCounter}`,\n          title: slideTitle,\n          content: slideDescription || slideTitle,\n          html: slideHtml\n        };\n\n        // Add to our slides array\n        generatedSlides.push(slide);\n\n        // Update the state with the slides we have so far\n        setSlides([...generatedSlides]);\n\n        slideCounter++;\n      }\n\n      // Set the final slides\n      setSlides(generatedSlides);\n\n    } catch (err) {\n      console.error('Error generating slides content:', err);\n      setError(err instanceof Error ? err.message : 'An unknown error occurred');\n    } finally {\n      setIsLoading(false);\n      setLoadingMessage(undefined);\n    }\n  }, [owner, repo, repoInfo, token, providerParam, modelParam, isCustomModelParam, customModelParam, language, isLoading, messages.loading, cachedWikiContent, fetchCachedWikiContent]);\n\n  // Export slides content\n  const exportSlides = useCallback(async () => {\n    if (!slides || slides.length === 0) {\n      setExportError('No slides to export');\n      return;\n    }\n\n    try {\n      setIsExporting(true);\n      setExportError(null);\n\n      // Create a full HTML document with all slides\n      const htmlContent = `\n<!DOCTYPE html>\n<html lang=\"${language}\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>${repo} Slides</title>\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css\">\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.css\">\n  <script src=\"https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/mermaid@10.0.0/dist/mermaid.min.js\"></script>\n  <style>\n    body {\n      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n      margin: 0;\n      padding: 0;\n      background-color: #0d1117;\n      color: #e6edf3;\n    }\n    .slide-container {\n      max-width: 1280px;\n      height: 720px; /* 16:9 aspect ratio */\n      margin: 2rem auto;\n      page-break-after: always;\n      position: relative;\n      overflow: hidden;\n      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);\n      border-radius: 8px;\n    }\n\n    /* Ensure proper horizontal layout in exported slides */\n    .slide-body {\n      display: flex;\n      flex: 1;\n      gap: 40px;\n      align-items: flex-start;\n    }\n\n    .left-column, .right-column {\n      flex: 1;\n      display: flex;\n      flex-direction: column;\n    }\n\n    /* Ensure content is properly sized */\n    .slide-content {\n      font-size: 1.5rem;\n      line-height: 1.5;\n    }\n\n    /* Ensure bullet points are properly spaced */\n    .slide-content ul, .slide-content ol {\n      margin: 0.5em 0;\n      padding-left: 1.5em;\n    }\n\n    .slide-content li {\n      margin-bottom: 0.5em;\n    }\n\n    /* Ensure code snippets don't overflow */\n    .slide-content pre, .slide-content code {\n      max-width: 100%;\n      overflow-x: auto;\n      white-space: pre-wrap;\n      font-size: 1.2rem;\n    }\n    @media print {\n      .slide-container {\n        page-break-after: always;\n        margin: 0;\n        height: 100vh;\n        display: flex;\n        flex-direction: column;\n        justify-content: center;\n        box-shadow: none;\n        border-radius: 0;\n      }\n    }\n    /* Navigation controls for presentation mode */\n    .nav-controls {\n      position: fixed;\n      bottom: 20px;\n      left: 50%;\n      transform: translateX(-50%);\n      display: flex;\n      gap: 20px;\n      z-index: 1000;\n      background: rgba(13, 17, 23, 0.8);\n      padding: 10px 20px;\n      border-radius: 30px;\n      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);\n    }\n    .nav-btn {\n      background: rgba(56, 139, 253, 0.1);\n      border: 1px solid rgba(56, 139, 253, 0.4);\n      color: #58a6ff;\n      border-radius: 50%;\n      width: 40px;\n      height: 40px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      cursor: pointer;\n      font-size: 18px;\n      transition: all 0.2s ease;\n    }\n    .nav-btn:hover {\n      background: rgba(56, 139, 253, 0.2);\n    }\n    .slide-indicator {\n      display: flex;\n      align-items: center;\n      color: #8b949e;\n      font-size: 14px;\n    }\n    @media print {\n      .nav-controls {\n        display: none;\n      }\n    }\n  </style>\n</head>\n<body>\n  ${slides.map(slide => `<div class=\"slide-container\">${slide.html}</div>`).join('\\n')}\n\n  <!-- Navigation controls (only visible in browser) -->\n  <div class=\"nav-controls\">\n    <div class=\"nav-btn prev-slide\" onclick=\"prevSlide()\">\n      <i class=\"fas fa-chevron-left\"></i>\n    </div>\n    <div class=\"slide-indicator\">\n      <span id=\"current-slide\">1</span>/<span id=\"total-slides\">${slides.length}</span>\n    </div>\n    <div class=\"nav-btn next-slide\" onclick=\"nextSlide()\">\n      <i class=\"fas fa-chevron-right\"></i>\n    </div>\n  </div>\n\n  <script>\n    // Simple presentation navigation\n    let currentSlide = 1;\n    const totalSlides = ${slides.length};\n    const slideContainers = document.querySelectorAll('.slide-container');\n\n    // Initialize - show only first slide\n    function initSlides() {\n      slideContainers.forEach((slide, index) => {\n        if (index === 0) {\n          slide.style.display = 'block';\n        } else {\n          slide.style.display = 'none';\n        }\n      });\n      updateIndicator();\n    }\n\n    function showSlide(slideNumber) {\n      slideContainers.forEach((slide, index) => {\n        slide.style.display = index + 1 === slideNumber ? 'block' : 'none';\n      });\n      updateIndicator();\n    }\n\n    function nextSlide() {\n      if (currentSlide < totalSlides) {\n        currentSlide++;\n        showSlide(currentSlide);\n      }\n    }\n\n    function prevSlide() {\n      if (currentSlide > 1) {\n        currentSlide--;\n        showSlide(currentSlide);\n      }\n    }\n\n    function updateIndicator() {\n      document.getElementById('current-slide').textContent = currentSlide;\n    }\n\n    // Keyboard navigation\n    document.addEventListener('keydown', (e) => {\n      if (e.key === 'ArrowRight' || e.key === ' ') {\n        nextSlide();\n      } else if (e.key === 'ArrowLeft') {\n        prevSlide();\n      }\n    });\n\n    // Initialize on load\n    window.onload = function() {\n      initSlides();\n\n      // Initialize Mermaid diagrams if present\n      if (typeof mermaid !== 'undefined') {\n        mermaid.initialize({\n          theme: 'dark',\n          securityLevel: 'loose',\n          startOnLoad: true\n        });\n      }\n    };\n  </script>\n</body>\n</html>\n      `;\n\n      // Create a blob with the HTML content\n      const blob = new Blob([htmlContent], { type: 'text/html' });\n      const url = window.URL.createObjectURL(blob);\n      const a = document.createElement('a');\n      a.href = url;\n      a.download = `${repo}_slides.html`;\n      document.body.appendChild(a);\n      a.click();\n      window.URL.revokeObjectURL(url);\n      document.body.removeChild(a);\n\n    } catch (err) {\n      console.error('Error exporting slides:', err);\n      setExportError(err instanceof Error ? err.message : 'An unknown error occurred');\n    } finally {\n      setIsExporting(false);\n    }\n  }, [slides, repo, language]);\n\n  // Navigation functions\n  const goToNextSlide = useCallback(() => {\n    if (currentSlideIndex < slides.length - 1) {\n      setCurrentSlideIndex(prev => prev + 1);\n    }\n  }, [currentSlideIndex, slides.length]);\n\n  const goToPrevSlide = useCallback(() => {\n    if (currentSlideIndex > 0) {\n      setCurrentSlideIndex(prev => prev - 1);\n    }\n  }, [currentSlideIndex]);\n\n  const toggleFullscreen = useCallback(() => {\n    setIsFullscreen(prev => !prev);\n  }, []);\n\n  // Handle keyboard navigation\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'ArrowRight' || e.key === 'Space') {\n        goToNextSlide();\n      } else if (e.key === 'ArrowLeft') {\n        goToPrevSlide();\n      } else if (e.key === 'f' || e.key === 'F') {\n        toggleFullscreen();\n      } else if (e.key === 'Escape' && isFullscreen) {\n        setIsFullscreen(false);\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [goToNextSlide, goToPrevSlide, toggleFullscreen, isFullscreen]);\n\n  // Track if we've already generated content\n  const contentGeneratedRef = useRef(false);\n\n  // Generate slides content on page load, but only once\n  useEffect(() => {\n    if (!contentGeneratedRef.current) {\n      contentGeneratedRef.current = true;\n\n      // First fetch the cached wiki content, then generate the slides\n      (async () => {\n        await fetchCachedWikiContent();\n        generateSlidesContent();\n      })();\n    }\n  }, [generateSlidesContent, fetchCachedWikiContent]);\n\n  return (\n    <div className={`min-h-screen flex flex-col ${isFullscreen ? 'fixed inset-0 z-50 bg-[#0d1117]' : 'bg-[var(--background)]'}`}>\n      {/* Header - Hide in fullscreen mode */}\n      {!isFullscreen && (\n        <header className=\"sticky top-0 z-10 bg-[var(--card-bg)] border-b border-[var(--border-color)] shadow-sm\">\n          <div className=\"container mx-auto px-4 py-3 flex items-center justify-between\">\n            <div className=\"flex items-center space-x-4\">\n              <Link\n                href={`/${owner}/${repo}${window.location.search}`}\n                className=\"flex items-center text-[var(--foreground)] hover:text-[var(--accent-primary)] transition-colors\"\n              >\n                <FaArrowLeft className=\"mr-2\" />\n                <span>{messages.slides?.backToWiki || 'Back to Wiki'}</span>\n              </Link>\n              <h1 className=\"text-xl font-bold text-[var(--accent-primary)]\">\n                {messages.slides?.title || 'Slides'}: {repo}\n              </h1>\n            </div>\n            <div className=\"flex items-center space-x-3\">\n              <button\n                onClick={generateSlidesContent}\n                disabled={isLoading}\n                className={`p-2 rounded-md ${isLoading ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n                title={messages.slides?.regenerate || 'Regenerate Slides'}\n              >\n                <FaSync className={`${isLoading ? 'animate-spin' : ''}`} />\n              </button>\n              <button\n                onClick={exportSlides}\n                disabled={!slides.length || isExporting}\n                className={`p-2 rounded-md ${!slides.length || isExporting ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n                title={messages.slides?.export || 'Export Slides'}\n              >\n                <FaDownload />\n              </button>\n              <button\n                onClick={toggleFullscreen}\n                className=\"p-2 rounded-md bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20 transition-colors\"\n                title={messages.slides?.fullscreen || 'Toggle Fullscreen'}\n              >\n                <FaArrowUp />\n              </button>\n              <ThemeToggle />\n            </div>\n          </div>\n        </header>\n      )}\n\n      {/* Main content */}\n      <main className={`flex-1 flex flex-col ${isFullscreen ? 'p-0' : 'container mx-auto px-4 py-6'}`}>\n        {isLoading && !slides.length ? (\n          <div className=\"flex flex-col items-center justify-center p-8 flex-grow\">\n            <div className=\"w-12 h-12 border-4 border-[var(--accent-primary)]/30 border-t-[var(--accent-primary)] rounded-full animate-spin mb-4\"></div>\n            <p className=\"text-[var(--foreground)]\">{loadingMessage}</p>\n          </div>\n        ) : error ? (\n          <div className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-6\">\n            <h3 className=\"text-red-800 dark:text-red-400 font-medium mb-2\">{messages.common?.error || 'Error'}</h3>\n            <p className=\"text-red-700 dark:text-red-300\">{error}</p>\n          </div>\n        ) : slides.length > 0 ? (\n          <div className=\"flex flex-col flex-grow\">\n            {/* Slide content */}\n            <div className={`flex-grow flex flex-col items-center justify-center ${isFullscreen ? 'p-0 bg-[#0d1117]' : 'bg-[var(--card-bg)] border border-[var(--border-color)] rounded-lg shadow-sm p-6 mb-4'}`}>\n              {exportError && (\n                <div className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3 mb-4 w-full\">\n                  <p className=\"text-red-700 dark:text-red-300 text-sm\">{exportError}</p>\n                </div>\n              )}\n\n              {/* Current slide */}\n              <div\n                className={`${isFullscreen ? 'w-full h-full' : 'w-full max-w-[1280px] aspect-[16/9]'} flex items-center justify-center overflow-hidden`}\n              >\n                {/* Include Font Awesome for icons */}\n                <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css\" />\n                <div className=\"w-full h-full\" dangerouslySetInnerHTML={{ __html: slides[currentSlideIndex]?.html || '' }} />\n              </div>\n            </div>\n\n            {/* Navigation controls */}\n            <div className={`flex items-center justify-between ${isFullscreen ? 'fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-[#0d1117]/80 px-6 py-3 rounded-full z-10 shadow-lg' : 'mt-4'}`}>\n              <button\n                onClick={goToPrevSlide}\n                disabled={currentSlideIndex === 0}\n                className={`p-2 rounded-md ${currentSlideIndex === 0 ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n              >\n                <FaArrowLeft />\n              </button>\n\n              <div className={`text-[var(--foreground)] ${isFullscreen ? 'mx-4' : ''}`}>\n                Slide {currentSlideIndex + 1} of {slides.length}\n              </div>\n\n              <button\n                onClick={goToNextSlide}\n                disabled={currentSlideIndex === slides.length - 1}\n                className={`p-2 rounded-md ${currentSlideIndex === slides.length - 1 ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n              >\n                <FaArrowRight />\n              </button>\n\n              {isFullscreen && (\n                <button\n                  onClick={toggleFullscreen}\n                  className=\"p-2 ml-4 rounded-md bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20 transition-colors\"\n                  title={messages.slides?.fullscreen || 'Exit Fullscreen'}\n                >\n                  <FaTimes />\n                </button>\n              )}\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-col items-center justify-center p-8 flex-grow\">\n            <p className=\"text-[var(--foreground)]\">{messages.slides?.noSlides || 'No slides generated yet. Click the refresh button to generate slides.'}</p>\n          </div>\n        )}\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/[owner]/[repo]/workshop/page.tsx",
    "content": "'use client';\n\nimport React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';\nimport { useParams, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { FaArrowLeft, FaSync, FaDownload } from 'react-icons/fa';\nimport ThemeToggle from '@/components/theme-toggle';\nimport Markdown from '@/components/Markdown';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport { RepoInfo } from '@/types/repoinfo';\nimport getRepoUrl from '@/utils/getRepoUrl';\n\n// Helper function to add tokens and other parameters to request body\nconst addTokensToRequestBody = (\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  requestBody: Record<string, any>,\n  token: string,\n  repoType: string,\n  provider: string = '',\n  model: string = '',\n  isCustomModel: boolean = false,\n  customModel: string = '',\n  language: string = 'en',\n) => {\n  if (token !== '') {\n    requestBody.token = token;\n  }\n\n  // Add provider-based model selection parameters\n  requestBody.provider = provider;\n  requestBody.model = model;\n  if (isCustomModel && customModel) {\n    requestBody.custom_model = customModel;\n  }\n\n  requestBody.language = language;\n};\n\nexport default function WorkshopPage() {\n  // Get route parameters and search params\n  const params = useParams();\n  const searchParams = useSearchParams();\n\n  // Extract owner and repo from route params\n  const owner = params.owner as string;\n  const repo = params.repo as string;\n\n  // Extract tokens from search params\n  const token = searchParams.get('token') || '';\n  const repoType = searchParams.get('type') || 'github';\n  const localPath = searchParams.get('local_path') ? decodeURIComponent(searchParams.get('local_path') || '') : undefined;\n  const repoUrl = searchParams.get('repo_url') ? decodeURIComponent(searchParams.get('repo_url') || '') : undefined;\n  const providerParam = searchParams.get('provider') || '';\n  const modelParam = searchParams.get('model') || '';\n  const isCustomModelParam = searchParams.get('is_custom_model') === 'true';\n  const customModelParam = searchParams.get('custom_model') || '';\n  const language = searchParams.get('language') || 'en';\n\n  // Import language context for translations\n  const { messages } = useLanguage();\n\n  // Initialize repo info with useMemo to prevent unnecessary re-renders\n  const repoInfo = useMemo<RepoInfo>(() => ({\n    owner,\n    repo,\n    type: repoType,\n    token: token || null,\n    localPath: localPath || null,\n    repoUrl: repoUrl || null\n  }), [owner, repo, repoType, token, localPath, repoUrl]);\n\n  // State variables\n  const [isLoading, setIsLoading] = useState(false);\n  const [loadingMessage, setLoadingMessage] = useState<string | undefined>(\n    messages.loading?.initializing || 'Initializing workshop generation...'\n  );\n  const [error, setError] = useState<string | null>(null);\n  const [workshopContent, setWorkshopContent] = useState<string>('');\n  const [isExporting, setIsExporting] = useState(false);\n  const [exportError, setExportError] = useState<string | null>(null);\n  // Define a type for the wiki content\n  interface WikiPage {\n    id: string;\n    title: string;\n    content: string;\n    importance: string;\n    filePaths: string[];\n    relatedPages: string[];\n  }\n\n  interface WikiSection {\n    id: string;\n    title: string;\n    pages: string[];\n    subsections: string[];\n  }\n\n  interface WikiStructure {\n    description: string;\n    pages: WikiPage[];\n    sections: WikiSection[];\n    rootSections: string[];\n  }\n\n  interface WikiCacheData {\n    wiki_structure: WikiStructure;\n    generated_pages: Record<string, WikiPage>;\n  }\n\n  const [cachedWikiContent, setCachedWikiContent] = useState<WikiCacheData | null>(null);\n\n  // Function to fetch cached wiki content\n  const fetchCachedWikiContent = useCallback(async () => {\n    try {\n      const params = new URLSearchParams({\n        owner: repoInfo.owner,\n        repo: repoInfo.repo,\n        repo_type: repoInfo.type,\n        language: language,\n      });\n      const response = await fetch(`/api/wiki_cache?${params.toString()}`);\n\n      if (response.ok) {\n        const cachedData = await response.json();\n        if (cachedData && cachedData.wiki_structure && cachedData.generated_pages &&\n            Object.keys(cachedData.generated_pages).length > 0) {\n          console.log('Successfully fetched cached wiki data for workshop generation');\n          setCachedWikiContent(cachedData);\n          return cachedData;\n        } else {\n          console.log('No valid wiki data in server cache or cache is empty.');\n          return null;\n        }\n      } else {\n        console.error('Error fetching wiki cache from server:', response.status);\n        return null;\n      }\n    } catch (error) {\n      console.error('Error loading from server cache:', error);\n      return null;\n    }\n  }, [repoInfo.owner, repoInfo.repo, repoInfo.type, language]);\n\n  // Generate workshop content\n  const generateWorkshopContent = useCallback(async () => {\n    if (isLoading) return;\n\n    setIsLoading(true);\n    setError(null);\n    // Clear previous content\n    setWorkshopContent('');\n    setLoadingMessage(messages.loading?.generatingWorkshop || 'Generating workshop content...');\n\n    try {\n      // Get repository URL\n      const repoUrl = getRepoUrl(repoInfo);\n\n      // Fetch cached wiki content if not already available\n      let wikiData = cachedWikiContent;\n      if (!wikiData) {\n        wikiData = await fetchCachedWikiContent();\n      }\n\n      // We'll just pass the entire wiki data to the LLM without complex processing\n      let wikiContent = '';\n\n      if (wikiData && wikiData.wiki_structure && wikiData.generated_pages) {\n        // Add the wiki structure description\n        wikiContent += `## Project Overview\\n${wikiData.wiki_structure.description || ''}\\n\\n`;\n\n        // Add all wiki pages content\n        const pages = wikiData.wiki_structure.pages || [];\n        const generatedPages = wikiData.generated_pages || {};\n\n        // Limit the total content to avoid token limits\n        let totalContentLength = 0;\n        const maxContentLength = 30000; // Approximate limit to avoid token issues\n\n        // First add high importance pages\n        const highImportancePages = pages.filter(page => page.importance === 'high');\n        for (const page of highImportancePages) {\n          if (generatedPages[page.id] && generatedPages[page.id].content) {\n            const content = `## ${page.title}\\n${generatedPages[page.id].content}\\n\\n`;\n            wikiContent += content;\n            totalContentLength += content.length;\n\n            if (totalContentLength > maxContentLength) break;\n          }\n        }\n\n        // Then add other pages if we still have space\n        if (totalContentLength < maxContentLength) {\n          for (const page of pages) {\n            // Skip high importance pages we've already added\n            if (page.importance === 'high') continue;\n\n            if (generatedPages[page.id] && generatedPages[page.id].content) {\n              const content = `## ${page.title}\\n${generatedPages[page.id].content}\\n\\n`;\n\n              // Check if adding this content would exceed our limit\n              if (totalContentLength + content.length > maxContentLength) {\n                // If it would exceed, just add a summary\n                const summaryMatch = generatedPages[page.id].content.match(/# .*?\\n\\n(.*?)(\\n\\n|$)/);\n                const summary = summaryMatch ? summaryMatch[1].trim() : 'No summary available';\n                const summaryContent = `## ${page.title}\\n${summary}\\n\\n`;\n\n                wikiContent += summaryContent;\n                totalContentLength += summaryContent.length;\n              } else {\n                // Otherwise add the full content\n                wikiContent += content;\n                totalContentLength += content.length;\n              }\n\n              if (totalContentLength > maxContentLength) break;\n            }\n          }\n        }\n      }\n\n      // Prepare request body with enhanced context from wiki\n      const requestBody: Record<string, unknown> = {\n        repo_url: repoUrl,\n        type: repoInfo.type,\n        messages: [{\n          role: 'user',\n          content: `Create a comprehensive workshop for learning how to use and contribute to the ${owner}/${repo} repository.\n\nI'll provide you with information from the project's wiki to help you create a more accurate and relevant workshop.\n\n${wikiContent}\n\nThis workshop should be designed as a hands-on tutorial that guides users through understanding, using, and potentially contributing to this project. The workshop should be highly readable and optimized for quick onboarding of new users.\n\nThe workshop should include:\n\n1. A series of progressive exercises that build on each other (at least 3-4 exercises)\n2. Clear instructions for each exercise with step-by-step guidance\n3. Code examples and snippets where appropriate\n4. \"Challenge\" sections that encourage deeper exploration\n5. Solutions for each exercise and challenge (in collapsible sections using <details> tags)\n6. Explanations that connect the exercises to the actual codebase\n\nFormat the workshop in Markdown with the following structure:\n\n# ${repo} Workshop\n\n## Introduction\n- Brief overview of the project\n- What users will learn in this workshop\n- Prerequisites and setup instructions\n\n## Exercise 1: [First Core Concept]\n- Explanation of the concept\n- Step-by-step instructions with clear formatting\n- Expected outcome\n- Challenge (optional harder task)\n- Solution (in a collapsible section using <details> tags)\n\n## Exercise 2: [Second Core Concept]\n...\n\n## Exercise 3: [Third Core Concept]\n...\n\n## Final Project\n- A culminating exercise that brings together multiple concepts\n- Clear success criteria\n- Solution\n\n## Next Steps\n- Suggestions for further learning\n- How to contribute to the project\n- Additional resources\n\nIMPORTANT FORMATTING GUIDELINES:\n1. Use clear headings and subheadings with proper hierarchy\n2. Use bullet points and numbered lists for clarity\n3. Highlight important information in **bold** or with blockquotes\n4. Use code blocks with proper syntax highlighting\n5. Include Mermaid diagrams where they would help illustrate concepts or workflows\n6. Put solutions in collapsible <details> sections\n7. Use tables for comparing options or summarizing information\n8. Break long sections into smaller, digestible chunks\n9. Use consistent formatting throughout\n\nIMPORTANT CONTENT GUIDELINES:\n1. Make sure each exercise focuses on a REAL aspect of the ${repo} repository\n2. Use REAL code examples from the repository, not generic examples\n3. Create exercises that are practical and relevant to the actual codebase\n4. Include at least 3-4 exercises covering different aspects of the repository\n5. The final project should be challenging but achievable\n6. Ensure the workshop is specific to this repository, not generic\n7. Focus on the most important/core features of the repository\n8. Include diagrams to visualize complex concepts\n9. Make sure the workshop is engaging and interactive\n\nMake the workshop content in ${language === 'en' ? 'English' :\n  language === 'ja' ? 'Japanese (日本語)' :\n  language === 'zh' ? 'Mandarin Chinese (中文)' :\n  language === 'zh-tw' ? 'Traditional Chinese (繁體中文)' :\n  language === 'es' ? 'Spanish (Español)' :\n  language === 'kr' ? 'Korean (한국어)' :\n  language === 'vi' ? 'Vietnamese (Tiếng Việt)' : \n  language === \"pt-br\" ? \"Brazilian Portuguese (Português Brasileiro)\" :\n  language === \"fr\" ? \"Français (French)\" :\n  language === \"ru\" ? \"Русский (Russian)\" :\n  'English'} language.`\n        }]\n      };\n\n      // Add tokens if available\n      addTokensToRequestBody(requestBody, token, repoInfo.type, providerParam, modelParam, isCustomModelParam, customModelParam, language);\n\n      // Use WebSocket for communication\n      let content = '';\n\n      try {\n        // Create WebSocket URL from the server base URL\n        const serverBaseUrl = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n        const wsBaseUrl = serverBaseUrl.replace(/^http/, 'ws')? serverBaseUrl.replace(/^https/, 'wss'): serverBaseUrl.replace(/^http/, 'ws');\n        const wsUrl = `${wsBaseUrl}/ws/chat`;\n\n        // Create a new WebSocket connection\n        const ws = new WebSocket(wsUrl);\n\n        // Create a promise that resolves when the WebSocket connection is complete\n        await new Promise<void>((resolve, reject) => {\n          // Set up event handlers\n          ws.onopen = () => {\n            console.log('WebSocket connection established for workshop generation');\n            // Send the request as JSON\n            ws.send(JSON.stringify(requestBody));\n            resolve();\n          };\n\n          ws.onerror = (error) => {\n            console.error('WebSocket error:', error);\n            reject(new Error('WebSocket connection failed'));\n          };\n\n          // If the connection doesn't open within 5 seconds, fall back to HTTP\n          const timeout = setTimeout(() => {\n            reject(new Error('WebSocket connection timeout'));\n          }, 5000);\n\n          // Clear the timeout if the connection opens successfully\n          ws.onopen = () => {\n            clearTimeout(timeout);\n            console.log('WebSocket connection established for workshop generation');\n            // Send the request as JSON\n            ws.send(JSON.stringify(requestBody));\n            resolve();\n          };\n        });\n\n        // Create a promise that resolves when the WebSocket response is complete\n        await new Promise<void>((resolve, reject) => {\n          // Use a local variable to accumulate content\n          let accumulatedContent = '';\n\n          // Handle incoming messages\n          ws.onmessage = (event) => {\n            const chunk = event.data;\n            content += chunk;\n            accumulatedContent += chunk;\n\n            // Update the state with the accumulated content\n            setWorkshopContent(accumulatedContent);\n          };\n\n          // Handle WebSocket close\n          ws.onclose = () => {\n            console.log('WebSocket connection closed for workshop generation');\n            resolve();\n          };\n\n          // Handle WebSocket errors\n          ws.onerror = (error) => {\n            console.error('WebSocket error during message reception:', error);\n            reject(new Error('WebSocket error during message reception'));\n          };\n        });\n      } catch (wsError) {\n        console.error('WebSocket error, falling back to HTTP:', wsError);\n\n        // Fall back to HTTP if WebSocket fails\n        const response = await fetch(`/api/chat/stream`, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(requestBody)\n        });\n\n        if (!response.ok) {\n          const errorText = await response.text().catch(() => 'No error details available');\n          throw new Error(`Error generating workshop content: ${response.status} - ${errorText}`);\n        }\n\n        // Process the response\n        content = '';\n        const reader = response.body?.getReader();\n        const decoder = new TextDecoder();\n\n        if (!reader) {\n          throw new Error('Failed to get response reader');\n        }\n\n        try {\n          // Use a local variable to accumulate content\n          let accumulatedContent = '';\n\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n            const chunk = decoder.decode(value, { stream: true });\n            content += chunk;\n            accumulatedContent += chunk;\n\n            // Update the state with the accumulated content\n            setWorkshopContent(accumulatedContent);\n          }\n          // Ensure final decoding\n          const finalChunk = decoder.decode();\n          content += finalChunk;\n          accumulatedContent += finalChunk;\n          setWorkshopContent(accumulatedContent);\n        } catch (readError) {\n          console.error('Error reading stream:', readError);\n          throw new Error('Error processing response stream');\n        }\n      }\n\n      // Clean up markdown delimiters\n      content = content.replace(/^```markdown\\s*/i, '').replace(/```\\s*$/i, '');\n\n      // Add a table of contents if it doesn't already have one\n      if (!content.includes('## Table of Contents') && !content.includes('## Contents')) {\n        const headings = content.match(/^## (.*)$/gm) || [];\n        if (headings.length > 0) {\n          let toc = '## Table of Contents\\n\\n';\n          headings.forEach(heading => {\n            const headingText = heading.replace('## ', '');\n            // Create a link-friendly version of the heading\n            const headingLink = headingText\n              .toLowerCase()\n              .replace(/[^\\w\\s-]/g, '')\n              .replace(/\\s+/g, '-');\n            toc += `- [${headingText}](#${headingLink})\\n`;\n          });\n          toc += '\\n';\n\n          // Find the position after the introduction heading\n          const introPos = content.indexOf('# ') + 1;\n          const nextHeadingPos = content.indexOf('## ', introPos);\n\n          if (nextHeadingPos > introPos) {\n            // Insert the TOC after the introduction\n            content = content.slice(0, nextHeadingPos) + toc + content.slice(nextHeadingPos);\n          }\n        }\n      }\n\n      // Add progress indicators to exercises\n      const exerciseHeadings = content.match(/^## Exercise \\d+:/gm) || [];\n      if (exerciseHeadings.length > 0) {\n        const totalExercises = exerciseHeadings.length;\n\n        // Replace each exercise heading with a heading that includes a progress indicator\n        for (let i = 0; i < totalExercises; i++) {\n          const exerciseHeading = exerciseHeadings[i];\n\n          // Estimate time to complete based on exercise number (earlier exercises are usually simpler)\n          let estimatedTime = 10; // default 10 minutes\n          if (i === 0) estimatedTime = 5; // first exercise is usually simpler\n          else if (i === totalExercises - 1) estimatedTime = 15; // last exercise is usually more complex\n          else if (i > Math.floor(totalExercises / 2)) estimatedTime = 12; // later exercises are more complex\n\n          const progressIndicator = `<div style=\"text-align: right; font-size: 0.85em; color: #666;\">\nExercise ${i + 1} of ${totalExercises} | Estimated time: ${estimatedTime} minutes\n</div>\\n\\n`;\n\n          // Find the position of the exercise heading\n          const headingPos = content.indexOf(exerciseHeading);\n          if (headingPos !== -1) {\n            // Find the end of the line\n            const lineEndPos = content.indexOf('\\n', headingPos);\n            if (lineEndPos !== -1) {\n              // Insert the progress indicator after the heading\n              content = content.slice(0, lineEndPos + 1) + progressIndicator + content.slice(lineEndPos + 1);\n            }\n          }\n        }\n      }\n\n      // Add a note about the final project\n      const finalProjectHeading = content.match(/^## Final Project/m);\n      if (finalProjectHeading) {\n        const headingPos = content.indexOf(finalProjectHeading[0]);\n        if (headingPos !== -1) {\n          const lineEndPos = content.indexOf('\\n', headingPos);\n          if (lineEndPos !== -1) {\n            const finalProjectNote = `<div style=\"text-align: right; font-size: 0.85em; color: #666;\">\nEstimated time: 20-30 minutes | Combines concepts from all exercises\n</div>\\n\\n`;\n            content = content.slice(0, lineEndPos + 1) + finalProjectNote + content.slice(lineEndPos + 1);\n          }\n        }\n      }\n\n      setWorkshopContent(content);\n\n    } catch (err) {\n      console.error('Error generating workshop content:', err);\n      setError(err instanceof Error ? err.message : 'An unknown error occurred');\n    } finally {\n      setIsLoading(false);\n      setLoadingMessage(undefined);\n    }\n  }, [owner, repo, repoInfo, token, providerParam, modelParam, isCustomModelParam, customModelParam, language, isLoading, messages.loading, cachedWikiContent, fetchCachedWikiContent]);\n\n  // Export workshop content\n  const exportWorkshop = useCallback(async () => {\n    if (!workshopContent) {\n      setExportError('No workshop content to export');\n      return;\n    }\n\n    try {\n      setIsExporting(true);\n      setExportError(null);\n\n      // Create a blob with the workshop content\n      const blob = new Blob([workshopContent], { type: 'text/markdown' });\n      const url = window.URL.createObjectURL(blob);\n      const a = document.createElement('a');\n      a.href = url;\n      a.download = `${repo}_workshop.md`;\n      document.body.appendChild(a);\n      a.click();\n      window.URL.revokeObjectURL(url);\n      document.body.removeChild(a);\n\n    } catch (err) {\n      console.error('Error exporting workshop:', err);\n      setExportError(err instanceof Error ? err.message : 'An unknown error occurred');\n    } finally {\n      setIsExporting(false);\n    }\n  }, [workshopContent, repo]);\n\n  // Track if we've already generated content\n  const contentGeneratedRef = useRef(false);\n\n  // Generate workshop content on page load, but only once\n  useEffect(() => {\n    if (!contentGeneratedRef.current) {\n      contentGeneratedRef.current = true;\n\n      // First fetch the cached wiki content, then generate the workshop\n      (async () => {\n        await fetchCachedWikiContent();\n        generateWorkshopContent();\n      })();\n    }\n  }, [generateWorkshopContent, fetchCachedWikiContent]);\n\n  return (\n    <div className=\"min-h-screen flex flex-col bg-[var(--background)]\">\n      {/* Header */}\n      <header className=\"sticky top-0 z-10 bg-[var(--card-bg)] border-b border-[var(--border-color)] shadow-sm\">\n        <div className=\"container mx-auto px-4 py-3 flex items-center justify-between\">\n          <div className=\"flex items-center space-x-4\">\n            <Link\n              href={`/${owner}/${repo}${window.location.search}`}\n              className=\"flex items-center text-[var(--foreground)] hover:text-[var(--accent-primary)] transition-colors\"\n            >\n              <FaArrowLeft className=\"mr-2\" />\n              <span>{messages.workshop?.backToWiki || 'Back to Wiki'}</span>\n            </Link>\n            <h1 className=\"text-xl font-bold text-[var(--accent-primary)]\">\n              {messages.workshop?.title || 'Workshop'}: {repo}\n            </h1>\n          </div>\n          <div className=\"flex items-center space-x-3\">\n            <button\n              onClick={generateWorkshopContent}\n              disabled={isLoading}\n              className={`p-2 rounded-md ${isLoading ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n              title={messages.workshop?.regenerate || 'Regenerate Workshop'}\n            >\n              <FaSync className={`${isLoading ? 'animate-spin' : ''}`} />\n            </button>\n            <button\n              onClick={exportWorkshop}\n              disabled={!workshopContent || isExporting}\n              className={`p-2 rounded-md ${!workshopContent || isExporting ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)]' : 'bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/20'} transition-colors`}\n              title={messages.workshop?.export || 'Export Workshop'}\n            >\n              <FaDownload />\n            </button>\n            <ThemeToggle />\n          </div>\n        </div>\n      </header>\n\n      {/* Main content */}\n      <main className=\"flex-1 container mx-auto px-4 py-6\">\n        {isLoading && !workshopContent ? (\n          <div className=\"flex flex-col items-center justify-center p-8\">\n            <div className=\"w-12 h-12 border-4 border-[var(--accent-primary)]/30 border-t-[var(--accent-primary)] rounded-full animate-spin mb-4\"></div>\n            <p className=\"text-[var(--foreground)]\">{loadingMessage}</p>\n          </div>\n        ) : error ? (\n          <div className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-6\">\n            <h3 className=\"text-red-800 dark:text-red-400 font-medium mb-2\">{messages.common?.error || 'Error'}</h3>\n            <p className=\"text-red-700 dark:text-red-300\">{error}</p>\n          </div>\n        ) : (\n          <div className=\"bg-[var(--card-bg)] border border-[var(--border-color)] rounded-lg shadow-sm p-6\">\n            {exportError && (\n              <div className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3 mb-4\">\n                <p className=\"text-red-700 dark:text-red-300 text-sm\">{exportError}</p>\n              </div>\n            )}\n            <Markdown content={workshopContent} />\n          </div>\n        )}\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/api/auth/status/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nconst TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\nexport async function GET() {\n  try {\n    // Forward the request to the backend API\n    const response = await fetch(`${TARGET_SERVER_BASE_URL}/auth/status`, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    });\n    \n    if (!response.ok) {\n      return NextResponse.json(\n        { error: `Backend server returned ${response.status}` },\n        { status: response.status }\n      );\n    }\n    \n    const data = await response.json();\n    return NextResponse.json(data);\n  } catch (error) {\n    console.error('Error forwarding request to backend:', error);\n    return NextResponse.json(\n      { error: 'Internal Server Error' },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "src/app/api/auth/validate/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nconst TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\nexport async function POST(request: NextRequest) {\n  try {\n    const body = await request.json();\n    \n    // Forward the request to the backend API\n    const response = await fetch(`${TARGET_SERVER_BASE_URL}/auth/validate`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify(body),\n    });\n    \n    if (!response.ok) {\n      return NextResponse.json(\n        { error: `Backend server returned ${response.status}` },\n        { status: response.status }\n      );\n    }\n    \n    const data = await response.json();\n    return NextResponse.json(data);\n  } catch (error) {\n    console.error('Error forwarding request to backend:', error);\n    return NextResponse.json(\n      { error: 'Internal Server Error' },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "src/app/api/chat/stream/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\n\n// The target backend server base URL, derived from environment variable or defaulted.\n// This should match the logic in your frontend's page.tsx for consistency.\nconst TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\n// This is a fallback HTTP implementation that will be used if WebSockets are not available\n// or if there's an error with the WebSocket connection\nexport async function POST(req: NextRequest) {\n  try {\n    const requestBody = await req.json(); // Assuming the frontend sends JSON\n\n    // Note: This endpoint now uses the HTTP fallback instead of WebSockets\n    // The WebSocket implementation is in src/utils/websocketClient.ts\n    // This HTTP endpoint is kept for backward compatibility\n    console.log('Using HTTP fallback for chat completion instead of WebSockets');\n\n    const targetUrl = `${TARGET_SERVER_BASE_URL}/chat/completions/stream`;\n\n    // Make the actual request to the backend service\n    const backendResponse = await fetch(targetUrl, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'text/event-stream', // Indicate that we expect a stream\n      },\n      body: JSON.stringify(requestBody),\n    });\n\n    // If the backend service returned an error, forward that error to the client\n    if (!backendResponse.ok) {\n      const errorBody = await backendResponse.text();\n      const errorHeaders = new Headers();\n      backendResponse.headers.forEach((value, key) => {\n        errorHeaders.set(key, value);\n      });\n      return new NextResponse(errorBody, {\n        status: backendResponse.status,\n        statusText: backendResponse.statusText,\n        headers: errorHeaders,\n      });\n    }\n\n    // Ensure the backend response has a body to stream\n    if (!backendResponse.body) {\n      return new NextResponse('Stream body from backend is null', { status: 500 });\n    }\n\n    // Create a new ReadableStream to pipe the data from the backend to the client\n    const stream = new ReadableStream({\n      async start(controller) {\n        const reader = backendResponse.body!.getReader();\n        try {\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) {\n              break;\n            }\n            controller.enqueue(value);\n          }\n        } catch (error) {\n          console.error('Error reading from backend stream in proxy:', error);\n          controller.error(error);\n        } finally {\n          controller.close();\n          reader.releaseLock(); // Important to release the lock on the reader\n        }\n      },\n      cancel(reason) {\n        console.log('Client cancelled stream request:', reason);\n      }\n    });\n\n    // Set up headers for the response to the client\n    const responseHeaders = new Headers();\n    // Copy the Content-Type from the backend response (e.g., 'text/event-stream')\n    const contentType = backendResponse.headers.get('Content-Type');\n    if (contentType) {\n      responseHeaders.set('Content-Type', contentType);\n    }\n    // It's good practice for streams not to be cached or transformed by intermediaries.\n    responseHeaders.set('Cache-Control', 'no-cache, no-transform');\n\n    return new NextResponse(stream, {\n      status: backendResponse.status, // Should be 200 for a successful stream start\n      headers: responseHeaders,\n    });\n\n  } catch (error) {\n    console.error('Error in API proxy route (/api/chat/stream):', error);\n    let errorMessage = 'Internal Server Error in proxy';\n    if (error instanceof Error) {\n      errorMessage = error.message;\n    }\n    return new NextResponse(JSON.stringify({ error: errorMessage }), {\n      status: 500,\n      headers: { 'Content-Type': 'application/json' },\n    });\n  }\n}\n\n// Optional: Handle OPTIONS requests for CORS if you ever call this from a different origin\n// or use custom headers that trigger preflight requests. For same-origin, it's less critical.\nexport async function OPTIONS() {\n  return new NextResponse(null, {\n    status: 204, // No Content\n    headers: {\n      'Access-Control-Allow-Origin': '*', // Be more specific in production if needed\n      'Access-Control-Allow-Methods': 'POST, OPTIONS',\n      'Access-Control-Allow-Headers': 'Content-Type, Authorization', // Adjust as per client's request headers\n    },\n  });\n}"
  },
  {
    "path": "src/app/api/models/config/route.ts",
    "content": "import { NextResponse } from 'next/server';\n\n// The target backend server base URL, derived from environment variable or defaulted.\nconst TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\nexport async function GET() {\n  try {\n    const targetUrl = `${TARGET_SERVER_BASE_URL}/models/config`;\n\n    // Make the actual request to the backend service\n    const backendResponse = await fetch(targetUrl, {\n      method: 'GET',\n      headers: {\n        'Accept': 'application/json',\n      }\n    });\n\n    // If the backend service responds with an error\n    if (!backendResponse.ok) {\n      return NextResponse.json(\n        { error: `Backend service responded with status: ${backendResponse.status}` },\n        { status: backendResponse.status }\n      );\n    }\n\n    // Forward the response from the backend\n    const modelConfig = await backendResponse.json();\n    return NextResponse.json(modelConfig);\n  } catch (error) {\n    console.error('Error fetching model configurations:', error);    \n    return new NextResponse(JSON.stringify({ error: error }), {\n        status: 500,\n        headers: { 'Content-Type': 'application/json' },\n      });\n  }\n}\n\n// Handle OPTIONS requests for CORS if needed\nexport function OPTIONS() {\n  return new NextResponse(null, {\n    status: 204,\n    headers: {\n      'Access-Control-Allow-Origin': '*',\n      'Access-Control-Allow-Methods': 'GET',\n      'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n    },\n  });\n}\n"
  },
  {
    "path": "src/app/api/wiki/projects/route.ts",
    "content": "import { NextResponse } from 'next/server';\n\n// This should match the expected structure from your Python backend\ninterface ApiProcessedProject {\n  id: string;\n  owner: string;\n  repo: string;\n  name: string;\n  repo_type: string;\n  submittedAt: number;\n  language: string;\n}\n// Payload for deleting a project cache\ninterface DeleteProjectCachePayload {\n  owner: string;\n  repo: string;\n  repo_type: string;\n  language: string;\n}\n\n/** Type guard to validate DeleteProjectCachePayload at runtime */\nfunction isDeleteProjectCachePayload(obj: unknown): obj is DeleteProjectCachePayload {\n  return (\n    obj != null &&\n    typeof obj === 'object' &&\n    'owner' in obj && typeof (obj as Record<string, unknown>).owner === 'string' && ((obj as Record<string, unknown>).owner as string).trim() !== '' &&\n    'repo' in obj && typeof (obj as Record<string, unknown>).repo === 'string' && ((obj as Record<string, unknown>).repo as string).trim() !== '' &&\n    'repo_type' in obj && typeof (obj as Record<string, unknown>).repo_type === 'string' && ((obj as Record<string, unknown>).repo_type as string).trim() !== '' &&\n    'language' in obj && typeof (obj as Record<string, unknown>).language === 'string' && ((obj as Record<string, unknown>).language as string).trim() !== ''\n  );\n}\n\n// Ensure this matches your Python backend configuration\nconst PYTHON_BACKEND_URL = process.env.PYTHON_BACKEND_HOST || 'http://localhost:8001';\nconst PROJECTS_API_ENDPOINT = `${PYTHON_BACKEND_URL}/api/processed_projects`;\nconst CACHE_API_ENDPOINT = `${PYTHON_BACKEND_URL}/api/wiki_cache`;\n\nexport async function GET() {\n  try {\n    const response = await fetch(PROJECTS_API_ENDPOINT, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n        // Add any other headers your Python backend might require, e.g., API keys\n      },\n      cache: 'no-store', // Ensure fresh data is fetched every time\n    });\n\n    if (!response.ok) {\n      // Try to parse error from backend, otherwise use status text\n      let errorBody = { error: `Failed to fetch from Python backend: ${response.statusText}` };\n      try {\n        errorBody = await response.json();\n      } catch {\n        // If parsing JSON fails, errorBody will retain its default value\n        // The error from backend is logged in the next line anyway\n      }\n      console.error(`Error from Python backend (${PROJECTS_API_ENDPOINT}): ${response.status} - ${JSON.stringify(errorBody)}`);\n      return NextResponse.json(errorBody, { status: response.status });\n    }\n\n    const projects: ApiProcessedProject[] = await response.json();\n    return NextResponse.json(projects);\n\n  } catch (error: unknown) {\n    console.error(`Network or other error when fetching from ${PROJECTS_API_ENDPOINT}:`, error);\n    const message = error instanceof Error ? error.message : 'An unknown error occurred';\n    return NextResponse.json(\n      { error: `Failed to connect to the Python backend. ${message}` },\n      { status: 503 } // Service Unavailable\n    );\n  }\n}\n\nexport async function DELETE(request: Request) {\n  try {\n    const body: unknown = await request.json();\n    if (!isDeleteProjectCachePayload(body)) {\n      return NextResponse.json(\n        { error: 'Invalid request body: owner, repo, repo_type, and language are required and must be non-empty strings.' },\n        { status: 400 }\n      );\n    }\n    const { owner, repo, repo_type, language } = body;\n    const params = new URLSearchParams({ owner, repo, repo_type, language });\n    const response = await fetch(`${CACHE_API_ENDPOINT}?${params}`, {\n      method: 'DELETE',\n      headers: { 'Content-Type': 'application/json' },\n    });\n    if (!response.ok) {\n      let errorBody = { error: response.statusText };\n      try {\n        errorBody = await response.json();\n      } catch {}\n      console.error(`Error deleting project cache (${CACHE_API_ENDPOINT}): ${response.status} - ${JSON.stringify(errorBody)}`);\n      return NextResponse.json(errorBody, { status: response.status });\n    }\n    return NextResponse.json({ message: 'Project deleted successfully' });\n  } catch (error: unknown) {\n    console.error('Error in DELETE /api/wiki/projects:', error);\n    const message = error instanceof Error ? error.message : 'An unknown error occurred';\n    return NextResponse.json({ error: `Failed to delete project: ${message}` }, { status: 500 });\n  }\n}"
  },
  {
    "path": "src/app/globals.css",
    "content": "@import \"tailwindcss\";\n\n/* Define dark mode variant */\n@custom-variant dark (&:where([data-theme=\"dark\"], [data-theme=\"dark\"] *));\n\n:root {\n  /* Japanese aesthetic color palette - light mode */\n  --background: #f8f4e6; /* Warm off-white like washi paper */\n  --foreground: #333333; /* Soft black for text */\n  --shadow-color: rgba(0, 0, 0, 0.05);\n  --accent-primary: #9b7cb9; /* Soft purple (Fuji) */\n  --accent-secondary: #d7c4bb; /* Soft beige (Kinari) */\n  --border-color: #e0d8c8; /* Soft beige border */\n  --card-bg: #fffaf0; /* Slightly warmer than background */\n  --highlight: #e8927c; /* Soft coral (Akane) */\n  --muted: #a59e8c; /* Soft gray-brown (Nezumi) */\n  --link-color: #7c5aa0; /* Slightly darker purple for links */\n}\n\nhtml[data-theme='dark'] {\n  /* Japanese aesthetic color palette - dark mode */\n  --background: #1a1a1a; /* Deep charcoal */\n  --foreground: #f0f0f0; /* Soft white */\n  --shadow-color: rgba(0, 0, 0, 0.2);\n  --accent-primary: #9370db; /* Soft lavender */\n  --accent-secondary: #5d4037; /* Warm brown */\n  --border-color: #2c2c2c; /* Dark border */\n  --card-bg: #222222; /* Slightly lighter than background */\n  --highlight: #e57373; /* Soft red */\n  --muted: #8c8c8c; /* Muted gray */\n  --link-color: #b19cd9; /* Lighter purple for dark mode links */\n}\n\n/* Fix for unreadable <select> options in Chrome's dark mode */\n[data-theme=\"dark\"] select option {\n  background: var(--background);\n}\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: 'Noto Sans JP', sans-serif;\n  --font-mono: 'Geist Mono', monospace;\n  --font-geist-sans: 'Noto Sans JP', sans-serif;\n  --font-geist-mono: 'Geist Mono', monospace;\n  --font-serif-jp: 'Noto Serif JP', serif;\n}\n\nbody {\n  background: var(--background);\n  color: var(--foreground);\n  font-family: var(--font-sans), sans-serif;\n}\n\n/* Custom shadow styles - more subtle for Japanese aesthetic */\n.shadow-custom {\n  box-shadow: 0 4px 8px -2px var(--shadow-color);\n}\n\n/* Paper texture background */\n.paper-texture {\n  background-color: var(--card-bg);\n  background-image: url(\"data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23e0d8c8' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E\");\n}\n\n/* Dark mode paper texture */\nhtml[data-theme='dark'] .paper-texture {\n  background-image: url(\"data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23333333' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E\");\n}\n\n/* Japanese-style buttons */\n.btn-japanese {\n  background-color: var(--accent-primary);\n  color: white;\n  border: none;\n  border-radius: 0.25rem;\n  padding: 0.5rem 1.5rem;\n  font-weight: 500;\n  transition: all 0.3s ease;\n  position: relative;\n  overflow: hidden;\n}\n\n.btn-japanese:hover {\n  background-color: var(--highlight);\n}\n\n.btn-japanese:before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 0;\n  height: 100%;\n  background-color: rgba(255, 255, 255, 0.2);\n  transition: width 0.3s ease;\n}\n\n.btn-japanese:hover:before {\n  width: 100%;\n}\n\n/* Japanese-style inputs */\n.input-japanese {\n  background-color: transparent;\n  border: 1px solid var(--border-color);\n  border-radius: 0.25rem;\n  padding: 0.5rem 1rem;\n  transition: all 0.3s ease;\n}\n\n.input-japanese:focus {\n  border-color: var(--accent-primary);\n  box-shadow: 0 0 0 2px rgba(155, 124, 185, 0.2);\n  outline: none;\n}\n\n/* Japanese-style cards */\n.card-japanese {\n  background-color: var(--card-bg);\n  border: 1px solid var(--border-color);\n  border-radius: 0.5rem;\n  overflow: hidden;\n  transition: all 0.3s ease;\n}\n\n.card-japanese:hover {\n  box-shadow: 0 4px 12px var(--shadow-color);\n}\n\n/* Line clamp utilities */\n.line-clamp-1 {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 1;\n}\n\n.line-clamp-2 {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n}\n\n.line-clamp-3 {\n  overflow: hidden;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 3;\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport \"./globals.css\";\nimport { ThemeProvider } from \"next-themes\";\nimport { LanguageProvider } from \"@/contexts/LanguageContext\";\n\nexport const metadata: Metadata = {\n  title: \"Deepwiki Open Source | Sheing Ng\",\n  description: \"Created by Sheing Ng\",\n};\n\nexport default function RootLayout({\n  children\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <head>\n        <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n        <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossOrigin=\"anonymous\" />\n        <link\n          href=\"https://fonts.googleapis.com/css2?family=Geist+Mono&family=Noto+Sans+JP:wght@400;500;700&family=Noto+Serif+JP:wght@400;500;700&display=swap\"\n          rel=\"stylesheet\"\n        />\n      </head>\n      <body className=\"antialiased\">\n        <ThemeProvider attribute=\"data-theme\" defaultTheme=\"system\" enableSystem>\n          <LanguageProvider>\n            {children}\n          </LanguageProvider>\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { useRouter } from 'next/navigation';\nimport Link from 'next/link';\nimport { FaWikipediaW, FaGithub, FaCoffee, FaTwitter } from 'react-icons/fa';\nimport ThemeToggle from '@/components/theme-toggle';\nimport Mermaid from '../components/Mermaid';\nimport ConfigurationModal from '@/components/ConfigurationModal';\nimport ProcessedProjects from '@/components/ProcessedProjects';\nimport { extractUrlPath, extractUrlDomain } from '@/utils/urlDecoder';\nimport { useProcessedProjects } from '@/hooks/useProcessedProjects';\n\nimport { useLanguage } from '@/contexts/LanguageContext';\n\n// Define the demo mermaid charts outside the component\nconst DEMO_FLOW_CHART = `graph TD\n  A[Code Repository] --> B[DeepWiki]\n  B --> C[Architecture Diagrams]\n  B --> D[Component Relationships]\n  B --> E[Data Flow]\n  B --> F[Process Workflows]\n\n  style A fill:#f9d3a9,stroke:#d86c1f\n  style B fill:#d4a9f9,stroke:#6c1fd8\n  style C fill:#a9f9d3,stroke:#1fd86c\n  style D fill:#a9d3f9,stroke:#1f6cd8\n  style E fill:#f9a9d3,stroke:#d81f6c\n  style F fill:#d3f9a9,stroke:#6cd81f`;\n\nconst DEMO_SEQUENCE_CHART = `sequenceDiagram\n  participant User\n  participant DeepWiki\n  participant GitHub\n\n  User->>DeepWiki: Enter repository URL\n  DeepWiki->>GitHub: Request repository data\n  GitHub-->>DeepWiki: Return repository data\n  DeepWiki->>DeepWiki: Process and analyze code\n  DeepWiki-->>User: Display wiki with diagrams\n\n  %% Add a note to make text more visible\n  Note over User,GitHub: DeepWiki supports sequence diagrams for visualizing interactions`;\n\nexport default function Home() {\n  const router = useRouter();\n  const { language, setLanguage, messages, supportedLanguages } = useLanguage();\n  const { projects, isLoading: projectsLoading } = useProcessedProjects();\n\n  // Create a simple translation function\n  const t = (key: string, params: Record<string, string | number> = {}): string => {\n    // Split the key by dots to access nested properties\n    const keys = key.split('.');\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    let value: any = messages;\n\n    // Navigate through the nested properties\n    for (const k of keys) {\n      if (value && typeof value === 'object' && k in value) {\n        value = value[k];\n      } else {\n        // Return the key if the translation is not found\n        return key;\n      }\n    }\n\n    // If the value is a string, replace parameters\n    if (typeof value === 'string') {\n      return Object.entries(params).reduce((acc: string, [paramKey, paramValue]) => {\n        return acc.replace(`{${paramKey}}`, String(paramValue));\n      }, value);\n    }\n\n    // Return the key if the value is not a string\n    return key;\n  };\n\n  const [repositoryInput, setRepositoryInput] = useState('https://github.com/AsyncFuncAI/deepwiki-open');\n\n  const REPO_CONFIG_CACHE_KEY = 'deepwikiRepoConfigCache';\n\n  const loadConfigFromCache = (repoUrl: string) => {\n    if (!repoUrl) return;\n    try {\n      const cachedConfigs = localStorage.getItem(REPO_CONFIG_CACHE_KEY);\n      if (cachedConfigs) {\n        const configs = JSON.parse(cachedConfigs);\n        const config = configs[repoUrl.trim()];\n        if (config) {\n          setSelectedLanguage(config.selectedLanguage || language);\n          setIsComprehensiveView(config.isComprehensiveView === undefined ? true : config.isComprehensiveView);\n          setProvider(config.provider || '');\n          setModel(config.model || '');\n          setIsCustomModel(config.isCustomModel || false);\n          setCustomModel(config.customModel || '');\n          setSelectedPlatform(config.selectedPlatform || 'github');\n          setExcludedDirs(config.excludedDirs || '');\n          setExcludedFiles(config.excludedFiles || '');\n          setIncludedDirs(config.includedDirs || '');\n          setIncludedFiles(config.includedFiles || '');\n        }\n      }\n    } catch (error) {\n      console.error('Error loading config from localStorage:', error);\n    }\n  };\n\n  const handleRepositoryInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newRepoUrl = e.target.value;\n    setRepositoryInput(newRepoUrl);\n    if (newRepoUrl.trim() === \"\") {\n      // Optionally reset fields if input is cleared\n    } else {\n        loadConfigFromCache(newRepoUrl);\n    }\n  };\n\n  useEffect(() => {\n    if (repositoryInput) {\n      loadConfigFromCache(repositoryInput);\n    }\n  }, []);\n\n  // Provider-based model selection state\n  const [provider, setProvider] = useState<string>('');\n  const [model, setModel] = useState<string>('');\n  const [isCustomModel, setIsCustomModel] = useState<boolean>(false);\n  const [customModel, setCustomModel] = useState<string>('');\n\n  // Wiki type state - default to comprehensive view\n  const [isComprehensiveView, setIsComprehensiveView] = useState<boolean>(true);\n\n  const [excludedDirs, setExcludedDirs] = useState('');\n  const [excludedFiles, setExcludedFiles] = useState('');\n  const [includedDirs, setIncludedDirs] = useState('');\n  const [includedFiles, setIncludedFiles] = useState('');\n  const [selectedPlatform, setSelectedPlatform] = useState<'github' | 'gitlab' | 'bitbucket'>('github');\n  const [accessToken, setAccessToken] = useState('');\n  const [error, setError] = useState<string | null>(null);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [selectedLanguage, setSelectedLanguage] = useState<string>(language);\n\n  // Authentication state\n  const [authRequired, setAuthRequired] = useState<boolean>(false);\n  const [authCode, setAuthCode] = useState<string>('');\n  const [isAuthLoading, setIsAuthLoading] = useState<boolean>(true);\n\n  // Sync the language context with the selectedLanguage state\n  useEffect(() => {\n    setLanguage(selectedLanguage);\n  }, [selectedLanguage, setLanguage]);\n\n  // Fetch authentication status on component mount\n  useEffect(() => {\n    const fetchAuthStatus = async () => {\n      try {\n        setIsAuthLoading(true);\n        const response = await fetch('/api/auth/status');\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n        const data = await response.json();\n        setAuthRequired(data.auth_required);\n      } catch (err) {\n        console.error(\"Failed to fetch auth status:\", err);\n        // Assuming auth is required if fetch fails to avoid blocking UI for safety\n        setAuthRequired(true);\n      } finally {\n        setIsAuthLoading(false);\n      }\n    };\n\n    fetchAuthStatus();\n  }, []);\n\n  // Parse repository URL/input and extract owner and repo\n  const parseRepositoryInput = (input: string): {\n    owner: string,\n    repo: string,\n    type: string,\n    fullPath?: string,\n    localPath?: string\n  } | null => {\n    input = input.trim();\n\n    let owner = '', repo = '', type = 'github', fullPath;\n    let localPath: string | undefined;\n\n    // Handle Windows absolute paths (e.g., C:\\path\\to\\folder)\n    const windowsPathRegex = /^[a-zA-Z]:\\\\(?:[^\\\\/:*?\"<>|\\r\\n]+\\\\)*[^\\\\/:*?\"<>|\\r\\n]*$/;\n    const customGitRegex = /^(?:https?:\\/\\/)?([^\\/]+)\\/(.+?)\\/([^\\/]+)(?:\\.git)?\\/?$/;\n\n    if (windowsPathRegex.test(input)) {\n      type = 'local';\n      localPath = input;\n      repo = input.split('\\\\').pop() || 'local-repo';\n      owner = 'local';\n    }\n    // Handle Unix/Linux absolute paths (e.g., /path/to/folder)\n    else if (input.startsWith('/')) {\n      type = 'local';\n      localPath = input;\n      repo = input.split('/').filter(Boolean).pop() || 'local-repo';\n      owner = 'local';\n    }\n    else if (customGitRegex.test(input)) {\n      // Detect repository type based on domain\n      const domain = extractUrlDomain(input);\n      if (domain?.includes('github.com')) {\n        type = 'github';\n      } else if (domain?.includes('gitlab.com') || domain?.includes('gitlab.')) {\n        type = 'gitlab';\n      } else if (domain?.includes('bitbucket.org') || domain?.includes('bitbucket.')) {\n        type = 'bitbucket';\n      } else {\n        type = 'web'; // fallback for other git hosting services\n      }\n\n      fullPath = extractUrlPath(input)?.replace(/\\.git$/, '');\n      const parts = fullPath?.split('/') ?? [];\n      if (parts.length >= 2) {\n        repo = parts[parts.length - 1] || '';\n        owner = parts[parts.length - 2] || '';\n      }\n    }\n    // Unsupported URL formats\n    else {\n      console.error('Unsupported URL format:', input);\n      return null;\n    }\n\n    if (!owner || !repo) {\n      return null;\n    }\n\n    // Clean values\n    owner = owner.trim();\n    repo = repo.trim();\n\n    // Remove .git suffix if present\n    if (repo.endsWith('.git')) {\n      repo = repo.slice(0, -4);\n    }\n\n    return { owner, repo, type, fullPath, localPath };\n  };\n\n  // State for configuration modal\n  const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);\n\n  const handleFormSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n\n    // Parse repository input to validate\n    const parsedRepo = parseRepositoryInput(repositoryInput);\n\n    if (!parsedRepo) {\n      setError('Invalid repository format. Use \"owner/repo\", GitHub/GitLab/BitBucket URL, or a local folder path like \"/path/to/folder\" or \"C:\\\\path\\\\to\\\\folder\".');\n      return;\n    }\n\n    // If valid, open the configuration modal\n    setError(null);\n    setIsConfigModalOpen(true);\n  };\n\n  const validateAuthCode = async () => {\n    try {\n      if(authRequired) {\n        if(!authCode) {\n          return false;\n        }\n        const response = await fetch('/api/auth/validate', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({'code': authCode})\n        });\n        if (!response.ok) {\n          return false;\n        }\n        const data = await response.json();\n        return data.success || false;\n      }\n    } catch {\n      return false;\n    }\n    return true;\n  };\n\n  const handleGenerateWiki = async () => {\n\n    // Check authorization code\n    const validation = await validateAuthCode();\n    if(!validation) {\n      setError(`Failed to validate the authorization code`);\n      console.error(`Failed to validate the authorization code`);\n      setIsConfigModalOpen(false);\n      return;\n    }\n\n    // Prevent multiple submissions\n    if (isSubmitting) {\n      console.log('Form submission already in progress, ignoring duplicate click');\n      return;\n    }\n\n    try {\n      const currentRepoUrl = repositoryInput.trim();\n      if (currentRepoUrl) {\n        const existingConfigs = JSON.parse(localStorage.getItem(REPO_CONFIG_CACHE_KEY) || '{}');\n        const configToSave = {\n          selectedLanguage,\n          isComprehensiveView,\n          provider,\n          model,\n          isCustomModel,\n          customModel,\n          selectedPlatform,\n          excludedDirs,\n          excludedFiles,\n          includedDirs,\n          includedFiles,\n        };\n        existingConfigs[currentRepoUrl] = configToSave;\n        localStorage.setItem(REPO_CONFIG_CACHE_KEY, JSON.stringify(existingConfigs));\n      }\n    } catch (error) {\n      console.error('Error saving config to localStorage:', error);\n    }\n\n    setIsSubmitting(true);\n\n    // Parse repository input\n    const parsedRepo = parseRepositoryInput(repositoryInput);\n\n    if (!parsedRepo) {\n      setError('Invalid repository format. Use \"owner/repo\", GitHub/GitLab/BitBucket URL, or a local folder path like \"/path/to/folder\" or \"C:\\\\path\\\\to\\\\folder\".');\n      setIsSubmitting(false);\n      return;\n    }\n\n    const { owner, repo, type, localPath } = parsedRepo;\n\n    // Store tokens in query params if they exist\n    const params = new URLSearchParams();\n    if (accessToken) {\n      params.append('token', accessToken);\n    }\n    // Always include the type parameter\n    params.append('type', (type == 'local' ? type : selectedPlatform) || 'github');\n    // Add local path if it exists\n    if (localPath) {\n      params.append('local_path', encodeURIComponent(localPath));\n    } else {\n      params.append('repo_url', encodeURIComponent(repositoryInput));\n    }\n    // Add model parameters\n    params.append('provider', provider);\n    params.append('model', model);\n    if (isCustomModel && customModel) {\n      params.append('custom_model', customModel);\n    }\n    // Add file filters configuration\n    if (excludedDirs) {\n      params.append('excluded_dirs', excludedDirs);\n    }\n    if (excludedFiles) {\n      params.append('excluded_files', excludedFiles);\n    }\n    if (includedDirs) {\n      params.append('included_dirs', includedDirs);\n    }\n    if (includedFiles) {\n      params.append('included_files', includedFiles);\n    }\n\n    // Add language parameter\n    params.append('language', selectedLanguage);\n\n    // Add comprehensive parameter\n    params.append('comprehensive', isComprehensiveView.toString());\n\n    const queryString = params.toString() ? `?${params.toString()}` : '';\n\n    // Navigate to the dynamic route\n    router.push(`/${owner}/${repo}${queryString}`);\n\n    // The isSubmitting state will be reset when the component unmounts during navigation\n  };\n\n  return (\n    <div className=\"h-screen paper-texture p-4 md:p-8 flex flex-col\">\n      <header className=\"max-w-6xl mx-auto mb-6 h-fit w-full\">\n        <div\n          className=\"flex flex-col md:flex-row md:items-center md:justify-between gap-4 bg-[var(--card-bg)] rounded-lg shadow-custom border border-[var(--border-color)] p-4\">\n          <div className=\"flex items-center\">\n            <div className=\"bg-[var(--accent-primary)] p-2 rounded-lg mr-3\">\n              <FaWikipediaW className=\"text-2xl text-white\" />\n            </div>\n            <div className=\"mr-6\">\n              <h1 className=\"text-xl md:text-2xl font-bold text-[var(--accent-primary)]\">{t('common.appName')}</h1>\n              <div className=\"flex flex-wrap items-baseline gap-x-2 md:gap-x-3 mt-0.5\">\n                <p className=\"text-xs text-[var(--muted)] whitespace-nowrap\">{t('common.tagline')}</p>\n                <div className=\"hidden md:inline-block\">\n                  <Link href=\"/wiki/projects\"\n                    className=\"text-xs font-medium text-[var(--accent-primary)] hover:text-[var(--highlight)] hover:underline whitespace-nowrap\">\n                    {t('nav.wikiProjects')}\n                  </Link>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <form onSubmit={handleFormSubmit} className=\"flex flex-col gap-3 w-full max-w-3xl\">\n            {/* Repository URL input and submit button */}\n            <div className=\"flex flex-col sm:flex-row gap-2\">\n              <div className=\"relative flex-1\">\n                <input\n                  type=\"text\"\n                  value={repositoryInput}\n                  onChange={handleRepositoryInputChange}\n                  placeholder={t('form.repoPlaceholder') || \"owner/repo, GitHub/GitLab/BitBucket URL, or local folder path\"}\n                  className=\"input-japanese block w-full pl-10 pr-3 py-2.5 border-[var(--border-color)] rounded-lg bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n                />\n                {error && (\n                  <div className=\"text-[var(--highlight)] text-xs mt-1\">\n                    {error}\n                  </div>\n                )}\n              </div>\n              <button\n                type=\"submit\"\n                className=\"btn-japanese px-6 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed\"\n                disabled={isSubmitting}\n              >\n                {isSubmitting ? t('common.processing') : t('common.generateWiki')}\n              </button>\n            </div>\n          </form>\n\n          {/* Configuration Modal */}\n          <ConfigurationModal\n            isOpen={isConfigModalOpen}\n            onClose={() => setIsConfigModalOpen(false)}\n            repositoryInput={repositoryInput}\n            selectedLanguage={selectedLanguage}\n            setSelectedLanguage={setSelectedLanguage}\n            supportedLanguages={supportedLanguages}\n            isComprehensiveView={isComprehensiveView}\n            setIsComprehensiveView={setIsComprehensiveView}\n            provider={provider}\n            setProvider={setProvider}\n            model={model}\n            setModel={setModel}\n            isCustomModel={isCustomModel}\n            setIsCustomModel={setIsCustomModel}\n            customModel={customModel}\n            setCustomModel={setCustomModel}\n            selectedPlatform={selectedPlatform}\n            setSelectedPlatform={setSelectedPlatform}\n            accessToken={accessToken}\n            setAccessToken={setAccessToken}\n            excludedDirs={excludedDirs}\n            setExcludedDirs={setExcludedDirs}\n            excludedFiles={excludedFiles}\n            setExcludedFiles={setExcludedFiles}\n            includedDirs={includedDirs}\n            setIncludedDirs={setIncludedDirs}\n            includedFiles={includedFiles}\n            setIncludedFiles={setIncludedFiles}\n            onSubmit={handleGenerateWiki}\n            isSubmitting={isSubmitting}\n            authRequired={authRequired}\n            authCode={authCode}\n            setAuthCode={setAuthCode}\n            isAuthLoading={isAuthLoading}\n          />\n\n        </div>\n      </header>\n\n      <main className=\"flex-1 max-w-6xl mx-auto w-full overflow-y-auto\">\n        <div\n          className=\"min-h-full flex flex-col items-center p-8 pt-10 bg-[var(--card-bg)] rounded-lg shadow-custom card-japanese\">\n\n          {/* Conditionally show processed projects or welcome content */}\n          {!projectsLoading && projects.length > 0 ? (\n            <div className=\"w-full\">\n              {/* Header section for existing projects */}\n              <div className=\"flex flex-col items-center w-full max-w-2xl mb-8 mx-auto\">\n                <div className=\"flex flex-col sm:flex-row items-center mb-6 gap-4\">\n                  <div className=\"relative\">\n                    <div className=\"absolute -inset-1 bg-[var(--accent-primary)]/20 rounded-full blur-md\"></div>\n                    <FaWikipediaW className=\"text-5xl text-[var(--accent-primary)] relative z-10\" />\n                  </div>\n                  <div className=\"text-center sm:text-left\">\n                    <h2 className=\"text-2xl font-bold text-[var(--foreground)] font-serif mb-1\">{t('projects.existingProjects')}</h2>\n                    <p className=\"text-[var(--accent-primary)] text-sm max-w-md\">{t('projects.browseExisting')}</p>\n                  </div>\n                </div>\n              </div>\n\n              {/* Show processed projects */}\n              <ProcessedProjects\n                showHeader={false}\n                maxItems={6}\n                messages={messages}\n                className=\"w-full\"\n              />\n            </div>\n          ) : (\n            <>\n              {/* Header section */}\n              <div className=\"flex flex-col items-center w-full max-w-2xl mb-8\">\n                <div className=\"flex flex-col sm:flex-row items-center mb-6 gap-4\">\n                  <div className=\"relative\">\n                    <div className=\"absolute -inset-1 bg-[var(--accent-primary)]/20 rounded-full blur-md\"></div>\n                    <FaWikipediaW className=\"text-5xl text-[var(--accent-primary)] relative z-10\" />\n                  </div>\n                  <div className=\"text-center sm:text-left\">\n                    <h2 className=\"text-2xl font-bold text-[var(--foreground)] font-serif mb-1\">{t('home.welcome')}</h2>\n                    <p className=\"text-[var(--accent-primary)] text-sm max-w-md\">{t('home.welcomeTagline')}</p>\n                  </div>\n                </div>\n\n                <p className=\"text-[var(--foreground)] text-center mb-8 text-lg leading-relaxed\">\n                  {t('home.description')}\n                </p>\n              </div>\n\n          {/* Quick Start section - redesigned for better spacing */}\n          <div\n            className=\"w-full max-w-2xl mb-10 bg-[var(--accent-primary)]/5 border border-[var(--accent-primary)]/20 rounded-lg p-5\">\n            <h3 className=\"text-sm font-semibold text-[var(--accent-primary)] mb-3 flex items-center\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"h-4 w-4 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2}\n                  d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n              </svg>\n              {t('home.quickStart')}\n            </h3>\n            <p className=\"text-sm text-[var(--foreground)] mb-3\">{t('home.enterRepoUrl')}</p>\n            <div className=\"grid grid-cols-1 gap-3 text-xs text-[var(--muted)]\">\n              <div\n                className=\"bg-[var(--background)]/70 p-3 rounded border border-[var(--border-color)] font-mono overflow-x-hidden whitespace-nowrap\"\n              >https://github.com/AsyncFuncAI/deepwiki-open\n              </div>\n              <div\n                className=\"bg-[var(--background)]/70 p-3 rounded border border-[var(--border-color)] font-mono overflow-x-hidden whitespace-nowrap\"\n              >https://gitlab.com/gitlab-org/gitlab\n              </div>\n              <div\n                className=\"bg-[var(--background)]/70 p-3 rounded border border-[var(--border-color)] font-mono overflow-x-hidden whitespace-nowrap\"\n              >AsyncFuncAI/deepwiki-open\n              </div>\n              <div\n                className=\"bg-[var(--background)]/70 p-3 rounded border border-[var(--border-color)] font-mono overflow-x-hidden whitespace-nowrap\"\n              >https://bitbucket.org/atlassian/atlaskit\n              </div>\n            </div>\n          </div>\n\n          {/* Visualization section - improved for better visibility */}\n          <div\n            className=\"w-full max-w-2xl mb-8 bg-[var(--background)]/70 rounded-lg p-6 border border-[var(--border-color)]\">\n            <div className=\"flex flex-col sm:flex-row items-start sm:items-center gap-2 mb-4\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\"\n                className=\"h-5 w-5 text-[var(--accent-primary)] flex-shrink-0 mt-0.5 sm:mt-0\" fill=\"none\"\n                viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2}\n                  d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\" />\n              </svg>\n              <h3 className=\"text-base font-semibold text-[var(--foreground)] font-serif\">{t('home.advancedVisualization')}</h3>\n            </div>\n            <p className=\"text-sm text-[var(--foreground)] mb-5 leading-relaxed\">\n              {t('home.diagramDescription')}\n            </p>\n\n            {/* Diagrams with improved layout */}\n            <div className=\"grid grid-cols-1 gap-6\">\n              <div className=\"bg-[var(--card-bg)] p-4 rounded-lg border border-[var(--border-color)] shadow-custom\">\n                <h4 className=\"text-sm font-medium text-[var(--foreground)] mb-3 font-serif\">{t('home.flowDiagram')}</h4>\n                <Mermaid chart={DEMO_FLOW_CHART} />\n              </div>\n\n              <div className=\"bg-[var(--card-bg)] p-4 rounded-lg border border-[var(--border-color)] shadow-custom\">\n                <h4 className=\"text-sm font-medium text-[var(--foreground)] mb-3 font-serif\">{t('home.sequenceDiagram')}</h4>\n                <Mermaid chart={DEMO_SEQUENCE_CHART} />\n              </div>\n            </div>\n          </div>\n            </>\n          )}\n        </div>\n      </main>\n\n      <footer className=\"max-w-6xl mx-auto mt-8 flex flex-col gap-4 w-full\">\n        <div\n          className=\"flex flex-col sm:flex-row justify-between items-center gap-4 bg-[var(--card-bg)] rounded-lg p-4 border border-[var(--border-color)] shadow-custom\">\n          <p className=\"text-[var(--muted)] text-sm font-serif\">{t('footer.copyright')}</p>\n\n          <div className=\"flex items-center gap-6\">\n            <div className=\"flex items-center space-x-5\">\n              <a href=\"https://github.com/AsyncFuncAI/deepwiki-open\" target=\"_blank\" rel=\"noopener noreferrer\"\n                className=\"text-[var(--muted)] hover:text-[var(--accent-primary)] transition-colors\">\n                <FaGithub className=\"text-xl\" />\n              </a>\n              <a href=\"https://buymeacoffee.com/sheing\" target=\"_blank\" rel=\"noopener noreferrer\"\n                className=\"text-[var(--muted)] hover:text-[var(--accent-primary)] transition-colors\">\n                <FaCoffee className=\"text-xl\" />\n              </a>\n              <a href=\"https://x.com/sashimikun_void\" target=\"_blank\" rel=\"noopener noreferrer\"\n                className=\"text-[var(--muted)] hover:text-[var(--accent-primary)] transition-colors\">\n                <FaTwitter className=\"text-xl\" />\n              </a>\n            </div>\n            <ThemeToggle />\n          </div>\n        </div>\n      </footer>\n    </div>\n  );\n}"
  },
  {
    "path": "src/app/wiki/projects/page.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport ProcessedProjects from '@/components/ProcessedProjects';\nimport { useLanguage } from '@/contexts/LanguageContext';\n\nexport default function WikiProjectsPage() {\n  const { messages } = useLanguage();\n\n  return (\n    <div className=\"container mx-auto p-4\">\n      <ProcessedProjects\n        showHeader={true}\n        messages={messages}\n        className=\"\"\n      />\n    </div>\n  );\n}"
  },
  {
    "path": "src/components/Ask.tsx",
    "content": "'use client';\n\nimport React, {useState, useRef, useEffect} from 'react';\nimport {FaChevronLeft, FaChevronRight } from 'react-icons/fa';\nimport Markdown from './Markdown';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport RepoInfo from '@/types/repoinfo';\nimport getRepoUrl from '@/utils/getRepoUrl';\nimport ModelSelectionModal from './ModelSelectionModal';\nimport { createChatWebSocket, closeWebSocket, ChatCompletionRequest } from '@/utils/websocketClient';\n\ninterface Model {\n  id: string;\n  name: string;\n}\n\ninterface Provider {\n  id: string;\n  name: string;\n  models: Model[];\n  supportsCustomModel?: boolean;\n}\n\ninterface Message {\n  role: 'user' | 'assistant' | 'system';\n  content: string;\n}\n\ninterface ResearchStage {\n  title: string;\n  content: string;\n  iteration: number;\n  type: 'plan' | 'update' | 'conclusion';\n}\n\ninterface AskProps {\n  repoInfo: RepoInfo;\n  provider?: string;\n  model?: string;\n  isCustomModel?: boolean;\n  customModel?: string;\n  language?: string;\n  onRef?: (ref: { clearConversation: () => void }) => void;\n}\n\nconst Ask: React.FC<AskProps> = ({\n  repoInfo,\n  provider = '',\n  model = '',\n  isCustomModel = false,\n  customModel = '',\n  language = 'en',\n  onRef\n}) => {\n  const [question, setQuestion] = useState('');\n  const [response, setResponse] = useState('');\n  const [isLoading, setIsLoading] = useState(false);\n  const [deepResearch, setDeepResearch] = useState(false);\n\n  // Model selection state\n  const [selectedProvider, setSelectedProvider] = useState(provider);\n  const [selectedModel, setSelectedModel] = useState(model);\n  const [isCustomSelectedModel, setIsCustomSelectedModel] = useState(isCustomModel);\n  const [customSelectedModel, setCustomSelectedModel] = useState(customModel);\n  const [isModelSelectionModalOpen, setIsModelSelectionModalOpen] = useState(false);\n  const [isComprehensiveView, setIsComprehensiveView] = useState(true);\n\n  // Get language context for translations\n  const { messages } = useLanguage();\n\n  // Research navigation state\n  const [researchStages, setResearchStages] = useState<ResearchStage[]>([]);\n  const [currentStageIndex, setCurrentStageIndex] = useState(0);\n  const [conversationHistory, setConversationHistory] = useState<Message[]>([]);\n  const [researchIteration, setResearchIteration] = useState(0);\n  const [researchComplete, setResearchComplete] = useState(false);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const responseRef = useRef<HTMLDivElement>(null);\n  const providerRef = useRef(provider);\n  const modelRef = useRef(model);\n\n  // Focus input on component mount\n  useEffect(() => {\n    if (inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, []);\n\n  // Expose clearConversation method to parent component\n  useEffect(() => {\n    if (onRef) {\n      onRef({ clearConversation });\n    }\n  }, [onRef]);\n\n  // Scroll to bottom of response when it changes\n  useEffect(() => {\n    if (responseRef.current) {\n      responseRef.current.scrollTop = responseRef.current.scrollHeight;\n    }\n  }, [response]);\n\n  // Close WebSocket when component unmounts\n  useEffect(() => {\n    return () => {\n      closeWebSocket(webSocketRef.current);\n    };\n  }, []);\n\n  useEffect(() => {\n    providerRef.current = provider;\n    modelRef.current = model;\n  }, [provider, model]);\n\n  useEffect(() => {\n    const fetchModel = async () => {\n      try {\n        setIsLoading(true);\n\n        const response = await fetch('/api/models/config');\n        if (!response.ok) {\n          throw new Error(`Error fetching model configurations: ${response.status}`);\n        }\n\n        const data = await response.json();\n\n        // use latest provider/model ref to check\n        if(providerRef.current == '' || modelRef.current== '') {\n          setSelectedProvider(data.defaultProvider);\n\n          // Find the default provider and set its default model\n          const selectedProvider = data.providers.find((p:Provider) => p.id === data.defaultProvider);\n          if (selectedProvider && selectedProvider.models.length > 0) {\n            setSelectedModel(selectedProvider.models[0].id);\n          }\n        } else {\n          setSelectedProvider(providerRef.current);\n          setSelectedModel(modelRef.current);\n        }\n      } catch (err) {\n        console.error('Failed to fetch model configurations:', err);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n    if(provider == '' || model == '') {\n      fetchModel()\n    }\n  }, [provider, model]);\n\n  const clearConversation = () => {\n    setQuestion('');\n    setResponse('');\n    setConversationHistory([]);\n    setResearchIteration(0);\n    setResearchComplete(false);\n    setResearchStages([]);\n    setCurrentStageIndex(0);\n    if (inputRef.current) {\n      inputRef.current.focus();\n    }\n  };\n  const downloadresponse = () =>{\n  const blob = new Blob([response], { type: 'text/markdown' });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = `response-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.md`;\n  document.body.appendChild(a);\n  a.click();\n  document.body.removeChild(a);\n  URL.revokeObjectURL(url);\n}\n\n  // Function to check if research is complete based on response content\n  const checkIfResearchComplete = (content: string): boolean => {\n    // Check for explicit final conclusion markers\n    if (content.includes('## Final Conclusion')) {\n      return true;\n    }\n\n    // Check for conclusion sections that don't indicate further research\n    if ((content.includes('## Conclusion') || content.includes('## Summary')) &&\n      !content.includes('I will now proceed to') &&\n      !content.includes('Next Steps') &&\n      !content.includes('next iteration')) {\n      return true;\n    }\n\n    // Check for phrases that explicitly indicate completion\n    if (content.includes('This concludes our research') ||\n      content.includes('This completes our investigation') ||\n      content.includes('This concludes the deep research process') ||\n      content.includes('Key Findings and Implementation Details') ||\n      content.includes('In conclusion,') ||\n      (content.includes('Final') && content.includes('Conclusion'))) {\n      return true;\n    }\n\n    // Check for topic-specific completion indicators\n    if (content.includes('Dockerfile') &&\n      (content.includes('This Dockerfile') || content.includes('The Dockerfile')) &&\n      !content.includes('Next Steps') &&\n      !content.includes('In the next iteration')) {\n      return true;\n    }\n\n    return false;\n  };\n\n  // Function to extract research stages from the response\n  const extractResearchStage = (content: string, iteration: number): ResearchStage | null => {\n    // Check for research plan (first iteration)\n    if (iteration === 1 && content.includes('## Research Plan')) {\n      const planMatch = content.match(/## Research Plan([\\s\\S]*?)(?:## Next Steps|$)/);\n      if (planMatch) {\n        return {\n          title: 'Research Plan',\n          content: content,\n          iteration: 1,\n          type: 'plan'\n        };\n      }\n    }\n\n    // Check for research updates (iterations 1-4)\n    if (iteration >= 1 && iteration <= 4) {\n      const updateMatch = content.match(new RegExp(`## Research Update ${iteration}([\\\\s\\\\S]*?)(?:## Next Steps|$)`));\n      if (updateMatch) {\n        return {\n          title: `Research Update ${iteration}`,\n          content: content,\n          iteration: iteration,\n          type: 'update'\n        };\n      }\n    }\n\n    // Check for final conclusion\n    if (content.includes('## Final Conclusion')) {\n      const conclusionMatch = content.match(/## Final Conclusion([\\s\\S]*?)$/);\n      if (conclusionMatch) {\n        return {\n          title: 'Final Conclusion',\n          content: content,\n          iteration: iteration,\n          type: 'conclusion'\n        };\n      }\n    }\n\n    return null;\n  };\n\n  // Function to navigate to a specific research stage\n  const navigateToStage = (index: number) => {\n    if (index >= 0 && index < researchStages.length) {\n      setCurrentStageIndex(index);\n      setResponse(researchStages[index].content);\n    }\n  };\n\n  // Function to navigate to the next research stage\n  const navigateToNextStage = () => {\n    if (currentStageIndex < researchStages.length - 1) {\n      navigateToStage(currentStageIndex + 1);\n    }\n  };\n\n  // Function to navigate to the previous research stage\n  const navigateToPreviousStage = () => {\n    if (currentStageIndex > 0) {\n      navigateToStage(currentStageIndex - 1);\n    }\n  };\n\n  // WebSocket reference\n  const webSocketRef = useRef<WebSocket | null>(null);\n\n  // Function to continue research automatically\n  const continueResearch = async () => {\n    if (!deepResearch || researchComplete || !response || isLoading) return;\n\n    // Add a small delay to allow the user to read the current response\n    await new Promise(resolve => setTimeout(resolve, 2000));\n\n    setIsLoading(true);\n\n    try {\n      // Store the current response for use in the history\n      const currentResponse = response;\n\n      // Create a new message from the AI's previous response\n      const newHistory: Message[] = [\n        ...conversationHistory,\n        {\n          role: 'assistant',\n          content: currentResponse\n        },\n        {\n          role: 'user',\n          content: '[DEEP RESEARCH] Continue the research'\n        }\n      ];\n\n      // Update conversation history\n      setConversationHistory(newHistory);\n\n      // Increment research iteration\n      const newIteration = researchIteration + 1;\n      setResearchIteration(newIteration);\n\n      // Clear previous response\n      setResponse('');\n\n      // Prepare the request body\n      const requestBody: ChatCompletionRequest = {\n        repo_url: getRepoUrl(repoInfo),\n        type: repoInfo.type,\n        messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })),\n        provider: selectedProvider,\n        model: isCustomSelectedModel ? customSelectedModel : selectedModel,\n        language: language\n      };\n\n      // Add tokens if available\n      if (repoInfo?.token) {\n        requestBody.token = repoInfo.token;\n      }\n\n      // Close any existing WebSocket connection\n      closeWebSocket(webSocketRef.current);\n\n      let fullResponse = '';\n\n      // Create a new WebSocket connection\n      webSocketRef.current = createChatWebSocket(\n        requestBody,\n        // Message handler\n        (message: string) => {\n          fullResponse += message;\n          setResponse(fullResponse);\n\n          // Extract research stage if this is a deep research response\n          if (deepResearch) {\n            const stage = extractResearchStage(fullResponse, newIteration);\n            if (stage) {\n              // Add the stage to the research stages if it's not already there\n              setResearchStages(prev => {\n                // Check if we already have this stage\n                const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type);\n                if (existingStageIndex >= 0) {\n                  // Update existing stage\n                  const newStages = [...prev];\n                  newStages[existingStageIndex] = stage;\n                  return newStages;\n                } else {\n                  // Add new stage\n                  return [...prev, stage];\n                }\n              });\n\n              // Update current stage index to the latest stage\n              setCurrentStageIndex(researchStages.length);\n            }\n          }\n        },\n        // Error handler\n        (error: Event) => {\n          console.error('WebSocket error:', error);\n          setResponse(prev => prev + '\\n\\nError: WebSocket connection failed. Falling back to HTTP...');\n\n          // Fallback to HTTP if WebSocket fails\n          fallbackToHttp(requestBody);\n        },\n        // Close handler\n        () => {\n          // Check if research is complete when the WebSocket closes\n          const isComplete = checkIfResearchComplete(fullResponse);\n\n          // Force completion after a maximum number of iterations (5)\n          const forceComplete = newIteration >= 5;\n\n          if (forceComplete && !isComplete) {\n            // If we're forcing completion, append a comprehensive conclusion to the response\n            const completionNote = \"\\n\\n## Final Conclusion\\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question.\";\n            fullResponse += completionNote;\n            setResponse(fullResponse);\n            setResearchComplete(true);\n          } else {\n            setResearchComplete(isComplete);\n          }\n\n          setIsLoading(false);\n        }\n      );\n    } catch (error) {\n      console.error('Error during API call:', error);\n      setResponse(prev => prev + '\\n\\nError: Failed to continue research. Please try again.');\n      setResearchComplete(true);\n      setIsLoading(false);\n    }\n  };\n\n  // Fallback to HTTP if WebSocket fails\n  const fallbackToHttp = async (requestBody: ChatCompletionRequest) => {\n    try {\n      // Make the API call using HTTP\n      const apiResponse = await fetch(`/api/chat/stream`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(requestBody)\n      });\n\n      if (!apiResponse.ok) {\n        throw new Error(`API error: ${apiResponse.status}`);\n      }\n\n      // Process the streaming response\n      const reader = apiResponse.body?.getReader();\n      const decoder = new TextDecoder();\n\n      if (!reader) {\n        throw new Error('Failed to get response reader');\n      }\n\n      // Read the stream\n      let fullResponse = '';\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        const chunk = decoder.decode(value, { stream: true });\n        fullResponse += chunk;\n        setResponse(fullResponse);\n\n        // Extract research stage if this is a deep research response\n        if (deepResearch) {\n          const stage = extractResearchStage(fullResponse, researchIteration);\n          if (stage) {\n            // Add the stage to the research stages\n            setResearchStages(prev => {\n              const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type);\n              if (existingStageIndex >= 0) {\n                const newStages = [...prev];\n                newStages[existingStageIndex] = stage;\n                return newStages;\n              } else {\n                return [...prev, stage];\n              }\n            });\n          }\n        }\n      }\n\n      // Check if research is complete\n      const isComplete = checkIfResearchComplete(fullResponse);\n\n      // Force completion after a maximum number of iterations (5)\n      const forceComplete = researchIteration >= 5;\n\n      if (forceComplete && !isComplete) {\n        // If we're forcing completion, append a comprehensive conclusion to the response\n        const completionNote = \"\\n\\n## Final Conclusion\\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question.\";\n        fullResponse += completionNote;\n        setResponse(fullResponse);\n        setResearchComplete(true);\n      } else {\n        setResearchComplete(isComplete);\n      }\n    } catch (error) {\n      console.error('Error during HTTP fallback:', error);\n      setResponse(prev => prev + '\\n\\nError: Failed to get a response. Please try again.');\n      setResearchComplete(true);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // Effect to continue research when response is updated\n  useEffect(() => {\n    if (deepResearch && response && !isLoading && !researchComplete) {\n      const isComplete = checkIfResearchComplete(response);\n      if (isComplete) {\n        setResearchComplete(true);\n      } else if (researchIteration > 0 && researchIteration < 5) {\n        // Only auto-continue if we're already in a research process and haven't reached max iterations\n        // Use setTimeout to avoid potential infinite loops\n        const timer = setTimeout(() => {\n          continueResearch();\n        }, 1000);\n        return () => clearTimeout(timer);\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [response, isLoading, deepResearch, researchComplete, researchIteration]);\n\n  // Effect to update research stages when the response changes\n  useEffect(() => {\n    if (deepResearch && response && !isLoading) {\n      // Try to extract a research stage from the response\n      const stage = extractResearchStage(response, researchIteration);\n      if (stage) {\n        // Add or update the stage in the research stages\n        setResearchStages(prev => {\n          // Check if we already have this stage\n          const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type);\n          if (existingStageIndex >= 0) {\n            // Update existing stage\n            const newStages = [...prev];\n            newStages[existingStageIndex] = stage;\n            return newStages;\n          } else {\n            // Add new stage\n            return [...prev, stage];\n          }\n        });\n\n        // Update current stage index to point to this stage\n        setCurrentStageIndex(prev => {\n          const newIndex = researchStages.findIndex(s => s.iteration === stage.iteration && s.type === stage.type);\n          return newIndex >= 0 ? newIndex : prev;\n        });\n      }\n    }\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [response, isLoading, deepResearch, researchIteration]);\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n\n    if (!question.trim() || isLoading) return;\n\n    handleConfirmAsk();\n  };\n\n  // Handle confirm and send request\n  const handleConfirmAsk = async () => {\n    setIsLoading(true);\n    setResponse('');\n    setResearchIteration(0);\n    setResearchComplete(false);\n\n    try {\n      // Create initial message\n      const initialMessage: Message = {\n        role: 'user',\n        content: deepResearch ? `[DEEP RESEARCH] ${question}` : question\n      };\n\n      // Set initial conversation history\n      const newHistory: Message[] = [initialMessage];\n      setConversationHistory(newHistory);\n\n      // Prepare request body\n      const requestBody: ChatCompletionRequest = {\n        repo_url: getRepoUrl(repoInfo),\n        type: repoInfo.type,\n        messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })),\n        provider: selectedProvider,\n        model: isCustomSelectedModel ? customSelectedModel : selectedModel,\n        language: language\n      };\n\n      // Add tokens if available\n      if (repoInfo?.token) {\n        requestBody.token = repoInfo.token;\n      }\n\n      // Close any existing WebSocket connection\n      closeWebSocket(webSocketRef.current);\n\n      let fullResponse = '';\n\n      // Create a new WebSocket connection\n      webSocketRef.current = createChatWebSocket(\n        requestBody,\n        // Message handler\n        (message: string) => {\n          fullResponse += message;\n          setResponse(fullResponse);\n\n          // Extract research stage if this is a deep research response\n          if (deepResearch) {\n            const stage = extractResearchStage(fullResponse, 1); // First iteration\n            if (stage) {\n              // Add the stage to the research stages\n              setResearchStages([stage]);\n              setCurrentStageIndex(0);\n            }\n          }\n        },\n        // Error handler\n        (error: Event) => {\n          console.error('WebSocket error:', error);\n          setResponse(prev => prev + '\\n\\nError: WebSocket connection failed. Falling back to HTTP...');\n\n          // Fallback to HTTP if WebSocket fails\n          fallbackToHttp(requestBody);\n        },\n        // Close handler\n        () => {\n          // If deep research is enabled, check if we should continue\n          if (deepResearch) {\n            const isComplete = checkIfResearchComplete(fullResponse);\n            setResearchComplete(isComplete);\n\n            // If not complete, start the research process\n            if (!isComplete) {\n              setResearchIteration(1);\n              // The continueResearch function will be triggered by the useEffect\n            }\n          }\n\n          setIsLoading(false);\n        }\n      );\n    } catch (error) {\n      console.error('Error during API call:', error);\n      setResponse(prev => prev + '\\n\\nError: Failed to get a response. Please try again.');\n      setResearchComplete(true);\n      setIsLoading(false);\n    }\n  };\n\n  const [buttonWidth, setButtonWidth] = useState(0);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  // Measure button width and update state\n  useEffect(() => {\n    if (buttonRef.current) {\n      const width = buttonRef.current.offsetWidth;\n      setButtonWidth(width);\n    }\n  }, [messages.ask?.askButton, isLoading]);\n\n  return (\n    <div>\n      <div className=\"p-4\">\n        <div className=\"flex items-center justify-end mb-4\">\n          {/* Model selection button */}\n          <button\n            type=\"button\"\n            onClick={() => setIsModelSelectionModalOpen(true)}\n            className=\"text-xs px-2.5 py-1 rounded border border-[var(--border-color)]/40 bg-[var(--background)]/10 text-[var(--foreground)]/80 hover:bg-[var(--background)]/30 hover:text-[var(--foreground)] transition-colors flex items-center gap-1.5\"\n          >\n            <span>{selectedProvider}/{isCustomSelectedModel ? customSelectedModel : selectedModel}</span>\n            <svg className=\"h-3.5 w-3.5 text-[var(--accent-primary)]/70\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n              <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z\" />\n            </svg>\n          </button>\n        </div>\n\n        {/* Question input */}\n        <form onSubmit={handleSubmit} className=\"mt-4\">\n          <div className=\"relative\">\n            <input\n              ref={inputRef}\n              type=\"text\"\n              value={question}\n              onChange={(e) => setQuestion(e.target.value)}\n              placeholder={messages.ask?.placeholder || 'What would you like to know about this codebase?'}\n              className=\"block w-full rounded-md border border-[var(--border-color)] bg-[var(--input-bg)] text-[var(--foreground)] px-5 py-3.5 text-base shadow-sm focus:border-[var(--accent-primary)] focus:ring-2 focus:ring-[var(--accent-primary)]/30 focus:outline-none transition-all\"\n              style={{ paddingRight: `${buttonWidth + 24}px` }}\n              disabled={isLoading}\n            />\n            <button\n              ref={buttonRef}\n              type=\"submit\"\n              disabled={isLoading || !question.trim()}\n              className={`absolute right-3 top-1/2 transform -translate-y-1/2 px-4 py-2 rounded-md font-medium text-sm ${\n                isLoading || !question.trim()\n                  ? 'bg-[var(--button-disabled-bg)] text-[var(--button-disabled-text)] cursor-not-allowed'\n                  : 'bg-[var(--accent-primary)] text-white hover:bg-[var(--accent-primary)]/90 shadow-sm'\n              } transition-all duration-200 flex items-center gap-1.5`}\n            >\n              {isLoading ? (\n                <div className=\"w-4 h-4 rounded-full border-2 border-t-transparent border-white animate-spin\" />\n              ) : (\n                <>\n                  <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" />\n                  </svg>\n                  <span>{messages.ask?.askButton || 'Ask'}</span>\n                </>\n              )}\n            </button>\n          </div>\n\n          {/* Deep Research toggle */}\n          <div className=\"flex items-center mt-2 justify-between\">\n            <div className=\"group relative\">\n              <label className=\"flex items-center cursor-pointer\">\n                <span className=\"text-xs text-gray-600 dark:text-gray-400 mr-2\">Deep Research</span>\n                <div className=\"relative\">\n                  <input\n                    type=\"checkbox\"\n                    checked={deepResearch}\n                    onChange={() => setDeepResearch(!deepResearch)}\n                    className=\"sr-only\"\n                  />\n                  <div className={`w-10 h-5 rounded-full transition-colors ${deepResearch ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'}`}></div>\n                  <div className={`absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-white transition-transform transform ${deepResearch ? 'translate-x-5' : ''}`}></div>\n                </div>\n              </label>\n              <div className=\"absolute bottom-full left-0 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded p-2 w-72 z-10\">\n                <div className=\"relative\">\n                  <div className=\"absolute -bottom-2 left-4 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800\"></div>\n                  <p className=\"mb-1\">Deep Research conducts a multi-turn investigation process:</p>\n                  <ul className=\"list-disc pl-4 text-xs\">\n                    <li><strong>Initial Research:</strong> Creates a research plan and initial findings</li>\n                    <li><strong>Iteration 1:</strong> Explores specific aspects in depth</li>\n                    <li><strong>Iteration 2:</strong> Investigates remaining questions</li>\n                    <li><strong>Iterations 3-4:</strong> Dives deeper into complex areas</li>\n                    <li><strong>Final Conclusion:</strong> Comprehensive answer based on all iterations</li>\n                  </ul>\n                  <p className=\"mt-1 text-xs italic\">The AI automatically continues research until complete (up to 5 iterations)</p>\n                </div>\n              </div>\n            </div>\n            {deepResearch && (\n              <div className=\"text-xs text-purple-600 dark:text-purple-400\">\n                Multi-turn research process enabled\n                {researchIteration > 0 && !researchComplete && ` (iteration ${researchIteration})`}\n                {researchComplete && ` (complete)`}\n              </div>\n            )}\n          </div>\n        </form>\n\n        {/* Response area */}\n        {response && (\n          <div className=\"border-t border-gray-200 dark:border-gray-700 mt-4\">\n            <div\n              ref={responseRef}\n              className=\"p-4 max-h-[500px] overflow-y-auto\"\n            >\n              <Markdown content={response} />\n            </div>\n\n            {/* Research navigation and clear button */}\n            <div className=\"p-2 flex justify-between items-center border-t border-gray-200 dark:border-gray-700\">\n              {/* Research navigation */}\n              {deepResearch && researchStages.length > 1 && (\n                <div className=\"flex items-center space-x-2\">\n                  <button\n                    onClick={() => navigateToPreviousStage()}\n                    disabled={currentStageIndex === 0}\n                    className={`p-1 rounded-md ${currentStageIndex === 0 ? 'text-gray-400 dark:text-gray-600' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'}`}\n                    aria-label=\"Previous stage\"\n                  >\n                    <FaChevronLeft size={12} />\n                  </button>\n\n                  <div className=\"text-xs text-gray-600 dark:text-gray-400\">\n                    {currentStageIndex + 1} / {researchStages.length}\n                  </div>\n\n                  <button\n                    onClick={() => navigateToNextStage()}\n                    disabled={currentStageIndex === researchStages.length - 1}\n                    className={`p-1 rounded-md ${currentStageIndex === researchStages.length - 1 ? 'text-gray-400 dark:text-gray-600' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'}`}\n                    aria-label=\"Next stage\"\n                  >\n                    <FaChevronRight size={12} />\n                  </button>\n\n                  <div className=\"text-xs text-gray-600 dark:text-gray-400 ml-2\">\n                    {researchStages[currentStageIndex]?.title || `Stage ${currentStageIndex + 1}`}\n                  </div>\n                </div>\n              )}\n\n            <div className=\"flex items-center space-x-2\">\n              {/* Download button */}\n              <button\n                onClick={downloadresponse}\n                className=\"text-xs text-gray-500 dark:text-gray-400 hover:text-green-600 dark:hover:text-green-400 px-2 py-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center gap-1\"\n                title=\"Download response as markdown file\"\n              >\n                <svg className=\"w-3 h-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                  <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" />\n                </svg>\n                Download\n              </button>\n\n              {/* Clear button */}\n              <button\n                id=\"ask-clear-conversation\"\n                onClick={clearConversation}\n                className=\"text-xs text-gray-500 dark:text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 px-2 py-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700\"\n              >\n                Clear conversation\n              </button>\n            </div>\n              </div>\n          </div>\n        )}\n\n        {/* Loading indicator */}\n        {isLoading && !response && (\n          <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n            <div className=\"flex items-center space-x-2\">\n              <div className=\"animate-pulse flex space-x-1\">\n                <div className=\"h-2 w-2 bg-purple-600 rounded-full\"></div>\n                <div className=\"h-2 w-2 bg-purple-600 rounded-full\"></div>\n                <div className=\"h-2 w-2 bg-purple-600 rounded-full\"></div>\n              </div>\n              <span className=\"text-xs text-gray-500 dark:text-gray-400\">\n                {deepResearch\n                  ? (researchIteration === 0\n                    ? \"Planning research approach...\"\n                    : `Research iteration ${researchIteration} in progress...`)\n                  : \"Thinking...\"}\n              </span>\n            </div>\n            {deepResearch && (\n              <div className=\"mt-2 text-xs text-gray-500 dark:text-gray-400 pl-5\">\n                <div className=\"flex flex-col space-y-1\">\n                  {researchIteration === 0 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-blue-500 rounded-full mr-2\"></div>\n                        <span>Creating research plan...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-green-500 rounded-full mr-2\"></div>\n                        <span>Identifying key areas to investigate...</span>\n                      </div>\n                    </>\n                  )}\n                  {researchIteration === 1 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-blue-500 rounded-full mr-2\"></div>\n                        <span>Exploring first research area in depth...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-green-500 rounded-full mr-2\"></div>\n                        <span>Analyzing code patterns and structures...</span>\n                      </div>\n                    </>\n                  )}\n                  {researchIteration === 2 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-amber-500 rounded-full mr-2\"></div>\n                        <span>Investigating remaining questions...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-purple-500 rounded-full mr-2\"></div>\n                        <span>Connecting findings from previous iterations...</span>\n                      </div>\n                    </>\n                  )}\n                  {researchIteration === 3 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-indigo-500 rounded-full mr-2\"></div>\n                        <span>Exploring deeper connections...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-blue-500 rounded-full mr-2\"></div>\n                        <span>Analyzing complex patterns...</span>\n                      </div>\n                    </>\n                  )}\n                  {researchIteration === 4 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-teal-500 rounded-full mr-2\"></div>\n                        <span>Refining research conclusions...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-cyan-500 rounded-full mr-2\"></div>\n                        <span>Addressing remaining edge cases...</span>\n                      </div>\n                    </>\n                  )}\n                  {researchIteration >= 5 && (\n                    <>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-purple-500 rounded-full mr-2\"></div>\n                        <span>Finalizing comprehensive answer...</span>\n                      </div>\n                      <div className=\"flex items-center\">\n                        <div className=\"w-2 h-2 bg-green-500 rounded-full mr-2\"></div>\n                        <span>Synthesizing all research findings...</span>\n                      </div>\n                    </>\n                  )}\n                </div>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* Model Selection Modal */}\n      <ModelSelectionModal\n        isOpen={isModelSelectionModalOpen}\n        onClose={() => setIsModelSelectionModalOpen(false)}\n        provider={selectedProvider}\n        setProvider={setSelectedProvider}\n        model={selectedModel}\n        setModel={setSelectedModel}\n        isCustomModel={isCustomSelectedModel}\n        setIsCustomModel={setIsCustomSelectedModel}\n        customModel={customSelectedModel}\n        setCustomModel={setCustomSelectedModel}\n        isComprehensiveView={isComprehensiveView}\n        setIsComprehensiveView={setIsComprehensiveView}\n        showFileFilters={false}\n        onApply={() => {\n          console.log('Model selection applied:', selectedProvider, selectedModel);\n        }}\n        showWikiType={false}\n        authRequired={false}\n        isAuthLoading={false}\n      />\n    </div>\n  );\n};\n\nexport default Ask;\n"
  },
  {
    "path": "src/components/ConfigurationModal.tsx",
    "content": "'use client';\n\nimport React, { useState } from 'react';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport UserSelector from './UserSelector';\nimport TokenInput from './TokenInput';\n\ninterface ConfigurationModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n\n  // Repository input\n  repositoryInput: string;\n\n  // Language selection\n  selectedLanguage: string;\n  setSelectedLanguage: (value: string) => void;\n  supportedLanguages: Record<string, string>;\n\n  // Wiki type options\n  isComprehensiveView: boolean;\n  setIsComprehensiveView: (value: boolean) => void;\n\n  // Model selection\n  provider: string;\n  setProvider: (value: string) => void;\n  model: string;\n  setModel: (value: string) => void;\n  isCustomModel: boolean;\n  setIsCustomModel: (value: boolean) => void;\n  customModel: string;\n  setCustomModel: (value: string) => void;\n\n  // Platform selection\n  selectedPlatform: 'github' | 'gitlab' | 'bitbucket';\n  setSelectedPlatform: (value: 'github' | 'gitlab' | 'bitbucket') => void;\n\n  // Access token\n  accessToken: string;\n  setAccessToken: (value: string) => void;\n\n  // File filter options\n  excludedDirs: string;\n  setExcludedDirs: (value: string) => void;\n  excludedFiles: string;\n  setExcludedFiles: (value: string) => void;\n  includedDirs: string;\n  setIncludedDirs: (value: string) => void;\n  includedFiles: string;\n  setIncludedFiles: (value: string) => void;\n\n  // Form submission\n  onSubmit: () => void;\n  isSubmitting: boolean;\n\n  // Authentication\n  authRequired?: boolean;\n  authCode?: string;\n  setAuthCode?: (code: string) => void;\n  isAuthLoading?: boolean;\n}\n\nexport default function ConfigurationModal({\n  isOpen,\n  onClose,\n  repositoryInput,\n  selectedLanguage,\n  setSelectedLanguage,\n  supportedLanguages,\n  isComprehensiveView,\n  setIsComprehensiveView,\n  provider,\n  setProvider,\n  model,\n  setModel,\n  isCustomModel,\n  setIsCustomModel,\n  customModel,\n  setCustomModel,\n  selectedPlatform,\n  setSelectedPlatform,\n  accessToken,\n  setAccessToken,\n  excludedDirs,\n  setExcludedDirs,\n  excludedFiles,\n  setExcludedFiles,\n  includedDirs,\n  setIncludedDirs,\n  includedFiles,\n  setIncludedFiles,\n  onSubmit,\n  isSubmitting,\n  authRequired,\n  authCode,\n  setAuthCode,\n  isAuthLoading\n}: ConfigurationModalProps) {\n  const { messages: t } = useLanguage();\n\n  // Show token section state\n  const [showTokenSection, setShowTokenSection] = useState(false);\n\n  if (!isOpen) return null;\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-y-auto\">\n      <div className=\"flex min-h-screen items-center justify-center p-4 text-center bg-black/50\">\n        <div className=\"relative transform overflow-hidden rounded-lg bg-[var(--card-bg)] text-left shadow-xl transition-all sm:my-8 sm:max-w-2xl sm:w-full\">\n          {/* Modal header with close button */}\n          <div className=\"flex items-center justify-between px-6 py-4 border-b border-[var(--border-color)]\">\n            <h3 className=\"text-lg font-medium text-[var(--accent-primary)]\">\n              <span className=\"text-[var(--accent-primary)]\">{t.form?.configureWiki || 'Configure Wiki'}</span>\n            </h3>\n            <button\n              type=\"button\"\n              onClick={onClose}\n              className=\"text-[var(--muted)] hover:text-[var(--foreground)] focus:outline-none transition-colors\"\n            >\n              <svg className=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n              </svg>\n            </button>\n          </div>\n\n          {/* Modal body */}\n          <div className=\"p-6 max-h-[70vh] overflow-y-auto\">\n            {/* Repository info */}\n            <div className=\"mb-4\">\n              <label className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                {t.form?.repository || 'Repository'}\n              </label>\n              <div className=\"bg-[var(--background)]/70 p-3 rounded-md border border-[var(--border-color)] text-sm text-[var(--foreground)]\">\n                {repositoryInput}\n              </div>\n            </div>\n\n            {/* Language selection */}\n            <div className=\"mb-4\">\n              <label htmlFor=\"language-select\" className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                {t.form?.wikiLanguage || 'Wiki Language'}\n              </label>\n              <select\n                id=\"language-select\"\n                value={selectedLanguage}\n                onChange={(e) => setSelectedLanguage(e.target.value)}\n                className=\"input-japanese block w-full px-3 py-2 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n              >\n                {\n                  Object.entries(supportedLanguages).map(([key, value])=> <option key={key} value={key}>{value}</option>)\n                }\n              </select>\n            </div>\n\n            {/* Wiki Type Selector - more compact version */}\n            <div className=\"mb-4\">\n              <label className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                {t.form?.wikiType || 'Wiki Type'}\n              </label>\n              <div className=\"flex gap-3\">\n                <button\n                  type=\"button\"\n                  onClick={() => setIsComprehensiveView(true)}\n                  className={`flex-1 flex items-center justify-between p-2 rounded-md border transition-colors ${\n                    isComprehensiveView\n                      ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)]/30 text-[var(--accent-primary)]'\n                      : 'bg-[var(--background)]/50 border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                  }`}\n                >\n                  <div className=\"flex items-center\">\n                    <div className=\"text-left\">\n                      <div className=\"font-medium text-sm\">{t.form?.comprehensive || 'Comprehensive'}</div>\n                      <div className=\"text-xs opacity-80\">\n                        {t.form?.comprehensiveDescription || 'Detailed wiki with structured sections'}\n                      </div>\n                    </div>\n                  </div>\n                  {isComprehensiveView && (\n                    <div className=\"ml-2 h-4 w-4 rounded-full bg-[var(--accent-primary)]/20 flex items-center justify-center\">\n                      <div className=\"h-2 w-2 rounded-full bg-[var(--accent-primary)]\"></div>\n                    </div>\n                  )}\n                </button>\n\n                <button\n                  type=\"button\"\n                  onClick={() => setIsComprehensiveView(false)}\n                  className={`flex-1 flex items-center justify-between p-2 rounded-md border transition-colors ${\n                    !isComprehensiveView\n                      ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)]/30 text-[var(--accent-primary)]'\n                      : 'bg-[var(--background)]/50 border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                  }`}\n                >\n                  <div className=\"flex items-center\">\n                    <div className=\"text-left\">\n                      <div className=\"font-medium text-sm\">{t.form?.concise || 'Concise'}</div>\n                      <div className=\"text-xs opacity-80\">\n                        {t.form?.conciseDescription || 'Simplified wiki with fewer pages'}\n                      </div>\n                    </div>\n                  </div>\n                  {!isComprehensiveView && (\n                    <div className=\"ml-2 h-4 w-4 rounded-full bg-[var(--accent-primary)]/20 flex items-center justify-center\">\n                      <div className=\"h-2 w-2 rounded-full bg-[var(--accent-primary)]\"></div>\n                    </div>\n                  )}\n                </button>\n              </div>\n            </div>\n\n            {/* Model Selector */}\n            <div className=\"mb-4\">\n              <UserSelector\n                provider={provider}\n                setProvider={setProvider}\n                model={model}\n                setModel={setModel}\n                isCustomModel={isCustomModel}\n                setIsCustomModel={setIsCustomModel}\n                customModel={customModel}\n                setCustomModel={setCustomModel}\n                showFileFilters={true}\n                excludedDirs={excludedDirs}\n                setExcludedDirs={setExcludedDirs}\n                excludedFiles={excludedFiles}\n                setExcludedFiles={setExcludedFiles}\n                includedDirs={includedDirs}\n                setIncludedDirs={setIncludedDirs}\n                includedFiles={includedFiles}\n                setIncludedFiles={setIncludedFiles}\n              />\n            </div>\n\n            {/* Access token section using TokenInput component */}\n            <TokenInput\n              selectedPlatform={selectedPlatform}\n              setSelectedPlatform={setSelectedPlatform}\n              accessToken={accessToken}\n              setAccessToken={setAccessToken}\n              showTokenSection={showTokenSection}\n              onToggleTokenSection={() => setShowTokenSection(!showTokenSection)}\n              allowPlatformChange={true}\n            />\n\n            {/* Authorization Code Input */}\n            {isAuthLoading && (\n              <div className=\"mb-4 p-3 bg-[var(--background)]/50 rounded-md border border-[var(--border-color)] text-sm text-[var(--muted)]\">\n                Loading authentication status...\n              </div>\n            )}\n            {!isAuthLoading && authRequired && (\n              <div className=\"mb-4 p-4 bg-[var(--background)]/50 rounded-md border border-[var(--border-color)]\">\n                <label htmlFor=\"authCode\" className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                  {t.form?.authorizationCode || 'Authorization Code'}\n                </label>\n                <input\n                  type=\"password\"\n                  id=\"authCode\"\n                  value={authCode || ''}\n                  onChange={(e) => setAuthCode?.(e.target.value)}\n                  className=\"input-japanese block w-full px-3 py-2 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n                  placeholder=\"Enter your authorization code\"\n                />\n                 <div className=\"flex items-center mt-2 text-xs text-[var(--muted)]\">\n                  <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"h-4 w-4 mr-1 text-[var(--muted)]\"\n                    fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2}\n                      d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                  </svg>\n                   {t.form?.authorizationRequired || 'Authentication is required to generate the wiki.'}\n                </div>\n              </div>\n            )}\n          </div>\n\n          {/* Modal footer */}\n          <div className=\"flex items-center justify-end gap-2 px-6 py-4 border-t border-[var(--border-color)]\">\n            <button\n              type=\"button\"\n              onClick={onClose}\n              className=\"px-4 py-2 text-sm font-medium rounded-md border border-[var(--border-color)]/50 text-[var(--muted)] bg-transparent hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-colors\"\n            >\n              {t.common?.cancel || 'Cancel'}\n            </button>\n            <button\n              type=\"button\"\n              onClick={onSubmit}\n              disabled={isSubmitting}\n              className=\"px-4 py-2 text-sm font-medium rounded-md border border-transparent bg-[var(--accent-primary)]/90 text-white hover:bg-[var(--accent-primary)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n            >\n              {isSubmitting ? (t.common?.processing || 'Processing...') : (t.common?.generateWiki || 'Generate Wiki')}\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Markdown.tsx",
    "content": "import React from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport rehypeRaw from 'rehype-raw';\nimport { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';\nimport { tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/prism';\nimport Mermaid from './Mermaid';\n\ninterface MarkdownProps {\n  content: string;\n}\n\nconst Markdown: React.FC<MarkdownProps> = ({ content }) => {\n  // Define markdown components\n  const MarkdownComponents: React.ComponentProps<typeof ReactMarkdown>['components'] = {\n    p({ children, ...props }: { children?: React.ReactNode }) {\n      return <p className=\"mb-3 text-sm leading-relaxed dark:text-white\" {...props}>{children}</p>;\n    },\n    h1({ children, ...props }: { children?: React.ReactNode }) {\n      return <h1 className=\"text-xl font-bold mt-6 mb-3 dark:text-white\" {...props}>{children}</h1>;\n    },\n    h2({ children, ...props }: { children?: React.ReactNode }) {\n      // Special styling for ReAct headings\n      if (children && typeof children === 'string') {\n        const text = children.toString();\n        if (text.includes('Thought') || text.includes('Action') || text.includes('Observation') || text.includes('Answer')) {\n          return (\n            <h2\n              className={`text-base font-bold mt-5 mb-3 p-2 rounded ${\n                text.includes('Thought') ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300' :\n                text.includes('Action') ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300' :\n                text.includes('Observation') ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300' :\n                text.includes('Answer') ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300' :\n                'dark:text-white'\n              }`}\n              {...props}\n            >\n              {children}\n            </h2>\n          );\n        }\n      }\n      return <h2 className=\"text-lg font-bold mt-5 mb-3 dark:text-white\" {...props}>{children}</h2>;\n    },\n    h3({ children, ...props }: { children?: React.ReactNode }) {\n      return <h3 className=\"text-base font-semibold mt-4 mb-2 dark:text-white\" {...props}>{children}</h3>;\n    },\n    h4({ children, ...props }: { children?: React.ReactNode }) {\n      return <h4 className=\"text-sm font-semibold mt-3 mb-2 dark:text-white\" {...props}>{children}</h4>;\n    },\n    ul({ children, ...props }: { children?: React.ReactNode }) {\n      return <ul className=\"list-disc pl-6 mb-4 text-sm dark:text-white space-y-2\" {...props}>{children}</ul>;\n    },\n    ol({ children, ...props }: { children?: React.ReactNode }) {\n      return <ol className=\"list-decimal pl-6 mb-4 text-sm dark:text-white space-y-2\" {...props}>{children}</ol>;\n    },\n    li({ children, ...props }: { children?: React.ReactNode }) {\n      return <li className=\"mb-2 text-sm leading-relaxed dark:text-white\" {...props}>{children}</li>;\n    },\n    a({ children, href, ...props }: { children?: React.ReactNode; href?: string }) {\n      return (\n        <a\n          href={href}\n          className=\"text-purple-600 dark:text-purple-400 hover:underline font-medium\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          {...props}\n        >\n          {children}\n        </a>\n      );\n    },\n    blockquote({ children, ...props }: { children?: React.ReactNode }) {\n      return (\n        <blockquote\n          className=\"border-l-4 border-gray-300 dark:border-gray-700 pl-4 py-1 text-gray-700 dark:text-gray-300 italic my-4 text-sm\"\n          {...props}\n        >\n          {children}\n        </blockquote>\n      );\n    },\n    table({ children, ...props }: { children?: React.ReactNode }) {\n      return (\n        <div className=\"overflow-x-auto my-6 rounded-md\">\n          <table className=\"min-w-full text-sm border-collapse\" {...props}>\n            {children}\n          </table>\n        </div>\n      );\n    },\n    thead({ children, ...props }: { children?: React.ReactNode }) {\n      return <thead className=\"bg-gray-100 dark:bg-gray-800\" {...props}>{children}</thead>;\n    },\n    tbody({ children, ...props }: { children?: React.ReactNode }) {\n      return <tbody className=\"divide-y divide-gray-200 dark:divide-gray-700\" {...props}>{children}</tbody>;\n    },\n    tr({ children, ...props }: { children?: React.ReactNode }) {\n      return <tr className=\"hover:bg-gray-50 dark:hover:bg-gray-900\" {...props}>{children}</tr>;\n    },\n    th({ children, ...props }: { children?: React.ReactNode }) {\n      return (\n        <th\n          className=\"px-4 py-3 text-left font-medium text-gray-700 dark:text-gray-300\"\n          {...props}\n        >\n          {children}\n        </th>\n      );\n    },\n    td({ children, ...props }: { children?: React.ReactNode }) {\n      return <td className=\"px-4 py-3 border-t border-gray-200 dark:border-gray-700\" {...props}>{children}</td>;\n    },\n    code(props: {\n      inline?: boolean;\n      className?: string;\n      children?: React.ReactNode;\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      [key: string]: any; // Using any here as it's required for ReactMarkdown components\n    }) {\n      const { inline, className, children, ...otherProps } = props;\n      const match = /language-(\\w+)/.exec(className || '');\n      const codeContent = children ? String(children).replace(/\\n$/, '') : '';\n\n      // Handle Mermaid diagrams\n      if (!inline && match && match[1] === 'mermaid') {\n        return (\n          <div className=\"my-8 bg-gray-50 dark:bg-gray-800 rounded-md overflow-hidden shadow-sm\">\n            <Mermaid\n              chart={codeContent}\n              className=\"w-full max-w-full\"\n              zoomingEnabled={true}\n            />\n          </div>\n        );\n      }\n\n      // Handle code blocks\n      if (!inline && match) {\n        return (\n          <div className=\"my-6 rounded-md overflow-hidden text-sm shadow-sm\">\n            <div className=\"bg-gray-800 text-gray-200 px-5 py-2 text-sm flex justify-between items-center\">\n              <span>{match[1]}</span>\n              <button\n                onClick={() => {\n                  navigator.clipboard.writeText(codeContent);\n                }}\n                className=\"text-gray-400 hover:text-white\"\n                title=\"Copy code\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  className=\"h-5 w-5\"\n                  fill=\"none\"\n                  viewBox=\"0 0 24 24\"\n                  stroke=\"currentColor\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"\n                  />\n                </svg>\n              </button>\n            </div>\n            <SyntaxHighlighter\n              language={match[1]}\n              style={tomorrow}\n              className=\"!text-sm\"\n              customStyle={{ margin: 0, borderRadius: '0 0 0.375rem 0.375rem', padding: '1rem' }}\n              showLineNumbers={true}\n              wrapLines={true}\n              wrapLongLines={true}\n              {...otherProps}\n            >\n              {codeContent}\n            </SyntaxHighlighter>\n          </div>\n        );\n      }\n\n      // Handle inline code\n      return (\n        <code\n          className={`${className} font-mono bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded text-pink-500 dark:text-pink-400 text-sm`}\n          {...otherProps}\n        >\n          {children}\n        </code>\n      );\n    },\n  };\n\n  return (\n    <div className=\"prose prose-base dark:prose-invert max-w-none px-2 py-4\">\n      <ReactMarkdown\n        remarkPlugins={[remarkGfm]}\n        rehypePlugins={[rehypeRaw]}\n        components={MarkdownComponents}\n      >\n        {content}\n      </ReactMarkdown>\n    </div>\n  );\n};\n\nexport default Markdown;"
  },
  {
    "path": "src/components/Mermaid.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport mermaid from 'mermaid';\n// We'll use dynamic import for svg-pan-zoom\n\n// Initialize mermaid with defaults - Japanese aesthetic\nmermaid.initialize({\n  startOnLoad: true,\n  theme: 'neutral',\n  securityLevel: 'loose',\n  suppressErrorRendering: true,\n  logLevel: 'error',\n  maxTextSize: 100000, // Increase text size limit\n  htmlLabels: true,\n  flowchart: {\n    htmlLabels: true,\n    curve: 'basis',\n    nodeSpacing: 60,\n    rankSpacing: 60,\n    padding: 20,\n  },\n  themeCSS: `\n    /* Japanese aesthetic styles for all diagrams */\n    .node rect, .node circle, .node ellipse, .node polygon, .node path {\n      fill: #f8f4e6;\n      stroke: #d7c4bb;\n      stroke-width: 1px;\n    }\n    .edgePath .path {\n      stroke: #9b7cb9;\n      stroke-width: 1.5px;\n    }\n    .edgeLabel {\n      background-color: transparent;\n      color: #333333;\n      p {\n        background-color: transparent !important;\n      }\n    }\n    .label {\n      color: #333333;\n    }\n    .cluster rect {\n      fill: #f8f4e6;\n      stroke: #d7c4bb;\n      stroke-width: 1px;\n    }\n\n    /* Sequence diagram specific styles */\n    .actor {\n      fill: #f8f4e6;\n      stroke: #d7c4bb;\n      stroke-width: 1px;\n    }\n    text.actor {\n      fill: #333333;\n      stroke: none;\n    }\n    .messageText {\n      fill: #333333;\n      stroke: none;\n    }\n    .messageLine0, .messageLine1 {\n      stroke: #9b7cb9;\n    }\n    .noteText {\n      fill: #333333;\n    }\n\n    /* Dark mode overrides - will be applied with data-theme=\"dark\" */\n    [data-theme=\"dark\"] .node rect,\n    [data-theme=\"dark\"] .node circle,\n    [data-theme=\"dark\"] .node ellipse,\n    [data-theme=\"dark\"] .node polygon,\n    [data-theme=\"dark\"] .node path {\n      fill: #222222;\n      stroke: #5d4037;\n    }\n    [data-theme=\"dark\"] .edgePath .path {\n      stroke: #9370db;\n    }\n    [data-theme=\"dark\"] .edgeLabel {\n      background-color: transparent;\n      color: #f0f0f0;\n    }\n    [data-theme=\"dark\"] .label {\n      color: #f0f0f0;\n    }\n    [data-theme=\"dark\"] .cluster rect {\n      fill: #222222;\n      stroke: #5d4037;\n    }\n    [data-theme=\"dark\"] .flowchart-link {\n      stroke: #9370db;\n    }\n\n    /* Dark mode sequence diagram overrides */\n    [data-theme=\"dark\"] .actor {\n      fill: #222222;\n      stroke: #5d4037;\n    }\n    [data-theme=\"dark\"] text.actor {\n      fill: #f0f0f0;\n      stroke: none;\n    }\n    [data-theme=\"dark\"] .messageText {\n      fill: #f0f0f0;\n      stroke: none;\n      font-weight: 500;\n    }\n    [data-theme=\"dark\"] .messageLine0, [data-theme=\"dark\"] .messageLine1 {\n      stroke: #9370db;\n      stroke-width: 1.5px;\n    }\n    [data-theme=\"dark\"] .noteText {\n      fill: #f0f0f0;\n    }\n    /* Additional styles for sequence diagram text */\n    [data-theme=\"dark\"] #sequenceNumber {\n      fill: #f0f0f0;\n    }\n    [data-theme=\"dark\"] text.sequenceText {\n      fill: #f0f0f0;\n      font-weight: 500;\n    }\n    [data-theme=\"dark\"] text.loopText, [data-theme=\"dark\"] text.loopText tspan {\n      fill: #f0f0f0;\n    }\n    /* Add a subtle background to message text for better readability */\n    [data-theme=\"dark\"] .messageText, [data-theme=\"dark\"] text.sequenceText {\n      paint-order: stroke;\n      stroke: #1a1a1a;\n      stroke-width: 2px;\n      stroke-linecap: round;\n      stroke-linejoin: round;\n    }\n\n    /* Force text elements to be properly colored */\n    text[text-anchor][dominant-baseline],\n    text[text-anchor][alignment-baseline],\n    .nodeLabel,\n    .edgeLabel,\n    .label,\n    text {\n      fill: #777 !important;\n    }\n\n    [data-theme=\"dark\"] text[text-anchor][dominant-baseline],\n    [data-theme=\"dark\"] text[text-anchor][alignment-baseline],\n    [data-theme=\"dark\"] .nodeLabel,\n    [data-theme=\"dark\"] .edgeLabel,\n    [data-theme=\"dark\"] .label,\n    [data-theme=\"dark\"] text {\n      fill: #f0f0f0 !important;\n    }\n\n    /* Add clickable element styles with subtle transitions */\n    .clickable {\n      transition: all 0.3s ease;\n    }\n    .clickable:hover {\n      transform: scale(1.03);\n      cursor: pointer;\n    }\n    .clickable:hover > * {\n      filter: brightness(0.95);\n    }\n  `,\n  fontFamily: 'var(--font-geist-sans), var(--font-serif-jp), sans-serif',\n  fontSize: 12,\n});\n\ninterface MermaidProps {\n  chart: string;\n  className?: string;\n  zoomingEnabled?: boolean;\n}\n\n// Full screen modal component for the diagram\nconst FullScreenModal: React.FC<{\n  isOpen: boolean;\n  onClose: () => void;\n  children: React.ReactNode;\n}> = ({ isOpen, onClose, children }) => {\n  const modalRef = useRef<HTMLDivElement>(null);\n  const [zoom, setZoom] = useState(1);\n\n  // Close on Escape key\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        onClose();\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener('keydown', handleKeyDown);\n    }\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [isOpen, onClose]);\n\n  // Handle click outside to close\n  useEffect(() => {\n    const handleOutsideClick = (e: MouseEvent) => {\n      if (modalRef.current && !modalRef.current.contains(e.target as Node)) {\n        onClose();\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener('mousedown', handleOutsideClick);\n    }\n\n    return () => {\n      document.removeEventListener('mousedown', handleOutsideClick);\n    };\n  }, [isOpen, onClose]);\n\n  // Reset zoom when modal opens\n  useEffect(() => {\n    if (isOpen) {\n      setZoom(1);\n    }\n  }, [isOpen]);\n\n  if (!isOpen) return null;\n\n  return (\n    <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4\">\n      <div\n        ref={modalRef}\n        className=\"bg-[var(--card-bg)] rounded-lg shadow-custom max-w-5xl max-h-[90vh] w-full overflow-hidden flex flex-col card-japanese\"\n      >\n        {/* Modal header with controls */}\n        <div className=\"flex items-center justify-between p-4 border-b border-[var(--border-color)]\">\n          <div className=\"font-medium text-[var(--foreground)] font-serif\">図表表示</div>\n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex items-center gap-2\">\n              <button\n                onClick={() => setZoom(Math.max(0.5, zoom - 0.1))}\n                className=\"text-[var(--foreground)] hover:bg-[var(--accent-primary)]/10 p-2 rounded-md border border-[var(--border-color)] transition-colors\"\n                aria-label=\"Zoom out\"\n              >\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                  <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n                  <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n                  <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"></line>\n                </svg>\n              </button>\n              <span className=\"text-sm text-[var(--muted)]\">{Math.round(zoom * 100)}%</span>\n              <button\n                onClick={() => setZoom(Math.min(2, zoom + 0.1))}\n                className=\"text-[var(--foreground)] hover:bg-[var(--accent-primary)]/10 p-2 rounded-md border border-[var(--border-color)] transition-colors\"\n                aria-label=\"Zoom in\"\n              >\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                  <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n                  <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n                  <line x1=\"11\" y1=\"8\" x2=\"11\" y2=\"14\"></line>\n                  <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"></line>\n                </svg>\n              </button>\n              <button\n                onClick={() => setZoom(1)}\n                className=\"text-[var(--foreground)] hover:bg-[var(--accent-primary)]/10 p-2 rounded-md border border-[var(--border-color)] transition-colors\"\n                aria-label=\"Reset zoom\"\n              >\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                  <path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\"></path>\n                  <path d=\"M21 3v5h-5\"></path>\n                </svg>\n              </button>\n            </div>\n            <button\n              onClick={onClose}\n              className=\"text-[var(--foreground)] hover:bg-[var(--accent-primary)]/10 p-2 rounded-md border border-[var(--border-color)] transition-colors\"\n              aria-label=\"Close\"\n            >\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n                <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n              </svg>\n            </button>\n          </div>\n        </div>\n\n        {/* Modal content with zoom */}\n        <div className=\"overflow-auto p-6 flex-1 flex items-center justify-center bg-[var(--background)]/50\">\n          <div\n            style={{\n              transform: `scale(${zoom})`,\n              transformOrigin: 'center center',\n              transition: 'transform 0.3s ease-out'\n            }}\n          >\n            {children}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst Mermaid: React.FC<MermaidProps> = ({ chart, className = '', zoomingEnabled = false }) => {\n  const [svg, setSvg] = useState<string>('');\n  const [error, setError] = useState<string | null>(null);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n  const mermaidRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const idRef = useRef(`mermaid-${Math.random().toString(36).substring(2, 9)}`);\n  const isDarkModeRef = useRef(\n    typeof window !== 'undefined' &&\n    window.matchMedia &&\n    window.matchMedia('(prefers-color-scheme: dark)').matches\n  );\n\n  // Initialize pan-zoom functionality when SVG is rendered\n  useEffect(() => {\n    if (svg && zoomingEnabled && containerRef.current) {\n      const initializePanZoom = async () => {\n        const svgElement = containerRef.current?.querySelector(\"svg\");\n        if (svgElement) {\n          // Remove any max-width constraints\n          svgElement.style.maxWidth = \"none\";\n          svgElement.style.width = \"100%\";\n          svgElement.style.height = \"100%\";\n\n          try {\n            // Dynamically import svg-pan-zoom only when needed in the browser\n            const svgPanZoom = (await import(\"svg-pan-zoom\")).default;\n\n            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            });\n          } catch (error) {\n            console.error(\"Failed to load svg-pan-zoom:\", error);\n          }\n        }\n      };\n\n      // Wait for the SVG to be rendered\n      setTimeout(() => {\n        void initializePanZoom();\n      }, 100);\n    }\n  }, [svg, zoomingEnabled]);\n\n  useEffect(() => {\n    if (!chart) return;\n\n    let isMounted = true;\n\n    const renderChart = async () => {\n      if (!isMounted) return;\n\n      try {\n        setError(null);\n        setSvg('');\n\n        // Render the chart directly without preprocessing\n        const { svg: renderedSvg } = await mermaid.render(idRef.current, chart);\n\n        if (!isMounted) return;\n\n        let processedSvg = renderedSvg;\n        if (isDarkModeRef.current) {\n          processedSvg = processedSvg.replace('<svg ', '<svg data-theme=\"dark\" ');\n        }\n\n        setSvg(processedSvg);\n\n        // Call mermaid.contentLoaded to ensure proper initialization\n        setTimeout(() => {\n          mermaid.contentLoaded();\n        }, 50);\n      } catch (err) {\n        console.error('Mermaid rendering error:', err);\n\n        const errorMessage = err instanceof Error ? err.message : String(err);\n\n        if (isMounted) {\n          setError(`Failed to render diagram: ${errorMessage}`);\n\n          if (mermaidRef.current) {\n            mermaidRef.current.innerHTML = `\n              <div class=\"text-red-500 dark:text-red-400 text-xs mb-1\">Syntax error in diagram</div>\n              <pre class=\"text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded\">${chart}</pre>\n            `;\n          }\n        }\n      }\n    };\n\n    renderChart();\n\n    return () => {\n      isMounted = false;\n    };\n  }, [chart]);\n\n  const handleDiagramClick = () => {\n    if (!error && svg) {\n      setIsFullscreen(true);\n    }\n  };\n\n  if (error) {\n    return (\n      <div className={`border border-[var(--highlight)]/30 rounded-md p-4 bg-[var(--highlight)]/5 ${className}`}>\n        <div className=\"flex items-center mb-3\">\n          <div className=\"text-[var(--highlight)] text-xs font-medium flex items-center\">\n            <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"h-4 w-4 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n              <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n            </svg>\n            図表レンダリングエラー\n          </div>\n        </div>\n        <div ref={mermaidRef} className=\"text-xs overflow-auto\"></div>\n        <div className=\"mt-3 text-xs text-[var(--muted)] font-serif\">\n          図表に構文エラーがあり、レンダリングできません。\n        </div>\n      </div>\n    );\n  }\n\n  if (!svg) {\n    return (\n      <div className={`flex justify-center items-center p-4 ${className}`}>\n        <div className=\"flex items-center space-x-2\">\n          <div className=\"w-2 h-2 bg-[var(--accent-primary)]/70 rounded-full animate-pulse\"></div>\n          <div className=\"w-2 h-2 bg-[var(--accent-primary)]/70 rounded-full animate-pulse delay-75\"></div>\n          <div className=\"w-2 h-2 bg-[var(--accent-primary)]/70 rounded-full animate-pulse delay-150\"></div>\n          <span className=\"text-[var(--muted)] text-xs ml-2 font-serif\">図表を描画中...</span>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div\n        ref={containerRef}\n        className={`w-full max-w-full ${zoomingEnabled ? \"h-[600px] p-4\" : \"\"}`}\n      >\n        <div\n          className={`relative group ${zoomingEnabled ? \"h-full rounded-lg border-2 border-black\" : \"\"}`}\n        >\n          <div\n            className={`flex justify-center overflow-auto text-center my-2 cursor-pointer hover:shadow-md transition-shadow duration-200 rounded-md ${className} ${zoomingEnabled ? \"h-full\" : \"\"}`}\n            dangerouslySetInnerHTML={{ __html: svg }}\n            onClick={zoomingEnabled ? undefined : handleDiagramClick}\n            title={zoomingEnabled ? undefined : \"Click to view fullscreen\"}\n          />\n\n          {!zoomingEnabled && (\n            <div className=\"absolute top-2 right-2 bg-gray-700/70 dark:bg-gray-900/70 text-white p-1.5 rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5 text-xs shadow-md pointer-events-none\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n                <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n                <line x1=\"11\" y1=\"8\" x2=\"11\" y2=\"14\"></line>\n                <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"></line>\n              </svg>\n              <span>Click to zoom</span>\n            </div>\n          )}\n        </div>\n      </div>\n\n      {!zoomingEnabled && (\n        <FullScreenModal\n          isOpen={isFullscreen}\n          onClose={() => setIsFullscreen(false)}\n        >\n          <div dangerouslySetInnerHTML={{ __html: svg }} />\n        </FullScreenModal>\n      )}\n    </>\n  );\n};\n\n\n\nexport default Mermaid;"
  },
  {
    "path": "src/components/ModelSelectionModal.tsx",
    "content": "'use client';\n\nimport React, {useEffect, useState} from 'react';\nimport {useLanguage} from '@/contexts/LanguageContext';\nimport UserSelector from './UserSelector';\nimport WikiTypeSelector from './WikiTypeSelector';\nimport TokenInput from './TokenInput';\n\ninterface ModelSelectionModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  provider: string;\n  setProvider: (value: string) => void;\n  model: string;\n  setModel: (value: string) => void;\n  isCustomModel: boolean;\n  setIsCustomModel: (value: boolean) => void;\n  customModel: string;\n  setCustomModel: (value: string) => void;\n  onApply: (token?: string) => void;\n\n  // Wiki type options\n  isComprehensiveView: boolean;\n  setIsComprehensiveView: (value: boolean) => void;\n\n  // File filter options - optional\n  excludedDirs?: string;\n  setExcludedDirs?: (value: string) => void;\n  excludedFiles?: string;\n  setExcludedFiles?: (value: string) => void;\n  includedDirs?: string;\n  setIncludedDirs?: (value: string) => void;\n  includedFiles?: string;\n  setIncludedFiles?: (value: string) => void;\n  showFileFilters?: boolean;\n  showWikiType: boolean;\n  \n  // Token input for refresh\n  showTokenInput?: boolean;\n  repositoryType?: 'github' | 'gitlab' | 'bitbucket';\n  // Authentication\n  authRequired?: boolean;\n  authCode?: string;\n  setAuthCode?: (code: string) => void;\n  isAuthLoading?: boolean;\n}\n\nexport default function ModelSelectionModal({\n  isOpen,\n  onClose,\n  provider,\n  setProvider,\n  model,\n  setModel,\n  isCustomModel,\n  setIsCustomModel,\n  customModel,\n  setCustomModel,\n  onApply,\n  isComprehensiveView,\n  setIsComprehensiveView,\n  excludedDirs = '',\n  setExcludedDirs,\n  excludedFiles = '',\n  setExcludedFiles,\n  includedDirs = '',\n  setIncludedDirs,\n  includedFiles = '',\n  setIncludedFiles,\n  showFileFilters = false,\n  authRequired = false,\n  authCode = '',\n  setAuthCode,\n  isAuthLoading,\n  showWikiType = true,\n  showTokenInput = false,\n  repositoryType = 'github',\n}: ModelSelectionModalProps) {\n  const { messages: t } = useLanguage();\n\n  // Local state for form values (to only apply changes when the user clicks \"Submit\")\n  const [localProvider, setLocalProvider] = useState(provider);\n  const [localModel, setLocalModel] = useState(model);\n  const [localIsCustomModel, setLocalIsCustomModel] = useState(isCustomModel);\n  const [localCustomModel, setLocalCustomModel] = useState(customModel);\n  const [localIsComprehensiveView, setLocalIsComprehensiveView] = useState(isComprehensiveView);\n  const [localExcludedDirs, setLocalExcludedDirs] = useState(excludedDirs);\n  const [localExcludedFiles, setLocalExcludedFiles] = useState(excludedFiles);\n  const [localIncludedDirs, setLocalIncludedDirs] = useState(includedDirs);\n  const [localIncludedFiles, setLocalIncludedFiles] = useState(includedFiles);\n  \n  // Token input state\n  const [localAccessToken, setLocalAccessToken] = useState('');\n  const [localSelectedPlatform, setLocalSelectedPlatform] = useState<'github' | 'gitlab' | 'bitbucket'>(repositoryType);\n  const [showTokenSection, setShowTokenSection] = useState(showTokenInput);\n\n  // Reset local state when modal is opened\n  useEffect(() => {\n    if (isOpen) {\n      setLocalProvider(provider);\n      setLocalModel(model);\n      setLocalIsCustomModel(isCustomModel);\n      setLocalCustomModel(customModel);\n      setLocalIsComprehensiveView(isComprehensiveView);\n      setLocalExcludedDirs(excludedDirs);\n      setLocalExcludedFiles(excludedFiles);\n      setLocalIncludedDirs(includedDirs);\n      setLocalIncludedFiles(includedFiles);\n      setLocalSelectedPlatform(repositoryType);\n      setLocalAccessToken('');\n      setShowTokenSection(showTokenInput);\n    }\n  }, [isOpen, provider, model, isCustomModel, customModel, isComprehensiveView, excludedDirs, excludedFiles, includedDirs, includedFiles, repositoryType, showTokenInput]);\n\n  // Handler for applying changes\n  const handleApply = () => {\n    setProvider(localProvider);\n    setModel(localModel);\n    setIsCustomModel(localIsCustomModel);\n    setCustomModel(localCustomModel);\n    setIsComprehensiveView(localIsComprehensiveView);\n    if (setExcludedDirs) setExcludedDirs(localExcludedDirs);\n    if (setExcludedFiles) setExcludedFiles(localExcludedFiles);\n    if (setIncludedDirs) setIncludedDirs(localIncludedDirs);\n    if (setIncludedFiles) setIncludedFiles(localIncludedFiles);\n    \n    // Pass token to onApply if needed\n    if (showTokenInput) {\n      onApply(localAccessToken);\n    } else {\n      onApply();\n    }\n    onClose();\n  };\n\n  if (!isOpen) return null;\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-y-auto\">\n      <div className=\"flex min-h-screen items-center justify-center p-4 text-center bg-black/50\">\n        <div className=\"relative transform overflow-hidden rounded-lg bg-[var(--card-bg)] text-left shadow-xl transition-all sm:my-8 sm:max-w-lg sm:w-full\">\n          {/* Modal header with close button */}\n          <div className=\"flex items-center justify-between px-6 py-4 border-b border-[var(--border-color)]\">\n            <h3 className=\"text-lg font-medium text-[var(--accent-primary)]\">\n              <span className=\"text-[var(--accent-primary)]\">{t.form?.modelSelection || 'Model Selection'}</span>\n            </h3>\n            <button\n              type=\"button\"\n              onClick={onClose}\n              className=\"text-[var(--muted)] hover:text-[var(--foreground)] focus:outline-none transition-colors\"\n            >\n              <svg className=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n              </svg>\n            </button>\n          </div>\n\n          {/* Modal body */}\n          <div className=\"p-6\">\n            {/* Wiki Type Selector */}\n            {\n              showWikiType && <WikiTypeSelector\n                    isComprehensiveView={localIsComprehensiveView}\n                    setIsComprehensiveView={setLocalIsComprehensiveView}\n                />\n            }\n\n            {/* Divider */}\n            <div className=\"my-4 border-t border-[var(--border-color)]/30\"></div>\n\n            {/* Model Selector */}\n            <UserSelector\n              provider={localProvider}\n              setProvider={setLocalProvider}\n              model={localModel}\n              setModel={setLocalModel}\n              isCustomModel={localIsCustomModel}\n              setIsCustomModel={setLocalIsCustomModel}\n              customModel={localCustomModel}\n              setCustomModel={setLocalCustomModel}\n              showFileFilters={showFileFilters}\n              excludedDirs={localExcludedDirs}\n              setExcludedDirs={showFileFilters ? (value: string) => setLocalExcludedDirs(value) : undefined}\n              excludedFiles={localExcludedFiles}\n              setExcludedFiles={showFileFilters ? (value: string) => setLocalExcludedFiles(value) : undefined}\n              includedDirs={localIncludedDirs}\n              setIncludedDirs={showFileFilters ? (value: string) => setLocalIncludedDirs(value) : undefined}\n              includedFiles={localIncludedFiles}\n              setIncludedFiles={showFileFilters ? (value: string) => setLocalIncludedFiles(value) : undefined}\n            />\n\n            {/* Token Input Section for refresh */}\n            {showTokenInput && (\n              <>\n                <div className=\"my-4 border-t border-[var(--border-color)]/30\"></div>\n                <TokenInput\n                  selectedPlatform={localSelectedPlatform}\n                  setSelectedPlatform={setLocalSelectedPlatform}\n                  accessToken={localAccessToken}\n                  setAccessToken={setLocalAccessToken}\n                  showTokenSection={showTokenSection}\n                  onToggleTokenSection={() => setShowTokenSection(!showTokenSection)}\n                  allowPlatformChange={false} // Don't allow platform change during refresh\n                />\n              </>\n            )}\n            {/* Authorization Code Input */}\n            {isAuthLoading && (\n                <div className=\"mb-4 p-3 bg-[var(--background)]/50 rounded-md border border-[var(--border-color)] text-sm text-[var(--muted)]\">\n                  Loading authentication status...\n                </div>\n            )}\n            {!isAuthLoading && authRequired && (\n                <div className=\"mb-4 p-4 bg-[var(--background)]/50 rounded-md border border-[var(--border-color)]\">\n                  <label htmlFor=\"authCode\" className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                    {t.form?.authorizationCode || 'Authorization Code'}\n                  </label>\n                  <input\n                      type=\"password\"\n                      id=\"authCode\"\n                      value={authCode || ''}\n                      onChange={(e) => setAuthCode?.(e.target.value)}\n                      className=\"input-japanese block w-full px-3 py-2 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n                      placeholder=\"Enter your authorization code\"\n                  />\n                  <div className=\"flex items-center mt-2 text-xs text-[var(--muted)]\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"h-4 w-4 mr-1 text-[var(--muted)]\"\n                         fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                      <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2}\n                            d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                    </svg>\n                    {t.form?.authorizationRequired || 'Authentication is required to generate the wiki.'}\n                  </div>\n                </div>\n            )}\n          </div>\n\n          {/* Modal footer */}\n          <div className=\"flex items-center justify-end gap-2 px-6 py-4 border-t border-[var(--border-color)]\">\n            <button\n              type=\"button\"\n              onClick={onClose}\n              className=\"px-4 py-2 text-sm font-medium rounded-md border border-[var(--border-color)]/50 text-[var(--muted)] bg-transparent hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-colors\"\n            >\n              {t.common?.cancel || 'Cancel'}\n            </button>\n            <button\n              type=\"button\"\n              onClick={handleApply}\n              className=\"px-4 py-2 text-sm font-medium rounded-md border border-transparent bg-[var(--accent-primary)]/90 text-white hover:bg-[var(--accent-primary)] transition-colors\"\n            >\n              {t.common?.submit || 'Submit'}\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ProcessedProjects.tsx",
    "content": "'use client';\n\nimport React, { useState, useEffect, useMemo } from 'react';\nimport Link from 'next/link';\nimport { FaTimes, FaTh, FaList } from 'react-icons/fa';\n\n// Interface should match the structure from the API\ninterface ProcessedProject {\n  id: string;\n  owner: string;\n  repo: string;\n  name: string;\n  repo_type: string;\n  submittedAt: number;\n  language: string;\n}\n\ninterface ProcessedProjectsProps {\n  showHeader?: boolean;\n  maxItems?: number;\n  className?: string;\n  messages?: Record<string, Record<string, string>>; // Translation messages with proper typing\n}\n\nexport default function ProcessedProjects({ \n  showHeader = true, \n  maxItems, \n  className = \"\",\n  messages \n}: ProcessedProjectsProps) {\n  const [projects, setProjects] = useState<ProcessedProject[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [searchQuery, setSearchQuery] = useState('');\n  const [viewMode, setViewMode] = useState<'card' | 'list'>('card');\n\n  // Default messages fallback\n  const defaultMessages = {\n    title: 'Processed Wiki Projects',\n    searchPlaceholder: 'Search projects by name, owner, or repository...',\n    noProjects: 'No projects found in the server cache. The cache might be empty or the server encountered an issue.',\n    noSearchResults: 'No projects match your search criteria.',\n    processedOn: 'Processed on:',\n    loadingProjects: 'Loading projects...',\n    errorLoading: 'Error loading projects:',\n    backToHome: 'Back to Home'\n  };\n\n  const t = (key: string) => {\n    if (messages?.projects?.[key]) {\n      return messages.projects[key];\n    }\n    return defaultMessages[key as keyof typeof defaultMessages] || key;\n  };\n\n  useEffect(() => {\n    const fetchProjects = async () => {\n      setIsLoading(true);\n      setError(null);\n      try {\n        const response = await fetch('/api/wiki/projects');\n        if (!response.ok) {\n          throw new Error(`Failed to fetch projects: ${response.statusText}`);\n        }\n        const data = await response.json();\n        if (data.error) {\n          throw new Error(data.error);\n        }\n        setProjects(data as ProcessedProject[]);\n      } catch (e: unknown) {\n        console.error(\"Failed to load projects from API:\", e);\n        const message = e instanceof Error ? e.message : \"An unknown error occurred.\";\n        setError(message);\n        setProjects([]);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchProjects();\n  }, []);\n\n  // Filter projects based on search query\n  const filteredProjects = useMemo(() => {\n    if (!searchQuery.trim()) {\n      return maxItems ? projects.slice(0, maxItems) : projects;\n    }\n\n    const query = searchQuery.toLowerCase();\n    const filtered = projects.filter(project => \n      project.name.toLowerCase().includes(query) ||\n      project.owner.toLowerCase().includes(query) ||\n      project.repo.toLowerCase().includes(query) ||\n      project.repo_type.toLowerCase().includes(query)\n    );\n\n    return maxItems ? filtered.slice(0, maxItems) : filtered;\n  }, [projects, searchQuery, maxItems]);\n\n  const clearSearch = () => {\n    setSearchQuery('');\n  };\n\n  const handleDelete = async (project: ProcessedProject) => {\n    if (!confirm(`Are you sure you want to delete project ${project.name}?`)) {\n      return;\n    }\n    try {\n      const response = await fetch('/api/wiki/projects', {\n        method: 'DELETE',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          owner: project.owner,\n          repo: project.repo,\n          repo_type: project.repo_type,\n          language: project.language,\n        }),\n      });\n      if (!response.ok) {\n        const errorBody = await response.json().catch(() => ({ error: response.statusText }));\n        throw new Error(errorBody.error || response.statusText);\n      }\n      setProjects(prev => prev.filter(p => p.id !== project.id));\n    } catch (e: unknown) {\n      console.error('Failed to delete project:', e);\n      alert(`Failed to delete project: ${e instanceof Error ? e.message : 'Unknown error'}`);\n    }\n  };\n\n  return (\n    <div className={`${className}`}>\n      {showHeader && (\n        <header className=\"mb-6\">\n          <div className=\"flex items-center justify-between\">\n            <h1 className=\"text-3xl font-bold text-[var(--accent-primary)]\">{t('title')}</h1>\n            <Link href=\"/\" className=\"text-[var(--accent-primary)] hover:underline\">\n              {t('backToHome')}\n            </Link>\n          </div>\n        </header>\n      )}\n\n      {/* Search Bar and View Toggle */}\n      <div className=\"mb-6 flex flex-col sm:flex-row gap-4\">\n        {/* Search Bar */}\n        <div className=\"relative flex-1\">\n          <input\n            type=\"text\"\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n            placeholder={t('searchPlaceholder')}\n            className=\"input-japanese block w-full pl-4 pr-12 py-2.5 border border-[var(--border-color)] rounded-lg bg-[var(--background)] text-[var(--foreground)] placeholder:text-[var(--muted)] focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]\"\n          />\n          {searchQuery && (\n            <button\n              onClick={clearSearch}\n              className=\"absolute inset-y-0 right-0 flex items-center pr-3 text-[var(--muted)] hover:text-[var(--foreground)] transition-colors\"\n            >\n              <FaTimes className=\"h-4 w-4\" />\n            </button>\n          )}\n        </div>\n\n        {/* View Toggle */}\n        <div className=\"flex items-center bg-[var(--background)] border border-[var(--border-color)] rounded-lg p-1\">\n          <button\n            onClick={() => setViewMode('card')}\n            className={`p-2 rounded transition-colors ${\n              viewMode === 'card'\n                ? 'bg-[var(--accent-primary)] text-white'\n                : 'text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--card-bg)]'\n            }`}\n            title=\"Card View\"\n          >\n            <FaTh className=\"h-4 w-4\" />\n          </button>\n          <button\n            onClick={() => setViewMode('list')}\n            className={`p-2 rounded transition-colors ${\n              viewMode === 'list'\n                ? 'bg-[var(--accent-primary)] text-white'\n                : 'text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--card-bg)]'\n            }`}\n            title=\"List View\"\n          >\n            <FaList className=\"h-4 w-4\" />\n          </button>\n        </div>\n      </div>\n\n      {isLoading && <p className=\"text-[var(--muted)]\">{t('loadingProjects')}</p>}\n      {error && <p className=\"text-[var(--highlight)]\">{t('errorLoading')} {error}</p>}\n\n      {!isLoading && !error && filteredProjects.length > 0 && (\n        <div className={viewMode === 'card' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' : 'space-y-2'}>\n            {filteredProjects.map((project) => (\n            viewMode === 'card' ? (\n              <div key={project.id} className=\"relative p-4 border border-[var(--border-color)] rounded-lg bg-[var(--card-bg)] shadow-sm hover:shadow-md transition-all duration-200 hover:scale-[1.02]\">\n                <button\n                  type=\"button\"\n                  onClick={() => handleDelete(project)}\n                  className=\"absolute top-2 right-2 text-[var(--muted)] hover:text-[var(--foreground)]\"\n                  title=\"Delete project\"\n                >\n                  <FaTimes className=\"h-4 w-4\" />\n                </button>\n                <Link\n                  href={`/${project.owner}/${project.repo}?type=${project.repo_type}&language=${project.language}`}\n                  className=\"block\"\n                >\n                  <h3 className=\"text-lg font-semibold text-[var(--link-color)] hover:underline mb-2 line-clamp-2\">\n                    {project.name}\n                  </h3>\n                  <div className=\"flex flex-wrap gap-2 mb-3\">\n                    <span className=\"px-2 py-1 text-xs bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] rounded-full border border-[var(--accent-primary)]/20\">\n                      {project.repo_type}\n                    </span>\n                    <span className=\"px-2 py-1 text-xs bg-[var(--background)] text-[var(--muted)] rounded-full border border-[var(--border-color)]\">\n                      {project.language}\n                    </span>\n                  </div>\n                  <p className=\"text-xs text-[var(--muted)]\">\n                    {t('processedOn')} {new Date(project.submittedAt).toLocaleDateString()}\n                  </p>\n                </Link>\n              </div>\n            ) : (\n              <div key={project.id} className=\"relative p-3 border border-[var(--border-color)] rounded-lg bg-[var(--card-bg)] hover:bg-[var(--background)] transition-colors\">\n                <button\n                  type=\"button\"\n                  onClick={() => handleDelete(project)}\n                  className=\"absolute top-2 right-2 text-[var(--muted)] hover:text-[var(--foreground)]\"\n                  title=\"Delete project\"\n                >\n                  <FaTimes className=\"h-4 w-4\" />\n                </button>\n                <Link\n                  href={`/${project.owner}/${project.repo}?type=${project.repo_type}&language=${project.language}`}\n                  className=\"flex items-center justify-between\"\n                >\n                  <div className=\"flex-1 min-w-0\">\n                    <h3 className=\"text-base font-medium text-[var(--link-color)] hover:underline truncate\">\n                      {project.name}\n                    </h3>\n                    <p className=\"text-xs text-[var(--muted)] mt-1\">\n                      {t('processedOn')} {new Date(project.submittedAt).toLocaleDateString()} • {project.repo_type} • {project.language}\n                    </p>\n                  </div>\n                  <div className=\"flex gap-2 ml-4\">\n                    <span className=\"px-2 py-1 text-xs bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] rounded border border-[var(--accent-primary)]/20\">\n                      {project.repo_type}\n                    </span>\n                  </div>\n                </Link>\n              </div>\n            )\n          ))}\n        </div>\n      )}\n\n      {!isLoading && !error && projects.length > 0 && filteredProjects.length === 0 && searchQuery && (\n        <p className=\"text-[var(--muted)]\">{t('noSearchResults')}</p>\n      )}\n\n      {!isLoading && !error && projects.length === 0 && (\n        <p className=\"text-[var(--muted)]\">{t('noProjects')}</p>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/TokenInput.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport { useLanguage } from '@/contexts/LanguageContext';\n\ninterface TokenInputProps {\n  selectedPlatform: 'github' | 'gitlab' | 'bitbucket';\n  setSelectedPlatform: (value: 'github' | 'gitlab' | 'bitbucket') => void;\n  accessToken: string;\n  setAccessToken: (value: string) => void;\n  showTokenSection?: boolean;\n  onToggleTokenSection?: () => void;\n  allowPlatformChange?: boolean;\n}\n\nexport default function TokenInput({\n  selectedPlatform,\n  setSelectedPlatform,\n  accessToken,\n  setAccessToken,\n  showTokenSection = true,\n  onToggleTokenSection,\n  allowPlatformChange = true\n}: TokenInputProps) {\n  const { messages: t } = useLanguage();\n\n  const platformName = selectedPlatform.charAt(0).toUpperCase() + selectedPlatform.slice(1);\n\n  return (\n    <div className=\"mb-4\">\n      {onToggleTokenSection && (\n        <button\n          type=\"button\"\n          onClick={onToggleTokenSection}\n          className=\"text-sm text-[var(--accent-primary)] hover:text-[var(--highlight)] flex items-center transition-colors border-b border-[var(--border-color)] hover:border-[var(--accent-primary)] pb-0.5 mb-2\"\n        >\n          {showTokenSection ? t.form?.hideTokens || 'Hide Access Tokens' : t.form?.addTokens || 'Add Access Tokens for Private Repositories'}\n        </button>\n      )}\n\n      {showTokenSection && (\n        <div className=\"mt-2 p-4 bg-[var(--background)]/50 rounded-md border border-[var(--border-color)]\">\n          {allowPlatformChange && (\n            <div className=\"mb-3\">\n              <label className=\"block text-xs font-medium text-[var(--foreground)] mb-2\">\n                {t.form?.selectPlatform || 'Select Platform'}\n              </label>\n              <div className=\"flex gap-2\">\n                <button\n                  type=\"button\"\n                  onClick={() => setSelectedPlatform('github')}\n                  className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md border transition-all ${selectedPlatform === 'github'\n                    ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)] shadow-sm'\n                    : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                    }`}\n                >\n                  <span className=\"text-sm\">GitHub</span>\n                </button>\n                <button\n                  type=\"button\"\n                  onClick={() => setSelectedPlatform('gitlab')}\n                  className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md border transition-all ${selectedPlatform === 'gitlab'\n                    ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)] shadow-sm'\n                    : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                    }`}\n                >\n                  <span className=\"text-sm\">GitLab</span>\n                </button>\n                <button\n                  type=\"button\"\n                  onClick={() => setSelectedPlatform('bitbucket')}\n                  className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md border transition-all ${selectedPlatform === 'bitbucket'\n                    ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)] shadow-sm'\n                    : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                    }`}\n                >\n                  <span className=\"text-sm\">Bitbucket</span>\n                </button>\n              </div>\n            </div>\n          )}\n\n          <div>\n            <label htmlFor=\"access-token\" className=\"block text-xs font-medium text-[var(--foreground)] mb-2\">\n              {(t.form?.personalAccessToken || 'Personal Access Token').replace('{platform}', platformName)}\n            </label>\n            <input\n              id=\"access-token\"\n              type=\"password\"\n              value={accessToken}\n              onChange={(e) => setAccessToken(e.target.value)}\n              placeholder={(t.form?.tokenPlaceholder || 'Enter your access token').replace('{platform}', platformName)}\n              className=\"input-japanese block w-full px-3 py-2 rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)] text-sm\"\n            />\n            <div className=\"flex items-center mt-2 text-xs text-[var(--muted)]\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"h-4 w-4 mr-1 text-[var(--muted)]\"\n                fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2}\n                  d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n              </svg>\n              {t.form?.tokenSecurityNote || 'Your token is stored locally and never sent to our servers.'}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n} "
  },
  {
    "path": "src/components/UserSelector.tsx",
    "content": "'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { useLanguage } from '@/contexts/LanguageContext';\n\n// Define the interfaces for our model configuration\ninterface Model {\n  id: string;\n  name: string;\n}\n\ninterface Provider {\n  id: string;\n  name: string;\n  models: Model[];\n  supportsCustomModel?: boolean;\n}\n\ninterface ModelConfig {\n  providers: Provider[];\n  defaultProvider: string;\n}\n\ninterface ModelSelectorProps {\n  provider: string;\n  setProvider: (value: string) => void;\n  model: string;\n  setModel: (value: string) => void;\n  isCustomModel: boolean;\n  setIsCustomModel: (value: boolean) => void;\n  customModel: string;\n  setCustomModel: (value: string) => void;\n\n  // File filter configuration\n  showFileFilters?: boolean;\n  excludedDirs?: string;\n  setExcludedDirs?: (value: string) => void;\n  excludedFiles?: string;\n  setExcludedFiles?: (value: string) => void;\n  includedDirs?: string;\n  setIncludedDirs?: (value: string) => void;\n  includedFiles?: string;\n  setIncludedFiles?: (value: string) => void;\n}\n\nexport default function UserSelector({\n  provider,\n  setProvider,\n  model,\n  setModel,\n  isCustomModel,\n  setIsCustomModel,\n  customModel,\n  setCustomModel,\n\n  // File filter configuration\n  showFileFilters = false,\n  excludedDirs = '',\n  setExcludedDirs,\n  excludedFiles = '',\n  setExcludedFiles,\n  includedDirs = '',\n  setIncludedDirs,\n  includedFiles = '',\n  setIncludedFiles\n}: ModelSelectorProps) {\n  // State to manage the visibility of the filters modal and filter section\n  const [isFilterSectionOpen, setIsFilterSectionOpen] = useState(false);\n  // State to manage filter mode: 'exclude' or 'include'\n  const [filterMode, setFilterMode] = useState<'exclude' | 'include'>('exclude');\n  const { messages: t } = useLanguage();\n\n  // State for model configurations from backend\n  const [modelConfig, setModelConfig] = useState<ModelConfig | null>(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n\n  // State for viewing default values\n  const [showDefaultDirs, setShowDefaultDirs] = useState(false);\n  const [showDefaultFiles, setShowDefaultFiles] = useState(false);\n\n  // Fetch model configurations from the backend\n  useEffect(() => {\n    const fetchModelConfig = async () => {\n      try {\n        setIsLoading(true);\n        setError(null);\n\n        const response = await fetch('/api/models/config');\n\n        if (!response.ok) {\n          throw new Error(`Error fetching model configurations: ${response.status}`);\n        }\n\n        const data = await response.json();\n        setModelConfig(data);\n\n        // Initialize provider and model with defaults from API if not already set\n        if (!provider && data.defaultProvider) {\n          setProvider(data.defaultProvider);\n\n          // Find the default provider and set its default model\n          const selectedProvider = data.providers.find((p: Provider) => p.id === data.defaultProvider);\n          if (selectedProvider && selectedProvider.models.length > 0) {\n            setModel(selectedProvider.models[0].id);\n          }\n        }\n      } catch (err) {\n        console.error('Failed to fetch model configurations:', err);\n        setError('Failed to load model configurations. Using default options.');\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchModelConfig();\n  }, [provider, setModel, setProvider]);\n\n  // Handler for changing provider\n  const handleProviderChange = (newProvider: string) => {\n    setProvider(newProvider);\n    setTimeout(() => {\n      // Reset custom model state when changing providers\n      setIsCustomModel(false);\n\n      // Set default model for the selected provider\n      if (modelConfig) {\n        const selectedProvider = modelConfig.providers.find((p: Provider) => p.id === newProvider);\n        if (selectedProvider && selectedProvider.models.length > 0) {\n          setModel(selectedProvider.models[0].id);\n        }\n      }\n    }, 10);\n  };\n\n  // Default excluded directories from config.py\n  const defaultExcludedDirs =\n`./.venv/\n./venv/\n./env/\n./virtualenv/\n./node_modules/\n./bower_components/\n./jspm_packages/\n./.git/\n./.svn/\n./.hg/\n./.bzr/\n./__pycache__/\n./.pytest_cache/\n./.mypy_cache/\n./.ruff_cache/\n./.coverage/\n./dist/\n./build/\n./out/\n./target/\n./bin/\n./obj/\n./docs/\n./_docs/\n./site-docs/\n./_site/\n./.idea/\n./.vscode/\n./.vs/\n./.eclipse/\n./.settings/\n./logs/\n./log/\n./tmp/\n./temp/\n./.eng`;\n\n  // Default excluded files from config.py\n  const defaultExcludedFiles =\n`package-lock.json\nyarn.lock\npnpm-lock.yaml\nnpm-shrinkwrap.json\npoetry.lock\nPipfile.lock\nrequirements.txt.lock\nCargo.lock\ncomposer.lock\n.lock\n.DS_Store\nThumbs.db\ndesktop.ini\n*.lnk\n.env\n.env.*\n*.env\n*.cfg\n*.ini\n.flaskenv\n.gitignore\n.gitattributes\n.gitmodules\n.github\n.gitlab-ci.yml\n.prettierrc\n.eslintrc\n.eslintignore\n.stylelintrc\n.editorconfig\n.jshintrc\n.pylintrc\n.flake8\nmypy.ini\npyproject.toml\ntsconfig.json\nwebpack.config.js\nbabel.config.js\nrollup.config.js\njest.config.js\nkarma.conf.js\nvite.config.js\nnext.config.js\n*.min.js\n*.min.css\n*.bundle.js\n*.bundle.css\n*.map\n*.gz\n*.zip\n*.tar\n*.tgz\n*.rar\n*.pyc\n*.pyo\n*.pyd\n*.so\n*.dll\n*.class\n*.exe\n*.o\n*.a\n*.jpg\n*.jpeg\n*.png\n*.gif\n*.ico\n*.svg\n*.webp\n*.mp3\n*.mp4\n*.wav\n*.avi\n*.mov\n*.webm\n*.csv\n*.tsv\n*.xls\n*.xlsx\n*.db\n*.sqlite\n*.sqlite3\n*.pdf\n*.docx\n*.pptx`;\n\n  // Display loading state\n  if (isLoading) {\n    return (\n      <div className=\"flex flex-col gap-2\">\n        <div className=\"text-sm text-[var(--muted)]\">Loading model configurations...</div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"space-y-4\">\n        {error && (\n          <div className=\"text-sm text-red-500 mb-2\">{error}</div>\n        )}\n\n        {/* Provider Selection */}\n        <div>\n          <label htmlFor=\"provider-dropdown\" className=\"block text-xs font-medium text-[var(--foreground)] mb-1.5\">\n            {t.form?.modelProvider || 'Model Provider'}\n          </label>\n          <select\n            id=\"provider-dropdown\"\n            value={provider}\n            onChange={(e) => handleProviderChange(e.target.value)}\n            className=\"input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n          >\n            <option value=\"\" disabled>{t.form?.selectProvider || 'Select Provider'}</option>\n            {modelConfig?.providers.map((providerOption) => (\n              <option key={providerOption.id} value={providerOption.id}>\n                {t.form?.[`provider${providerOption.id.charAt(0).toUpperCase() + providerOption.id.slice(1)}`] || providerOption.name}\n              </option>\n            ))}\n          </select>\n        </div>\n\n        {/* Model Selection - consistent height regardless of type */}\n        <div>\n          <label htmlFor={isCustomModel ? \"custom-model-input\" : \"model-dropdown\"} className=\"block text-xs font-medium text-[var(--foreground)] mb-1.5\">\n            {t.form?.modelSelection || 'Model Selection'}\n          </label>\n\n          {isCustomModel ? (\n            <input\n              id=\"custom-model-input\"\n              type=\"text\"\n              value={customModel}\n              onChange={(e) => {\n                setCustomModel(e.target.value);\n                setModel(e.target.value);\n              }}\n              placeholder={t.form?.customModelPlaceholder || 'Enter custom model name'}\n              className=\"input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n            />\n          ) : (\n            <select\n              id=\"model-dropdown\"\n              value={model}\n              onChange={(e) => setModel(e.target.value)}\n              className=\"input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]\"\n              disabled={!provider || isLoading || !modelConfig?.providers.find(p => p.id === provider)?.models?.length}\n            >\n              {modelConfig?.providers.find((p: Provider) => p.id === provider)?.models.map((modelOption) => (\n                <option key={modelOption.id} value={modelOption.id}>\n                  {modelOption.name}\n                </option>\n              )) || <option value=\"\">{t.form?.selectModel || 'Select Model'}</option>}\n            </select>\n          )}\n        </div>\n\n        {/* Custom model toggle - only when provider supports it */}\n        {modelConfig?.providers.find((p: Provider) => p.id === provider)?.supportsCustomModel && (\n          <div className=\"mb-2\">\n            <div className=\"flex items-center pb-1\">\n              <div\n                className=\"relative flex items-center cursor-pointer\"\n                onClick={() => {\n                  const newValue = !isCustomModel;\n                  setIsCustomModel(newValue);\n                  if (newValue) {\n                    setCustomModel(model);\n                  }\n                }}\n              >\n                <input\n                  id=\"use-custom-model\"\n                  type=\"checkbox\"\n                  checked={isCustomModel}\n                  onChange={() => {}}\n                  className=\"sr-only\"\n                />\n                <div className={`w-10 h-5 rounded-full transition-colors ${isCustomModel ? 'bg-[var(--accent-primary)]' : 'bg-gray-300 dark:bg-gray-600'}`}></div>\n                <div className={`absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-white transition-transform transform ${isCustomModel ? 'translate-x-5' : ''}`}></div>\n              </div>\n              <label\n                htmlFor=\"use-custom-model\"\n                className=\"ml-2 text-sm font-medium text-[var(--muted)] cursor-pointer\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  const newValue = !isCustomModel;\n                  setIsCustomModel(newValue);\n                  if (newValue) {\n                    setCustomModel(model);\n                  }\n                }}\n              >\n                {t.form?.useCustomModel || 'Use custom model'}\n              </label>\n            </div>\n          </div>\n        )}\n\n        {showFileFilters && (\n          <div className=\"mt-4\">\n            <button\n              type=\"button\"\n              onClick={() => setIsFilterSectionOpen(!isFilterSectionOpen)}\n              className=\"flex items-center text-sm text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors\"\n            >\n              <span className=\"mr-1.5 text-xs\">{isFilterSectionOpen ? '▼' : '►'}</span>\n              {t.form?.advancedOptions || 'Advanced Options'}\n            </button>\n\n            {isFilterSectionOpen && (\n              <div className=\"mt-3 p-3 border border-[var(--border-color)]/70 rounded-md bg-[var(--background)]/30\">\n                {/* Filter Mode Selection */}\n                <div className=\"mb-4\">\n                  <label className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n                    {t.form?.filterMode || 'Filter Mode'}\n                  </label>\n                  <div className=\"flex gap-2\">\n                    <button\n                      type=\"button\"\n                      onClick={() => setFilterMode('exclude')}\n                      className={`flex-1 px-3 py-2 rounded-md border text-sm transition-colors ${\n                        filterMode === 'exclude'\n                          ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)]'\n                          : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                      }`}\n                    >\n                      {t.form?.excludeMode || 'Exclude Paths'}\n                    </button>\n                    <button\n                      type=\"button\"\n                      onClick={() => setFilterMode('include')}\n                      className={`flex-1 px-3 py-2 rounded-md border text-sm transition-colors ${\n                        filterMode === 'include'\n                          ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)]'\n                          : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n                      }`}\n                    >\n                      {t.form?.includeMode || 'Include Only Paths'}\n                    </button>\n                  </div>\n                  <p className=\"text-xs text-[var(--muted)] mt-1\">\n                    {filterMode === 'exclude'\n                      ? (t.form?.excludeModeDescription || 'Specify paths to exclude from processing (default behavior)')\n                      : (t.form?.includeModeDescription || 'Specify only the paths to include, ignoring all others')\n                    }\n                  </p>\n                </div>\n\n                {/* Directories Section */}\n                <div className=\"mb-4\">\n                  <label className=\"block text-sm font-medium text-[var(--muted)] mb-1.5\">\n                    {filterMode === 'exclude'\n                      ? (t.form?.excludedDirs || 'Excluded Directories')\n                      : (t.form?.includedDirs || 'Included Directories')\n                    }\n                  </label>\n                  <textarea\n                    value={filterMode === 'exclude' ? excludedDirs : includedDirs}\n                    onChange={(e) => {\n                      if (filterMode === 'exclude') {\n                        setExcludedDirs?.(e.target.value);\n                      } else {\n                        setIncludedDirs?.(e.target.value);\n                      }\n                    }}\n                    rows={4}\n                    className=\"block w-full rounded-md border border-[var(--border-color)]/50 bg-[var(--input-bg)] text-[var(--foreground)] px-3 py-2 text-sm focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-opacity-50 shadow-sm\"\n                    placeholder={filterMode === 'exclude'\n                      ? (t.form?.enterExcludedDirs || 'Enter excluded directories, one per line...')\n                      : (t.form?.enterIncludedDirs || 'Enter included directories, one per line...')\n                    }\n                  />\n                  {filterMode === 'exclude' && (\n                    <>\n                      <div className=\"flex mt-1.5\">\n                        <button\n                          type=\"button\"\n                          onClick={() => setShowDefaultDirs(!showDefaultDirs)}\n                          className=\"text-xs text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors\"\n                        >\n                          {showDefaultDirs ? (t.form?.hideDefault || 'Hide Default') : (t.form?.viewDefault || 'View Default')}\n                        </button>\n                      </div>\n                      {showDefaultDirs && (\n                        <div className=\"mt-2 p-2 rounded bg-[var(--background)]/50 text-xs\">\n                          <p className=\"mb-1 text-[var(--muted)]\">{t.form?.defaultNote || 'These defaults are already applied. Add your custom exclusions above.'}</p>\n                          <pre className=\"whitespace-pre-wrap font-mono text-[var(--muted)] overflow-y-auto max-h-32\">{defaultExcludedDirs}</pre>\n                        </div>\n                      )}\n                    </>\n                  )}\n                </div>\n\n                {/* Files Section */}\n                <div>\n                  <label className=\"block text-sm font-medium text-[var(--muted)] mb-1.5\">\n                    {filterMode === 'exclude'\n                      ? (t.form?.excludedFiles || 'Excluded Files')\n                      : (t.form?.includedFiles || 'Included Files')\n                    }\n                  </label>\n                  <textarea\n                    value={filterMode === 'exclude' ? excludedFiles : includedFiles}\n                    onChange={(e) => {\n                      if (filterMode === 'exclude') {\n                        setExcludedFiles?.(e.target.value);\n                      } else {\n                        setIncludedFiles?.(e.target.value);\n                      }\n                    }}\n                    rows={4}\n                    className=\"block w-full rounded-md border border-[var(--border-color)]/50 bg-[var(--input-bg)] text-[var(--foreground)] px-3 py-2 text-sm focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-opacity-50 shadow-sm\"\n                    placeholder={filterMode === 'exclude'\n                      ? (t.form?.enterExcludedFiles || 'Enter excluded files, one per line...')\n                      : (t.form?.enterIncludedFiles || 'Enter included files, one per line...')\n                    }\n                  />\n                  {filterMode === 'exclude' && (\n                    <>\n                      <div className=\"flex mt-1.5\">\n                        <button\n                          type=\"button\"\n                          onClick={() => setShowDefaultFiles(!showDefaultFiles)}\n                          className=\"text-xs text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors\"\n                        >\n                          {showDefaultFiles ? (t.form?.hideDefault || 'Hide Default') : (t.form?.viewDefault || 'View Default')}\n                        </button>\n                      </div>\n                      {showDefaultFiles && (\n                        <div className=\"mt-2 p-2 rounded bg-[var(--background)]/50 text-xs\">\n                          <p className=\"mb-1 text-[var(--muted)]\">{t.form?.defaultNote || 'These defaults are already applied. Add your custom exclusions above.'}</p>\n                          <pre className=\"whitespace-pre-wrap font-mono text-[var(--muted)] overflow-y-auto max-h-32\">{defaultExcludedFiles}</pre>\n                        </div>\n                      )}\n                    </>\n                  )}\n                </div>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/WikiTreeView.tsx",
    "content": "'use client';\n\nimport React, { useState } from 'react';\nimport { FaChevronRight, FaChevronDown } from 'react-icons/fa';\n\n// Import interfaces from the page component\ninterface WikiPage {\n  id: string;\n  title: string;\n  content: string;\n  filePaths: string[];\n  importance: 'high' | 'medium' | 'low';\n  relatedPages: string[];\n  parentId?: string;\n  isSection?: boolean;\n  children?: string[];\n}\n\ninterface WikiSection {\n  id: string;\n  title: string;\n  pages: string[];\n  subsections?: string[];\n}\n\ninterface WikiStructure {\n  id: string;\n  title: string;\n  description: string;\n  pages: WikiPage[];\n  sections: WikiSection[];\n  rootSections: string[];\n}\n\ninterface WikiTreeViewProps {\n  wikiStructure: WikiStructure;\n  currentPageId: string | undefined;\n  onPageSelect: (pageId: string) => void;\n  messages?: {\n    pages?: string;\n    [key: string]: string | undefined;\n  };\n}\n\nconst WikiTreeView: React.FC<WikiTreeViewProps> = ({\n  wikiStructure,\n  currentPageId,\n  onPageSelect,\n}) => {\n  const [expandedSections, setExpandedSections] = useState<Set<string>>(\n    new Set(wikiStructure.rootSections)\n  );\n\n  const toggleSection = (sectionId: string, event: React.MouseEvent) => {\n    event.stopPropagation();\n    setExpandedSections(prev => {\n      const newSet = new Set(prev);\n      if (newSet.has(sectionId)) {\n        newSet.delete(sectionId);\n      } else {\n        newSet.add(sectionId);\n      }\n      return newSet;\n    });\n  };\n\n  const renderSection = (sectionId: string, level = 0) => {\n    const section = wikiStructure.sections.find(s => s.id === sectionId);\n    if (!section) return null;\n\n    const isExpanded = expandedSections.has(sectionId);\n\n    return (\n      <div key={sectionId} className=\"mb-2\">\n        <button\n          className={`flex items-center w-full text-left px-2 py-1.5 rounded-md text-sm font-medium text-[var(--foreground)] hover:bg-[var(--background)]/70 transition-colors ${\n            level === 0 ? 'bg-[var(--background)]/50' : ''\n          }`}\n          onClick={(e) => toggleSection(sectionId, e)}\n        >\n          {isExpanded ? (\n            <FaChevronDown className=\"mr-2 text-xs\" />\n          ) : (\n            <FaChevronRight className=\"mr-2 text-xs\" />\n          )}\n          <span className=\"truncate\">{section.title}</span>\n        </button>\n\n        {isExpanded && (\n          <div className={`ml-4 mt-1 space-y-1 ${level > 0 ? 'pl-2 border-l border-[var(--border-color)]/30' : ''}`}>\n            {/* Render pages in this section */}\n            {section.pages.map(pageId => {\n              const page = wikiStructure.pages.find(p => p.id === pageId);\n              if (!page) return null;\n\n              return (\n                <button\n                  key={pageId}\n                  className={`w-full text-left px-3 py-1.5 rounded-md text-sm transition-colors ${\n                    currentPageId === pageId\n                      ? 'bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] border border-[var(--accent-primary)]/30'\n                      : 'text-[var(--foreground)] hover:bg-[var(--background)] border border-transparent'\n                  }`}\n                  onClick={() => onPageSelect(pageId)}\n                >\n                  <div className=\"flex items-center\">\n                    <div\n                      className={`w-2 h-2 rounded-full mr-2 flex-shrink-0 ${\n                        page.importance === 'high'\n                          ? 'bg-[#9b7cb9]'\n                          : page.importance === 'medium'\n                          ? 'bg-[#d7c4bb]'\n                          : 'bg-[#e8927c]'\n                      }`}\n                    ></div>\n                    <span className=\"truncate\">{page.title}</span>\n                  </div>\n                </button>\n              );\n            })}\n\n            {/* Render subsections recursively */}\n            {section.subsections?.map(subsectionId =>\n              renderSection(subsectionId, level + 1)\n            )}\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  // If there are no sections defined yet, or if sections/rootSections are empty arrays, fall back to the flat list view\n  if (!wikiStructure.sections || wikiStructure.sections.length === 0 || !wikiStructure.rootSections || wikiStructure.rootSections.length === 0) {\n    console.log(\"WikiTreeView: Falling back to flat list view due to missing or empty sections/rootSections\");\n    return (\n      <ul className=\"space-y-2\">\n        {wikiStructure.pages.map(page => (\n          <li key={page.id}>\n            <button\n              className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${\n                currentPageId === page.id\n                  ? 'bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] border border-[var(--accent-primary)]/30'\n                  : 'text-[var(--foreground)] hover:bg-[var(--background)] border border-transparent'\n              }`}\n              onClick={() => onPageSelect(page.id)}\n            >\n              <div className=\"flex items-center\">\n                <div\n                  className={`w-2 h-2 rounded-full mr-2 flex-shrink-0 ${\n                    page.importance === 'high'\n                      ? 'bg-[#9b7cb9]'\n                      : page.importance === 'medium'\n                      ? 'bg-[#d7c4bb]'\n                      : 'bg-[#e8927c]'\n                  }`}\n                ></div>\n                <span className=\"truncate\">{page.title}</span>\n              </div>\n            </button>\n          </li>\n        ))}\n      </ul>\n    );\n  }\n\n  // Log information about the sections for debugging\n  console.log(\"WikiTreeView: Rendering tree view with sections:\", wikiStructure.sections);\n  console.log(\"WikiTreeView: Root sections:\", wikiStructure.rootSections);\n\n  return (\n    <div className=\"space-y-1\">\n      {wikiStructure.rootSections.map(sectionId => {\n        const section = wikiStructure.sections.find(s => s.id === sectionId);\n        if (!section) {\n          console.warn(`WikiTreeView: Could not find section with id ${sectionId}`);\n          return null;\n        }\n        return renderSection(sectionId);\n      })}\n    </div>\n  );\n};\n\nexport default WikiTreeView;"
  },
  {
    "path": "src/components/WikiTypeSelector.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport { useLanguage } from '@/contexts/LanguageContext';\nimport { FaBookOpen, FaList } from 'react-icons/fa';\n\ninterface WikiTypeSelectorProps {\n  isComprehensiveView: boolean;\n  setIsComprehensiveView: (value: boolean) => void;\n}\n\nconst WikiTypeSelector: React.FC<WikiTypeSelectorProps> = ({\n  isComprehensiveView,\n  setIsComprehensiveView,\n}) => {\n  const { messages: t } = useLanguage();\n\n  return (\n    <div className=\"mb-4\">\n      <label className=\"block text-sm font-medium text-[var(--foreground)] mb-2\">\n        {t.form?.wikiType || 'Wiki Type'}\n      </label>\n      <div className=\"flex flex-col sm:flex-row gap-3\">\n        <button\n          type=\"button\"\n          onClick={() => setIsComprehensiveView(true)}\n          className={`flex items-center justify-between p-3 rounded-md border transition-colors ${\n            isComprehensiveView\n              ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)]/30 text-[var(--accent-primary)]'\n              : 'bg-[var(--background)]/50 border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n          }`}\n        >\n          <div className=\"flex items-center\">\n            <FaBookOpen className=\"mr-2\" />\n            <div className=\"text-left\">\n              <div className=\"font-medium\">{t.form?.comprehensive || 'Comprehensive'}</div>\n              <div className=\"text-xs opacity-80\">\n                {t.form?.comprehensiveDescription || 'Detailed wiki with structured sections and more pages'}\n              </div>\n            </div>\n          </div>\n          {isComprehensiveView && (\n            <div className=\"ml-2 h-4 w-4 rounded-full bg-[var(--accent-primary)]/20 flex items-center justify-center\">\n              <div className=\"h-2 w-2 rounded-full bg-[var(--accent-primary)]\"></div>\n            </div>\n          )}\n        </button>\n        \n        <button\n          type=\"button\"\n          onClick={() => setIsComprehensiveView(false)}\n          className={`flex items-center justify-between p-3 rounded-md border transition-colors ${\n            !isComprehensiveView\n              ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)]/30 text-[var(--accent-primary)]'\n              : 'bg-[var(--background)]/50 border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]'\n          }`}\n        >\n          <div className=\"flex items-center\">\n            <FaList className=\"mr-2\" />\n            <div className=\"text-left\">\n              <div className=\"font-medium\">{t.form?.concise || 'Concise'}</div>\n              <div className=\"text-xs opacity-80\">\n                {t.form?.conciseDescription || 'Simplified wiki with fewer pages and essential information'}\n              </div>\n            </div>\n          </div>\n          {!isComprehensiveView && (\n            <div className=\"ml-2 h-4 w-4 rounded-full bg-[var(--accent-primary)]/20 flex items-center justify-center\">\n              <div className=\"h-2 w-2 rounded-full bg-[var(--accent-primary)]\"></div>\n            </div>\n          )}\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default WikiTypeSelector;\n"
  },
  {
    "path": "src/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\n\nexport default function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n\n  return (\n    <button\n      type=\"button\"\n      className=\"theme-toggle-button cursor-pointer bg-transparent border border-[var(--border-color)] text-[var(--foreground)] hover:border-[var(--accent-primary)] active:bg-[var(--accent-secondary)]/10 rounded-md p-2 transition-all duration-300\"\n      title=\"Toggle theme\"\n      aria-label=\"Toggle theme\"\n      onClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n    >\n      {/* Japanese-inspired sun and moon icons */}\n      <div className=\"relative w-5 h-5\">\n        {/* Sun icon (light mode) */}\n        <div className={`absolute inset-0 transition-opacity duration-300 ${theme === 'dark' ? 'opacity-0' : 'opacity-100'}`}>\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"w-5 h-5\" aria-label=\"Light Mode\">\n            <circle cx=\"12\" cy=\"12\" r=\"5\" stroke=\"currentColor\" strokeWidth=\"2\" />\n            <path d=\"M12 2V4\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M12 20V22\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M4 12L2 12\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M22 12L20 12\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M19.778 4.22183L17.6569 6.34315\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M6.34309 17.6569L4.22177 19.7782\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M19.778 19.7782L17.6569 17.6569\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n            <path d=\"M6.34309 6.34315L4.22177 4.22183\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n          </svg>\n        </div>\n\n        {/* Moon icon (dark mode) */}\n        <div className={`absolute inset-0 transition-opacity duration-300 ${theme === 'dark' ? 'opacity-100' : 'opacity-0'}`}>\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"w-5 h-5\" aria-label=\"Dark Mode\">\n            <path\n              d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              fill=\"none\"\n            />\n          </svg>\n        </div>\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/contexts/LanguageContext.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n'use client';\n\nimport React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';\nimport { locales } from '@/i18n';\n\ntype Messages = Record<string, any>;\ntype LanguageContextType = {\n  language: string;\n  setLanguage: (lang: string) => void;\n  messages: Messages;\n  supportedLanguages: Record<string, string>;\n};\n\nconst LanguageContext = createContext<LanguageContextType | undefined>(undefined);\n\nexport function LanguageProvider({ children }: { children: ReactNode }) {\n  // Initialize with 'en' or get from localStorage if available\n  const [language, setLanguageState] = useState<string>('en');\n  const [messages, setMessages] = useState<Messages>({});\n  const [isLoading, setIsLoading] = useState<boolean>(true);\n  const [supportedLanguages, setSupportedLanguages] = useState({})\n  const [defaultLanguage, setDefaultLanguage] = useState('en')\n\n  // Helper function to detect browser language\n  const detectBrowserLanguage = (): string => {\n    try {\n      if (typeof window === 'undefined' || typeof navigator === 'undefined') {\n        return 'en'; // Default to English on server-side\n      }\n\n      // Get browser language (navigator.language returns full locale like 'en-US')\n      const browserLang = navigator.language || (navigator as any).userLanguage || '';\n      console.log('Detected browser language:', browserLang);\n\n      if (!browserLang) {\n        return 'en'; // Default to English if browser language is not available\n      }\n\n      // Extract the language code (first 2 characters)\n      const langCode = browserLang.split('-')[0].toLowerCase();\n      console.log('Extracted language code:', langCode);\n\n      // Check if the detected language is supported\n      if (locales.includes(langCode as any)) {\n        console.log('Language supported, using:', langCode);\n        return langCode;\n      }\n\n      // Special case for Chinese variants\n      if (langCode === 'zh') {\n        console.log('Chinese language detected');\n        // Check for traditional Chinese variants\n        if (browserLang.includes('TW') || browserLang.includes('HK')) {\n          console.log('Traditional Chinese variant detected');\n          return 'zh'; // Use Mandarin for traditional Chinese\n        }\n        return 'zh'; // Use Mandarin for simplified Chinese\n      }\n\n      console.log('Language not supported, defaulting to English');\n      return 'en'; // Default to English if not supported\n    } catch (error) {\n      console.error('Error detecting browser language:', error);\n      return 'en'; // Default to English on error\n    }\n  };\n\n  useEffect(() => {\n    const getSupportedLanguages = async () => {\n      try {\n        const response = await fetch('/api/lang/config');\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n        const data = await response.json();\n        setSupportedLanguages(data.supported_languages);\n        setDefaultLanguage(data.default);\n      } catch (err) {\n        console.error(\"Failed to fetch auth status:\", err);\n        // Assuming auth is required if fetch fails to avoid blocking UI for safety\n        const defaultSupportedLanguages = {\n          \"en\": \"English\",\n          \"ja\": \"Japanese (日本語)\",\n          \"zh\": \"Mandarin Chinese (中文)\",\n          \"zh-tw\": \"Traditional Chinese (繁體中文)\",\n          \"es\": \"Spanish (Español)\",\n          \"kr\": \"Korean (한국어)\",\n          \"vi\": \"Vietnamese (Tiếng Việt)\",\n          \"pt-br\": \"Brazilian Portuguese (Português Brasileiro)\",\n          \"fr\": \"Français (French)\",\n          \"ru\": \"Русский (Russian)\"\n        };\n        setSupportedLanguages(defaultSupportedLanguages);\n        setDefaultLanguage(\"en\");\n      }\n    }\n    getSupportedLanguages();\n  }, []);\n\n  useEffect(() => {\n    if (Object.keys(supportedLanguages).length > 0) {\n      const loadLanguage = async () => {\n        try {\n          // Only access localStorage in the browser\n          let storedLanguage;\n          if (typeof window !== 'undefined') {\n            storedLanguage = localStorage.getItem('language');\n    \n            // If no language is stored, detect browser language\n            if (!storedLanguage) {\n              console.log('No language in localStorage, detecting browser language');\n              storedLanguage = detectBrowserLanguage();\n    \n              // Store the detected language\n              localStorage.setItem('language', storedLanguage);\n            }\n          } else {\n            console.log('Running on server-side, using default language');\n            storedLanguage = 'en';\n          }\n    \n          console.log('Supported languages loaded, validating language:', storedLanguage);\n          const validLanguage = Object.keys(supportedLanguages).includes(storedLanguage as any) ? storedLanguage : defaultLanguage;\n          console.log('Valid language determined:', validLanguage);\n    \n          // Load messages for the language\n          const langMessages = (await import(`../messages/${validLanguage}.json`)).default;\n    \n          setLanguageState(validLanguage);\n          setMessages(langMessages);\n    \n          // Update HTML lang attribute (only in browser)\n          if (typeof document !== 'undefined') {\n            document.documentElement.lang = validLanguage;\n          }\n        } catch (error) {\n          console.error('Failed to load language:', error);\n          // Fallback to English\n          console.log('Falling back to English due to error');\n          const enMessages = (await import('../messages/en.json')).default;\n          setMessages(enMessages);\n        } finally {\n          setIsLoading(false);\n        }\n      };\n      \n      loadLanguage();\n    }\n  }, [supportedLanguages, defaultLanguage]);\n\n  // Update language and load new messages\n  const setLanguage = async (lang: string) => {\n    try {\n      console.log('Setting language to:', lang);\n      const validLanguage = Object.keys(supportedLanguages).includes(lang as any) ? lang : defaultLanguage;\n\n      // Load messages for the new language\n      const langMessages = (await import(`../messages/${validLanguage}.json`)).default;\n\n      setLanguageState(validLanguage);\n      setMessages(langMessages);\n\n      // Store in localStorage (only in browser)\n      if (typeof window !== 'undefined') {\n        localStorage.setItem('language', validLanguage);\n      }\n\n      // Update HTML lang attribute (only in browser)\n      if (typeof document !== 'undefined') {\n        document.documentElement.lang = validLanguage;\n      }\n    } catch (error) {\n      console.error('Failed to set language:', error);\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-600 dark:text-gray-400\">Loading...</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <LanguageContext.Provider value={{ language, setLanguage, messages, supportedLanguages }}>\n      {children}\n    </LanguageContext.Provider>\n  );\n}\n\nexport function useLanguage() {\n  const context = useContext(LanguageContext);\n  if (context === undefined) {\n    throw new Error('useLanguage must be used within a LanguageProvider');\n  }\n  return context;\n}\n"
  },
  {
    "path": "src/hooks/useProcessedProjects.ts",
    "content": "import { useState, useEffect } from 'react';\n\ninterface ProcessedProject {\n  id: string;\n  owner: string;\n  repo: string;\n  name: string;\n  repo_type: string;\n  submittedAt: number;\n  language: string;\n}\n\nexport function useProcessedProjects() {\n  const [projects, setProjects] = useState<ProcessedProject[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n\n  useEffect(() => {\n    const fetchProjects = async () => {\n      setIsLoading(true);\n      setError(null);\n      try {\n        const response = await fetch('/api/wiki/projects');\n        if (!response.ok) {\n          throw new Error(`Failed to fetch projects: ${response.statusText}`);\n        }\n        const data = await response.json();\n        if (data.error) {\n          throw new Error(data.error);\n        }\n        setProjects(data as ProcessedProject[]);\n      } catch (e: unknown) {\n        console.error(\"Failed to load projects from API:\", e);\n        const message = e instanceof Error ? e.message : \"An unknown error occurred.\";\n        setError(message);\n        setProjects([]);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchProjects();\n  }, []);\n\n  return { projects, isLoading, error };\n}\n"
  },
  {
    "path": "src/i18n.ts",
    "content": "import { getRequestConfig } from 'next-intl/server';\n\n// Define the list of supported locales\nexport const locales = ['en', 'ja', 'zh', 'es', 'kr', 'vi', 'pt-br'];\n\nexport default getRequestConfig(async ({ locale }) => {\n  // Use a default locale if the requested one isn't supported\n  const safeLocale = locales.includes(locale as string) ? locale : 'en';\n\n  return {\n    locale: safeLocale as string,\n    messages: (await import(`./messages/${safeLocale}.json`)).default\n  };\n});\n"
  },
  {
    "path": "src/messages/en.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"AI-powered documentation\",\n    \"generateWiki\": \"Generate Wiki\",\n    \"processing\": \"Processing...\",\n    \"error\": \"Error\",\n    \"submit\": \"Submit\",\n    \"cancel\": \"Cancel\",\n    \"close\": \"Close\",\n    \"loading\": \"Loading...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Initializing wiki generation...\",\n    \"fetchingStructure\": \"Fetching repository structure...\",\n    \"determiningStructure\": \"Determining wiki structure...\",\n    \"clearingCache\": \"Clearing server cache...\",\n    \"preparingDownload\": \"Please wait while we prepare your download...\"\n  },\n  \"home\": {\n    \"welcome\": \"Welcome to DeepWiki-Open\",\n    \"welcomeTagline\": \"AI-powered documentation for your code repositories\",\n    \"description\": \"Generate comprehensive documentation from GitHub, GitLab, or Bitbucket repositories with just a few clicks.\",\n    \"quickStart\": \"Quick Start\",\n    \"enterRepoUrl\": \"Enter a repository URL in one of these formats:\",\n    \"advancedVisualization\": \"Advanced Visualization with Mermaid Diagrams\",\n    \"diagramDescription\": \"DeepWiki automatically generates interactive diagrams to help you understand code structure and relationships:\",\n    \"flowDiagram\": \"Flow Diagram\",\n    \"sequenceDiagram\": \"Sequence Diagram\"\n  },\n  \"form\": {\n    \"repository\": \"Repository\",\n    \"configureWiki\": \"Configure Wiki\",\n    \"repoPlaceholder\": \"owner/repo or GitHub/GitLab/Bitbucket URL\",\n    \"wikiLanguage\": \"Wiki Language\",\n    \"modelOptions\": \"Model Options\",\n    \"modelProvider\": \"Model Provider\",\n    \"modelSelection\": \"Model Selection\",\n    \"wikiType\": \"Wiki Type\",\n    \"comprehensive\": \"Comprehensive\",\n    \"concise\": \"Concise\",\n    \"comprehensiveDescription\": \"Detailed wiki with structured sections and more pages\",\n    \"conciseDescription\": \"Simplified wiki with fewer pages and essential information\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (Local)\",\n    \"localOllama\": \"Local Ollama Model\",\n    \"experimental\": \"Experimental\",\n    \"useOpenRouter\": \"Use OpenRouter API\",\n    \"openRouterModel\": \"OpenRouter Model\",\n    \"useOpenai\": \"Use Openai API\",\n    \"openaiModel\": \"Openai Model\",\n    \"useCustomModel\": \"Use custom model\",\n    \"customModelPlaceholder\": \"Enter custom model name\",\n    \"addTokens\": \"+ Add access tokens for private repositories\",\n    \"hideTokens\": \"- Hide access tokens\",\n    \"accessToken\": \"Access Token for Private Repositories\",\n    \"selectPlatform\": \"Select Platform\",\n    \"personalAccessToken\": \"{platform} Personal Access Token\",\n    \"tokenPlaceholder\": \"Enter your {platform} token\",\n    \"tokenSecurityNote\": \"Token is stored in memory only and never persisted.\",\n    \"defaultFiltersInfo\": \"Default filters include common directories like node_modules, .git, and common build artifact files.\",\n    \"fileFilterTitle\": \"File Filter Configuration\",\n    \"advancedOptions\": \"Advanced Options\",\n    \"viewDefaults\": \"View Default Filters\",\n    \"showFilters\": \"Show Filters\",\n    \"hideFilters\": \"Hide Filters\",\n    \"excludedDirs\": \"Directories to Exclude\",\n    \"excludedDirsHelp\": \"One directory path per line. Paths starting with ./ are relative to repository root.\",\n    \"enterExcludedDirs\": \"Enter excluded directories, one per line...\",\n    \"excludedFiles\": \"Files to Exclude\",\n    \"excludedFilesHelp\": \"One filename per line. Wildcards (*) are supported.\",\n    \"enterExcludedFiles\": \"Enter excluded files, one per line...\",\n    \"defaultFilters\": \"Default Excluded Files & Directories\",\n    \"directories\": \"Directories\",\n    \"files\": \"Files\",\n    \"scrollToViewMore\": \"Scroll to view more\",\n    \"changeModel\": \"Change Model\",\n    \"defaultNote\": \"These defaults are already applied. Add your custom exclusions above.\",\n    \"hideDefault\": \"Hide Default\",\n    \"viewDefault\": \"View Default\",\n    \"includedDirs\": \"Included Directories\",\n    \"includedFiles\": \"Included Files\",\n    \"enterIncludedDirs\": \"Enter included directories, one per line...\",\n    \"enterIncludedFiles\": \"Enter included files, one per line...\",\n    \"filterMode\": \"Filter Mode\",\n    \"excludeMode\": \"Exclude Paths\",\n    \"includeMode\": \"Include Only Paths\",\n    \"excludeModeDescription\": \"Specify paths to exclude from processing (default behavior)\",\n    \"includeModeDescription\": \"Specify only the paths to include, ignoring all others\",\n    \"authorizationCode\": \"Authorization Code\",\n    \"authorizationRequired\": \"Authentication is required to generate the wiki.\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - AI-powered documentation for code repositories\"\n  },\n  \"ask\": {\n    \"placeholder\": \"Ask a question about this repository...\",\n    \"askButton\": \"Ask\",\n    \"deepResearch\": \"Deep Research\",\n    \"researchInProgress\": \"Research in progress...\",\n    \"continueResearch\": \"Continue Research\",\n    \"viewPlan\": \"View Plan\",\n    \"viewUpdates\": \"View Updates\",\n    \"viewConclusion\": \"View Conclusion\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Refresh Wiki\",\n    \"confirmRefresh\": \"Confirm Refresh\",\n    \"cancel\": \"Cancel\",\n    \"home\": \"Home\",\n    \"errorTitle\": \"Error\",\n    \"errorMessageDefault\": \"Please check that your repository exists and is public. Valid formats are \\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\", or local folder paths like \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" or \\\"/path/to/folder\\\".\",\n    \"embeddingErrorDefault\": \"This error is related to the document embedding system used for analyzing your repository. Please verify your embedding model configuration, API keys, and try again. If the issue persists, consider switching to a different embedding provider in the model settings.\",\n    \"backToHome\": \"Back to Home\",\n    \"exportWiki\": \"Export Wiki\",\n    \"exportAsMarkdown\": \"Export as Markdown\",\n    \"exportAsJson\": \"Export as JSON\",\n    \"pages\": \"Pages\",\n    \"relatedFiles\": \"Related Files:\",\n    \"relatedPages\": \"Related Pages:\",\n    \"selectPagePrompt\": \"Select a page from the navigation to view its content\",\n    \"askAboutRepo\": \"Ask questions about this repository\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"Wiki Projects\"\n  },\n  \"projects\": {\n    \"title\": \"Processed Wiki Projects\",\n    \"searchPlaceholder\": \"Search projects by name, owner, or repository...\",\n    \"noProjects\": \"No projects found in the server cache. The cache might be empty or the server encountered an issue.\",\n    \"noSearchResults\": \"No projects match your search criteria.\",\n    \"processedOn\": \"Processed on:\",\n    \"loadingProjects\": \"Loading projects...\",\n    \"errorLoading\": \"Error loading projects:\",\n    \"backToHome\": \"Back to Home\",\n    \"browseExisting\": \"Browse Existing Projects\",\n    \"existingProjects\": \"Existing Projects\",\n    \"recentProjects\": \"Recent Projects\"\n  }\n}\n"
  },
  {
    "path": "src/messages/es.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"Documentación impulsada por IA\",\n    \"generateWiki\": \"Generar Wiki\",\n    \"processing\": \"Procesando...\",\n    \"error\": \"Error\",\n    \"submit\": \"Enviar\",\n    \"cancel\": \"Cancelar\",\n    \"close\": \"Cerrar\",\n    \"loading\": \"Cargando...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Inicializando generación de wiki...\",\n    \"fetchingStructure\": \"Obteniendo estructura del repositorio...\",\n    \"determiningStructure\": \"Determinando estructura del wiki...\",\n    \"clearingCache\": \"Limpiando caché del servidor...\",\n    \"preparingDownload\": \"Por favor espere mientras preparamos su descarga...\"\n  },\n  \"home\": {\n    \"welcome\": \"Bienvenido a DeepWiki\",\n    \"welcomeTagline\": \"Documentación impulsada por IA para repositorios de código\",\n    \"description\": \"Genera documentación completa de repositorios GitHub, GitLab o Bitbucket con solo unos clics.\",\n    \"quickStart\": \"Inicio Rápido\",\n    \"enterRepoUrl\": \"Ingresa una URL de repositorio en uno de estos formatos:\",\n    \"advancedVisualization\": \"Visualización Avanzada con Diagramas Mermaid\",\n    \"diagramDescription\": \"DeepWiki genera automáticamente diagramas interactivos para ayudarte a entender la estructura y relaciones del código:\",\n    \"flowDiagram\": \"Diagrama de Flujo\",\n    \"sequenceDiagram\": \"Diagrama de Secuencia\"\n  },\n  \"form\": {\n    \"repository\": \"Repositorio\",\n    \"configureWiki\": \"Configurar Wiki\",\n    \"repoPlaceholder\": \"propietario/repositorio o URL de GitHub/GitLab/Bitbucket\",\n    \"wikiLanguage\": \"Idioma del Wiki\",\n    \"modelOptions\": \"Opciones de Modelo\",\n    \"modelProvider\": \"Proveedor de Modelo\",\n    \"modelSelection\": \"Selección de Modelo\",\n    \"wikiType\": \"Tipo de Wiki\",\n    \"comprehensive\": \"Completo\",\n    \"concise\": \"Conciso\",\n    \"comprehensiveDescription\": \"Wiki detallado con secciones estructuradas y más páginas\",\n    \"conciseDescription\": \"Wiki simplificado con menos páginas e información esencial\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (Local)\",\n    \"localOllama\": \"Modelo Ollama Local\",\n    \"experimental\": \"Experimental\",\n    \"useOpenRouter\": \"Usar API de OpenRouter\",\n    \"openRouterModel\": \"Modelo OpenRouter\",\n    \"useOpenai\": \"Usar API de Openai\",\n    \"openaiModel\": \"Modelo Openai\",\n    \"useCustomModel\": \"Usar modelo personalizado\",\n    \"customModelPlaceholder\": \"Ingrese nombre de modelo personalizado\",\n    \"addTokens\": \"+ Agregar tokens de acceso para repositorios privados\",\n    \"hideTokens\": \"- Ocultar tokens de acceso\",\n    \"accessToken\": \"Token de Acceso para Repositorios Privados\",\n    \"selectPlatform\": \"Seleccionar Plataforma\",\n    \"personalAccessToken\": \"Token de Acceso Personal de {platform}\",\n    \"tokenPlaceholder\": \"Ingresa tu token de {platform}\",\n    \"tokenSecurityNote\": \"El token solo se almacena en memoria y nunca se persiste.\",\n    \"defaultFiltersInfo\": \"Los filtros predeterminados incluyen directorios comunes como node_modules, .git y archivos de artefactos de construcción comunes.\",\n    \"fileFilterTitle\": \"Configuración de Filtros de Archivos\",\n    \"advancedOptions\": \"Opciones Avanzadas\",\n    \"viewDefaults\": \"Ver Filtros Predeterminados\",\n    \"showFilters\": \"Mostrar Filtros\",\n    \"hideFilters\": \"Ocultar Filtros\",\n    \"excludedDirs\": \"Directorios a Excluir\",\n    \"excludedDirsHelp\": \"Una ruta de directorio por línea. Las rutas que comienzan con ./ son relativas a la raíz del repositorio.\",\n    \"enterExcludedDirs\": \"Ingrese directorios a excluir, uno por línea...\",\n    \"excludedFiles\": \"Archivos a Excluir\",\n    \"excludedFilesHelp\": \"Un nombre de archivo por línea. Se admiten comodines (*).\",\n    \"enterExcludedFiles\": \"Ingrese archivos a excluir, uno por línea...\",\n    \"defaultFilters\": \"Archivos y Directorios Excluidos por Defecto\",\n    \"directories\": \"Directorios\",\n    \"files\": \"Archivos\",\n    \"scrollToViewMore\": \"Desplazar para ver más\",\n    \"changeModel\": \"Cambiar Modelo\",\n    \"defaultNote\": \"Estos valores predeterminados ya están aplicados. Agregue sus exclusiones personalizadas arriba.\",\n    \"hideDefault\": \"Ocultar Predeterminados\",\n    \"viewDefault\": \"Ver Predeterminados\",\n    \"authorizationCode\": \"Código de Autorización\",\n    \"authorizationRequired\": \"Generar Wiki requiere código de autorización\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - Documentación impulsada por IA para repositorios de código\"\n  },\n  \"ask\": {\n    \"placeholder\": \"Haz una pregunta sobre este repositorio...\",\n    \"askButton\": \"Preguntar\",\n    \"deepResearch\": \"Investigación Profunda\",\n    \"researchInProgress\": \"Investigación en progreso...\",\n    \"continueResearch\": \"Continuar Investigación\",\n    \"viewPlan\": \"Ver Plan\",\n    \"viewUpdates\": \"Ver Actualizaciones\",\n    \"viewConclusion\": \"Ver Conclusión\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Actualizar Wiki\",\n    \"confirmRefresh\": \"Confirmar Actualización\",\n    \"cancel\": \"Cancelar\",\n    \"home\": \"Inicio\",\n    \"errorTitle\": \"Error\",\n    \"errorMessageDefault\": \"Por favor, compruebe que su repositorio existe y es público. Los formatos válidos son \\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\", o rutas de carpetas locales como \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" o \\\"/path/to/folder\\\".\",\n    \"embeddingErrorDefault\": \"Este error está relacionado con el sistema de embebido utilizado para analizar su repositorio. Por favor, verifique la configuración del modelo de embebido, las claves de API y vuelva a intentarlo. Si el problema persiste, considere cambiar al proveedor de embebido diferente en la configuración del modelo.\",\n    \"backToHome\": \"Volver al Inicio\",\n    \"exportWiki\": \"Exportar Wiki\",\n    \"exportAsMarkdown\": \"Exportar como Markdown\",\n    \"exportAsJson\": \"Exportar como JSON\",\n    \"pages\": \"Páginas\",\n    \"relatedFiles\": \"Archivos Relacionados:\",\n    \"relatedPages\": \"Páginas Relacionadas:\",\n    \"selectPagePrompt\": \"Seleccione una página de la navegación para ver su contenido\",\n    \"askAboutRepo\": \"Hacer preguntas sobre este repositorio\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"Lista de Proyectos\"\n  },\n  \"projects\": {\n    \"title\": \"Proyectos Wiki Procesados\",\n    \"searchPlaceholder\": \"Buscar proyectos por nombre, propietario o repositorio...\",\n    \"noProjects\": \"No se encontraron proyectos en la caché del servidor. La caché podría estar vacía o el servidor encontró un problema.\",\n    \"noSearchResults\": \"Ningún proyecto coincide con sus criterios de búsqueda.\",\n    \"processedOn\": \"Procesado el:\",\n    \"loadingProjects\": \"Cargando proyectos...\",\n    \"errorLoading\": \"Error al cargar proyectos:\",\n    \"backToHome\": \"Volver al Inicio\",\n    \"browseExisting\": \"Explorar Proyectos Existentes\",\n    \"existingProjects\": \"Proyectos Existentes\",\n    \"recentProjects\": \"Proyectos Recientes\"\n  }\n}\n"
  },
  {
    "path": "src/messages/fr.json",
    "content": "{\n    \"common\": {\n      \"appName\": \"DeepWiki-Open\",\n      \"tagline\": \"Documentation propulsée par l’IA\",\n      \"generateWiki\": \"Générer un Wiki\",\n      \"processing\": \"Traitement en cours...\",\n      \"error\": \"Erreur\",\n      \"submit\": \"Soumettre\",\n      \"cancel\": \"Annuler\",\n      \"close\": \"Fermer\",\n      \"loading\": \"Chargement...\"\n    },\n    \"loading\": {\n      \"initializing\": \"Initialisation de la génération du wiki...\",\n      \"fetchingStructure\": \"Récupération de la structure du dépôt...\",\n      \"determiningStructure\": \"Détermination de la structure du wiki...\",\n      \"clearingCache\": \"Nettoyage du cache serveur...\",\n      \"preparingDownload\": \"Veuillez patienter pendant que nous préparons votre téléchargement...\"\n    },\n    \"home\": {\n      \"welcome\": \"Bienvenue sur DeepWiki-Open\",\n      \"welcomeTagline\": \"Documentation propulsée par l’IA pour vos dépôts de code\",\n      \"description\": \"Générez une documentation complète à partir de dépôts GitHub, GitLab ou Bitbucket en quelques clics.\",\n      \"quickStart\": \"Démarrage rapide\",\n      \"enterRepoUrl\": \"Entrez une URL de dépôt dans l’un des formats suivants :\",\n      \"advancedVisualization\": \"Visualisation avancée avec des diagrammes Mermaid\",\n      \"diagramDescription\": \"DeepWiki génère automatiquement des diagrammes interactifs pour vous aider à comprendre la structure du code et ses relations :\",\n      \"flowDiagram\": \"Diagramme de flux\",\n      \"sequenceDiagram\": \"Diagramme de séquence\"\n    },\n    \"form\": {\n      \"repository\": \"Dépôt\",\n      \"configureWiki\": \"Configurer le Wiki\",\n      \"repoPlaceholder\": \"propriétaire/dépôt ou URL GitHub/GitLab/Bitbucket\",\n      \"wikiLanguage\": \"Langue du Wiki\",\n      \"modelOptions\": \"Options du Modèle\",\n      \"modelProvider\": \"Fournisseur du Modèle\",\n      \"modelSelection\": \"Sélection du Modèle\",\n      \"wikiType\": \"Type de Wiki\",\n      \"comprehensive\": \"Complet\",\n      \"concise\": \"Concis\",\n      \"comprehensiveDescription\": \"Wiki détaillé avec des sections structurées et plus de pages\",\n      \"conciseDescription\": \"Wiki simplifié avec moins de pages et les informations essentielles\",\n      \"providerGoogle\": \"Google\",\n      \"providerOpenAI\": \"OpenAI\",\n      \"providerOpenRouter\": \"OpenRouter\",\n      \"providerOllama\": \"Ollama (Local)\",\n      \"localOllama\": \"Modèle Ollama local\",\n      \"experimental\": \"Expérimental\",\n      \"useOpenRouter\": \"Utiliser l’API OpenRouter\",\n      \"openRouterModel\": \"Modèle OpenRouter\",\n      \"useOpenai\": \"Utiliser l’API OpenAI\",\n      \"openaiModel\": \"Modèle OpenAI\",\n      \"useCustomModel\": \"Utiliser un modèle personnalisé\",\n      \"customModelPlaceholder\": \"Entrez le nom du modèle personnalisé\",\n      \"addTokens\": \"+ Ajouter des jetons d’accès pour les dépôts privés\",\n      \"hideTokens\": \"- Masquer les jetons d’accès\",\n      \"accessToken\": \"Jeton d’accès pour les dépôts privés\",\n      \"selectPlatform\": \"Sélectionnez une plateforme\",\n      \"personalAccessToken\": \"Jeton d’accès personnel {platform}\",\n      \"tokenPlaceholder\": \"Entrez votre jeton {platform}\",\n      \"tokenSecurityNote\": \"Le jeton est stocké uniquement en mémoire et jamais sauvegardé.\",\n      \"defaultFiltersInfo\": \"Les filtres par défaut incluent les répertoires courants comme node_modules, .git et les fichiers de build.\",\n      \"fileFilterTitle\": \"Configuration du filtre de fichiers\",\n      \"advancedOptions\": \"Options avancées\",\n      \"viewDefaults\": \"Voir les filtres par défaut\",\n      \"showFilters\": \"Afficher les filtres\",\n      \"hideFilters\": \"Masquer les filtres\",\n      \"excludedDirs\": \"Répertoires à exclure\",\n      \"excludedDirsHelp\": \"Un chemin de répertoire par ligne. Les chemins commençant par ./ sont relatifs à la racine du dépôt.\",\n      \"enterExcludedDirs\": \"Entrez les répertoires à exclure, un par ligne...\",\n      \"excludedFiles\": \"Fichiers à exclure\",\n      \"excludedFilesHelp\": \"Un nom de fichier par ligne. Les jokers (*) sont pris en charge.\",\n      \"enterExcludedFiles\": \"Entrez les fichiers à exclure, un par ligne...\",\n      \"defaultFilters\": \"Fichiers & répertoires exclus par défaut\",\n      \"directories\": \"Répertoires\",\n      \"files\": \"Fichiers\",\n      \"scrollToViewMore\": \"Faites défiler pour en voir plus\",\n      \"changeModel\": \"Changer de modèle\",\n      \"defaultNote\": \"Ces valeurs par défaut sont déjà appliquées. Ajoutez vos exclusions personnalisées ci-dessus.\",\n      \"hideDefault\": \"Masquer les valeurs par défaut\",\n      \"viewDefault\": \"Afficher les valeurs par défaut\",\n      \"includedDirs\": \"Répertoires inclus\",\n      \"includedFiles\": \"Fichiers inclus\",\n      \"enterIncludedDirs\": \"Entrez les répertoires à inclure, un par ligne...\",\n      \"enterIncludedFiles\": \"Entrez les fichiers à inclure, un par ligne...\",\n      \"filterMode\": \"Mode de filtrage\",\n      \"excludeMode\": \"Exclure des chemins\",\n      \"includeMode\": \"Inclure uniquement ces chemins\",\n      \"excludeModeDescription\": \"Spécifie les chemins à exclure du traitement (comportement par défaut)\",\n      \"includeModeDescription\": \"Spécifie uniquement les chemins à inclure, en ignorant les autres\",\n      \"authorizationCode\": \"Code d’autorisation\",\n      \"authorizationRequired\": \"Une authentification est requise pour générer le wiki.\"\n    },\n    \"footer\": {\n      \"copyright\": \"DeepWiki - Documentation assistée par l'IA pour les dépôts de code\"\n    },\n    \"ask\": {\n      \"placeholder\": \"Posez une question sur ce dépôt...\",\n      \"askButton\": \"Poser la question\",\n      \"deepResearch\": \"Recherche approfondie\",\n      \"researchInProgress\": \"Recherche en cours...\",\n      \"continueResearch\": \"Continuer la recherche\",\n      \"viewPlan\": \"Voir le plan\",\n      \"viewUpdates\": \"Voir les mises à jour\",\n      \"viewConclusion\": \"Voir la conclusion\"\n    },\n    \"repoPage\": {\n      \"refreshWiki\": \"Rafraîchir le Wiki\",\n      \"confirmRefresh\": \"Confirmer le rafraîchissement\",\n      \"cancel\": \"Annuler\",\n      \"home\": \"Accueil\",\n      \"errorTitle\": \"Erreur\",\n      \"errorMessageDefault\": \"Veuillez vérifier que votre dépôt existe et est public. Les formats valides sont \\\"propriétaire/dépôt\\\", \\\"https://github.com/propriétaire/dépôt\\\", \\\"https://gitlab.com/propriétaire/dépôt\\\", \\\"https://bitbucket.org/propriétaire/dépôt\\\" ou des chemins locaux comme \\\"C:\\\\\\\\chemin\\\\\\\\vers\\\\\\\\dossier\\\" ou \\\"/chemin/vers/dossier\\\".\",\n      \"embeddingErrorDefault\": \"Cette erreur est liée au système d’indexation utilisé pour analyser votre dépôt. Veuillez vérifier la configuration du modèle d’indexation, les clés API, puis réessayez. Si le problème persiste, envisagez d’utiliser un autre fournisseur d’indexation dans les paramètres du modèle.\",\n      \"backToHome\": \"Retour à l’accueil\",\n      \"exportWiki\": \"Exporter le Wiki\",\n      \"exportAsMarkdown\": \"Exporter en Markdown\",\n      \"exportAsJson\": \"Exporter en JSON\",\n      \"pages\": \"Pages\",\n      \"relatedFiles\": \"Fichiers associés :\",\n      \"relatedPages\": \"Pages associées :\",\n      \"selectPagePrompt\": \"Sélectionnez une page dans la navigation pour en voir le contenu\",\n      \"askAboutRepo\": \"Poser des questions sur ce dépôt\"\n    },\n    \"nav\": {\n      \"wikiProjects\": \"Projets Wiki\"\n    },\n    \"projects\": {\n      \"title\": \"Projets Wiki traités\",\n      \"searchPlaceholder\": \"Rechercher des projets par nom, propriétaire ou dépôt...\",\n      \"noProjects\": \"Aucun projet trouvé dans le cache du serveur. Le cache est peut-être vide ou le serveur a rencontré un problème.\",\n      \"noSearchResults\": \"Aucun projet ne correspond à vos critères de recherche.\",\n      \"processedOn\": \"Traité le :\",\n      \"loadingProjects\": \"Chargement des projets...\",\n      \"errorLoading\": \"Erreur lors du chargement des projets :\",\n      \"backToHome\": \"Retour à l’accueil\",\n      \"browseExisting\": \"Parcourir les projets existants\",\n      \"existingProjects\": \"Projets existants\",\n      \"recentProjects\": \"Projets récents\"\n    }\n  }\n  "
  },
  {
    "path": "src/messages/ja.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"AI駆動のドキュメンテーション\",\n    \"generateWiki\": \"Wikiを生成\",\n    \"processing\": \"処理中...\",\n    \"error\": \"エラー\",\n    \"submit\": \"送信\",\n    \"cancel\": \"キャンセル\",\n    \"close\": \"閉じる\",\n    \"loading\": \"読み込み中...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Wiki生成を初期化中...\",\n    \"fetchingStructure\": \"リポジトリ構造を取得中...\",\n    \"determiningStructure\": \"Wiki構造を決定中...\",\n    \"clearingCache\": \"サーバーキャッシュをクリア中...\",\n    \"preparingDownload\": \"ダウンロードの準備中です...\"\n  },\n  \"home\": {\n    \"welcome\": \"DeepWikiへようこそ\",\n    \"welcomeTagline\": \"コードリポジトリのためのAI駆動ドキュメンテーション\",\n    \"description\": \"GitHub、GitLab、またはBitbucketリポジトリから包括的なドキュメントを数クリックで生成します。\",\n    \"quickStart\": \"クイックスタート\",\n    \"enterRepoUrl\": \"以下のいずれかの形式でリポジトリURLを入力してください：\",\n    \"advancedVisualization\": \"Mermaidダイアグラムによる高度な可視化\",\n    \"diagramDescription\": \"DeepWikiは、コード構造と関係を理解するのに役立つインタラクティブな図を自動的に生成します：\",\n    \"flowDiagram\": \"フロー図\",\n    \"sequenceDiagram\": \"シーケンス図\"\n  },\n  \"form\": {\n    \"repository\": \"リポジトリ\",\n    \"configureWiki\": \"Wiki設定\",\n    \"repoPlaceholder\": \"所有者/リポジトリまたはGitHub/GitLab/BitbucketのURL\",\n    \"wikiLanguage\": \"Wiki言語\",\n    \"modelOptions\": \"モデルオプション\",\n    \"modelProvider\": \"モデルプロバイダー\",\n    \"modelSelection\": \"モデル選択\",\n    \"wikiType\": \"Wikiタイプ\",\n    \"comprehensive\": \"包括的\",\n    \"concise\": \"簡潔\",\n    \"comprehensiveDescription\": \"構造化されたセクションとより多くのページを持つ詳細なWiki\",\n    \"conciseDescription\": \"ページ数が少なく、必要な情報のみを含む簡素化されたWiki\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama（ローカル）\",\n    \"localOllama\": \"ローカルOllamaモデル\",\n    \"experimental\": \"実験的\",\n    \"useOpenRouter\": \"OpenRouter APIを使用\",\n    \"openRouterModel\": \"OpenRouterモデル\",\n    \"useOpenai\": \"Openai APIを使用\",\n    \"openaiModel\": \"Openaiモデル\",\n    \"useCustomModel\": \"カスタムモデルを使用\",\n    \"customModelPlaceholder\": \"カスタムモデル名を入力\",\n    \"addTokens\": \"+ プライベートリポジトリ用のアクセストークンを追加\",\n    \"hideTokens\": \"- アクセストークンを隠す\",\n    \"accessToken\": \"プライベートリポジトリ用のアクセストークン\",\n    \"selectPlatform\": \"プラットフォームを選択\",\n    \"personalAccessToken\": \"{platform}個人アクセストークン\",\n    \"tokenPlaceholder\": \"{platform}トークンを入力してください\",\n    \"tokenSecurityNote\": \"トークンはメモリ内にのみ保存され、永続化されることはありません。\",\n    \"defaultFiltersInfo\": \"デフォルトのフィルターは、node_modules、.git、および一般的なビルドアーティファクトファイルのような一般的なディレクトリを含みます。\",\n    \"fileFilterTitle\": \"ファイルフィルター設定\",\n    \"advancedOptions\": \"詳細オプション\",\n    \"viewDefaults\": \"デフォルトフィルターを表示\",\n    \"showFilters\": \"フィルターを表示\",\n    \"hideFilters\": \"フィルターを非表示\",\n    \"excludedDirs\": \"除外するディレクトリ\",\n    \"excludedDirsHelp\": \"一行につき一つのディレクトリパス。./で始まるパスはリポジトリルートからの相対パスです。\",\n    \"enterExcludedDirs\": \"除外するディレクトリを一行ずつ入力...\",\n    \"excludedFiles\": \"除外するファイル\",\n    \"excludedFilesHelp\": \"一行につき一つのファイル名。ワイルドカード(*)が使用可能です。\",\n    \"enterExcludedFiles\": \"除外するファイルを一行ずつ入力...\",\n    \"defaultFilters\": \"デフォルトで除外されるファイルとディレクトリ\",\n    \"directories\": \"ディレクトリ\",\n    \"files\": \"ファイル\",\n    \"scrollToViewMore\": \"スクロールしてさらに表示\",\n    \"changeModel\": \"モデルを変更\",\n    \"defaultNote\": \"これらのデフォルト設定は既に適用されています。上記に追加の除外項目を入力してください。\",\n    \"hideDefault\": \"デフォルトを隠す\",\n    \"viewDefault\": \"デフォルトを表示\",\n    \"authorizationCode\": \"認証コード\",\n    \"authorizationRequired\": \"Wiki生成には認証コードが必要です\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - コードリポジトリのためのAI駆動ドキュメンテーション\"\n  },\n  \"ask\": {\n    \"placeholder\": \"このリポジトリについて質問する...\",\n    \"askButton\": \"質問する\",\n    \"deepResearch\": \"詳細調査\",\n    \"researchInProgress\": \"調査進行中...\",\n    \"continueResearch\": \"調査を続ける\",\n    \"viewPlan\": \"計画を見る\",\n    \"viewUpdates\": \"更新を見る\",\n    \"viewConclusion\": \"結論を見る\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Wikiを更新\",\n    \"confirmRefresh\": \"更新を確認\",\n    \"cancel\": \"キャンセル\",\n    \"home\": \"ホーム\",\n    \"errorTitle\": \"エラー\",\n    \"errorMessageDefault\": \"リポジトリが存在し、公開されていることを確認してください。有効な形式は「owner/repo」、「https://github.com/owner/repo」、「https://gitlab.com/owner/repo」、「https://bitbucket.org/owner/repo」、またはローカルフォルダパス（例: 「C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder」、「/path/to/folder」）です。\",\n    \"embeddingErrorDefault\": \"このエラーは、リポジトリを分析するために使用されるドキュメント埋め込みシステムに関連しています。モデル設定で異なる埋め込みプロバイダーを試してみてください。\",\n    \"backToHome\": \"ホームに戻る\",\n    \"exportWiki\": \"Wikiをエクスポート\",\n    \"exportAsMarkdown\": \"Markdownとしてエクスポート\",\n    \"exportAsJson\": \"JSONとしてエクスポート\",\n    \"pages\": \"ページ\",\n    \"relatedFiles\": \"関連ファイル:\",\n    \"relatedPages\": \"関連ページ:\",\n    \"selectPagePrompt\": \"ナビゲーションからページを選択してコンテンツを表示\",\n    \"askAboutRepo\": \"このリポジトリについて質問する\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"プロジェクト一覧\"\n  },\n  \"projects\": {\n    \"title\": \"処理済みWikiプロジェクト\",\n    \"searchPlaceholder\": \"プロジェクト名、所有者、リポジトリで検索...\",\n    \"noProjects\": \"サーバーキャッシュにプロジェクトが見つかりません。キャッシュが空であるか、サーバーで問題が発生した可能性があります。\",\n    \"noSearchResults\": \"検索条件に一致するプロジェクトがありません。\",\n    \"processedOn\": \"処理日時:\",\n    \"loadingProjects\": \"プロジェクトを読み込み中...\",\n    \"errorLoading\": \"プロジェクトの読み込みエラー:\",\n    \"backToHome\": \"ホームに戻る\",\n    \"browseExisting\": \"既存プロジェクトを閲覧\",\n    \"existingProjects\": \"既存プロジェクト\",\n    \"recentProjects\": \"最近のプロジェクト\"\n  }\n}\n"
  },
  {
    "path": "src/messages/kr.json",
    "content": "{\n    \"common\": {\n      \"appName\": \"DeepWiki-Open\",\n      \"tagline\": \"AI 기반 문서화\",\n      \"generateWiki\": \"위키 생성\",\n      \"processing\": \"처리 중...\",\n      \"error\": \"오류\",\n      \"submit\": \"제출\",\n      \"cancel\": \"취소\",\n      \"close\": \"닫기\",\n      \"loading\": \"로딩 중...\"\n    },\n    \"loading\": {\n      \"initializing\": \"위키 생성을 초기화하는 중...\",\n      \"fetchingStructure\": \"저장소 구조를 가져오는 중...\",\n      \"determiningStructure\": \"위키 구조를 결정하는 중...\",\n      \"clearingCache\": \"서버 캐시를 지우는 중...\",\n      \"preparingDownload\": \"다운로드를 준비 중입니다. 잠시만 기다려 주세요...\"\n    },\n    \"home\": {\n      \"welcome\": \"DeepWiki-Open에 오신 것을 환영합니다\",\n      \"welcomeTagline\": \"코드 저장소를 위한 AI 기반 문서화\",\n      \"description\": \"GitHub, GitLab 또는 Bitbucket 저장소에서 클릭 한 번으로 종합 문서를 생성하세요.\",\n      \"quickStart\": \"빠른 시작\",\n      \"enterRepoUrl\": \"다음 형식 중 하나로 저장소 URL을 입력하세요:\",\n      \"advancedVisualization\": \"Mermaid 다이어그램을 활용한 고급 시각화\",\n      \"diagramDescription\": \"DeepWiki는 코드 구조와 관계를 이해하는 데 도움이 되는 대화형 다이어그램을 자동 생성합니다:\",\n      \"flowDiagram\": \"흐름도\",\n      \"sequenceDiagram\": \"시퀀스 다이어그램\"\n    },\n    \"form\": {\n      \"repository\": \"저장소\",\n      \"configureWiki\": \"위키 구성\",\n      \"repoPlaceholder\": \"owner/repo 또는 GitHub/GitLab/Bitbucket URL\",\n      \"wikiLanguage\": \"위키 언어\",\n      \"modelOptions\": \"모델 옵션\",\n      \"modelProvider\": \"모델 제공자\",\n      \"modelSelection\": \"모델 선택\",\n      \"wikiType\": \"위키 유형\",\n      \"comprehensive\": \"종합적\",\n      \"concise\": \"간결함\",\n      \"comprehensiveDescription\": \"구조화된 섹션과 더 많은 페이지가 있는 상세한 위키\",\n      \"conciseDescription\": \"페이지 수가 적고 필수 정보만 포함된 간소화된 위키\",\n      \"providerGoogle\": \"구글\",\n      \"providerOpenAI\": \"OpenAI\",\n      \"providerOpenRouter\": \"OpenRouter\",\n      \"providerOllama\": \"Ollama (로컬)\",\n      \"localOllama\": \"로컬 Ollama 모델\",\n      \"experimental\": \"실험적\",\n      \"useOpenRouter\": \"OpenRouter API 사용\",\n      \"openRouterModel\": \"OpenRouter 모델\",\n      \"useOpenai\": \"Openai API 사용\",\n      \"openaiModel\": \"Openai 모델\",\n      \"useCustomModel\": \"사용자 정의 모델 사용\",\n      \"customModelPlaceholder\": \"사용자 정의 모델 이름 입력\",\n      \"addTokens\": \"+ 비공개 저장소 액세스 토큰 추가\",\n      \"hideTokens\": \"- 액세스 토큰 숨기기\",\n      \"accessToken\": \"비공개 저장소용 액세스 토큰\",\n      \"selectPlatform\": \"플랫폼 선택\",\n      \"personalAccessToken\": \"{platform} 개인 액세스 토큰\",\n      \"tokenPlaceholder\": \"{platform} 토큰을 입력하세요\",\n      \"tokenSecurityNote\": \"토큰은 메모리에만 저장되며, 영구적으로 보존되지 않습니다.\",\n      \"defaultFiltersInfo\": \"기본 필터에는 node_modules,.git 및 일반적인 빌드 파일이 포함됩니다.\",\n      \"fileFilterTitle\": \"파일 필터 구성\",\n      \"advancedOptions\": \"고급 옵션\",\n      \"viewDefaults\": \"기본 필터 보기\",\n      \"showFilters\": \"필터 표시\",\n      \"hideFilters\": \"필터 숨기기\",\n      \"excludedDirs\": \"제외할 디렉토리\",\n      \"excludedDirsHelp\": \"한 줄에 하나의 디렉토리 경로. ./로 시작하는 경로는 저장소 루트에서의 상대 경로입니다.\",\n      \"enterExcludedDirs\": \"제외할 디렉토리를 한 줄에 하나씩 입력하세요...\",\n      \"excludedFiles\": \"제외할 파일\",\n      \"excludedFilesHelp\": \"한 줄에 하나의 파일 이름. 와일드카드(*)가 지원됩니다.\",\n      \"enterExcludedFiles\": \"제외할 파일을 한 줄에 하나씩 입력하세요...\",\n      \"defaultFilters\": \"기본적으로 제외되는 파일 및 디렉토리\",\n      \"directories\": \"디렉토리\",\n      \"files\": \"파일\",\n      \"scrollToViewMore\": \"더 보려면 스크롤하세요\",\n      \"changeModel\": \"모델 변경\",\n      \"defaultNote\": \"이 기본 설정은 이미 적용되었습니다. 위에 사용자 지정 제외 항목을 추가하세요.\",\n      \"hideDefault\": \"기본값 숨기기\",\n      \"viewDefault\": \"기본값 보기\",\n      \"authorizationCode\": \"인증코드\",\n      \"authorizationRequired\": \"Wiki 생성에는 인증코드가 필요합니다\"\n    },\n    \"footer\": {\n      \"copyright\": \"DeepWiki - 코드 저장소를 위한 AI 기반 문서화\"\n    },\n    \"ask\": {\n      \"placeholder\": \"이 저장소에 대해 질문해 보세요...\",\n      \"askButton\": \"질문하기\",\n      \"deepResearch\": \"심층 분석\",\n      \"researchInProgress\": \"심층 분석 진행 중...\",\n      \"continueResearch\": \"분석 계속하기\",\n      \"viewPlan\": \"계획 보기\",\n      \"viewUpdates\": \"업데이트 보기\",\n      \"viewConclusion\": \"결론 보기\"\n    },\n    \"repoPage\": {\n      \"refreshWiki\": \"위키 새로고침\",\n      \"confirmRefresh\": \"새로고침 확인\",\n      \"cancel\": \"취소\",\n      \"home\": \"홈\",\n      \"errorTitle\": \"오류\",\n      \"errorMessageDefault\": \"저장소가 존재하며 공개 상태인지 확인해 주세요. 유효한 형식은 \\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\" 또는 로컬 폴더 경로 \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" 혹은 \\\"/path/to/folder\\\" 입니다.\",\n      \"embeddingErrorDefault\": \"이 오류는 저장소를 분석하는 데 사용되는 문서 임베딩 시스템과 관련이 있습니다. 임베딩 모델 설정에서 다른 임베딩 제공자를 시도해 보세요. 문제가 지속되면 모델 설정에서 다른 임베딩 제공자를 변경해 보세요.\",\n      \"backToHome\": \"홈으로 돌아가기\",\n      \"exportWiki\": \"위키 내보내기\",\n      \"exportAsMarkdown\": \"마크다운으로 내보내기\",\n      \"exportAsJson\": \"JSON으로 내보내기\",\n      \"pages\": \"페이지\",\n      \"relatedFiles\": \"관련 파일:\",\n      \"relatedPages\": \"관련 페이지:\",\n      \"selectPagePrompt\": \"목록에서 페이지를 선택하여 내용을 확인하세요\",\n      \"askAboutRepo\": \"이 저장소에 대해 질문하기\"\n    },\n    \"nav\": {\n      \"wikiProjects\": \"프로젝트 목록\"\n    },\n    \"projects\": {\n      \"title\": \"처리된 위키 프로젝트\",\n      \"searchPlaceholder\": \"프로젝트 이름, 소유자 또는 저장소로 검색...\",\n      \"noProjects\": \"서버 캐시에서 프로젝트를 찾을 수 없습니다. 캐시가 비어있거나 서버에 문제가 발생했을 수 있습니다.\",\n      \"noSearchResults\": \"검색 조건에 맞는 프로젝트가 없습니다.\",\n      \"processedOn\": \"처리 날짜:\",\n      \"loadingProjects\": \"프로젝트 로딩 중...\",\n      \"errorLoading\": \"프로젝트 로딩 오류:\",\n      \"backToHome\": \"홈으로 돌아가기\",\n      \"browseExisting\": \"기존 프로젝트 탐색\",\n      \"existingProjects\": \"기존 프로젝트\",\n      \"recentProjects\": \"최근 프로젝트\"\n    }\n  }\n"
  },
  {
    "path": "src/messages/pt-br.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"Documentação com IA\",\n    \"generateWiki\": \"Gerar Wiki\",\n    \"processing\": \"Processando...\",\n    \"error\": \"Erro\",\n    \"submit\": \"Enviar\",\n    \"cancel\": \"Cancelar\",\n    \"close\": \"Fechar\",\n    \"loading\": \"Carregando...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Inicializando geração da wiki...\",\n    \"fetchingStructure\": \"Obtendo estrutura do repositório...\",\n    \"determiningStructure\": \"Determinando estrutura da wiki...\",\n    \"clearingCache\": \"Limpando cache do servidor...\",\n    \"preparingDownload\": \"Aguarde enquanto preparamos seu download...\"\n  },\n  \"home\": {\n    \"welcome\": \"Bem-vindo ao DeepWiki-Open\",\n    \"welcomeTagline\": \"Documentação com IA para seus repositórios de código\",\n    \"description\": \"Gere documentação completa a partir de repositórios GitHub, GitLab ou Bitbucket com apenas alguns cliques.\",\n    \"quickStart\": \"Início Rápido\",\n    \"enterRepoUrl\": \"Digite uma URL de repositório em um destes formatos:\",\n    \"advancedVisualization\": \"Visualização Avançada com Diagramas Mermaid\",\n    \"diagramDescription\": \"O DeepWiki gera automaticamente diagramas interativos para ajudar você a entender a estrutura e os relacionamentos do código:\",\n    \"flowDiagram\": \"Diagrama de Fluxo\",\n    \"sequenceDiagram\": \"Diagrama de Sequência\"\n  },\n  \"form\": {\n    \"repository\": \"Repositório\",\n    \"configureWiki\": \"Configurar Wiki\",\n    \"repoPlaceholder\": \"proprietário/repo ou URL do GitHub/GitLab/Bitbucket\",\n    \"wikiLanguage\": \"Idioma da Wiki\",\n    \"modelOptions\": \"Opções de Modelo\",\n    \"modelProvider\": \"Provedor de Modelo\",\n    \"modelSelection\": \"Seleção de Modelo\",\n    \"wikiType\": \"Tipo de Wiki\",\n    \"comprehensive\": \"Abrangente\",\n    \"concise\": \"Concisa\",\n    \"comprehensiveDescription\": \"Wiki detalhada com seções estruturadas e mais páginas\",\n    \"conciseDescription\": \"Wiki simplificada com menos páginas e informações essenciais\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (Local)\",\n    \"localOllama\": \"Modelo Ollama Local\",\n    \"experimental\": \"Experimental\",\n    \"useOpenRouter\": \"Usar API OpenRouter\",\n    \"openRouterModel\": \"Modelo OpenRouter\",\n    \"useOpenai\": \"Usar API OpenAI\",\n    \"openaiModel\": \"Modelo OpenAI\",\n    \"useCustomModel\": \"Usar modelo personalizado\",\n    \"customModelPlaceholder\": \"Digite o nome do modelo personalizado\",\n    \"addTokens\": \"+ Adicionar tokens de acesso para repositórios privados\",\n    \"hideTokens\": \"- Ocultar tokens de acesso\",\n    \"accessToken\": \"Token de Acesso para Repositórios Privados\",\n    \"selectPlatform\": \"Selecionar Plataforma\",\n    \"personalAccessToken\": \"Token de Acesso Pessoal do {platform}\",\n    \"tokenPlaceholder\": \"Digite seu token do {platform}\",\n    \"tokenSecurityNote\": \"O token é armazenado apenas na memória e nunca é persistido.\",\n    \"defaultFiltersInfo\": \"Os filtros padrão incluem diretórios comuns como node_modules, .git e arquivos de artefatos de compilação comuns.\",\n    \"fileFilterTitle\": \"Configuração de Filtro de Arquivos\",\n    \"advancedOptions\": \"Opções Avançadas\",\n    \"viewDefaults\": \"Ver Filtros Padrão\",\n    \"showFilters\": \"Mostrar Filtros\",\n    \"hideFilters\": \"Ocultar Filtros\",\n    \"excludedDirs\": \"Diretórios a Excluir\",\n    \"excludedDirsHelp\": \"Um caminho de diretório por linha. Caminhos começando com ./ são relativos à raiz do repositório.\",\n    \"enterExcludedDirs\": \"Digite os diretórios excluídos, um por linha...\",\n    \"excludedFiles\": \"Arquivos a Excluir\",\n    \"excludedFilesHelp\": \"Um nome de arquivo por linha. Curingas (*) são suportados.\",\n    \"enterExcludedFiles\": \"Digite os arquivos excluídos, um por linha...\",\n    \"defaultFilters\": \"Arquivos e Diretórios Excluídos por Padrão\",\n    \"directories\": \"Diretórios\",\n    \"files\": \"Arquivos\",\n    \"scrollToViewMore\": \"Role para ver mais\",\n    \"changeModel\": \"Alterar Modelo\",\n    \"defaultNote\": \"Esses padrões já estão aplicados. Adicione suas exclusões personalizadas acima.\",\n    \"hideDefault\": \"Ocultar Padrão\",\n    \"viewDefault\": \"Ver Padrão\",\n    \"includedDirs\": \"Diretórios Incluídos\",\n    \"includedFiles\": \"Arquivos Incluídos\",\n    \"enterIncludedDirs\": \"Digite os diretórios incluídos, um por linha...\",\n    \"enterIncludedFiles\": \"Digite os arquivos incluídos, um por linha...\",\n    \"filterMode\": \"Modo de Filtro\",\n    \"excludeMode\": \"Excluir Caminhos\",\n    \"includeMode\": \"Incluir Apenas Caminhos\",\n    \"excludeModeDescription\": \"Especificar caminhos a serem excluídos do processamento (comportamento padrão)\",\n    \"includeModeDescription\": \"Especificar apenas os caminhos a serem incluídos, ignorando todos os outros\",\n    \"authorizationCode\": \"Código de Autorização\",\n    \"authorizationRequired\": \"A autenticação é necessária para gerar a wiki.\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - Documentação com IA para repositórios de código\"\n  },\n  \"ask\": {\n    \"placeholder\": \"Faça uma pergunta sobre este repositório...\",\n    \"askButton\": \"Perguntar\",\n    \"deepResearch\": \"Pesquisa Aprofundada\",\n    \"researchInProgress\": \"Pesquisa em andamento...\",\n    \"continueResearch\": \"Continuar Pesquisa\",\n    \"viewPlan\": \"Ver Plano\",\n    \"viewUpdates\": \"Ver Atualizações\",\n    \"viewConclusion\": \"Ver Conclusão\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Atualizar Wiki\",\n    \"confirmRefresh\": \"Confirmar Atualização\",\n    \"cancel\": \"Cancelar\",\n    \"home\": \"Início\",\n    \"errorTitle\": \"Erro\",\n    \"errorMessageDefault\": \"Verifique se o seu repositório existe e é público. Formatos válidos são \\\"proprietário/repo\\\", \\\"https://github.com/proprietário/repo\\\", \\\"https://gitlab.com/proprietário/repo\\\", \\\"https://bitbucket.org/proprietário/repo\\\", ou caminhos de pastas locais como \\\"C:\\\\\\\\caminho\\\\\\\\para\\\\\\\\pasta\\\" ou \\\"/caminho/para/pasta\\\".\",\n    \"embeddingErrorDefault\": \"Este erro está relacionado com o sistema de embebimento utilizado para analisar o seu repositório. Verifique a configuração do modelo de embebimento, as chaves de API e tente novamente. Se o problema persistir, considere mudar para um provedor de embebimento diferente na configuração do modelo.\",\n    \"backToHome\": \"Voltar ao Início\",\n    \"exportWiki\": \"Exportar Wiki\",\n    \"exportAsMarkdown\": \"Exportar como Markdown\",\n    \"exportAsJson\": \"Exportar como JSON\",\n    \"pages\": \"Páginas\",\n    \"relatedFiles\": \"Arquivos Relacionados:\",\n    \"relatedPages\": \"Páginas Relacionadas:\",\n    \"selectPagePrompt\": \"Selecione uma página da navegação para ver seu conteúdo\",\n    \"askAboutRepo\": \"Faça perguntas sobre este repositório\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"Projetos Wiki\"\n  },\n  \"projects\": {\n    \"title\": \"Projetos Wiki Processados\",\n    \"searchPlaceholder\": \"Pesquisar projetos por nome, proprietário ou repositório...\",\n    \"noProjects\": \"Nenhum projeto encontrado no cache do servidor. O cache pode estar vazio ou o servidor encontrou um problema.\",\n    \"noSearchResults\": \"Nenhum projeto corresponde aos seus critérios de pesquisa.\",\n    \"processedOn\": \"Processado em:\",\n    \"loadingProjects\": \"Carregando projetos...\",\n    \"errorLoading\": \"Erro ao carregar projetos:\",\n    \"backToHome\": \"Voltar ao Início\",\n    \"browseExisting\": \"Navegar por Projetos Existentes\",\n    \"existingProjects\": \"Projetos Existentes\",\n    \"recentProjects\": \"Projetos Recentes\"\n  }\n}\n"
  },
  {
    "path": "src/messages/ru.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"Документация с поддержкой ИИ\",\n    \"generateWiki\": \"Создать Wiki\",\n    \"processing\": \"Обработка...\",\n    \"error\": \"Ошибка\",\n    \"submit\": \"Отправить\",\n    \"cancel\": \"Отмена\",\n    \"close\": \"Закрыть\",\n    \"loading\": \"Загрузка...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Инициализация генерации wiki...\",\n    \"fetchingStructure\": \"Получение структуры репозитория...\",\n    \"determiningStructure\": \"Определение структуры wiki...\",\n    \"clearingCache\": \"Очистка кеша сервера...\",\n    \"preparingDownload\": \"Пожалуйста, подождите, идет подготовка загрузки...\"\n  },\n  \"home\": {\n    \"welcome\": \"Добро пожаловать в DeepWiki-Open\",\n    \"welcomeTagline\": \"Документация с поддержкой ИИ для ваших репозиториев кода\",\n    \"description\": \"Создавайте подробную документацию из репозиториев GitHub, GitLab или Bitbucket всего за несколько кликов.\",\n    \"quickStart\": \"Быстрый старт\",\n    \"enterRepoUrl\": \"Введите URL репозитория в одном из следующих форматов:\",\n    \"advancedVisualization\": \"Продвинутая визуализация с диаграммами Mermaid\",\n    \"diagramDescription\": \"DeepWiki автоматически генерирует интерактивные диаграммы, чтобы помочь вам понять структуру и связи в коде:\",\n    \"flowDiagram\": \"Диаграмма потока\",\n    \"sequenceDiagram\": \"Диаграмма последовательности\"\n  },\n  \"form\": {\n    \"repository\": \"Репозиторий\",\n    \"configureWiki\": \"Настроить Wiki\",\n    \"repoPlaceholder\": \"owner/repo или URL GitHub/GitLab/Bitbucket\",\n    \"wikiLanguage\": \"Язык Wiki\",\n    \"modelOptions\": \"Настройки модели\",\n    \"modelProvider\": \"Поставщик модели\",\n    \"modelSelection\": \"Выбор модели\",\n    \"wikiType\": \"Тип Wiki\",\n    \"comprehensive\": \"Подробная\",\n    \"concise\": \"Краткая\",\n    \"comprehensiveDescription\": \"Детализированная Wiki со структурированными разделами и большим числом страниц\",\n    \"conciseDescription\": \"Упрощённая Wiki с меньшим числом страниц и основной информацией\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (локально)\",\n    \"localOllama\": \"Локальная модель Ollama\",\n    \"experimental\": \"Экспериментально\",\n    \"useOpenRouter\": \"Использовать API OpenRouter\",\n    \"openRouterModel\": \"Модель OpenRouter\",\n    \"useOpenai\": \"Использовать API OpenAI\",\n    \"openaiModel\": \"Модель OpenAI\",\n    \"useCustomModel\": \"Использовать пользовательскую модель\",\n    \"customModelPlaceholder\": \"Введите имя пользовательской модели\",\n    \"addTokens\": \"+ Добавить токены доступа для приватных репозиториев\",\n    \"hideTokens\": \"- Скрыть токены доступа\",\n    \"accessToken\": \"Токен доступа для приватных репозиториев\",\n    \"selectPlatform\": \"Выбрать платформу\",\n    \"personalAccessToken\": \"Персональный токен доступа {platform}\",\n    \"tokenPlaceholder\": \"Введите ваш токен {platform}\",\n    \"tokenSecurityNote\": \"Токен хранится только в памяти и не сохраняется.\",\n    \"defaultFiltersInfo\": \"Фильтры по умолчанию исключают общие директории, такие как node_modules, .git и артефакты сборки.\",\n    \"fileFilterTitle\": \"Настройка фильтра файлов\",\n    \"advancedOptions\": \"Дополнительные параметры\",\n    \"viewDefaults\": \"Показать фильтры по умолчанию\",\n    \"showFilters\": \"Показать фильтры\",\n    \"hideFilters\": \"Скрыть фильтры\",\n    \"excludedDirs\": \"Исключённые директории\",\n    \"excludedDirsHelp\": \"Один путь к директории на строку. Пути, начинающиеся с ./, относительны к корню репозитория.\",\n    \"enterExcludedDirs\": \"Введите исключённые директории, по одной на строку...\",\n    \"excludedFiles\": \"Исключённые файлы\",\n    \"excludedFilesHelp\": \"Один файл на строку. Поддерживаются подстановочные знаки (*).\",\n    \"enterExcludedFiles\": \"Введите исключённые файлы, по одному на строку...\",\n    \"defaultFilters\": \"Исключённые файлы и директории по умолчанию\",\n    \"directories\": \"Директории\",\n    \"files\": \"Файлы\",\n    \"scrollToViewMore\": \"Прокрутите для просмотра\",\n    \"changeModel\": \"Сменить модель\",\n    \"defaultNote\": \"Эти значения уже применены. Добавьте свои исключения выше.\",\n    \"hideDefault\": \"Скрыть по умолчанию\",\n    \"viewDefault\": \"Показать по умолчанию\",\n    \"includedDirs\": \"Включённые директории\",\n    \"includedFiles\": \"Включённые файлы\",\n    \"enterIncludedDirs\": \"Введите включённые директории, по одной на строку...\",\n    \"enterIncludedFiles\": \"Введите включённые файлы, по одному на строку...\",\n    \"filterMode\": \"Режим фильтрации\",\n    \"excludeMode\": \"Исключить пути\",\n    \"includeMode\": \"Включить только пути\",\n    \"excludeModeDescription\": \"Укажите пути, которые нужно исключить из обработки (поведение по умолчанию)\",\n    \"includeModeDescription\": \"Укажите только те пути, которые нужно включить, игнорируя остальные\",\n    \"authorizationCode\": \"Код авторизации\",\n    \"authorizationRequired\": \"Для генерации Wiki требуется авторизация.\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki — документация с поддержкой ИИ для репозиториев кода\"\n  },\n  \"ask\": {\n    \"placeholder\": \"Задайте вопрос об этом репозитории...\",\n    \"askButton\": \"Спросить\",\n    \"deepResearch\": \"Глубокое исследование\",\n    \"researchInProgress\": \"Идёт исследование...\",\n    \"continueResearch\": \"Продолжить исследование\",\n    \"viewPlan\": \"Просмотреть план\",\n    \"viewUpdates\": \"Просмотреть обновления\",\n    \"viewConclusion\": \"Просмотреть выводы\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Обновить Wiki\",\n    \"confirmRefresh\": \"Подтвердить обновление\",\n    \"cancel\": \"Отмена\",\n    \"home\": \"Главная\",\n    \"errorTitle\": \"Ошибка\",\n    \"errorMessageDefault\": \"Пожалуйста, убедитесь, что ваш репозиторий существует и является публичным. Допустимые форматы: \\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\" или локальные пути вроде \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" или \\\"/path/to/folder\\\".\",\n    \"embeddingErrorDefault\": \"Ошибка связана с системой встраивания документов для анализа репозитория. Проверьте конфигурацию модели встраивания, API-ключи и повторите попытку. Если проблема сохраняется, попробуйте сменить поставщика модели в настройках.\",\n    \"backToHome\": \"Назад на главную\",\n    \"exportWiki\": \"Экспортировать Wiki\",\n    \"exportAsMarkdown\": \"Экспорт в Markdown\",\n    \"exportAsJson\": \"Экспорт в JSON\",\n    \"pages\": \"Страницы\",\n    \"relatedFiles\": \"Связанные файлы:\",\n    \"relatedPages\": \"Связанные страницы:\",\n    \"selectPagePrompt\": \"Выберите страницу в навигации для просмотра её содержимого\",\n    \"askAboutRepo\": \"Задайте вопросы об этом репозитории\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"Проекты Wiki\"\n  },\n  \"projects\": {\n    \"title\": \"Обработанные проекты Wiki\",\n    \"searchPlaceholder\": \"Поиск проектов по названию, владельцу или репозиторию...\",\n    \"noProjects\": \"На сервере не найдено проектов. Кеш может быть пуст или сервер столкнулся с проблемой.\",\n    \"noSearchResults\": \"По вашему запросу проектов не найдено.\",\n    \"processedOn\": \"Обработано:\",\n    \"loadingProjects\": \"Загрузка проектов...\",\n    \"errorLoading\": \"Ошибка загрузки проектов:\",\n    \"backToHome\": \"Назад на главную\",\n    \"browseExisting\": \"Просмотреть существующие проекты\",\n    \"existingProjects\": \"Существующие проекты\",\n    \"recentProjects\": \"Недавние проекты\"\n  }\n}\n"
  },
  {
    "path": "src/messages/vi.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"Tài liệu hỗ trợ bởi AI\",\n    \"generateWiki\": \"Tạo Wiki\",\n    \"processing\": \"Đang xử lý...\",\n    \"error\": \"Lỗi\",\n    \"submit\": \"Gửi\",\n    \"cancel\": \"Hủy\",\n    \"close\": \"Đóng\",\n    \"loading\": \"Đang tải...\"\n  },\n  \"loading\": {\n    \"initializing\": \"Đang khởi tạo wiki...\",\n    \"fetchingStructure\": \"Đang lấy cấu trúc repository...\",\n    \"determiningStructure\": \"Đang xác định cấu trúc wiki...\",\n    \"clearingCache\": \"Đang xóa bộ nhớ đệm máy chủ...\",\n    \"preparingDownload\": \"Đang tải! Vui lòng chờ...\"\n  },\n  \"home\": {\n    \"welcome\": \"Chào mừng đến với DeepWiki-Open\",\n    \"welcomeTagline\": \"Tài liệu hỗ trợ bởi AI cho các repository của bạn\",\n    \"description\": \"Tạo tài liệu từ các repository GitHub, GitLab, hoặc Bitbucket chỉ với vài cú nhấp chuột.\",\n    \"quickStart\": \"Bắt đầu nhanh\",\n    \"enterRepoUrl\": \"Nhập URL repository\",\n    \"advancedVisualization\": \"Tùy chỉnh sơ đồ trực quan với Mermaid\",\n    \"diagramDescription\": \"DeepWiki tự động tạo các sơ đồ tương tác giúp bạn hiểu cấu trúc source codes và mối quan hệ giữa chúng:\",\n    \"flowDiagram\": \"Sơ đồ luồng\",\n    \"sequenceDiagram\": \"Sơ đồ tuần tự\"\n  },\n  \"form\": {\n    \"repository\": \"Repository\",\n    \"configureWiki\": \"Cấu hình Wiki\",\n    \"repoPlaceholder\": \"owner/repo hoặc URL GitHub/GitLab/Bitbucket\",\n    \"wikiLanguage\": \"Ngôn ngữ Wiki\",\n    \"modelOptions\": \"Tùy chọn mô hình\",\n    \"modelProvider\": \"Nhà cung cấp mô hình\",\n    \"modelSelection\": \"Lựa chọn mô hình\",\n    \"wikiType\": \"Loại Wiki\",\n    \"comprehensive\": \"Toàn diện\",\n    \"concise\": \"Súc tích\",\n    \"comprehensiveDescription\": \"Wiki chi tiết với các phần có cấu trúc và nhiều trang hơn\",\n    \"conciseDescription\": \"Wiki đơn giản hóa với ít trang hơn và thông tin thiết yếu\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (Cục bộ)\",\n    \"localOllama\": \"Mô hình Ollama cục bộ\",\n    \"experimental\": \"Thử nghiệm\",\n    \"useOpenRouter\": \"Sử dụng API OpenRouter\",\n    \"openRouterModel\": \"Mô hình OpenRouter\",\n    \"useOpenai\": \"Sử dụng API Openai\",\n    \"openaiModel\": \"Mô hình Openai\",\n    \"useCustomModel\": \"Sử dụng mô hình tùy chỉnh\",\n    \"customModelPlaceholder\": \"Nhập tên mô hình tùy chỉnh\",\n    \"addTokens\": \"+ Thêm token truy cập cho private repositories\",\n    \"hideTokens\": \"- Ẩn token truy cập\",\n    \"accessToken\": \"Token truy cập cho private repositories\",\n    \"selectPlatform\": \"Chọn nền tảng\",\n    \"personalAccessToken\": \"Token truy cập cá nhân {platform}\",\n    \"tokenPlaceholder\": \"Nhập token {platform} của bạn\",\n    \"tokenSecurityNote\": \"Token chỉ được lưu trong bộ nhớ và không bao giờ được lưu trữ vĩnh viễn.\",\n    \"defaultFiltersInfo\": \"Lọc mặc định bao gồm các thư mục thông thường như node_modules, .git và các tệp tài liệu xây dựng thông thường.\",\n    \"fileFilterTitle\": \"Cấu hình Lọc Tệp\",\n    \"advancedOptions\": \"Tùy chọn nâng cao\",\n    \"viewDefaults\": \"Xem Lọc Mặc định\",\n    \"showFilters\": \"Hiển thị Lọc\",\n    \"hideFilters\": \"Ẩn Lọc\",\n    \"excludedDirs\": \"Thư mục để Loại trừ\",\n    \"excludedDirsHelp\": \"Một đường dẫn thư mục trên một dòng. Đường dẫn bắt đầu bằng ./ là tương đối so với gốc kho lưu trữ.\",\n    \"enterExcludedDirs\": \"Nhập thư mục cần loại trừ, mỗi dòng một thư mục...\",\n    \"excludedFiles\": \"Tệp để Loại trừ\",\n    \"excludedFilesHelp\": \"Một tên tệp trên một dòng. Hỗ trợ ký tự đại diện (*).\",\n    \"enterExcludedFiles\": \"Nhập tệp cần loại trừ, mỗi dòng một tệp...\",\n    \"defaultFilters\": \"Tệp và Thư mục Loại trừ Mặc định\",\n    \"directories\": \"Thư mục\",\n    \"files\": \"Tệp\",\n    \"scrollToViewMore\": \"Dịch chuyển để xem thêm\",\n    \"changeModel\": \"Thay đổi mô hình\",\n    \"defaultNote\": \"Các giá trị mặc định này đã được áp dụng. Thêm các loại trừ tùy chỉnh của bạn ở trên.\",\n    \"hideDefault\": \"Ẩn mặc định\",\n    \"viewDefault\": \"Xem mặc định\",\n    \"authorizationCode\": \"Mã xác thực\",\n    \"authorizationRequired\": \"Mã xác thực cần thiết để tạo Wiki\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - Tài liệu hỗ trợ bởi AI cho repository\"\n  },\n  \"ask\": {\n    \"placeholder\": \"Đặt một câu hỏi về repository này...\",\n    \"askButton\": \"Hỏi\",\n    \"deepResearch\": \"Nghiên cứu sâu\",\n    \"researchInProgress\": \"Đang tiến hành nghiên cứu...\",\n    \"continueResearch\": \"Tiếp tục nghiên cứu\",\n    \"viewPlan\": \"Xem kế hoạch\",\n    \"viewUpdates\": \"Xem cập nhật\",\n    \"viewConclusion\": \"Xem kết luận\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"Làm mới Wiki\",\n    \"confirmRefresh\": \"Xác nhận làm mới\",\n    \"cancel\": \"Hủy bỏ\",\n    \"home\": \"Trang chủ\",\n    \"errorTitle\": \"Lỗi\",\n    \"errorMessageDefault\": \"Vui lòng kiểm tra xem repository có tồn tại và công khai hay không. Các định dạng hợp lệ là \\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\", hoặc các đường dẫn thư mục cục bộ như \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" hoặc \\\"/path/to/folder\\\".\",\n    \"embeddingErrorDefault\": \"Lỗi này liên quan đến hệ thống embedding được sử dụng để phân tích repository của bạn. Vui lòng kiểm tra cấu hình mô hình embedding, API keys và thử lại. Nếu vấn đề vẫn tiếp diễn, hãy xem xét chuyển sang nhà cung cấp embedding khác trong cấu hình mô hình.\",\n    \"backToHome\": \"Quay lại trang chủ\",\n    \"exportWiki\": \"Xuất Wiki\",\n    \"exportAsMarkdown\": \"Xuất dưới dạng Markdown\",\n    \"exportAsJson\": \"Xuất dưới dạng JSON\",\n    \"pages\": \"Trang\",\n    \"relatedFiles\": \"Tệp liên quan:\",\n    \"relatedPages\": \"Trang liên quan:\",\n    \"selectPagePrompt\": \"Chọn một trang từ thanh điều hướng để xem nội dung của nó\",\n    \"askAboutRepo\": \"Hỏi về repository này\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"Danh sách dự án\"\n  },\n  \"projects\": {\n    \"title\": \"Dự án Wiki đã xử lý\",\n    \"searchPlaceholder\": \"Tìm kiếm dự án theo tên, chủ sở hữu hoặc repository...\",\n    \"noProjects\": \"Không tìm thấy dự án nào trong bộ nhớ đệm máy chủ. Bộ nhớ đệm có thể trống hoặc máy chủ gặp sự cố.\",\n    \"noSearchResults\": \"Không có dự án nào phù hợp với tiêu chí tìm kiếm của bạn.\",\n    \"processedOn\": \"Xử lý vào:\",\n    \"loadingProjects\": \"Đang tải dự án...\",\n    \"errorLoading\": \"Lỗi khi tải dự án:\",\n    \"backToHome\": \"Về trang chủ\",\n    \"browseExisting\": \"Duyệt dự án hiện có\",\n    \"existingProjects\": \"Dự án hiện có\",\n    \"recentProjects\": \"Dự án gần đây\"\n  }\n}"
  },
  {
    "path": "src/messages/zh-tw.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"AI 驅動的文件\",\n    \"generateWiki\": \"產生 Wiki\",\n    \"processing\": \"處理中...\",\n    \"error\": \"錯誤\",\n    \"submit\": \"提交\",\n    \"cancel\": \"取消\",\n    \"close\": \"關閉\",\n    \"loading\": \"載入中...\"\n  },\n  \"loading\": {\n    \"initializing\": \"初始化 Wiki 產生...\",\n    \"fetchingStructure\": \"取得儲存庫結構...\",\n    \"determiningStructure\": \"驗證 Wiki 結構...\",\n    \"clearingCache\": \"清除伺服器快取...\",\n    \"preparingDownload\": \"請稍候，我們正在準備您的下載...\"\n  },\n  \"home\": {\n    \"welcome\": \"歡迎使用 DeepWiki\",\n    \"welcomeTagline\": \"為程式碼儲存庫提供 AI 驅動的文件\",\n    \"description\": \"只需一次點擊，即可從 GitHub、GitLab 或 Bitbucket 儲存庫產生全面的文件。\",\n    \"quickStart\": \"快速開始\",\n    \"enterRepoUrl\": \"請以下列格式之一輸入儲存庫 URL：\",\n    \"advancedVisualization\": \"使用 Mermaid 圖表進行進階視覺化\",\n    \"diagramDescription\": \"DeepWiki 自動產生互動式圖表，協助您理解程式碼結構和關係：\",\n    \"flowDiagram\": \"流程圖\",\n    \"sequenceDiagram\": \"序列圖\"\n  },\n  \"form\": {\n    \"repository\": \"儲存庫\",\n    \"configureWiki\": \"設定 Wiki\",\n    \"repoPlaceholder\": \"擁有者/儲存庫或 GitHub/GitLab/Bitbucket URL\",\n    \"wikiLanguage\": \"Wiki 語言\",\n    \"modelOptions\": \"模型選項\",\n    \"modelProvider\": \"模型提供商\",\n    \"modelSelection\": \"模型選擇\",\n    \"wikiType\": \"Wiki 類型\",\n    \"comprehensive\": \"全面型\",\n    \"concise\": \"簡潔型\",\n    \"comprehensiveDescription\": \"包含結構化章節和更多頁面的詳細 Wiki\",\n    \"conciseDescription\": \"頁面更少，僅包含核心資訊的簡化 Wiki\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama（本機）\",\n    \"localOllama\": \"本機 Ollama 模型\",\n    \"experimental\": \"實驗性\",\n    \"useOpenRouter\": \"使用 OpenRouter API\",\n    \"openRouterModel\": \"OpenRouter 模型\",\n    \"useOpenai\": \"使用 OpenAI API\",\n    \"openaiModel\": \"OpenAI 模型\",\n    \"useCustomModel\": \"使用自訂模型\",\n    \"customModelPlaceholder\": \"輸入自訂模型名稱\",\n    \"addTokens\": \"+ 新增私人儲存庫存取權杖\",\n    \"hideTokens\": \"- 隱藏存取權杖\",\n    \"accessToken\": \"私人儲存庫存取權杖\",\n    \"selectPlatform\": \"選擇平台\",\n    \"personalAccessToken\": \"{platform} 個人存取權杖\",\n    \"tokenPlaceholder\": \"輸入您的 {platform} 權杖\",\n    \"tokenSecurityNote\": \"權杖僅儲存在記憶體中，絕不會持久化。\",\n    \"defaultFiltersInfo\": \"預設過濾器包括 node_modules、.git 和常見的建置檔案。\",\n    \"fileFilterTitle\": \"檔案過濾設定\",\n    \"advancedOptions\": \"進階選項\",\n    \"viewDefaults\": \"檢視預設過濾\",\n    \"showFilters\": \"顯示過濾器\",\n    \"hideFilters\": \"隱藏過濾器\",\n    \"excludedDirs\": \"要排除的目錄\",\n    \"excludedDirsHelp\": \"每行一個目錄路徑。以 ./ 開頭表示相對於儲存庫根目錄的路徑。\",\n    \"enterExcludedDirs\": \"輸入要排除的目錄，每行一個...\",\n    \"excludedFiles\": \"要排除的檔案\",\n    \"excludedFilesHelp\": \"每行一個檔案名稱。支援萬用字元（*）。\",\n    \"enterExcludedFiles\": \"輸入要排除的檔案，每行一個...\",\n    \"defaultFilters\": \"預設排除的檔案和目錄\",\n    \"directories\": \"目錄\",\n    \"files\": \"檔案\",\n    \"scrollToViewMore\": \"可滑動檢視更多\",\n    \"changeModel\": \"修改模型\",\n    \"defaultNote\": \"這些預設設定已經被套用。請在上方新增您的自訂排除項目。\",\n    \"hideDefault\": \"隱藏預設設定\",\n    \"viewDefault\": \"檢視預設設定\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - 為程式碼儲存庫提供 AI 驅動的文件\"\n  },\n  \"ask\": {\n    \"placeholder\": \"詢問關於此儲存庫的問題...\",\n    \"askButton\": \"提問\",\n    \"deepResearch\": \"深度研究\",\n    \"researchInProgress\": \"研究進行中...\",\n    \"continueResearch\": \"繼續研究\",\n    \"viewPlan\": \"檢視計畫\",\n    \"viewUpdates\": \"檢視更新\",\n    \"viewConclusion\": \"檢視結論\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"重新整理 Wiki\",\n    \"confirmRefresh\": \"確認重新整理\",\n    \"cancel\": \"取消\",\n    \"home\": \"首頁\",\n    \"errorTitle\": \"錯誤\",\n    \"errorMessageDefault\": \"請檢查您的儲存庫是否存在且為公開儲存庫。有效格式為 \\\"owner/repo\\\"、\\\"https://github.com/owner/repo\\\"、\\\"https://gitlab.com/owner/repo\\\"、\\\"https://bitbucket.org/owner/repo\\\"，或本機資料夾路徑，如 \\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\" 或 \\\"/path/to/folder\\\"。\",\n    \"embeddingErrorDefault\": \"這個錯誤與用於分析您的儲存庫的文件嵌入系統有關。請檢查您的嵌入模型配置、API 密鑰，並重試。如果問題持續存在，請考慮在模型設置中切換到不同的嵌入提供者。\",\n    \"backToHome\": \"返回首頁\",\n    \"exportWiki\": \"匯出 Wiki\",\n    \"exportAsMarkdown\": \"匯出為 Markdown\",\n    \"exportAsJson\": \"匯出為 JSON\",\n    \"pages\": \"頁面\",\n    \"relatedFiles\": \"相關檔案：\",\n    \"relatedPages\": \"相關頁面：\",\n    \"selectPagePrompt\": \"從導覽中選擇一個頁面以檢視其內容\",\n    \"askAboutRepo\": \"詢問關於此儲存庫的問題\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"專案清單\"\n  },\n  \"projects\": {\n    \"title\": \"已處理的 Wiki 專案\",\n    \"searchPlaceholder\": \"按專案名稱、擁有者或儲存庫搜尋...\",\n    \"noProjects\": \"伺服器快取中未找到專案。快取可能為空或伺服器遇到問題。\",\n    \"noSearchResults\": \"沒有專案符合您的搜尋條件。\",\n    \"processedOn\": \"處理時間：\",\n    \"loadingProjects\": \"正在載入專案...\",\n    \"errorLoading\": \"載入專案時發生錯誤：\",\n    \"backToHome\": \"返回首頁\",\n    \"browseExisting\": \"瀏覽現有專案\",\n    \"existingProjects\": \"現有專案\",\n    \"recentProjects\": \"最近專案\"\n  }\n} "
  },
  {
    "path": "src/messages/zh.json",
    "content": "{\n  \"common\": {\n    \"appName\": \"DeepWiki-Open\",\n    \"tagline\": \"AI驱动的文档\",\n    \"generateWiki\": \"生成Wiki\",\n    \"processing\": \"处理中...\",\n    \"error\": \"错误\",\n    \"submit\": \"提交\",\n    \"cancel\": \"取消\",\n    \"close\": \"关闭\",\n    \"loading\": \"加载中...\"\n  },\n  \"loading\": {\n    \"initializing\": \"初始化Wiki生成...\",\n    \"fetchingStructure\": \"获取仓库结构...\",\n    \"determiningStructure\": \"验证Wiki结构...\",\n    \"clearingCache\": \"清除服务器缓存...\",\n    \"preparingDownload\": \"请等待，我们正在准备您的下载...\"\n  },\n  \"home\": {\n    \"welcome\": \"欢迎使用DeepWiki\",\n    \"welcomeTagline\": \"为代码仓库提供AI驱动的文档\",\n    \"description\": \"只需一次点击，即可从GitHub、GitLab或Bitbucket仓库生成全面的文档。\",\n    \"quickStart\": \"快速开始\",\n    \"enterRepoUrl\": \"请以下列格式之一输入仓库URL：\",\n    \"advancedVisualization\": \"使用Mermaid图表进行高级可视化\",\n    \"diagramDescription\": \"DeepWiki自动生成交互式图表，帮助您理解代码结构和关系：\",\n    \"flowDiagram\": \"流程图\",\n    \"sequenceDiagram\": \"序列图\"\n  },\n  \"form\": {\n    \"repository\": \"仓库\",\n    \"configureWiki\": \"配置Wiki\",\n    \"repoPlaceholder\": \"所有者/仓库或GitHub/GitLab/Bitbucket URL\",\n    \"wikiLanguage\": \"Wiki语言\",\n    \"modelOptions\": \"模型选项\",\n    \"modelProvider\": \"模型提供商\",\n    \"modelSelection\": \"模型选择\",\n    \"wikiType\": \"Wiki类型\",\n    \"comprehensive\": \"全面型\",\n    \"concise\": \"简洁型\",\n    \"comprehensiveDescription\": \"包含结构化章节和更多页面的详细Wiki\",\n    \"conciseDescription\": \"页面更少，仅包含核心信息的简化Wiki\",\n    \"providerGoogle\": \"Google\",\n    \"providerOpenAI\": \"OpenAI\",\n    \"providerOpenRouter\": \"OpenRouter\",\n    \"providerOllama\": \"Ollama (本地)\",\n    \"localOllama\": \"本地Ollama模型\",\n    \"experimental\": \"实验性\",\n    \"useOpenRouter\": \"使用OpenRouter API\",\n    \"openRouterModel\": \"OpenRouter模型\",\n    \"useOpenai\": \"使用Openai API\",\n    \"openaiModel\": \"Openai 模型\",\n    \"useCustomModel\": \"使用自定义模型\",\n    \"customModelPlaceholder\": \"输入自定义模型名称\",\n    \"addTokens\": \"+ 添加私有仓库访问令牌\",\n    \"hideTokens\": \"- 隐藏访问令牌\",\n    \"accessToken\": \"私有仓库访问令牌\",\n    \"selectPlatform\": \"选择平台\",\n    \"personalAccessToken\": \"{platform}个人访问令牌\",\n    \"tokenPlaceholder\": \"输入您的{platform}令牌\",\n    \"tokenSecurityNote\": \"令牌仅存储在内存中，从不持久化。\",\n    \"defaultFiltersInfo\": \"默认过滤器包括node_modules、.git和常见的构建文件。\",\n    \"fileFilterTitle\": \"文件过滤配置\",\n    \"advancedOptions\": \"高级选项\",\n    \"viewDefaults\": \"查看默认过滤\",\n    \"showFilters\": \"显示过滤器\",\n    \"hideFilters\": \"隐藏过滤器\",\n    \"excludedDirs\": \"要排除的目录\",\n    \"excludedDirsHelp\": \"每行一个目录路径。以./开头表示相对于仓库根目录的路径。\",\n    \"enterExcludedDirs\": \"输入要排除的目录，每行一个...\",\n    \"excludedFiles\": \"要排除的文件\",\n    \"excludedFilesHelp\": \"每行一个文件名。支持通配符(*)。\",\n    \"enterExcludedFiles\": \"输入要排除的文件，每行一个...\",\n    \"defaultFilters\": \"默认排除的文件和目录\",\n    \"directories\": \"目录\",\n    \"files\": \"文件\",\n    \"scrollToViewMore\": \"可滑动查看更多\",\n    \"changeModel\": \"修改模型\",\n    \"defaultNote\": \"这些默认配置已经被应用。请在上方添加您的自定义排除项。\",\n    \"hideDefault\": \"隐藏默认配置\",\n    \"viewDefault\": \"查看默认配置\",\n    \"authorizationCode\": \"授权码\",\n    \"authorizationRequired\": \"生成wiki页面需要填写授权码\"\n  },\n  \"footer\": {\n    \"copyright\": \"DeepWiki - 为代码仓库提供AI驱动的文档\"\n  },\n  \"ask\": {\n    \"placeholder\": \"询问关于此仓库的问题...\",\n    \"askButton\": \"提问\",\n    \"deepResearch\": \"深度研究\",\n    \"researchInProgress\": \"研究进行中...\",\n    \"continueResearch\": \"继续研究\",\n    \"viewPlan\": \"查看计划\",\n    \"viewUpdates\": \"查看更新\",\n    \"viewConclusion\": \"查看结论\"\n  },\n  \"repoPage\": {\n    \"refreshWiki\": \"刷新Wiki\",\n    \"confirmRefresh\": \"确认刷新\",\n    \"cancel\": \"取消\",\n    \"home\": \"首页\",\n    \"errorTitle\": \"错误\",\n    \"errorMessageDefault\": \"请检查您的仓库是否存在且为公开仓库。有效格式为\\\"owner/repo\\\", \\\"https://github.com/owner/repo\\\", \\\"https://gitlab.com/owner/repo\\\", \\\"https://bitbucket.org/owner/repo\\\", 或本地文件夹路径，如\\\"C:\\\\\\\\path\\\\\\\\to\\\\\\\\folder\\\"或\\\"/path/to/folder\\\"。\",\n    \"embeddingErrorDefault\": \"这个错误与用于分析您的仓库的文件嵌入系统有关。请检查您的嵌入模型配置、API 密钥，并重试。如果问题持续存在，请考虑在模型设置中切换到不同的嵌入提供者。\",\n    \"backToHome\": \"返回首页\",\n    \"exportWiki\": \"导出Wiki\",\n    \"exportAsMarkdown\": \"导出为Markdown\",\n    \"exportAsJson\": \"导出为JSON\",\n    \"pages\": \"页面\",\n    \"relatedFiles\": \"相关文件：\",\n    \"relatedPages\": \"相关页面：\",\n    \"selectPagePrompt\": \"从导航中选择一个页面以查看其内容\",\n    \"askAboutRepo\": \"询问关于此仓库的问题\"\n  },\n  \"nav\": {\n    \"wikiProjects\": \"项目列表\"\n  },\n  \"projects\": {\n    \"title\": \"已处理的Wiki项目\",\n    \"searchPlaceholder\": \"按项目名称、所有者或仓库搜索...\",\n    \"noProjects\": \"服务器缓存中未找到项目。缓存可能为空或服务器遇到问题。\",\n    \"noSearchResults\": \"没有项目符合您的搜索条件。\",\n    \"processedOn\": \"处理时间:\",\n    \"loadingProjects\": \"正在加载项目...\",\n    \"errorLoading\": \"加载项目时出错:\",\n    \"backToHome\": \"返回首页\",\n    \"browseExisting\": \"浏览现有项目\",\n    \"existingProjects\": \"现有项目\",\n    \"recentProjects\": \"最近项目\"\n  }\n}\n"
  },
  {
    "path": "src/types/repoinfo.tsx",
    "content": "export interface RepoInfo {\n    owner: string;\n    repo: string;\n    type: string;\n    token: string | null;\n    localPath: string | null;\n    repoUrl: string | null;\n}\n\nexport default RepoInfo;"
  },
  {
    "path": "src/types/wiki/wikipage.tsx",
    "content": "// Wiki Interfaces\nexport interface WikiPage {\n  id: string;\n  title: string;\n  content: string;\n  filePaths: string[];\n  importance: 'high' | 'medium' | 'low';\n  relatedPages: string[];\n  // New fields for hierarchy\n  parentId?: string;\n  isSection?: boolean;\n  children?: string[]; // IDs of child pages\n}"
  },
  {
    "path": "src/types/wiki/wikistructure.tsx",
    "content": "import { WikiPage } from \"./wikipage\";\n\n/**\n * @fileoverview This file defines the structure of a wiki page and its sections.\n */\nexport interface WikiStructure {\n    id: string;\n    title: string;\n    description: string;\n    pages: WikiPage[];\n}"
  },
  {
    "path": "src/utils/getRepoUrl.tsx",
    "content": "import RepoInfo from \"@/types/repoinfo\";\n\nexport default function getRepoUrl(repoInfo: RepoInfo): string {\n  console.log('getRepoUrl', repoInfo);\n  if (repoInfo.type === 'local' && repoInfo.localPath) {\n    return repoInfo.localPath;\n  } else {\n    if(repoInfo.repoUrl) {\n      return repoInfo.repoUrl;\n    } else {\n      if(repoInfo.owner && repoInfo.repo) {\n        return \"http://example/\" + repoInfo.owner + \"/\" + repoInfo.repo;\n      }\n      return '';\n    }\n  }\n};"
  },
  {
    "path": "src/utils/urlDecoder.tsx",
    "content": "export function extractUrlDomain(input: string): string | null {\n    try {\n        const normalizedInput = input.startsWith('http') ? input : `https://${input}`;\n        const url = new URL(normalizedInput);\n        return `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`; // Inclut le protocole et le domaine\n    } catch {\n        return null; // Not a valid URL\n    }\n}\n\nexport function extractUrlPath(input: string): string | null {\n    try {\n        const normalizedInput = input.startsWith('http') ? input : `https://${input}`;\n        const url = new URL(normalizedInput);\n        return url.pathname.replace(/^\\/|\\/$/g, ''); // Remove leading and trailing slashes\n    } catch {\n        return null; // Not a valid URL\n    }\n}"
  },
  {
    "path": "src/utils/websocketClient.ts",
    "content": "/**\n * WebSocket client for chat completions\n * This replaces the HTTP streaming endpoint with a WebSocket connection\n */\n\n// Get the server base URL from environment or use default\nconst SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001';\n\n// Convert HTTP URL to WebSocket URL\nconst getWebSocketUrl = () => {\n  const baseUrl = SERVER_BASE_URL;\n  // Replace http:// with ws:// or https:// with wss://\n  const wsBaseUrl = baseUrl.replace(/^http/, 'ws');\n  return `${wsBaseUrl}/ws/chat`;\n};\n\nexport interface ChatMessage {\n  role: 'user' | 'assistant' | 'system';\n  content: string;\n}\n\nexport interface ChatCompletionRequest {\n  repo_url: string;\n  messages: ChatMessage[];\n  filePath?: string;\n  token?: string;\n  type?: string;\n  provider?: string;\n  model?: string;\n  language?: string;\n  excluded_dirs?: string;\n  excluded_files?: string;\n}\n\n/**\n * Creates a WebSocket connection for chat completions\n * @param request The chat completion request\n * @param onMessage Callback for received messages\n * @param onError Callback for errors\n * @param onClose Callback for when the connection closes\n * @returns The WebSocket connection\n */\nexport const createChatWebSocket = (\n  request: ChatCompletionRequest,\n  onMessage: (message: string) => void,\n  onError: (error: Event) => void,\n  onClose: () => void\n): WebSocket => {\n  // Create WebSocket connection\n  const ws = new WebSocket(getWebSocketUrl());\n  \n  // Set up event handlers\n  ws.onopen = () => {\n    console.log('WebSocket connection established');\n    // Send the request as JSON\n    ws.send(JSON.stringify(request));\n  };\n  \n  ws.onmessage = (event) => {\n    // Call the message handler with the received text\n    onMessage(event.data);\n  };\n  \n  ws.onerror = (error) => {\n    console.error('WebSocket error:', error);\n    onError(error);\n  };\n  \n  ws.onclose = () => {\n    console.log('WebSocket connection closed');\n    onClose();\n  };\n  \n  return ws;\n};\n\n/**\n * Closes a WebSocket connection\n * @param ws The WebSocket connection to close\n */\nexport const closeWebSocket = (ws: WebSocket | null): void => {\n  if (ws && ws.readyState === WebSocket.OPEN) {\n    ws.close();\n  }\n};\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "module.exports = {\n  darkMode: 'selector',\n  content: [\n    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',\n    './src/components/**/*.{js,ts,jsx,tsx,mdx}',\n    './src/app/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n}"
  },
  {
    "path": "test/__init__.py",
    "content": "# Test package for deepwiki-open data pipeline\n"
  },
  {
    "path": "test/test_extract_repo_name.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nFocused test script for the _extract_repo_name_from_url method\n\nRun this script to test only the repository name extraction functionality.\nUsage: python test_extract_repo_name.py\n\"\"\"\n\nimport pytest\nimport os\nimport sys\nfrom unittest.mock import Mock, patch\n\n# Add the parent directory to the path to import the data_pipeline module\nsys.path.append(os.path.join(os.path.dirname(__file__), '..'))\n\n# Import the modules under test\nfrom api.data_pipeline import DatabaseManager\n\n\nclass TestExtractRepoNameFromUrl:\n    \"\"\"Comprehensive tests for the _extract_repo_name_from_url method\"\"\"\n    \n    def setup_method(self):\n        \"\"\"Set up test fixtures before each test method.\"\"\"\n        self.db_manager = DatabaseManager()\n    \n    def test_extract_repo_name_github_standard_url(self):\n        \n        # Test standard GitHub URL\n        github_url = \"https://github.com/owner/repo\"\n        result = self.db_manager._extract_repo_name_from_url(github_url, \"github\")\n        assert result == \"owner_repo\"\n        \n        # Test GitHub URL with .git suffix\n        github_url_git = \"https://github.com/owner/repo.git\"\n        result = self.db_manager._extract_repo_name_from_url(github_url_git, \"github\")\n        assert result == \"owner_repo\"\n\n        # Test GitHub URL with trailing slash\n        github_url_slash = \"https://github.com/owner/repo/\"\n        result = self.db_manager._extract_repo_name_from_url(github_url_slash, \"github\")\n        assert result == \"owner_repo\"\n        \n        print(\"✓ GitHub URL tests passed\")\n    \n    def test_extract_repo_name_gitlab_urls(self):\n        \"\"\"Test repository name extraction from GitLab URLs\"\"\"\n        \n        # Test standard GitLab URL\n        gitlab_url = \"https://gitlab.com/owner/repo\"\n        result = self.db_manager._extract_repo_name_from_url(gitlab_url, \"gitlab\")\n        assert result == \"owner_repo\"\n        \n        # Test GitLab URL with subgroups\n        gitlab_subgroup = \"https://gitlab.com/group/subgroup/repo\"\n        result = self.db_manager._extract_repo_name_from_url(gitlab_subgroup, \"gitlab\")\n        assert result == \"subgroup_repo\"\n        \n        print(\"✓ GitLab URL tests passed\")\n    \n    def test_extract_repo_name_bitbucket_urls(self):\n        \"\"\"Test repository name extraction from Bitbucket URLs\"\"\"\n        bitbucket_url = \"https://bitbucket.org/owner/repo\"\n        result = self.db_manager._extract_repo_name_from_url(bitbucket_url, \"bitbucket\")\n        assert result == \"owner_repo\"\n\n        print(\"✓ Bitbucket URL tests passed\")\n    \n    def test_extract_repo_name_local_paths(self):\n        \"\"\"Test repository name extraction from local paths\"\"\"\n        result = self.db_manager._extract_repo_name_from_url(\"/home/user/projects/my-repo\", \"local\")\n        assert result == \"my-repo\"\n\n        result = self.db_manager._extract_repo_name_from_url(\"/var/repos/project.git\", \"local\")\n        assert result == \"project\"\n\n        print(\"✓ Local path tests passed\")\n\n    def test_extract_repo_name_current_implementation_bug(self):\n        \"\"\"Test that demonstrates the current implementation bug\"\"\"\n        # The current implementation references 'type' which is not in scope\n        try:\n            # This should raise a NameError due to undefined 'type' variable\n            result = self.db_manager._extract_repo_name_from_url(\"https://github.com/owner/repo\")\n            print(\"⚠️  WARNING: Expected the current implementation to fail due to undefined 'type' variable\")\n            print(f\"    But got result: {result}\")\n        except (NameError, TypeError) as e:\n            print(f\"✓ Current implementation correctly fails with: {type(e).__name__}: {e}\")\n        except Exception as e:\n            print(f\"⚠️  Unexpected error: {type(e).__name__}: {e}\")\n        \n        # Test absolute local path\n        local_path = \"/home/user/projects/my-repo\"\n        result = self.db_manager._extract_repo_name_from_url(local_path, \"local\")\n        assert result == \"my-repo\"\n        \n        # Test local path with .git suffix\n        local_git = \"/var/repos/project.git\"\n        result = self.db_manager._extract_repo_name_from_url(local_git, \"local\")\n        assert result == \"project\"\n        \n        print(\"✓ Local path tests passed\")\n    \n    def test_extract_repo_name_edge_cases(self):\n        \"\"\"Test edge cases for repository name extraction\"\"\"\n        \n        # Test URL with insufficient parts (should use fallback)\n        short_url = \"https://github.com/repo\"\n        result = self.db_manager._extract_repo_name_from_url(short_url, \"github\")\n        assert result == \"repo\"\n        \n        # Test single directory name\n        single_name = \"my-repo\"\n        result = self.db_manager._extract_repo_name_from_url(single_name, \"local\")\n        assert result == \"my-repo\"\n        \n        print(\"✓ Edge case tests passed\")\n"
  },
  {
    "path": "tests/README.md",
    "content": "# DeepWiki Tests\n\nThis directory contains all tests for the DeepWiki project, organized by type and scope.\n\n## Directory Structure\n\n```\ntests/\n├── unit/                 # Unit tests - test individual components in isolation\n│   ├── test_google_embedder.py          # Tests for Google AI embedder client\n│   └── test_google_embedder_fix.py      # Tests for embedding response parsing fix\n├── integration/          # Integration tests - test component interactions\n│   └── test_full_integration.py         # Full pipeline integration test\n├── api/                  # API tests - test HTTP endpoints\n│   └── test_api.py                      # API endpoint tests\n└── run_tests.py         # Test runner script\n```\n\n## Running Tests\n\n### All Tests\n```bash\npython tests/run_tests.py\n```\n\n### Unit Tests Only\n```bash\npython tests/run_tests.py --unit\n```\n\n### Integration Tests Only\n```bash\npython tests/run_tests.py --integration\n```\n\n### API Tests Only\n```bash\npython tests/run_tests.py --api\n```\n\n### Individual Test Files\n```bash\n# Unit tests\npython tests/unit/test_google_embedder.py\npython tests/unit/test_google_embedder_fix.py\n\n# Integration tests\npython tests/integration/test_full_integration.py\n\n# API tests\npython tests/api/test_api.py\n```\n\n## Test Requirements\n\n### Environment Variables\n- `GOOGLE_API_KEY`: Required for Google AI embedder tests\n- `OPENAI_API_KEY`: Required for some integration tests\n- `DEEPWIKI_EMBEDDER_TYPE`: Set to 'google' for Google embedder tests\n\n### Dependencies\nAll test dependencies are included in the main project requirements:\n- `python-dotenv`: For loading environment variables\n- `adalflow`: Core framework for embeddings\n- `google-generativeai`: Google AI API client\n- `requests`: For API testing\n\n## Test Categories\n\n### Unit Tests\n- **Purpose**: Test individual components in isolation\n- **Speed**: Fast (< 1 second per test)\n- **Dependencies**: Minimal external dependencies\n- **Examples**: Testing embedder response parsing, configuration loading\n\n### Integration Tests  \n- **Purpose**: Test how components work together\n- **Speed**: Medium (1-10 seconds per test)\n- **Dependencies**: May require API keys and external services\n- **Examples**: End-to-end embedding pipeline, RAG workflow\n\n### API Tests\n- **Purpose**: Test HTTP endpoints and WebSocket connections\n- **Speed**: Medium-slow (5-30 seconds per test)\n- **Dependencies**: Requires running API server\n- **Examples**: Chat completion endpoints, streaming responses\n\n## Adding New Tests\n\n1. **Choose the right category**: Determine if your test is unit, integration, or API\n2. **Create the test file**: Place it in the appropriate subdirectory\n3. **Follow naming convention**: `test_<component_name>.py`\n4. **Add proper imports**: Use the project root path setup pattern\n5. **Document the test**: Add docstrings explaining what the test does\n6. **Update this README**: Add your test to the appropriate section\n\n## Troubleshooting\n\n### Import Errors\nIf you get import errors, ensure the test file includes the project root path setup:\n\n```python\nfrom pathlib import Path\nimport sys\n\n# Add the project root to the Python path\nproject_root = Path(__file__).parent.parent.parent\nsys.path.insert(0, str(project_root))\n```\n\n### API Key Issues\nMake sure you have a `.env` file in the project root with the required API keys:\n\n```\nGOOGLE_API_KEY=your_google_api_key_here\nOPENAI_API_KEY=your_openai_api_key_here\nDEEPWIKI_EMBEDDER_TYPE=google\n```\n\n### Server Dependencies\nFor API tests, ensure the FastAPI server is running on the expected port:\n\n```bash\ncd api\npython main.py\n```"
  },
  {
    "path": "tests/__init__.py",
    "content": "# Tests for DeepWiki"
  },
  {
    "path": "tests/api/__init__.py",
    "content": "# API tests"
  },
  {
    "path": "tests/api/test_api.py",
    "content": "import requests\nimport json\nimport sys\n\ndef test_streaming_endpoint(repo_url, query, file_path=None):\n    \"\"\"\n    Test the streaming endpoint with a given repository URL and query.\n    \n    Args:\n        repo_url (str): The GitHub repository URL\n        query (str): The query to send\n        file_path (str, optional): Path to a file in the repository\n    \"\"\"\n    # Define the API endpoint\n    url = \"http://localhost:8000/chat/completions/stream\"\n    \n    # Define the request payload\n    payload = {\n        \"repo_url\": repo_url,\n        \"messages\": [\n            {\n                \"role\": \"user\",\n                \"content\": query\n            }\n        ],\n        \"filePath\": file_path\n    }\n    \n    print(f\"Testing streaming endpoint with:\")\n    print(f\"  Repository: {repo_url}\")\n    print(f\"  Query: {query}\")\n    if file_path:\n        print(f\"  File Path: {file_path}\")\n    print(\"\\nResponse:\")\n    \n    try:\n        # Make the request with streaming enabled\n        response = requests.post(url, json=payload, stream=True)\n        \n        # Check if the request was successful\n        if response.status_code != 200:\n            print(f\"Error: {response.status_code}\")\n            try:\n                error_data = json.loads(response.content)\n                print(f\"Error details: {error_data.get('detail', 'Unknown error')}\")\n            except:\n                print(f\"Error content: {response.content}\")\n            return\n        \n        # Process the streaming response\n        for chunk in response.iter_content(chunk_size=None):\n            if chunk:\n                print(chunk.decode('utf-8'), end='', flush=True)\n        \n        print(\"\\n\\nStreaming completed successfully.\")\n    \n    except Exception as e:\n        print(f\"Error: {str(e)}\")\n\nif __name__ == \"__main__\":\n    # Get command line arguments\n    if len(sys.argv) < 3:\n        print(\"Usage: python test_api.py <repo_url> <query> [file_path]\")\n        sys.exit(1)\n    \n    repo_url = sys.argv[1]\n    query = sys.argv[2]\n    file_path = sys.argv[3] if len(sys.argv) > 3 else None\n    \n    test_streaming_endpoint(repo_url, query, file_path)\n"
  },
  {
    "path": "tests/integration/__init__.py",
    "content": "# Integration tests"
  },
  {
    "path": "tests/integration/test_full_integration.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Full integration test for Google AI embeddings.\"\"\"\n\nimport os\nimport sys\nimport json\nfrom pathlib import Path\n\n# Add the project root to the Python path\nproject_root = Path(__file__).parent.parent.parent\nsys.path.insert(0, str(project_root))\n\ndef test_config_loading():\n    \"\"\"Test that configurations load properly.\"\"\"\n    print(\"🔧 Testing configuration loading...\")\n    \n    try:\n        from api.config import configs, CLIENT_CLASSES\n        \n        # Check if Google embedder config exists\n        if 'embedder_google' in configs:\n            print(\"✅ embedder_google configuration found\")\n            google_config = configs['embedder_google']\n            print(f\"📋 Google config: {json.dumps(google_config, indent=2, default=str)}\")\n        else:\n            print(\"❌ embedder_google configuration not found\")\n            return False\n            \n        # Check if GoogleEmbedderClient is in CLIENT_CLASSES\n        if 'GoogleEmbedderClient' in CLIENT_CLASSES:\n            print(\"✅ GoogleEmbedderClient found in CLIENT_CLASSES\")\n        else:\n            print(\"❌ GoogleEmbedderClient not found in CLIENT_CLASSES\")\n            return False\n            \n        return True\n        \n    except Exception as e:\n        print(f\"❌ Error loading configuration: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\ndef test_embedder_selection():\n    \"\"\"Test embedder selection mechanism.\"\"\"\n    print(\"\\n🔧 Testing embedder selection...\")\n    \n    try:\n        from api.tools.embedder import get_embedder\n        from api.config import get_embedder_type, is_google_embedder\n        \n        # Test default embedder type\n        current_type = get_embedder_type()\n        print(f\"📋 Current embedder type: {current_type}\")\n        \n        # Test is_google_embedder function\n        is_google = is_google_embedder()\n        print(f\"📋 Is Google embedder: {is_google}\")\n        \n        # Test get_embedder with google type\n        print(\"🧪 Testing get_embedder with embedder_type='google'...\")\n        embedder = get_embedder(embedder_type='google')\n        print(f\"✅ Google embedder created: {type(embedder)}\")\n        \n        return True\n        \n    except Exception as e:\n        print(f\"❌ Error testing embedder selection: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\ndef test_google_embedder_with_env():\n    \"\"\"Test Google embedder with environment variable.\"\"\"\n    print(\"\\n🔧 Testing with DEEPWIKI_EMBEDDER_TYPE=google...\")\n    \n    # Set environment variable\n    original_value = os.environ.get('DEEPWIKI_EMBEDDER_TYPE')\n    os.environ['DEEPWIKI_EMBEDDER_TYPE'] = 'google'\n    \n    try:\n        # Reload config module to pick up new env var\n        import importlib\n        import api.config\n        importlib.reload(api.config)\n        \n        from api.config import EMBEDDER_TYPE, get_embedder_type, get_embedder_config\n        from api.tools.embedder import get_embedder\n        \n        print(f\"📋 EMBEDDER_TYPE: {EMBEDDER_TYPE}\")\n        print(f\"📋 get_embedder_type(): {get_embedder_type()}\")\n        \n        # Test getting embedder config\n        config = get_embedder_config()\n        print(f\"📋 Current embedder config client: {config.get('client_class', 'Unknown')}\")\n        \n        # Test creating embedder\n        embedder = get_embedder()\n        print(f\"✅ Embedder created with google env var: {type(embedder)}\")\n        \n        return True\n        \n    except Exception as e:\n        print(f\"❌ Error testing with environment variable: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n        \n    finally:\n        # Restore original environment variable\n        if original_value is not None:\n            os.environ['DEEPWIKI_EMBEDDER_TYPE'] = original_value\n        elif 'DEEPWIKI_EMBEDDER_TYPE' in os.environ:\n            del os.environ['DEEPWIKI_EMBEDDER_TYPE']\n\ndef main():\n    \"\"\"Run all integration tests.\"\"\"\n    print(\"🚀 Starting Google AI Embeddings Integration Tests\")\n    print(\"=\" * 60)\n    \n    tests = [\n        test_config_loading,\n        test_embedder_selection,\n        test_google_embedder_with_env,\n    ]\n    \n    passed = 0\n    total = len(tests)\n    \n    for test in tests:\n        try:\n            if test():\n                passed += 1\n                print(\"✅ PASSED\")\n            else:\n                print(\"❌ FAILED\")\n        except Exception as e:\n            print(f\"❌ FAILED with exception: {e}\")\n        print(\"-\" * 40)\n    \n    print(f\"\\n📊 Test Results: {passed}/{total} tests passed\")\n    \n    if passed == total:\n        print(\"🎉 All integration tests passed!\")\n        return True\n    else:\n        print(\"💥 Some tests failed!\")\n        return False\n\nif __name__ == \"__main__\":\n    success = main()\n    sys.exit(0 if success else 1)"
  },
  {
    "path": "tests/run_tests.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nTest runner for DeepWiki project.\n\nThis script provides a unified way to run all tests or specific test categories.\n\"\"\"\n\nimport os\nimport sys\nimport argparse\nimport subprocess\nfrom pathlib import Path\n\n# Add the project root to the Python path\nproject_root = Path(__file__).parent.parent\nsys.path.insert(0, str(project_root))\n\ndef run_test_file(test_file):\n    \"\"\"Run a single test file and return success status.\"\"\"\n    print(f\"\\n🧪 Running {test_file}...\")\n    try:\n        result = subprocess.run([sys.executable, str(test_file)], \n                              capture_output=True, text=True, cwd=project_root)\n        \n        if result.returncode == 0:\n            print(f\"✅ {test_file.name} - PASSED\")\n            if result.stdout:\n                print(f\"📄 Output:\\n{result.stdout}\")\n            return True\n        else:\n            print(f\"❌ {test_file.name} - FAILED\")\n            if result.stderr:\n                print(f\"💥 Error:\\n{result.stderr}\")\n            if result.stdout:\n                print(f\"📄 Output:\\n{result.stdout}\")\n            return False\n    except Exception as e:\n        print(f\"💥 {test_file.name} - ERROR: {e}\")\n        return False\n\ndef run_tests(test_dirs):\n    \"\"\"Run all tests in the specified directories.\"\"\"\n    total_tests = 0\n    passed_tests = 0\n    failed_tests = []\n    \n    for test_dir in test_dirs:\n        test_path = Path(__file__).parent / test_dir\n        if not test_path.exists():\n            print(f\"⚠️  Warning: Test directory {test_dir} not found\")\n            continue\n            \n        test_files = list(test_path.glob(\"test_*.py\"))\n        if not test_files:\n            print(f\"⚠️  No test files found in {test_dir}\")\n            continue\n            \n        print(f\"\\n📁 Running {test_dir} tests...\")\n        for test_file in sorted(test_files):\n            total_tests += 1\n            if run_test_file(test_file):\n                passed_tests += 1\n            else:\n                failed_tests.append(str(test_file))\n    \n    # Print summary\n    print(f\"\\n{'='*50}\")\n    print(f\"📊 TEST SUMMARY\")\n    print(f\"{'='*50}\")\n    print(f\"Total tests: {total_tests}\")\n    print(f\"Passed: {passed_tests}\")\n    print(f\"Failed: {len(failed_tests)}\")\n    \n    if failed_tests:\n        print(f\"\\n❌ Failed tests:\")\n        for test in failed_tests:\n            print(f\"  - {test}\")\n        print(f\"\\n💡 Tip: Run individual failed tests for more details\")\n        return False\n    else:\n        print(f\"\\n🎉 All tests passed!\")\n        return True\n\ndef check_environment():\n    \"\"\"Check if required environment variables and dependencies are available.\"\"\"\n    print(\"🔧 Checking test environment...\")\n    \n    # Check for .env file\n    env_file = project_root / \".env\"\n    if env_file.exists():\n        print(\"✅ .env file found\")\n        from dotenv import load_dotenv\n        load_dotenv(env_file)\n    else:\n        print(\"⚠️  No .env file found - some tests may fail without API keys\")\n    \n    # Check for API keys\n    api_keys = {\n        \"GOOGLE_API_KEY\": \"Google AI embedder tests\",\n        \"OPENAI_API_KEY\": \"OpenAI integration tests\"\n    }\n    \n    for key, purpose in api_keys.items():\n        if os.getenv(key):\n            print(f\"✅ {key} is set ({purpose})\")\n        else:\n            print(f\"⚠️  {key} not set - {purpose} may fail\")\n    \n    # Check Python dependencies\n    try:\n        import adalflow\n        print(\"✅ adalflow available\")\n    except ImportError:\n        print(\"❌ adalflow not available - install with: pip install adalflow\")\n    \n    try:\n        import google.generativeai\n        print(\"✅ google-generativeai available\")\n    except ImportError:\n        print(\"❌ google-generativeai not available - install with: pip install google-generativeai\")\n    \n    try:\n        import requests\n        print(\"✅ requests available\")\n    except ImportError:\n        print(\"❌ requests not available - install with: pip install requests\")\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Run DeepWiki tests\")\n    parser.add_argument(\"--unit\", action=\"store_true\", help=\"Run only unit tests\")\n    parser.add_argument(\"--integration\", action=\"store_true\", help=\"Run only integration tests\")\n    parser.add_argument(\"--api\", action=\"store_true\", help=\"Run only API tests\")\n    parser.add_argument(\"--check-env\", action=\"store_true\", help=\"Only check environment setup\")\n    parser.add_argument(\"--verbose\", \"-v\", action=\"store_true\", help=\"Verbose output\")\n    \n    args = parser.parse_args()\n    \n    # Check environment first\n    check_environment()\n    \n    if args.check_env:\n        return\n    \n    # Determine which tests to run\n    test_dirs = []\n    if args.unit:\n        test_dirs.append(\"unit\")\n    if args.integration:\n        test_dirs.append(\"integration\")\n    if args.api:\n        test_dirs.append(\"api\")\n    \n    # If no specific category selected, run all\n    if not test_dirs:\n        test_dirs = [\"unit\", \"integration\", \"api\"]\n    \n    print(f\"\\n🚀 Starting test run for: {', '.join(test_dirs)}\")\n    \n    success = run_tests(test_dirs)\n    sys.exit(0 if success else 1)\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "tests/unit/__init__.py",
    "content": "# Unit tests"
  },
  {
    "path": "tests/unit/test_all_embedders.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nComprehensive test suite for all embedder types (OpenAI, Google, Ollama).\nThis test file validates the embedder system before any modifications are made.\n\"\"\"\n\nimport os\nimport sys\nimport logging\nfrom pathlib import Path\nfrom unittest.mock import patch, MagicMock\n\n# Add the project root to the Python path\nproject_root = Path(__file__).parent.parent.parent\nsys.path.insert(0, str(project_root))\n\n# Set up environment\nfrom dotenv import load_dotenv\nload_dotenv()\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)\n\n# Simple test framework without pytest\nclass TestRunner:\n    def __init__(self):\n        self.tests_run = 0\n        self.tests_passed = 0\n        self.tests_failed = 0\n        self.failures = []\n    \n    def run_test(self, test_func, test_name=None):\n        \"\"\"Run a single test function.\"\"\"\n        if test_name is None:\n            test_name = test_func.__name__\n        \n        self.tests_run += 1\n        try:\n            logger.info(f\"Running test: {test_name}\")\n            test_func()\n            self.tests_passed += 1\n            logger.info(f\"✅ {test_name} PASSED\")\n            return True\n        except Exception as e:\n            self.tests_failed += 1\n            self.failures.append((test_name, str(e)))\n            logger.error(f\"❌ {test_name} FAILED: {e}\")\n            return False\n    \n    def run_test_class(self, test_class):\n        \"\"\"Run all test methods in a test class.\"\"\"\n        instance = test_class()\n        test_methods = [getattr(instance, method) for method in dir(instance) \n                       if method.startswith('test_') and callable(getattr(instance, method))]\n        \n        for test_method in test_methods:\n            test_name = f\"{test_class.__name__}.{test_method.__name__}\"\n            self.run_test(test_method, test_name)\n    \n    def run_parametrized_test(self, test_func, parameters, test_name_base=None):\n        \"\"\"Run a test function with multiple parameter sets.\"\"\"\n        if test_name_base is None:\n            test_name_base = test_func.__name__\n        \n        for i, param in enumerate(parameters):\n            test_name = f\"{test_name_base}[{param}]\"\n            self.run_test(lambda: test_func(param), test_name)\n    \n    def summary(self):\n        \"\"\"Print test summary.\"\"\"\n        logger.info(f\"\\n📊 Test Summary:\")\n        logger.info(f\"Tests run: {self.tests_run}\")\n        logger.info(f\"Passed: {self.tests_passed}\")\n        logger.info(f\"Failed: {self.tests_failed}\")\n        \n        if self.failures:\n            logger.error(\"\\n❌ Failed tests:\")\n            for test_name, error in self.failures:\n                logger.error(f\"  - {test_name}: {error}\")\n        \n        return self.tests_failed == 0\n\nclass TestEmbedderConfiguration:\n    \"\"\"Test embedder configuration system.\"\"\"\n    \n    def test_config_loading(self):\n        \"\"\"Test that all embedder configurations load properly.\"\"\"\n        from api.config import configs, CLIENT_CLASSES\n        \n        # Check all embedder configurations exist\n        assert 'embedder' in configs, \"OpenAI embedder config missing\"\n        assert 'embedder_google' in configs, \"Google embedder config missing\"\n        assert 'embedder_ollama' in configs, \"Ollama embedder config missing\"\n        assert 'embedder_bedrock' in configs, \"Bedrock embedder config missing\"\n        \n        # Check client classes are available\n        assert 'OpenAIClient' in CLIENT_CLASSES, \"OpenAIClient missing from CLIENT_CLASSES\"\n        assert 'GoogleEmbedderClient' in CLIENT_CLASSES, \"GoogleEmbedderClient missing from CLIENT_CLASSES\"\n        assert 'OllamaClient' in CLIENT_CLASSES, \"OllamaClient missing from CLIENT_CLASSES\"\n        assert 'BedrockClient' in CLIENT_CLASSES, \"BedrockClient missing from CLIENT_CLASSES\"\n    \n    def test_embedder_type_detection(self):\n        \"\"\"Test embedder type detection functions.\"\"\"\n        from api.config import get_embedder_type, is_ollama_embedder, is_google_embedder, is_bedrock_embedder\n        \n        # Default type should be detected\n        current_type = get_embedder_type()\n        assert current_type in ['openai', 'google', 'ollama', 'bedrock'], f\"Invalid embedder type: {current_type}\"\n        \n        # Boolean functions should work\n        is_ollama = is_ollama_embedder()\n        is_google = is_google_embedder()\n        is_bedrock = is_bedrock_embedder()\n        assert isinstance(is_ollama, bool), \"is_ollama_embedder should return boolean\"\n        assert isinstance(is_google, bool), \"is_google_embedder should return boolean\"\n        assert isinstance(is_bedrock, bool), \"is_bedrock_embedder should return boolean\"\n        \n        # Only one should be true at a time (unless using openai default)\n        if current_type == 'bedrock':\n            assert is_bedrock and not is_ollama and not is_google\n        elif current_type == 'ollama':\n            assert is_ollama and not is_google and not is_bedrock\n        elif current_type == 'google':\n            assert is_google and not is_ollama and not is_bedrock\n        else:  # openai\n            assert not is_ollama and not is_google and not is_bedrock\n\n    def test_get_embedder_config(self, embedder_type=None):\n        \"\"\"Test getting embedder config for each type.\"\"\"\n        from api.config import get_embedder_config\n        \n        if embedder_type:\n            # Mock the EMBEDDER_TYPE for testing\n            with patch('api.config.EMBEDDER_TYPE', embedder_type):\n                config = get_embedder_config()\n                assert isinstance(config, dict), f\"Config for {embedder_type} should be dict\"\n                assert 'model_client' in config or 'client_class' in config, f\"No client specified for {embedder_type}\"\n        else:\n            # Test current configuration\n            config = get_embedder_config()\n            assert isinstance(config, dict), \"Config should be dict\"\n            assert 'model_client' in config or 'client_class' in config, \"No client specified\"\n\n\nclass TestEmbedderFactory:\n    \"\"\"Test the embedder factory function.\"\"\"\n    \n    def test_get_embedder_with_explicit_type(self):\n        \"\"\"Test get_embedder with explicit embedder_type parameter.\"\"\"\n        from api.tools.embedder import get_embedder\n        \n        # Test Google embedder\n        google_embedder = get_embedder(embedder_type='google')\n        assert google_embedder is not None, \"Google embedder should be created\"\n\n        # Test Bedrock embedder (mock boto3 to avoid hitting AWS credential providers)\n        with patch(\"api.bedrock_client.boto3.Session\") as mock_session_cls:\n            mock_session = MagicMock()\n            mock_session.client.return_value = MagicMock()\n            mock_session_cls.return_value = mock_session\n            bedrock_embedder = get_embedder(embedder_type='bedrock')\n            assert bedrock_embedder is not None, \"Bedrock embedder should be created\"\n        \n        # Test OpenAI embedder\n        openai_embedder = get_embedder(embedder_type='openai')\n        assert openai_embedder is not None, \"OpenAI embedder should be created\"\n        \n        # Test Ollama embedder (may fail if Ollama not available, but should not crash)\n        try:\n            ollama_embedder = get_embedder(embedder_type='ollama')\n            assert ollama_embedder is not None, \"Ollama embedder should be created\"\n        except Exception as e:\n            logger.warning(f\"Ollama embedder creation failed (expected if Ollama not available): {e}\")\n\n    def test_get_embedder_with_legacy_params(self):\n        \"\"\"Test get_embedder with legacy boolean parameters.\"\"\"\n        from api.tools.embedder import get_embedder\n        \n        # Test with use_google_embedder=True\n        google_embedder = get_embedder(use_google_embedder=True)\n        assert google_embedder is not None, \"Google embedder should be created with use_google_embedder=True\"\n        \n        # Test with is_local_ollama=True\n        try:\n            ollama_embedder = get_embedder(is_local_ollama=True)\n            assert ollama_embedder is not None, \"Ollama embedder should be created with is_local_ollama=True\"\n        except Exception as e:\n            logger.warning(f\"Ollama embedder creation failed (expected if Ollama not available): {e}\")\n\n    def test_get_embedder_auto_detection(self):\n        \"\"\"Test get_embedder with automatic type detection.\"\"\"\n        from api.tools.embedder import get_embedder\n        \n        # Test auto-detection (should use current configuration)\n        embedder = get_embedder()\n        assert embedder is not None, \"Auto-detected embedder should be created\"\n\n\nclass TestEmbedderClients:\n    \"\"\"Test individual embedder clients.\"\"\"\n\n    def test_google_embedder_client(self):\n        \"\"\"Test Google embedder client directly.\"\"\"\n        if not os.getenv('GOOGLE_API_KEY'):\n            logger.warning(\"Skipping Google embedder test - GOOGLE_API_KEY not available\")\n            return\n            \n        from api.google_embedder_client import GoogleEmbedderClient\n        from adalflow.core.types import ModelType\n        \n        client = GoogleEmbedderClient()\n        \n        # Test single embedding\n        api_kwargs = client.convert_inputs_to_api_kwargs(\n            input=\"Hello world\",\n            model_kwargs={\"model\": \"text-embedding-004\", \"task_type\": \"SEMANTIC_SIMILARITY\"},\n            model_type=ModelType.EMBEDDER\n        )\n        \n        response = client.call(api_kwargs, ModelType.EMBEDDER)\n        assert response is not None, \"Google embedder should return response\"\n        \n        # Parse the response\n        parsed = client.parse_embedding_response(response)\n        assert parsed.data is not None, \"Parsed response should have data\"\n        assert len(parsed.data) > 0, \"Should have at least one embedding\"\n        assert parsed.error is None, \"Should not have errors\"\n\n    def test_openai_embedder_via_adalflow(self):\n        \"\"\"Test OpenAI embedder through AdalFlow.\"\"\"\n        if not os.getenv('OPENAI_API_KEY'):\n            logger.warning(\"Skipping OpenAI embedder test - OPENAI_API_KEY not available\")\n            return\n            \n        import adalflow as adal\n        from api.openai_client import OpenAIClient\n        \n        client = OpenAIClient()\n        embedder = adal.Embedder(\n            model_client=client,\n            model_kwargs={\"model\": \"text-embedding-3-small\", \"dimensions\": 256}\n        )\n        \n        result = embedder(\"Hello world\")\n        assert result is not None, \"OpenAI embedder should return result\"\n        assert hasattr(result, 'data'), \"Result should have data attribute\"\n        assert len(result.data) > 0, \"Should have at least one embedding\"\n\n\nclass TestDataPipelineFunctions:\n    \"\"\"Test data pipeline functions that use embedders.\"\"\"\n    \n    def test_count_tokens(self, embedder_type=None):\n        \"\"\"Test token counting with different embedder types.\"\"\"\n        from api.data_pipeline import count_tokens\n        \n        test_text = \"This is a test string for token counting.\"\n        \n        if embedder_type is not None:\n            # Test with specific is_ollama_embedder value\n            token_count = count_tokens(test_text, is_ollama_embedder=embedder_type)\n            assert isinstance(token_count, int), \"Token count should be an integer\"\n            assert token_count > 0, \"Token count should be positive\"\n        else:\n            # Test with all values\n            for is_ollama in [None, True, False]:\n                token_count = count_tokens(test_text, is_ollama_embedder=is_ollama)\n                assert isinstance(token_count, int), \"Token count should be an integer\"\n                assert token_count > 0, \"Token count should be positive\"\n\n    def test_prepare_data_pipeline(self, is_ollama=None):\n        \"\"\"Test data pipeline preparation with different embedder types.\"\"\"\n        from api.data_pipeline import prepare_data_pipeline\n        \n        if is_ollama is not None:\n            try:\n                pipeline = prepare_data_pipeline(is_ollama_embedder=is_ollama)\n                assert pipeline is not None, \"Data pipeline should be created\"\n                assert hasattr(pipeline, '__call__'), \"Pipeline should be callable\"\n            except Exception as e:\n                # Some configurations might fail if services aren't available\n                logger.warning(f\"Pipeline creation failed (might be expected): {e}\")\n        else:\n            # Test with all values\n            for is_ollama_val in [None, True, False]:\n                try:\n                    pipeline = prepare_data_pipeline(is_ollama_embedder=is_ollama_val)\n                    assert pipeline is not None, \"Data pipeline should be created\"\n                    assert hasattr(pipeline, '__call__'), \"Pipeline should be callable\"\n                except Exception as e:\n                    logger.warning(f\"Pipeline creation failed for is_ollama={is_ollama_val}: {e}\")\n\n\nclass TestRAGIntegration:\n    \"\"\"Test RAG class integration with different embedders.\"\"\"\n    \n    def test_rag_initialization(self):\n        \"\"\"Test RAG initialization with different embedder configurations.\"\"\"\n        from api.rag import RAG\n        \n        # Test with default configuration\n        try:\n            rag = RAG(provider=\"google\", model=\"gemini-1.5-flash\")\n            assert rag is not None, \"RAG should be initialized\"\n            assert hasattr(rag, 'embedder'), \"RAG should have embedder\"\n            assert hasattr(rag, 'is_ollama_embedder'), \"RAG should have is_ollama_embedder attribute\"\n        except Exception as e:\n            logger.warning(f\"RAG initialization failed (might be expected if keys missing): {e}\")\n\n    def test_rag_embedder_type_detection(self):\n        \"\"\"Test that RAG correctly detects embedder type.\"\"\"\n        from api.rag import RAG\n        \n        try:\n            rag = RAG()\n            # Should have the embedder type detection logic\n            assert hasattr(rag, 'is_ollama_embedder'), \"RAG should detect embedder type\"\n            assert isinstance(rag.is_ollama_embedder, bool), \"is_ollama_embedder should be boolean\"\n        except Exception as e:\n            logger.warning(f\"RAG initialization failed: {e}\")\n\n\nclass TestEnvironmentVariableHandling:\n    \"\"\"Test embedder selection via environment variables.\"\"\"\n    \n    def test_embedder_type_env_var(self, embedder_type=None):\n        \"\"\"Test embedder selection via DEEPWIKI_EMBEDDER_TYPE environment variable.\"\"\"\n        import importlib\n        import api.config\n        \n        if embedder_type:\n            # Test specific embedder type\n            self._test_single_embedder_type(embedder_type)\n        else:\n            # Test all embedder types\n            for et in ['openai', 'google', 'ollama', 'bedrock']:\n                self._test_single_embedder_type(et)\n    \n    def _test_single_embedder_type(self, embedder_type):\n        \"\"\"Test a single embedder type.\"\"\"\n        import importlib\n        import api.config\n        \n        # Save original value\n        original_value = os.environ.get('DEEPWIKI_EMBEDDER_TYPE')\n        \n        try:\n            # Set environment variable\n            os.environ['DEEPWIKI_EMBEDDER_TYPE'] = embedder_type\n            \n            # Reload config to pick up new env var\n            importlib.reload(api.config)\n            \n            from api.config import EMBEDDER_TYPE, get_embedder_type\n            \n            assert EMBEDDER_TYPE == embedder_type, f\"EMBEDDER_TYPE should be {embedder_type}\"\n            assert get_embedder_type() == embedder_type, f\"get_embedder_type() should return {embedder_type}\"\n            \n        finally:\n            # Restore original value\n            if original_value is not None:\n                os.environ['DEEPWIKI_EMBEDDER_TYPE'] = original_value\n            elif 'DEEPWIKI_EMBEDDER_TYPE' in os.environ:\n                del os.environ['DEEPWIKI_EMBEDDER_TYPE']\n            \n            # Reload config to restore original state\n            importlib.reload(api.config)\n\n\nclass TestIssuesIdentified:\n    \"\"\"Test the specific issues identified in the codebase.\"\"\"\n    \n    def test_binary_assumptions_in_rag(self):\n        \"\"\"Test that RAG doesn't make binary assumptions about embedders.\"\"\"\n        from api.rag import RAG\n        \n        # The current implementation only considers is_ollama_embedder\n        # This test documents the current behavior and will help verify fixes\n        try:\n            rag = RAG()\n            \n            # Current implementation only has is_ollama_embedder\n            assert hasattr(rag, 'is_ollama_embedder'), \"RAG should have is_ollama_embedder\"\n            \n            # This is the issue: no explicit support for Google embedder detection\n            # The fix should add proper embedder type detection\n            \n        except Exception as e:\n            logger.warning(f\"RAG test failed: {e}\")\n\n    def test_binary_assumptions_in_data_pipeline(self):\n        \"\"\"Test binary assumptions in data pipeline functions.\"\"\"\n        from api.data_pipeline import prepare_data_pipeline, count_tokens\n        \n        # These functions currently only consider is_ollama_embedder parameter\n        # This test documents the issue and will verify fixes\n        \n        # count_tokens only considers ollama vs non-ollama\n        token_count_ollama = count_tokens(\"test\", is_ollama_embedder=True)\n        token_count_other = count_tokens(\"test\", is_ollama_embedder=False)\n        \n        assert isinstance(token_count_ollama, int)\n        assert isinstance(token_count_other, int)\n        \n        # prepare_data_pipeline only accepts is_ollama_embedder parameter\n        try:\n            pipeline_ollama = prepare_data_pipeline(is_ollama_embedder=True)\n            pipeline_other = prepare_data_pipeline(is_ollama_embedder=False)\n            \n            assert pipeline_ollama is not None\n            assert pipeline_other is not None\n        except Exception as e:\n            logger.warning(f\"Pipeline creation failed: {e}\")\n\n\ndef run_all_tests():\n    \"\"\"Run all tests and return results.\"\"\"\n    logger.info(\"Running comprehensive embedder tests...\")\n    \n    runner = TestRunner()\n    \n    # Test classes to run\n    test_classes = [\n        TestEmbedderConfiguration,\n        TestEmbedderFactory,\n        TestEmbedderClients,\n        TestDataPipelineFunctions,\n        TestRAGIntegration,\n        TestEnvironmentVariableHandling,\n        TestIssuesIdentified\n    ]\n    \n    # Run all test classes\n    for test_class in test_classes:\n        logger.info(f\"\\n🧪 Running {test_class.__name__}...\")\n        runner.run_test_class(test_class)\n    \n    # Run parametrized tests manually\n    logger.info(\"\\n🧪 Running parametrized tests...\")\n    \n    # Test embedder config with different types\n    config_test = TestEmbedderConfiguration()\n    for embedder_type in ['openai', 'google', 'ollama', 'bedrock']:\n        runner.run_test(\n            lambda et=embedder_type: config_test.test_get_embedder_config(et),\n            f\"TestEmbedderConfiguration.test_get_embedder_config[{embedder_type}]\"\n        )\n    \n    # Test token counting with different types\n    pipeline_test = TestDataPipelineFunctions()\n    for embedder_type in [None, True, False]:\n        runner.run_test(\n            lambda et=embedder_type: pipeline_test.test_count_tokens(et),\n            f\"TestDataPipelineFunctions.test_count_tokens[{embedder_type}]\"\n        )\n    \n    # Test pipeline preparation with different types\n    for is_ollama in [None, True, False]:\n        runner.run_test(\n            lambda ol=is_ollama: pipeline_test.test_prepare_data_pipeline(ol),\n            f\"TestDataPipelineFunctions.test_prepare_data_pipeline[{is_ollama}]\"\n        )\n    \n    # Test environment variable handling\n    env_test = TestEnvironmentVariableHandling()\n    for embedder_type in ['openai', 'google', 'ollama', 'bedrock']:\n        runner.run_test(\n            lambda et=embedder_type: env_test.test_embedder_type_env_var(et),\n            f\"TestEnvironmentVariableHandling.test_embedder_type_env_var[{embedder_type}]\"\n        )\n    \n    return runner.summary()\n\n\nif __name__ == \"__main__\":\n    success = run_all_tests()\n    sys.exit(0 if success else 1)\n"
  },
  {
    "path": "tests/unit/test_google_embedder.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nTest script to reproduce and fix Google embedder 'list' object has no attribute 'embedding' error.\n\"\"\"\n\nimport os\nimport sys\nimport logging\nfrom pathlib import Path\n\n# Add the project root to the Python path\nproject_root = Path(__file__).parent.parent.parent\nsys.path.insert(0, str(project_root))\n\n# Set up environment\nfrom dotenv import load_dotenv\nload_dotenv()\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)\n\ndef test_google_embedder_client():\n    \"\"\"Test the Google embedder client directly.\"\"\"\n    logger.info(\"Testing Google embedder client...\")\n    \n    try:\n        from api.google_embedder_client import GoogleEmbedderClient\n        from adalflow.core.types import ModelType\n        \n        # Initialize the client\n        client = GoogleEmbedderClient()\n        \n        # Test single embedding\n        logger.info(\"Testing single embedding...\")\n        api_kwargs = client.convert_inputs_to_api_kwargs(\n            input=\"Hello world\",\n            model_kwargs={\"model\": \"text-embedding-004\", \"task_type\": \"SEMANTIC_SIMILARITY\"},\n            model_type=ModelType.EMBEDDER\n        )\n        \n        response = client.call(api_kwargs, ModelType.EMBEDDER)\n        logger.info(f\"Single embedding response type: {type(response)}\")\n        logger.info(f\"Single embedding response keys: {list(response.keys()) if isinstance(response, dict) else 'Not a dict'}\")\n        \n        # Parse the response\n        parsed = client.parse_embedding_response(response)\n        logger.info(f\"Parsed response data length: {len(parsed.data) if parsed.data else 0}\")\n        logger.info(f\"Parsed response error: {parsed.error}\")\n        \n        # Test batch embedding\n        logger.info(\"Testing batch embedding...\")\n        api_kwargs = client.convert_inputs_to_api_kwargs(\n            input=[\"Hello world\", \"Test embedding\"],\n            model_kwargs={\"model\": \"text-embedding-004\", \"task_type\": \"SEMANTIC_SIMILARITY\"},\n            model_type=ModelType.EMBEDDER\n        )\n        \n        response = client.call(api_kwargs, ModelType.EMBEDDER)\n        logger.info(f\"Batch embedding response type: {type(response)}\")\n        logger.info(f\"Batch embedding response keys: {list(response.keys()) if isinstance(response, dict) else 'Not a dict'}\")\n        \n        # Parse the response\n        parsed = client.parse_embedding_response(response)\n        logger.info(f\"Parsed batch response data length: {len(parsed.data) if parsed.data else 0}\")\n        logger.info(f\"Parsed batch response error: {parsed.error}\")\n        \n        return True\n        \n    except Exception as e:\n        logger.error(f\"Error testing Google embedder client: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\ndef test_adalflow_embedder():\n    \"\"\"Test the AdalFlow embedder with Google client.\"\"\"\n    logger.info(\"Testing AdalFlow embedder with Google client...\")\n    \n    try:\n        import adalflow as adal\n        from api.google_embedder_client import GoogleEmbedderClient\n        \n        # Create embedder\n        client = GoogleEmbedderClient()\n        embedder = adal.Embedder(\n            model_client=client,\n            model_kwargs={\n                \"model\": \"text-embedding-004\",\n                \"task_type\": \"SEMANTIC_SIMILARITY\"\n            }\n        )\n        \n        # Test embedding\n        logger.info(\"Testing embedder with single input...\")\n        result = embedder(\"Hello world\")\n        logger.info(f\"Embedder result type: {type(result)}\")\n        logger.info(f\"Embedder result: {result}\")\n        \n        if hasattr(result, 'data'):\n            logger.info(f\"Result data length: {len(result.data) if result.data else 0}\")\n        \n        return True\n        \n    except Exception as e:\n        logger.error(f\"Error testing AdalFlow embedder: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\ndef test_document_processing():\n    \"\"\"Test document processing with Google embedder.\"\"\"\n    logger.info(\"Testing document processing with Google embedder...\")\n    \n    try:\n        from adalflow.core.types import Document\n        from adalflow.components.data_process import ToEmbeddings\n        from api.tools.embedder import get_embedder\n        \n        # Create some test documents\n        docs = [\n            Document(text=\"This is a test document.\", meta_data={\"file_path\": \"test1.txt\"}),\n            Document(text=\"Another test document here.\", meta_data={\"file_path\": \"test2.txt\"})\n        ]\n        \n        # Get the Google embedder\n        embedder = get_embedder(embedder_type='google')\n        logger.info(f\"Embedder type: {type(embedder)}\")\n        \n        # Process documents\n        embedder_transformer = ToEmbeddings(embedder=embedder, batch_size=100)\n        \n        # Transform documents\n        logger.info(\"Transforming documents...\")\n        transformed_docs = embedder_transformer(docs)\n        \n        logger.info(f\"Transformed docs type: {type(transformed_docs)}\")\n        logger.info(f\"Number of transformed docs: {len(transformed_docs)}\")\n        \n        # Check the structure\n        for i, doc in enumerate(transformed_docs):\n            logger.info(f\"Doc {i} type: {type(doc)}\")\n            logger.info(f\"Doc {i} attributes: {dir(doc)}\")\n            if hasattr(doc, 'vector'):\n                logger.info(f\"Doc {i} vector type: {type(doc.vector)}\")\n                logger.info(f\"Doc {i} vector length: {len(doc.vector) if doc.vector else 0}\")\n            else:\n                logger.info(f\"Doc {i} has no vector attribute\")\n        \n        return transformed_docs\n        \n    except Exception as e:\n        logger.error(f\"Error testing document processing: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\ndef main():\n    \"\"\"Main test function.\"\"\"\n    logger.info(\"Starting Google embedder tests...\")\n    \n    # Test 1: Direct client test\n    if not test_google_embedder_client():\n        logger.error(\"Google embedder client test failed\")\n        return False\n    \n    # Test 2: AdalFlow embedder test\n    if not test_adalflow_embedder():\n        logger.error(\"AdalFlow embedder test failed\")\n        return False\n    \n    # Test 3: Document processing test\n    result = test_document_processing()\n    if result is False:\n        logger.error(\"Document processing test failed\")\n        return False\n    \n    logger.info(\"All tests completed successfully!\")\n    return True\n\nif __name__ == \"__main__\":\n    success = main()\n    sys.exit(0 if success else 1)"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]