################################################################################
#                                                                              #
#   ██████╗ ██████╗ ██████╗ ███████╗██████╗  █████╗ ███████╗███████╗          #
#  ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝██╔════╝          #
#  ██║     ██║   ██║██║  ██║█████╗  ██████╔╝███████║███████╗█████╗            #
#  ██║     ██║   ██║██║  ██║██╔══╝  ██╔══██╗██╔══██║╚════██║██╔══╝            #
#  ╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝██║  ██║███████║███████╗          #
#   ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═╝  ╚═╝╚══════╝╚══════╝          #
#                                                                              #
#          COMPLETE CODEBASE DUMP — EVERY FILE, EVERY LINE                     #
#                                                                              #
################################################################################

==============================================================================
 PROJECT CODEBASE — FULL SOURCE DUMP
==============================================================================

 Generated:       2026-03-29 17:26:19
 Source Dir:       /Users/mahmoudaglan/son-of-anton
 Total Files:      60
 Total Lines:      14820
 Total Size:       550KB

 THIS FILE CONTAINS THE COMPLETE CODEBASE INCLUDING:
   • All source code files (every language found)
   • All configuration files (json, yaml, toml, xml, ini, etc.)
   • All environment files (.env, .env.local, .env.production, etc.)
   • All Docker files (Dockerfile, docker-compose.yml, .dockerignore)
   • All CI/CD configs (Jenkinsfile, GitHub Actions, etc.)
   • All build configs (webpack, vite, tsconfig, Makefile, etc.)
   • All package manifests (package.json, Cargo.toml, go.mod, etc.)
   • All lock files (package-lock.json, yarn.lock, etc.)
   • All documentation (README, CHANGELOG, LICENSE, etc.)
   • All scripts (shell, python, etc.)

 STRUCTURE OF THIS FILE:
   1. DIRECTORY TREE
   2. FILE MAP (indexed table of every file)
   3. FILE TYPE BREAKDOWN (stats by extension)
   4. SKIPPED FILES (binary/oversized — for transparency)
   5. COMPLETE FILE CONTENTS (every file printed in full)

==============================================================================


╔══════════════════════════════════════════════════════════════════════════════╗
║  SECTION 1: DIRECTORY TREE                                                  ║
╚══════════════════════════════════════════════════════════════════════════════╝

(For a visual tree: brew install tree)

Collected file paths:
  .dockerignore
  .env.example
  DEPLOY_GUIDE.md
  Dockerfile
  atchfiles.sh
  backend/auth.py
  backend/config.py
  backend/database.py
  backend/main.py
  backend/migrations/005_attachments.sql
  backend/models.py
  backend/routes/admin_routes.py
  backend/routes/attachment_routes.py
  backend/routes/attachment_routes_15.py
  backend/routes/attachment_routes_16.py
  backend/routes/attachments.py
  backend/routes/auth_routes.py
  backend/routes/chat_routes.py
  backend/routes/files_routes.py
  backend/routes/gitlab_routes.py
  backend/routes/knowledge_routes.py
  backend/routes/messages_patch.py
  backend/seed.py
  backend/services/attachment_service.py
  backend/services/bedrock_service.py
  backend/services/code_extractor.py
  backend/services/file_processor.py
  backend/services/generation_manager.py
  backend/services/gitlab_service.py
  backend/services/memory_service.py
  backend/services/rag_service.py
  backend/system_prompt.py
  create-project.ps1
  fix-and-deploy.sh
  frontend/index.html
  frontend/package-lock.json
  frontend/package.json
  frontend/postcss.config.js
  frontend/src/App.jsx
  frontend/src/api.js
  frontend/src/components/AttachmentPreview.jsx
  frontend/src/components/ChatView.jsx
  frontend/src/components/CodeBlock.jsx
  frontend/src/components/FileUploadButton.jsx
  frontend/src/components/MessageBubble.jsx
  frontend/src/components/Sidebar.jsx
  frontend/src/index.css
  frontend/src/main.jsx
  frontend/src/pages/AdminPage.jsx
  frontend/src/pages/ChatPage.jsx
  frontend/src/pages/GitLabPage.jsx
  frontend/src/pages/KnowledgePage.jsx
  frontend/src/pages/LoginPage.jsx
  frontend/src/store.jsx
  frontend/src/streamManager.js
  frontend/tailwind.config.js
  frontend/vite.config.js
  main.py
  requirements.txt
  warmup.py


╔══════════════════════════════════════════════════════════════════════════════╗
║  SECTION 2: FILE MAP — INDEXED LIST OF ALL 60 FILES
╚══════════════════════════════════════════════════════════════════════════════╝

 Each file below appears in SECTION 5 with full contents.
 Use [###] index to jump to any file.

 INDEX   LINES    SIZE      FILE PATH
 -----   -----    ------    ----------------------------------------------
 [001]  28       282B      .dockerignore
 [002]  30       983B      .env.example
 [003]  23       749B      DEPLOY_GUIDE.md
 [004]  55       1KB       Dockerfile
 [005]  1805     72KB      atchfiles.sh
 [006]  66       2KB       backend/auth.py
 [007]  37       1KB       backend/config.py
 [008]  28       777B      backend/database.py
 [009]  155      5KB       backend/main.py
 [010]  17       818B      backend/migrations/005_attachments.sql
 [011]  171      6KB       backend/models.py
 [012]  129      4KB       backend/routes/admin_routes.py
 [013]  125      4KB       backend/routes/attachment_routes.py
 [014]  158      4KB       backend/routes/attachment_routes_15.py
 [015]  164      5KB       backend/routes/attachment_routes_16.py
 [016]  156      4KB       backend/routes/attachments.py
 [017]  90       2KB       backend/routes/auth_routes.py
 [018]  193      6KB       backend/routes/chat_routes.py
 [019]  62       1KB       backend/routes/files_routes.py
 [020]  610      22KB      backend/routes/gitlab_routes.py
 [021]  309      10KB      backend/routes/knowledge_routes.py
 [022]  127      3KB       backend/routes/messages_patch.py
 [023]  28       855B      backend/seed.py
 [024]  260      8KB       backend/services/attachment_service.py
 [025]  242      8KB       backend/services/bedrock_service.py
 [026]  56       1KB       backend/services/code_extractor.py
 [027]  165      5KB       backend/services/file_processor.py
 [028]  266      9KB       backend/services/generation_manager.py
 [029]  493      16KB      backend/services/gitlab_service.py
 [030]  49       1KB       backend/services/memory_service.py
 [031]  110      3KB       backend/services/rag_service.py
 [032]  60       3KB       backend/system_prompt.py
 [033]  182      7KB       create-project.ps1
 [034]  381      15KB      fix-and-deploy.sh
 [035]  34       1KB       frontend/index.html
 [036]  4542     158KB     frontend/package-lock.json
 [037]  27       646B      frontend/package.json
 [038]  5        80B       frontend/postcss.config.js
 [039]  58       1KB       frontend/src/App.jsx
 [040]  194      11KB      frontend/src/api.js
 [041]  158      4KB       frontend/src/components/AttachmentPreview.jsx
 [042]  418      17KB      frontend/src/components/ChatView.jsx
 [043]  95       3KB       frontend/src/components/CodeBlock.jsx
 [044]  81       1KB       frontend/src/components/FileUploadButton.jsx
 [045]  188      8KB       frontend/src/components/MessageBubble.jsx
 [046]  142      6KB       frontend/src/components/Sidebar.jsx
 [047]  284      8KB       frontend/src/index.css
 [048]  15       409B      frontend/src/main.jsx
 [049]  260      12KB      frontend/src/pages/AdminPage.jsx
 [050]  100      3KB       frontend/src/pages/ChatPage.jsx
 [051]  539      30KB      frontend/src/pages/GitLabPage.jsx
 [052]  384      19KB      frontend/src/pages/KnowledgePage.jsx
 [053]  122      4KB       frontend/src/pages/LoginPage.jsx
 [054]  104      2KB       frontend/src/store.jsx
 [055]  158      5KB       frontend/src/streamManager.js
 [056]  26       714B      frontend/tailwind.config.js
 [057]  23       596B      frontend/vite.config.js
 [058]  16       528B      main.py
 [059]  11       213B      requirements.txt
 [060]  6        221B      warmup.py


╔══════════════════════════════════════════════════════════════════════════════╗
║  SECTION 3: FILE TYPE BREAKDOWN                                             ║
╚══════════════════════════════════════════════════════════════════════════════╝

 EXTENSION/TYPE             COUNT
 ───────────────────────    ─────
 .py                        28
 .jsx                       14
 .js                        5
 .sh                        2
 .json                      2
 .txt                       1
 .sql                       1
 .ps1                       1
 .md                        1
 .html                      1
 .example                   1
 .dockerignore              1
 .css                       1
 .Dockerfile                1


╔══════════════════════════════════════════════════════════════════════════════╗
║  SECTION 4: SKIPPED FILES (listed for completeness)                         ║
╚══════════════════════════════════════════════════════════════════════════════╝

 Binary files (not text):
   ⊘ .gitignore
   ⊘ backend/__init__.py
   ⊘ backend/routes/__init__.py
   ⊘ backend/services/__init__.py


╔══════════════════════════════════════════════════════════════════════════════╗
║  SECTION 5: COMPLETE FILE CONTENTS                                          ║
║                                                                              ║
║  Every file printed in full with:                                            ║
║    • Clear start/end markers                                                 ║
║    • File path, size, line count in the header                               ║
║    • Language hint for syntax context                                        ║
╚══════════════════════════════════════════════════════════════════════════════╝


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [001/60]: .dockerignore
│ LANGUAGE: plaintext | LINES: 28 | SIZE: 282 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# === Node ===
     2	frontend/node_modules
     3	frontend/dist
     4	frontend/.vite
     5	
     6	# === Python ===
     7	**/__pycache__
     8	**/*.pyc
     9	**/*.pyo
    10	*.egg-info
    11	
    12	# === Git ===
    13	.git
    14	.gitignore
    15	
    16	# === Dev files ===
    17	.env
    18	.env.local
    19	*.md
    20	create-project.ps1
    21	main.py
    22	**/.DS_Store
    23	
    24	# === IDE ===
    25	.idea
    26	.vscode
    27	*.swp
    28	*.swo
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [001]: .dockerignore


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [002/60]: .env.example
│ LANGUAGE: dotenv | LINES: 30 | SIZE: 983 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# ============================================
     2	# Son of Anton — Environment Variables
     3	# ============================================
     4	
     5	# AWS Bedrock API Key (Bearer Token)
     6	BEDROCK_API_KEY=ABSKQmVkcm9ja0FQSUtleS12ZGdrLWF0LTc2OTE4NzYxNDEyODpnc0ZzdmVGUlRVendqcVpJUURzR0Nzb2RSZ09hK1JLTnJTekFBY3FJQjBqL0F1UXVyekxic3VmaEtpRT0=
     7	
     8	# JWT secret for authentication tokens (change this!)
     9	JWT_SECRET=CreatedSystemsOverloadedFunctionsBySonOfAnton
    10	
    11	# Default superadmin password (change this!)
    12	SUPERADMIN_PASSWORD=admin123
    13	
    14	# AWS Region for Bedrock
    15	AWS_REGION=eu-central-1
    16	
    17	# Primary model
    18	PRIMARY_MODEL=eu.anthropic.claude-opus-4-6-v1
    19	
    20	# Fast model for summaries / title generation
    21	FAST_MODEL=eu.anthropic.claude-haiku-4-5-20251001-v1:0
    22	
    23	# Database URL (default: SQLite in /data)
    24	# DATABASE_URL=sqlite:////data/sonofanton.db
    25	# For PostgreSQL: postgresql://user:pass@host:5432/dbname
    26	
    27	# Default monthly token quota for new users
    28	DEFAULT_QUOTA=2000000
    29	
    30	# Max file upload size in MB
    31	MAX_UPLOAD_MB=50│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [002]: .env.example


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [003/60]: DEPLOY_GUIDE.md
│ LANGUAGE: markdown | LINES: 23 | SIZE: 749 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# 🔥 Son of Anton — Deployment Guide (Zero Tech Friendly)
     2	
     3	## What You're Building
     4	A private AI chat app powered by Claude Opus 4.6 on AWS Bedrock, with:
     5	- Multiple chat sessions with memory
     6	- Downloadable code files from AI responses
     7	- Knowledge base / RAG (upload docs, AI learns from them)
     8	- User management with quotas
     9	- Superadmin dashboard
    10	
    11	---
    12	
    13	## STEP 1 — Get the Code onto Your GitLab
    14	
    15	1. On your computer, create a folder called `son-of-anton`
    16	2. Copy ALL the files from this guide into that folder (keep the folder structure!)
    17	3. Open a terminal in that folder and run:
    18	
    19	```bash
    20	git init
    21	git remote add origin https://YOUR-GITLAB-SERVER/YOUR-USERNAME/son-of-anton.git
    22	git add .
    23	git commit -m "Initial commit"
    24	git push -u origin main│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [003]: DEPLOY_GUIDE.md


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [004/60]: Dockerfile
│ LANGUAGE: dockerfile | LINES: 55 | SIZE: 1523 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# ============================================
     2	# Stage 1: Build React Frontend
     3	# ============================================
     4	FROM node:20-alpine AS frontend-build
     5	
     6	WORKDIR /build/frontend
     7	
     8	# Nuke NODE_ENV — CapRover/Docker can inject production
     9	# which prevents devDependencies (vite, tailwind) from installing
    10	ENV NODE_ENV=
    11	
    12	COPY frontend/package.json frontend/package-lock.json* ./
    13	
    14	# Force ALL deps including dev
    15	RUN npm install --legacy-peer-deps --include=dev && \
    16	  echo "=== vite check ===" && \
    17	  npx vite --version
    18	
    19	COPY frontend/ ./
    20	
    21	RUN NODE_ENV=production npx vite build && \
    22	  echo "=== dist ===" && \
    23	  ls -la dist/
    24	
    25	# ============================================
    26	# Stage 2: Python Backend + Serve Frontend
    27	# ============================================
    28	FROM python:3.11-slim
    29	
    30	RUN apt-get update && apt-get install -y --no-install-recommends \
    31	  build-essential \
    32	  ffmpeg \
    33	  && rm -rf /var/lib/apt/lists/*
    34	
    35	WORKDIR /app
    36	
    37	COPY requirements.txt .
    38	RUN pip install --no-cache-dir -r requirements.txt
    39	
    40	COPY backend/ ./backend/
    41	
    42	COPY --from=frontend-build /build/frontend/dist ./frontend/dist
    43	
    44	RUN echo "=== Frontend ===" && ls -la frontend/dist/ && \
    45	  echo "=== Assets ===" && ls -la frontend/dist/assets/ || true
    46	
    47	COPY warmup.py /tmp/warmup.py
    48	RUN python /tmp/warmup.py && rm /tmp/warmup.py
    49	
    50	RUN mkdir -p /data/chromadb /data/uploads /data/uploads/chat_attachments
    51	
    52	ENV PYTHONUNBUFFERED=1
    53	
    54	EXPOSE 80
    55	
    56	CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "1"]│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [004]: Dockerfile


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [005/60]: atchfiles.sh
│ LANGUAGE: bash | LINES: 1805 | SIZE: 73738 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	#!/usr/bin/env bash
     2	set -euo pipefail
     3	
     4	# ═══════════════════════════════════════════════════════════════
     5	# 🔥 Son of Anton — Patch All Modified/New Files
     6	#    Run from the son-of-anton root directory
     7	# ═══════════════════════════════════════════════════════════════
     8	
     9	PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
    10	cd "$PROJECT_DIR"
    11	
    12	RED='\033[0;31m'
    13	GREEN='\033[0;32m'
    14	CYAN='\033[0;36m'
    15	BOLD='\033[1m'
    16	NC='\033[0m'
    17	
    18	ok()   { echo -e "  ${GREEN}✔${NC} $1"; }
    19	step() { echo -e "  ${CYAN}▸${NC} Writing $1..."; }
    20	
    21	echo ""
    22	echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
    23	echo -e "${BOLD}  Patching all files...${NC}"
    24	echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
    25	echo ""
    26	
    27	# ────────────────────────────────────────
    28	# .dockerignore
    29	# ────────────────────────────────────────
    30	step ".dockerignore"
    31	cat > .dockerignore << 'ENDOFFILE'
    32	# === Node ===
    33	frontend/node_modules
    34	frontend/dist
    35	frontend/.vite
    36	
    37	# === Python ===
    38	**/__pycache__
    39	**/*.pyc
    40	**/*.pyo
    41	*.egg-info
    42	
    43	# === Git ===
    44	.git
    45	.gitignore
    46	
    47	# === Dev files ===
    48	.env
    49	.env.local
    50	*.md
    51	create-project.ps1
    52	main.py
    53	**/.DS_Store
    54	
    55	# === IDE ===
    56	.idea
    57	.vscode
    58	*.swp
    59	*.swo
    60	ENDOFFILE
    61	ok ".dockerignore"
    62	
    63	# ────────────────────────────────────────
    64	# backend/config.py
    65	# ────────────────────────────────────────
    66	step "backend/config.py"
    67	cat > backend/config.py << 'ENDOFFILE'
    68	"""
    69	Application configuration — reads from environment variables.
    70	"""
    71	
    72	import os
    73	import secrets
    74	
    75	BEDROCK_API_KEY: str = os.getenv(
    76	    "BEDROCK_API_KEY",
    77	    os.getenv("AWS_BEARER_TOKEN_BEDROCK", ""),
    78	)
    79	AWS_REGION: str = os.getenv("AWS_REGION", "eu-central-1")
    80	PRIMARY_MODEL: str = os.getenv("PRIMARY_MODEL", "eu.anthropic.claude-opus-4-6-v1")
    81	FAST_MODEL: str = os.getenv("FAST_MODEL", "eu.anthropic.claude-haiku-4-5-20251001-v1:0")
    82	
    83	JWT_SECRET: str = os.getenv("JWT_SECRET", secrets.token_hex(32))
    84	JWT_ALGORITHM: str = "HS256"
    85	JWT_EXPIRY_HOURS: int = 72
    86	
    87	SUPERADMIN_PASSWORD: str = os.getenv("SUPERADMIN_PASSWORD", "admin123")
    88	
    89	DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:////data/sonofanton.db")
    90	
    91	CHROMADB_PATH: str = os.getenv("CHROMADB_PATH", "/data/chromadb")
    92	UPLOAD_PATH: str = os.getenv("UPLOAD_PATH", "/data/uploads")
    93	ATTACHMENT_PATH: str = os.getenv("ATTACHMENT_PATH", "/data/uploads/chat_attachments")
    94	
    95	DEFAULT_QUOTA: int = int(os.getenv("DEFAULT_QUOTA", "2000000"))
    96	MAX_UPLOAD_BYTES: int = int(os.getenv("MAX_UPLOAD_MB", "50")) * 1024 * 1024
    97	MAX_ATTACHMENT_BYTES: int = int(os.getenv("MAX_ATTACHMENT_MB", "25")) * 1024 * 1024
    98	
    99	MAX_IMAGE_DIMENSION: int = 1568
   100	MAX_VIDEO_FRAMES: int = 6
   101	
   102	BEDROCK_ENDPOINT: str = (
   103	    f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
   104	)
   105	ENDOFFILE
   106	ok "backend/config.py"
   107	
   108	# ────────────────────────────────────────
   109	# backend/models.py
   110	# ────────────────────────────────────────
   111	step "backend/models.py"
   112	cat > backend/models.py << 'ENDOFFILE'
   113	"""
   114	SQLAlchemy ORM models.
   115	"""
   116	
   117	from datetime import datetime, timedelta
   118	from uuid import uuid4
   119	
   120	from sqlalchemy import (
   121	    Column, String, Text, Boolean, BigInteger, Integer, DateTime, ForeignKey,
   122	)
   123	from sqlalchemy.orm import relationship
   124	
   125	from backend.database import Base
   126	
   127	
   128	def new_id() -> str:
   129	    return str(uuid4())
   130	
   131	
   132	def next_month() -> datetime:
   133	    now = datetime.utcnow()
   134	    if now.month == 12:
   135	        return datetime(now.year + 1, 1, 1)
   136	    return datetime(now.year, now.month + 1, 1)
   137	
   138	
   139	class User(Base):
   140	    __tablename__ = "users"
   141	
   142	    id = Column(String(36), primary_key=True, default=new_id)
   143	    username = Column(String(50), unique=True, nullable=False, index=True)
   144	    email = Column(String(120), unique=True, nullable=False)
   145	    password_hash = Column(String(200), nullable=False)
   146	    role = Column(String(20), default="user")
   147	    is_active = Column(Boolean, default=True)
   148	    quota_tokens_monthly = Column(BigInteger, default=2_000_000)
   149	    tokens_used_this_month = Column(BigInteger, default=0)
   150	    quota_reset_date = Column(DateTime, default=next_month)
   151	    created_at = Column(DateTime, default=datetime.utcnow)
   152	
   153	    chats = relationship("Chat", back_populates="user", cascade="all,delete-orphan")
   154	
   155	
   156	class Chat(Base):
   157	    __tablename__ = "chats"
   158	
   159	    id = Column(String(36), primary_key=True, default=new_id)
   160	    user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
   161	    title = Column(String(200), default="New Chat")
   162	    model = Column(String(100), default="eu.anthropic.claude-opus-4-6-v1")
   163	    knowledge_base_id = Column(String(36), nullable=True)
   164	    max_tokens = Column(Integer, default=4096)
   165	    reasoning_budget = Column(Integer, default=0)
   166	    created_at = Column(DateTime, default=datetime.utcnow)
   167	    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
   168	
   169	    user = relationship("User", back_populates="chats")
   170	    messages = relationship(
   171	        "Message", back_populates="chat",
   172	        cascade="all,delete-orphan", order_by="Message.created_at",
   173	    )
   174	    attachments = relationship(
   175	        "ChatAttachment", back_populates="chat",
   176	        cascade="all,delete-orphan",
   177	    )
   178	
   179	
   180	class Message(Base):
   181	    __tablename__ = "messages"
   182	
   183	    id = Column(String(36), primary_key=True, default=new_id)
   184	    chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
   185	    role = Column(String(20), nullable=False)
   186	    content = Column(Text, default="")
   187	    thinking_content = Column(Text, nullable=True)
   188	    input_tokens = Column(Integer, default=0)
   189	    output_tokens = Column(Integer, default=0)
   190	    created_at = Column(DateTime, default=datetime.utcnow)
   191	
   192	    chat = relationship("Chat", back_populates="messages")
   193	    attachments = relationship("ChatAttachment", back_populates="message")
   194	
   195	
   196	class ChatAttachment(Base):
   197	    __tablename__ = "chat_attachments"
   198	
   199	    id = Column(String(36), primary_key=True, default=new_id)
   200	    chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
   201	    message_id = Column(String(36), ForeignKey("messages.id", ondelete="SET NULL"), nullable=True)
   202	    filename = Column(String(200), nullable=False)
   203	    original_filename = Column(String(200), nullable=False)
   204	    mime_type = Column(String(100), nullable=False)
   205	    file_type = Column(String(20), nullable=False)
   206	    file_size = Column(Integer, default=0)
   207	    storage_path = Column(String(500), nullable=False)
   208	    text_extract = Column(Text, nullable=True)
   209	    created_at = Column(DateTime, default=datetime.utcnow)
   210	
   211	    chat = relationship("Chat", back_populates="attachments")
   212	    message = relationship("Message", back_populates="attachments")
   213	
   214	
   215	class KnowledgeBase(Base):
   216	    __tablename__ = "knowledge_bases"
   217	
   218	    id = Column(String(36), primary_key=True, default=new_id)
   219	    user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
   220	    name = Column(String(200), nullable=False)
   221	    description = Column(Text, default="")
   222	    document_count = Column(Integer, default=0)
   223	    chunk_count = Column(Integer, default=0)
   224	    total_characters = Column(BigInteger, default=0)
   225	    created_at = Column(DateTime, default=datetime.utcnow)
   226	
   227	
   228	class KnowledgeDocument(Base):
   229	    __tablename__ = "knowledge_documents"
   230	
   231	    id = Column(String(36), primary_key=True, default=new_id)
   232	    knowledge_base_id = Column(
   233	        String(36), ForeignKey("knowledge_bases.id", ondelete="CASCADE"), nullable=False,
   234	    )
   235	    filename = Column(String(200), nullable=False)
   236	    file_size = Column(Integer, default=0)
   237	    chunk_count = Column(Integer, default=0)
   238	    created_at = Column(DateTime, default=datetime.utcnow)
   239	ENDOFFILE
   240	ok "backend/models.py"
   241	
   242	# ────────────────────────────────────────
   243	# backend/main.py
   244	# ────────────────────────────────────────
   245	step "backend/main.py"
   246	cat > backend/main.py << 'ENDOFFILE'
   247	"""
   248	Son of Anton — Main FastAPI Application
   249	"""
   250	
   251	import os
   252	from pathlib import Path
   253	from contextlib import asynccontextmanager
   254	
   255	from fastapi import FastAPI, HTTPException
   256	from fastapi.staticfiles import StaticFiles
   257	from fastapi.responses import FileResponse
   258	from fastapi.middleware.cors import CORSMiddleware
   259	
   260	from backend.database import engine, Base
   261	from backend.seed import seed_superadmin
   262	from backend.routes.auth_routes import router as auth_router
   263	from backend.routes.chat_routes import router as chat_router
   264	from backend.routes.admin_routes import router as admin_router
   265	from backend.routes.knowledge_routes import router as knowledge_router
   266	from backend.routes.files_routes import router as files_router
   267	from backend.routes.attachment_routes import router as attachment_router
   268	from backend.services.bedrock_service import close_http_client
   269	
   270	
   271	def _run_migrations():
   272	    """Add new columns/tables to existing DB if they're missing."""
   273	    from sqlalchemy import inspect, text
   274	    try:
   275	        inspector = inspect(engine)
   276	        existing_tables = inspector.get_table_names()
   277	
   278	        if "chats" in existing_tables:
   279	            columns = {c["name"] for c in inspector.get_columns("chats")}
   280	            with engine.connect() as conn:
   281	                if "max_tokens" not in columns:
   282	                    conn.execute(text("ALTER TABLE chats ADD COLUMN max_tokens INTEGER DEFAULT 4096"))
   283	                    print("  Added chats.max_tokens column")
   284	                if "reasoning_budget" not in columns:
   285	                    conn.execute(text("ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"))
   286	                    print("  Added chats.reasoning_budget column")
   287	                conn.commit()
   288	
   289	        if "chat_attachments" not in existing_tables:
   290	            from backend.models import ChatAttachment
   291	            ChatAttachment.__table__.create(bind=engine, checkfirst=True)
   292	            print("  Created chat_attachments table")
   293	
   294	    except Exception as e:
   295	        print(f"  Migration note: {e}")
   296	
   297	
   298	@asynccontextmanager
   299	async def lifespan(app: FastAPI):
   300	    Base.metadata.create_all(bind=engine)
   301	    _run_migrations()
   302	    seed_superadmin()
   303	    print("Son of Anton is online.")
   304	    yield
   305	    await close_http_client()
   306	    print("Son of Anton shutting down.")
   307	
   308	
   309	app = FastAPI(
   310	    title="Son of Anton",
   311	    description="Avatar of All Elements of Code",
   312	    version="2.0.0",
   313	    lifespan=lifespan,
   314	)
   315	
   316	app.add_middleware(
   317	    CORSMiddleware,
   318	    allow_origins=["*"],
   319	    allow_credentials=True,
   320	    allow_methods=["*"],
   321	    allow_headers=["*"],
   322	)
   323	
   324	app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
   325	app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
   326	app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
   327	app.include_router(knowledge_router, prefix="/api/knowledge", tags=["Knowledge"])
   328	app.include_router(files_router, prefix="/api/files", tags=["Files"])
   329	app.include_router(attachment_router, prefix="/api", tags=["Attachments"])
   330	
   331	FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
   332	
   333	if (FRONTEND_DIR / "assets").exists():
   334	    app.mount(
   335	        "/assets",
   336	        StaticFiles(directory=str(FRONTEND_DIR / "assets")),
   337	        name="static-assets",
   338	    )
   339	
   340	
   341	@app.get("/{full_path:path}", include_in_schema=False)
   342	async def serve_frontend(full_path: str):
   343	    if full_path.startswith("api"):
   344	        raise HTTPException(status_code=404, detail="Not found")
   345	    file_path = FRONTEND_DIR / full_path
   346	    if full_path and file_path.is_file():
   347	        return FileResponse(str(file_path))
   348	    index = FRONTEND_DIR / "index.html"
   349	    if index.is_file():
   350	        return FileResponse(str(index))
   351	    return {"message": "Son of Anton API is running. Frontend not built."}
   352	ENDOFFILE
   353	ok "backend/main.py"
   354	
   355	# ────────────────────────────────────────
   356	# backend/services/attachment_service.py
   357	# ────────────────────────────────────────
   358	step "backend/services/attachment_service.py"
   359	cat > backend/services/attachment_service.py << 'ENDOFFILE'
   360	"""
   361	Attachment processing service.
   362	Handles images, videos (frame extraction), PDFs, and text files.
   363	"""
   364	
   365	import os
   366	import io
   367	import base64
   368	import shutil
   369	import subprocess
   370	import tempfile
   371	import mimetypes
   372	from pathlib import Path
   373	from uuid import uuid4
   374	from typing import Optional
   375	
   376	from backend import config
   377	
   378	os.makedirs(config.ATTACHMENT_PATH, exist_ok=True)
   379	
   380	IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
   381	VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv", ".m4v"}
   382	PDF_EXTENSIONS = {".pdf"}
   383	TEXT_EXTENSIONS = {
   384	    ".txt", ".md", ".py", ".js", ".ts", ".jsx", ".tsx", ".cs", ".java",
   385	    ".cpp", ".c", ".h", ".hpp", ".go", ".rs", ".rb", ".php", ".swift",
   386	    ".kt", ".lua", ".gd", ".html", ".css", ".scss", ".json", ".yaml",
   387	    ".yml", ".xml", ".toml", ".ini", ".cfg", ".conf", ".sh", ".bash",
   388	    ".sql", ".r", ".dart", ".vue", ".svelte", ".csv", ".log", ".env",
   389	}
   390	
   391	IMAGE_MIMES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
   392	VIDEO_MIMES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm"}
   393	
   394	
   395	def classify_file(filename, mime):
   396	    ext = Path(filename).suffix.lower()
   397	    if ext in IMAGE_EXTENSIONS or mime in IMAGE_MIMES:
   398	        return "image"
   399	    if ext in VIDEO_EXTENSIONS or mime in VIDEO_MIMES:
   400	        return "video"
   401	    if ext in PDF_EXTENSIONS or mime == "application/pdf":
   402	        return "document"
   403	    return "text"
   404	
   405	
   406	def get_mime_type(filename, content_type=None):
   407	    if content_type and content_type != "application/octet-stream":
   408	        return content_type
   409	    mime, _ = mimetypes.guess_type(filename)
   410	    return mime or "application/octet-stream"
   411	
   412	
   413	def save_attachment(chat_id, filename, content, content_type=None):
   414	    mime = get_mime_type(filename, content_type)
   415	    file_type = classify_file(filename, mime)
   416	    attachment_id = str(uuid4())
   417	
   418	    chat_dir = os.path.join(config.ATTACHMENT_PATH, chat_id)
   419	    os.makedirs(chat_dir, exist_ok=True)
   420	
   421	    safe_name = Path(filename).name.replace(" ", "_")
   422	    stored_name = f"{attachment_id}_{safe_name}"
   423	    storage_path = os.path.join(chat_dir, stored_name)
   424	
   425	    with open(storage_path, "wb") as f:
   426	        f.write(content)
   427	
   428	    text_extract = None
   429	    if file_type == "text":
   430	        text_extract = _extract_text_content(storage_path)
   431	    elif file_type == "document":
   432	        text_extract = _extract_pdf_text(storage_path)
   433	
   434	    return {
   435	        "id": attachment_id,
   436	        "filename": stored_name,
   437	        "original_filename": filename,
   438	        "mime_type": mime,
   439	        "file_type": file_type,
   440	        "file_size": len(content),
   441	        "storage_path": storage_path,
   442	        "text_extract": text_extract,
   443	    }
   444	
   445	
   446	def delete_attachment_file(storage_path):
   447	    try:
   448	        if os.path.exists(storage_path):
   449	            os.remove(storage_path)
   450	    except Exception:
   451	        pass
   452	
   453	
   454	def delete_chat_attachments(chat_id):
   455	    chat_dir = os.path.join(config.ATTACHMENT_PATH, chat_id)
   456	    if os.path.isdir(chat_dir):
   457	        shutil.rmtree(chat_dir, ignore_errors=True)
   458	
   459	
   460	def build_claude_content_blocks(attachments):
   461	    blocks = []
   462	    for att in attachments:
   463	        try:
   464	            result = _process_single_attachment(att)
   465	            if isinstance(result, list):
   466	                blocks.extend(result)
   467	            elif result:
   468	                blocks.append(result)
   469	        except Exception as e:
   470	            blocks.append({
   471	                "type": "text",
   472	                "text": f"[Failed to process {att.original_filename}: {str(e)}]",
   473	            })
   474	    return blocks
   475	
   476	
   477	def _process_single_attachment(att):
   478	    if att.file_type == "image":
   479	        return _build_image_block(att)
   480	    elif att.file_type == "video":
   481	        return _build_video_blocks(att)
   482	    elif att.file_type == "document":
   483	        return _build_document_block(att)
   484	    elif att.file_type == "text":
   485	        return _build_text_block(att)
   486	    return None
   487	
   488	
   489	def _build_image_block(att):
   490	    data = _read_and_resize_image(att.storage_path, att.mime_type)
   491	    mime = att.mime_type if att.mime_type in IMAGE_MIMES else "image/jpeg"
   492	    return {
   493	        "type": "image",
   494	        "source": {"type": "base64", "media_type": mime, "data": data},
   495	    }
   496	
   497	
   498	def _build_video_blocks(att):
   499	    frames = _extract_video_frames(att.storage_path)
   500	    if not frames:
   501	        return [{"type": "text", "text": f"[Video: {att.original_filename} - could not extract frames]"}]
   502	    blocks = [{"type": "text", "text": f"[Video: {att.original_filename} - {len(frames)} frames extracted]"}]
   503	    for frame_b64 in frames:
   504	        blocks.append({
   505	            "type": "image",
   506	            "source": {"type": "base64", "media_type": "image/jpeg", "data": frame_b64},
   507	        })
   508	    return blocks
   509	
   510	
   511	def _build_document_block(att):
   512	    if att.mime_type == "application/pdf":
   513	        with open(att.storage_path, "rb") as f:
   514	            data = base64.b64encode(f.read()).decode("utf-8")
   515	        return {
   516	            "type": "document",
   517	            "source": {"type": "base64", "media_type": "application/pdf", "data": data},
   518	        }
   519	    return _build_text_block(att)
   520	
   521	
   522	def _build_text_block(att):
   523	    text = att.text_extract or _extract_text_content(att.storage_path)
   524	    if not text:
   525	        text = f"[Could not extract text from {att.original_filename}]"
   526	    return {
   527	        "type": "text",
   528	        "text": f"--- File: {att.original_filename} ---\n{text}\n--- End of {att.original_filename} ---",
   529	    }
   530	
   531	
   532	def _read_and_resize_image(path, mime_type):
   533	    try:
   534	        from PIL import Image
   535	        img = Image.open(path)
   536	        if img.mode in ("RGBA", "LA", "P"):
   537	            bg = Image.new("RGB", img.size, (255, 255, 255))
   538	            if img.mode == "P":
   539	                img = img.convert("RGBA")
   540	            bg.paste(img, mask=img.split()[-1] if "A" in img.mode else None)
   541	            img = bg
   542	        elif img.mode != "RGB":
   543	            img = img.convert("RGB")
   544	        mx = config.MAX_IMAGE_DIMENSION
   545	        if img.width > mx or img.height > mx:
   546	            ratio = min(mx / img.width, mx / img.height)
   547	            img = img.resize((int(img.width * ratio), int(img.height * ratio)), Image.LANCZOS)
   548	        buf = io.BytesIO()
   549	        fmt = "PNG" if mime_type == "image/png" else "JPEG"
   550	        kwargs = {"quality": 85} if fmt == "JPEG" else {}
   551	        img.save(buf, format=fmt, **kwargs)
   552	        return base64.b64encode(buf.getvalue()).decode("utf-8")
   553	    except ImportError:
   554	        with open(path, "rb") as f:
   555	            return base64.b64encode(f.read()).decode("utf-8")
   556	    except Exception:
   557	        with open(path, "rb") as f:
   558	            return base64.b64encode(f.read()).decode("utf-8")
   559	
   560	
   561	def _extract_video_frames(video_path):
   562	    if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
   563	        return []
   564	    frames = []
   565	    try:
   566	        result = subprocess.run(
   567	            ["ffprobe", "-v", "error", "-show_entries", "format=duration",
   568	             "-of", "default=noprint_wrappers=1:nokey=1", video_path],
   569	            capture_output=True, text=True, timeout=30,
   570	        )
   571	        duration = float(result.stdout.strip() or "0")
   572	        if duration <= 0:
   573	            return []
   574	        max_frames = config.MAX_VIDEO_FRAMES
   575	        with tempfile.TemporaryDirectory() as tmpdir:
   576	            interval = duration / (max_frames + 1)
   577	            for i in range(max_frames):
   578	                ts = interval * (i + 1)
   579	                out = os.path.join(tmpdir, f"frame_{i}.jpg")
   580	                subprocess.run(
   581	                    ["ffmpeg", "-ss", str(ts), "-i", video_path, "-vframes", "1",
   582	                     "-vf", f"scale='min({config.MAX_IMAGE_DIMENSION},iw)':'min({config.MAX_IMAGE_DIMENSION},ih)':force_original_aspect_ratio=decrease",
   583	                     "-q:v", "3", out],
   584	                    capture_output=True, timeout=30,
   585	                )
   586	                if os.path.exists(out) and os.path.getsize(out) > 0:
   587	                    with open(out, "rb") as f:
   588	                        frames.append(base64.b64encode(f.read()).decode("utf-8"))
   589	    except Exception:
   590	        pass
   591	    return frames
   592	
   593	
   594	def _extract_text_content(path):
   595	    try:
   596	        with open(path, "r", encoding="utf-8") as f:
   597	            return f.read(500_000)
   598	    except UnicodeDecodeError:
   599	        try:
   600	            with open(path, "r", encoding="latin-1") as f:
   601	                return f.read(500_000)
   602	        except Exception:
   603	            return None
   604	    except Exception:
   605	        return None
   606	
   607	
   608	def _extract_pdf_text(path):
   609	    try:
   610	        from PyPDF2 import PdfReader
   611	        reader = PdfReader(path)
   612	        pages = []
   613	        for page in reader.pages[:100]:
   614	            text = page.extract_text()
   615	            if text:
   616	                pages.append(text)
   617	        return "\n\n".join(pages) if pages else None
   618	    except Exception:
   619	        return None
   620	ENDOFFILE
   621	ok "backend/services/attachment_service.py"
   622	
   623	# ────────────────────────────────────────
   624	# backend/routes/attachment_routes.py
   625	# ────────────────────────────────────────
   626	step "backend/routes/attachment_routes.py"
   627	cat > backend/routes/attachment_routes.py << 'ENDOFFILE'
   628	"""
   629	Chat attachment upload, serve, and delete routes.
   630	"""
   631	
   632	import os
   633	from typing import Optional
   634	
   635	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
   636	from fastapi.responses import FileResponse
   637	from sqlalchemy.orm import Session
   638	
   639	from backend.database import get_db
   640	from backend.models import User, Chat, ChatAttachment
   641	from backend.auth import get_current_user, decode_token
   642	from backend.services import attachment_service
   643	from backend.config import MAX_ATTACHMENT_BYTES
   644	
   645	router = APIRouter()
   646	
   647	
   648	def _get_user_flexible(request: Request, db: Session, token_param: Optional[str] = None) -> User:
   649	    raw_token = None
   650	    auth_header = request.headers.get("authorization", "")
   651	    if auth_header.startswith("Bearer "):
   652	        raw_token = auth_header[7:]
   653	    if not raw_token and token_param:
   654	        raw_token = token_param
   655	    if not raw_token:
   656	        raise HTTPException(401, "Authentication required")
   657	    payload = decode_token(raw_token)
   658	    user = db.query(User).filter(User.id == payload["sub"]).first()
   659	    if not user or not user.is_active:
   660	        raise HTTPException(401, "User not found or inactive")
   661	    return user
   662	
   663	
   664	@router.post("/chats/{chat_id}/attachments")
   665	async def upload_attachments(
   666	    chat_id: str,
   667	    files: list[UploadFile] = File(...),
   668	    user: User = Depends(get_current_user),
   669	    db: Session = Depends(get_db),
   670	):
   671	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   672	    if not chat:
   673	        raise HTTPException(404, "Chat not found")
   674	
   675	    results = []
   676	    for file in files:
   677	        filename = file.filename or "file"
   678	        try:
   679	            content = await file.read()
   680	            if len(content) > MAX_ATTACHMENT_BYTES:
   681	                results.append({"error": f"Too large: {filename}"})
   682	                continue
   683	
   684	            meta = attachment_service.save_attachment(
   685	                chat_id=chat_id, filename=filename,
   686	                content=content, content_type=file.content_type,
   687	            )
   688	
   689	            att = ChatAttachment(
   690	                id=meta["id"], chat_id=chat_id,
   691	                filename=meta["filename"],
   692	                original_filename=meta["original_filename"],
   693	                mime_type=meta["mime_type"],
   694	                file_type=meta["file_type"],
   695	                file_size=meta["file_size"],
   696	                storage_path=meta["storage_path"],
   697	                text_extract=meta.get("text_extract"),
   698	            )
   699	            db.add(att)
   700	            db.commit()
   701	            db.refresh(att)
   702	            results.append(_att_dict(att))
   703	        except Exception as e:
   704	            results.append({"error": f"Failed: {filename}: {str(e)}"})
   705	
   706	    return {"attachments": results}
   707	
   708	
   709	@router.get("/attachments/{attachment_id}/file")
   710	def serve_attachment(
   711	    attachment_id: str,
   712	    request: Request,
   713	    token: Optional[str] = Query(None),
   714	    db: Session = Depends(get_db),
   715	):
   716	    user = _get_user_flexible(request, db, token)
   717	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   718	    if not att:
   719	        raise HTTPException(404, "Attachment not found")
   720	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   721	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   722	        raise HTTPException(403, "Access denied")
   723	    if not os.path.exists(att.storage_path):
   724	        raise HTTPException(404, "File not found on disk")
   725	    return FileResponse(att.storage_path, media_type=att.mime_type, filename=att.original_filename)
   726	
   727	
   728	@router.delete("/attachments/{attachment_id}")
   729	def delete_attachment(
   730	    attachment_id: str,
   731	    user: User = Depends(get_current_user),
   732	    db: Session = Depends(get_db),
   733	):
   734	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   735	    if not att:
   736	        raise HTTPException(404)
   737	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   738	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   739	        raise HTTPException(403)
   740	    attachment_service.delete_attachment_file(att.storage_path)
   741	    db.delete(att)
   742	    db.commit()
   743	    return {"ok": True}
   744	
   745	
   746	def _att_dict(att):
   747	    return {
   748	        "id": att.id, "chat_id": att.chat_id, "message_id": att.message_id,
   749	        "filename": att.filename, "original_filename": att.original_filename,
   750	        "mime_type": att.mime_type, "file_type": att.file_type,
   751	        "file_size": att.file_size, "created_at": str(att.created_at),
   752	    }
   753	ENDOFFILE
   754	ok "backend/routes/attachment_routes.py"
   755	
   756	# ────────────────────────────────────────
   757	# backend/routes/chat_routes.py
   758	# ────────────────────────────────────────
   759	step "backend/routes/chat_routes.py"
   760	cat > backend/routes/chat_routes.py << 'ENDOFFILE'
   761	"""
   762	Chat CRUD and message streaming with multimodal attachment support.
   763	"""
   764	
   765	import json
   766	from datetime import datetime
   767	from pydantic import BaseModel
   768	from typing import Optional
   769	
   770	from fastapi import APIRouter, Depends, HTTPException
   771	from fastapi.responses import StreamingResponse
   772	from sqlalchemy.orm import Session
   773	
   774	from backend.database import get_db, SessionLocal
   775	from backend.models import User, Chat, Message, ChatAttachment
   776	from backend.auth import get_current_user
   777	from backend.system_prompt import build_full_prompt
   778	from backend.services import bedrock_service, memory_service, rag_service, attachment_service
   779	
   780	router = APIRouter()
   781	
   782	
   783	class CreateChatBody(BaseModel):
   784	    title: str = "New Chat"
   785	    model: str = "eu.anthropic.claude-opus-4-6-v1"
   786	    knowledge_base_id: Optional[str] = None
   787	    max_tokens: int = 4096
   788	    reasoning_budget: int = 0
   789	
   790	
   791	class UpdateChatBody(BaseModel):
   792	    title: Optional[str] = None
   793	    model: Optional[str] = None
   794	    max_tokens: Optional[int] = None
   795	    reasoning_budget: Optional[int] = None
   796	    knowledge_base_id: Optional[str] = None
   797	
   798	
   799	class SendMessageBody(BaseModel):
   800	    content: str
   801	    model: Optional[str] = None
   802	    max_tokens: int = 4096
   803	    reasoning_budget: int = 0
   804	    knowledge_base_id: Optional[str] = None
   805	    attachment_ids: list[str] = []
   806	
   807	
   808	@router.get("")
   809	def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   810	    chats = db.query(Chat).filter(Chat.user_id == user.id).order_by(Chat.updated_at.desc()).all()
   811	    return [_chat_dict(c) for c in chats]
   812	
   813	
   814	@router.post("")
   815	def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   816	    chat = Chat(
   817	        user_id=user.id, title=body.title, model=body.model,
   818	        knowledge_base_id=body.knowledge_base_id or None,
   819	        max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget,
   820	    )
   821	    db.add(chat)
   822	    db.commit()
   823	    db.refresh(chat)
   824	    return _chat_dict(chat)
   825	
   826	
   827	@router.get("/{chat_id}")
   828	def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   829	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   830	    if not chat:
   831	        raise HTTPException(404, "Chat not found")
   832	    return _chat_dict(chat)
   833	
   834	
   835	@router.put("/{chat_id}")
   836	def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   837	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   838	    if not chat:
   839	        raise HTTPException(404)
   840	    if body.title is not None:
   841	        chat.title = body.title
   842	    if body.model is not None:
   843	        chat.model = body.model
   844	    if body.max_tokens is not None:
   845	        chat.max_tokens = body.max_tokens
   846	    if body.reasoning_budget is not None:
   847	        chat.reasoning_budget = body.reasoning_budget
   848	    if body.knowledge_base_id is not None:
   849	        chat.knowledge_base_id = body.knowledge_base_id or None
   850	    db.commit()
   851	    return _chat_dict(chat)
   852	
   853	
   854	@router.delete("/{chat_id}")
   855	def delete_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   856	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   857	    if not chat:
   858	        raise HTTPException(404)
   859	    attachment_service.delete_chat_attachments(chat_id)
   860	    db.delete(chat)
   861	    db.commit()
   862	    return {"ok": True}
   863	
   864	
   865	@router.get("/{chat_id}/messages")
   866	def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   867	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   868	    if not chat:
   869	        raise HTTPException(404)
   870	    msgs = []
   871	    for m in chat.messages:
   872	        d = _msg_dict(m)
   873	        atts = db.query(ChatAttachment).filter(ChatAttachment.message_id == m.id).all()
   874	        d["attachments"] = [_att_brief(a) for a in atts]
   875	        msgs.append(d)
   876	    return msgs
   877	
   878	
   879	@router.post("/{chat_id}/messages")
   880	async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)):
   881	    user_id = user.id
   882	
   883	    async def generate():
   884	        db = SessionLocal()
   885	        try:
   886	            chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
   887	            if not chat:
   888	                yield _sse({"type": "error", "message": "Chat not found"})
   889	                return
   890	
   891	            db_user = db.query(User).filter(User.id == user_id).first()
   892	
   893	            now = datetime.utcnow()
   894	            if db_user.quota_reset_date and now >= db_user.quota_reset_date:
   895	                db_user.tokens_used_this_month = 0
   896	                if now.month == 12:
   897	                    db_user.quota_reset_date = datetime(now.year + 1, 1, 1)
   898	                else:
   899	                    db_user.quota_reset_date = datetime(now.year, now.month + 1, 1)
   900	                db.commit()
   901	
   902	            if db_user.tokens_used_this_month >= db_user.quota_tokens_monthly:
   903	                yield _sse({"type": "error", "message": "Monthly token quota exceeded."})
   904	                return
   905	
   906	            attachments = []
   907	            if body.attachment_ids:
   908	                attachments = (
   909	                    db.query(ChatAttachment)
   910	                    .filter(ChatAttachment.id.in_(body.attachment_ids), ChatAttachment.chat_id == chat_id)
   911	                    .all()
   912	                )
   913	
   914	            stored_content = body.content
   915	            if attachments:
   916	                labels = {"image": "Image", "video": "Video", "document": "Document", "text": "File"}
   917	                notes = [f"[{labels.get(a.file_type, 'File')}: {a.original_filename}]" for a in attachments]
   918	                stored_content = "\n".join(notes) + "\n" + body.content
   919	
   920	            user_msg = Message(chat_id=chat_id, role="user", content=stored_content)
   921	            db.add(user_msg)
   922	            db.commit()
   923	            db.refresh(user_msg)
   924	
   925	            for att in attachments:
   926	                att.message_id = user_msg.id
   927	            if attachments:
   928	                db.commit()
   929	
   930	            kb_id = body.knowledge_base_id or chat.knowledge_base_id
   931	            rag_context = None
   932	            if kb_id:
   933	                try:
   934	                    rag_context = rag_service.query(kb_id, body.content, n_results=8)
   935	                except Exception:
   936	                    pass
   937	
   938	            system_prompt = build_full_prompt(rag_context)
   939	            messages = memory_service.build_messages(chat, db)
   940	
   941	            if attachments and messages and messages[-1]["role"] == "user":
   942	                content_blocks = attachment_service.build_claude_content_blocks(attachments)
   943	                content_blocks.append({"type": "text", "text": body.content})
   944	                messages[-1]["content"] = content_blocks
   945	
   946	            model_id = body.model or chat.model
   947	            max_tokens = body.max_tokens
   948	            thinking_config = None
   949	            if body.reasoning_budget > 0:
   950	                thinking_config = {"enabled": True, "budget_tokens": body.reasoning_budget}
   951	                max_tokens = max_tokens + body.reasoning_budget
   952	
   953	            full_text = ""
   954	            full_thinking = ""
   955	            input_tokens = 0
   956	            output_tokens = 0
   957	            current_block_type = "text"
   958	
   959	            async for event in bedrock_service.stream_response(
   960	                messages=messages, system_prompt=system_prompt,
   961	                model_id=model_id, max_tokens=min(max_tokens, 65536),
   962	                thinking_config=thinking_config,
   963	            ):
   964	                evt_type = event.get("type", "")
   965	
   966	                if evt_type == "message_start":
   967	                    usage = event.get("message", {}).get("usage", {})
   968	                    input_tokens = usage.get("input_tokens", 0)
   969	                elif evt_type == "content_block_start":
   970	                    blk = event.get("content_block", {})
   971	                    current_block_type = blk.get("type", "text")
   972	                    if current_block_type == "thinking":
   973	                        yield _sse({"type": "thinking_start"})
   974	                elif evt_type == "content_block_delta":
   975	                    delta = event.get("delta", {})
   976	                    dt = delta.get("type", "")
   977	                    if dt == "thinking_delta":
   978	                        t = delta.get("thinking", "")
   979	                        full_thinking += t
   980	                        yield _sse({"type": "thinking_delta", "content": t})
   981	                    elif dt == "text_delta":
   982	                        t = delta.get("text", "")
   983	                        full_text += t
   984	                        yield _sse({"type": "text_delta", "content": t})
   985	                elif evt_type == "content_block_stop":
   986	                    if current_block_type == "thinking":
   987	                        yield _sse({"type": "thinking_end"})
   988	                elif evt_type == "message_delta":
   989	                    usage = event.get("usage", {})
   990	                    output_tokens = usage.get("output_tokens", 0)
   991	
   992	            assistant_msg = Message(
   993	                chat_id=chat_id, role="assistant", content=full_text,
   994	                thinking_content=full_thinking or None,
   995	                input_tokens=input_tokens, output_tokens=output_tokens,
   996	            )
   997	            db.add(assistant_msg)
   998	            db_user.tokens_used_this_month += input_tokens + output_tokens
   999	            chat.model = model_id
  1000	            chat.max_tokens = body.max_tokens
  1001	            chat.reasoning_budget = body.reasoning_budget
  1002	            chat.knowledge_base_id = body.knowledge_base_id or None
  1003	            chat.updated_at = datetime.utcnow()
  1004	            db.commit()
  1005	
  1006	            msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
  1007	            if msg_count <= 2 and chat.title == "New Chat":
  1008	                try:
  1009	                    title = await _generate_title(body.content, full_text[:300])
  1010	                    chat.title = title[:120]
  1011	                    db.commit()
  1012	                    yield _sse({"type": "title_update", "title": chat.title})
  1013	                except Exception:
  1014	                    pass
  1015	
  1016	            yield _sse({"type": "usage", "input_tokens": input_tokens, "output_tokens": output_tokens})
  1017	            yield _sse({"type": "done", "message_id": assistant_msg.id})
  1018	
  1019	        except Exception as exc:
  1020	            yield _sse({"type": "error", "message": str(exc)})
  1021	        finally:
  1022	            db.close()
  1023	
  1024	    return StreamingResponse(generate(), media_type="text/event-stream")
  1025	
  1026	
  1027	async def _generate_title(user_msg, ai_msg):
  1028	    from backend.config import FAST_MODEL
  1029	    result = await bedrock_service.invoke_model_simple(
  1030	        model_id=FAST_MODEL,
  1031	        prompt=f"Generate a concise title (max 6 words) for this conversation:\nUser: {user_msg[:200]}\nAssistant: {ai_msg[:200]}\nRespond ONLY with the title.",
  1032	        max_tokens=30,
  1033	    )
  1034	    return result.strip().strip('"').strip("'")
  1035	
  1036	
  1037	def _sse(data):
  1038	    return f"data: {json.dumps(data)}\n\n"
  1039	
  1040	
  1041	def _chat_dict(c):
  1042	    return {
  1043	        "id": c.id, "title": c.title, "model": c.model,
  1044	        "knowledge_base_id": c.knowledge_base_id,
  1045	        "max_tokens": c.max_tokens or 4096,
  1046	        "reasoning_budget": c.reasoning_budget or 0,
  1047	        "created_at": str(c.created_at), "updated_at": str(c.updated_at),
  1048	    }
  1049	
  1050	
  1051	def _msg_dict(m):
  1052	    return {
  1053	        "id": m.id, "role": m.role, "content": m.content,
  1054	        "thinking_content": m.thinking_content,
  1055	        "input_tokens": m.input_tokens, "output_tokens": m.output_tokens,
  1056	        "created_at": str(m.created_at),
  1057	    }
  1058	
  1059	
  1060	def _att_brief(a):
  1061	    return {
  1062	        "id": a.id, "original_filename": a.original_filename,
  1063	        "mime_type": a.mime_type, "file_type": a.file_type,
  1064	        "file_size": a.file_size,
  1065	    }
  1066	ENDOFFILE
  1067	ok "backend/routes/chat_routes.py"
  1068	
  1069	# ────────────────────────────────────────
  1070	# backend/services/memory_service.py
  1071	# ────────────────────────────────────────
  1072	step "backend/services/memory_service.py"
  1073	cat > backend/services/memory_service.py << 'ENDOFFILE'
  1074	"""
  1075	Build the messages list for the Bedrock/Anthropic API from chat history.
  1076	"""
  1077	
  1078	from sqlalchemy.orm import Session
  1079	from backend.models import Chat, Message
  1080	
  1081	MAX_CONTEXT_CHARS = 400_000
  1082	MAX_MESSAGES = 80
  1083	
  1084	
  1085	def build_messages(chat: Chat, db: Session) -> list[dict]:
  1086	    rows: list[Message] = (
  1087	        db.query(Message)
  1088	        .filter(Message.chat_id == chat.id)
  1089	        .order_by(Message.created_at.desc())
  1090	        .limit(MAX_MESSAGES)
  1091	        .all()
  1092	    )
  1093	    rows.reverse()
  1094	
  1095	    if not rows:
  1096	        return []
  1097	
  1098	    total_chars = sum(len(m.content or "") for m in rows)
  1099	    idx = 0
  1100	    while total_chars > MAX_CONTEXT_CHARS and idx < len(rows) - 2:
  1101	        total_chars -= len(rows[idx].content or "")
  1102	        idx += 1
  1103	
  1104	    trimmed = rows[idx:]
  1105	
  1106	    while trimmed and trimmed[0].role != "user":
  1107	        trimmed = trimmed[1:]
  1108	
  1109	    result: list[dict] = []
  1110	    for m in trimmed:
  1111	        content = m.content or ""
  1112	        if not content.strip():
  1113	            continue
  1114	        role = m.role
  1115	        if role not in ("user", "assistant"):
  1116	            continue
  1117	        if result and result[-1]["role"] == role:
  1118	            result[-1]["content"] += "\n" + content
  1119	        else:
  1120	            result.append({"role": role, "content": content})
  1121	
  1122	    return result
  1123	ENDOFFILE
  1124	ok "backend/services/memory_service.py"
  1125	
  1126	# ────────────────────────────────────────
  1127	# frontend/src/api.js
  1128	# ────────────────────────────────────────
  1129	step "frontend/src/api.js"
  1130	cat > frontend/src/api.js << 'ENDOFFILE'
  1131	const BASE = "/api";
  1132	
  1133	function headers(token) {
  1134	  const h = { "Content-Type": "application/json" };
  1135	  if (token) h["Authorization"] = `Bearer ${token}`;
  1136	  return h;
  1137	}
  1138	
  1139	function authHeader(token) {
  1140	  return token ? { Authorization: `Bearer ${token}` } : {};
  1141	}
  1142	
  1143	async function request(method, path, token, body) {
  1144	  const opts = { method, headers: headers(token) };
  1145	  if (body) opts.body = JSON.stringify(body);
  1146	  const res = await fetch(`${BASE}${path}`, opts);
  1147	  if (!res.ok) {
  1148	    const err = await res.json().catch(() => ({ detail: res.statusText }));
  1149	    throw new Error(err.detail || err.message || "Request failed");
  1150	  }
  1151	  return res.json();
  1152	}
  1153	
  1154	export const login = (username, password) =>
  1155	  request("POST", "/auth/login", null, { username, password });
  1156	
  1157	export const register = (username, email, password) =>
  1158	  request("POST", "/auth/register", null, { username, email, password });
  1159	
  1160	export const getMe = (token) => request("GET", "/auth/me", token);
  1161	
  1162	export const listChats = (token) => request("GET", "/chats", token);
  1163	
  1164	export const createChat = (token, data = {}) => request("POST", "/chats", token, data);
  1165	
  1166	export const updateChat = (token, chatId, data) =>
  1167	  request("PUT", `/chats/${chatId}`, token, data);
  1168	
  1169	export const renameChat = (token, chatId, title) =>
  1170	  updateChat(token, chatId, { title });
  1171	
  1172	export const deleteChat = (token, chatId) =>
  1173	  request("DELETE", `/chats/${chatId}`, token);
  1174	
  1175	export const getMessages = (token, chatId) =>
  1176	  request("GET", `/chats/${chatId}/messages`, token);
  1177	
  1178	export async function* streamMessage(token, chatId, body, signal) {
  1179	  const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
  1180	    method: "POST", headers: headers(token),
  1181	    body: JSON.stringify(body), signal,
  1182	  });
  1183	  if (!res.ok) {
  1184	    const err = await res.json().catch(() => ({ detail: res.statusText }));
  1185	    throw new Error(err.detail || "Stream failed");
  1186	  }
  1187	  const reader = res.body.getReader();
  1188	  const decoder = new TextDecoder();
  1189	  let buffer = "";
  1190	  while (true) {
  1191	    const { done, value } = await reader.read();
  1192	    if (done) break;
  1193	    buffer += decoder.decode(value, { stream: true });
  1194	    const parts = buffer.split("\n\n");
  1195	    buffer = parts.pop() || "";
  1196	    for (const part of parts) {
  1197	      const line = part.trim();
  1198	      if (line.startsWith("data: ")) {
  1199	        try { yield JSON.parse(line.slice(6)); } catch { }
  1200	      }
  1201	    }
  1202	  }
  1203	  if (buffer.trim().startsWith("data: ")) {
  1204	    try { yield JSON.parse(buffer.trim().slice(6)); } catch { }
  1205	  }
  1206	}
  1207	
  1208	export async function uploadAttachments(token, chatId, files) {
  1209	  const form = new FormData();
  1210	  for (const file of files) form.append("files", file);
  1211	  const res = await fetch(`${BASE}/chats/${chatId}/attachments`, {
  1212	    method: "POST", headers: authHeader(token), body: form,
  1213	  });
  1214	  if (!res.ok) {
  1215	    const err = await res.json().catch(() => ({}));
  1216	    throw new Error(err.detail || "Upload failed");
  1217	  }
  1218	  return res.json();
  1219	}
  1220	
  1221	export function getAttachmentUrl(attachmentId) {
  1222	  return `${BASE}/attachments/${attachmentId}/file`;
  1223	}
  1224	
  1225	export const deleteAttachment = (token, attachmentId) =>
  1226	  request("DELETE", `/attachments/${attachmentId}`, token);
  1227	
  1228	export const listKnowledgeBases = (token) => request("GET", "/knowledge", token);
  1229	
  1230	export const createKnowledgeBase = (token, name, description = "") =>
  1231	  request("POST", "/knowledge", token, { name, description });
  1232	
  1233	export const getKnowledgeBase = (token, kbId) =>
  1234	  request("GET", `/knowledge/${kbId}`, token);
  1235	
  1236	export const deleteKnowledgeBase = (token, kbId) =>
  1237	  request("DELETE", `/knowledge/${kbId}`, token);
  1238	
  1239	export async function uploadDocuments(token, kbId, files) {
  1240	  const form = new FormData();
  1241	  for (const file of files) form.append("files", file);
  1242	  const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, {
  1243	    method: "POST", headers: authHeader(token), body: form,
  1244	  });
  1245	  if (!res.ok) {
  1246	    const err = await res.json().catch(() => ({}));
  1247	    throw new Error(err.detail || "Upload failed");
  1248	  }
  1249	  return res.json();
  1250	}
  1251	
  1252	export const uploadDocument = (token, kbId, file) =>
  1253	  uploadDocuments(token, kbId, [file]);
  1254	
  1255	export const adminStats = (token) => request("GET", "/admin/stats", token);
  1256	
  1257	export const adminListUsers = (token) => request("GET", "/admin/users", token);
  1258	
  1259	export const adminCreateUser = (token, data) =>
  1260	  request("POST", "/admin/users", token, data);
  1261	
  1262	export const adminUpdateUser = (token, userId, data) =>
  1263	  request("PUT", `/admin/users/${userId}`, token, data);
  1264	
  1265	export const adminDeleteUser = (token, userId) =>
  1266	  request("DELETE", `/admin/users/${userId}`, token);
  1267	
  1268	export const adminListChats = (token) => request("GET", "/admin/chats", token);
  1269	
  1270	export async function downloadZip(token, markdown) {
  1271	  const res = await fetch(`${BASE}/files/download-zip`, {
  1272	    method: "POST", headers: headers(token),
  1273	    body: JSON.stringify({ markdown }),
  1274	  });
  1275	  if (!res.ok) throw new Error("Download failed");
  1276	  const ct = res.headers.get("content-type") || "";
  1277	  if (ct.includes("application/zip")) {
  1278	    const blob = await res.blob();
  1279	    const url = URL.createObjectURL(blob);
  1280	    const a = document.createElement("a");
  1281	    a.href = url;
  1282	    a.download = "son-of-anton-code.zip";
  1283	    a.click();
  1284	    URL.revokeObjectURL(url);
  1285	  } else {
  1286	    const data = await res.json();
  1287	    if (data.error) throw new Error(data.error);
  1288	  }
  1289	}
  1290	ENDOFFILE
  1291	ok "frontend/src/api.js"
  1292	
  1293	# ────────────────────────────────────────
  1294	# frontend/src/components/MessageBubble.jsx
  1295	# ────────────────────────────────────────
  1296	step "frontend/src/components/MessageBubble.jsx"
  1297	cat > frontend/src/components/MessageBubble.jsx << 'ENDOFFILE'
  1298	import React, { useState } from "react";
  1299	import ReactMarkdown from "react-markdown";
  1300	import remarkGfm from "remark-gfm";
  1301	import CodeBlock from "./CodeBlock";
  1302	import { getAttachmentUrl } from "../api";
  1303	import {
  1304	  User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check,
  1305	  Image, Film, FileText, ExternalLink,
  1306	} from "lucide-react";
  1307	
  1308	const FILE_TYPE_ICONS = {
  1309	  image: Image, video: Film, document: FileText, text: FileText,
  1310	};
  1311	
  1312	const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, isThinking, token }) {
  1313	  const { role, content, thinking_content, input_tokens, output_tokens, attachments } = message;
  1314	  const isUser = role === "user";
  1315	  const [showThinking, setShowThinking] = useState(false);
  1316	  const [copied, setCopied] = useState(false);
  1317	  const [expandedImage, setExpandedImage] = useState(null);
  1318	
  1319	  function handleCopy() {
  1320	    navigator.clipboard.writeText(content || "");
  1321	    setCopied(true);
  1322	    setTimeout(() => setCopied(false), 2000);
  1323	  }
  1324	
  1325	  const hasAttachments = attachments && attachments.length > 0;
  1326	
  1327	  return (
  1328	    <div className={`flex gap-3 animate-fade-in ${isUser ? "justify-end" : ""}`}>
  1329	      {!isUser && (
  1330	        <div className="shrink-0 mt-1">
  1331	          <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/10">
  1332	            <Flame size={16} className="text-white" />
  1333	          </div>
  1334	        </div>
  1335	      )}
  1336	
  1337	      <div className={`max-w-[80%] ${isUser ? "order-first" : ""}`}>
  1338	        {thinking_content && (
  1339	          <div className="mb-2">
  1340	            <button onClick={() => setShowThinking(!showThinking)}
  1341	              className="flex items-center gap-1.5 text-xs text-purple-400 hover:text-purple-300 transition mb-1">
  1342	              <Brain size={12} />
  1343	              {showThinking ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
  1344	              {isThinking ? <span className="thinking-pulse">Reasoning…</span> : <span>View reasoning</span>}
  1345	            </button>
  1346	            {(showThinking || isThinking) && (
  1347	              <div className="bg-purple-500/5 border border-purple-500/20 rounded-lg p-3 text-xs text-purple-300/80 font-mono whitespace-pre-wrap max-h-60 overflow-y-auto">
  1348	                {thinking_content}
  1349	                {isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
  1350	              </div>
  1351	            )}
  1352	          </div>
  1353	        )}
  1354	
  1355	        {hasAttachments && (
  1356	          <div className="mb-2 flex flex-wrap gap-2">
  1357	            {attachments.map((att) => {
  1358	              const Icon = FILE_TYPE_ICONS[att.file_type] || FileText;
  1359	              const url = getAttachmentUrl(att.id);
  1360	              if (att.file_type === "image") {
  1361	                return (
  1362	                  <div key={att.id} className="relative group">
  1363	                    <img src={`${url}?token=${token}`} alt={att.original_filename}
  1364	                      className="max-w-[240px] max-h-[200px] rounded-lg border border-anton-border object-cover cursor-pointer hover:opacity-90 transition"
  1365	                      onClick={() => setExpandedImage(expandedImage === att.id ? null : att.id)}
  1366	                      onError={(e) => { e.target.style.display = "none"; }} />
  1367	                    {expandedImage === att.id && (
  1368	                      <div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-8 cursor-pointer"
  1369	                        onClick={() => setExpandedImage(null)}>
  1370	                        <img src={`${url}?token=${token}`} alt={att.original_filename}
  1371	                          className="max-w-full max-h-full object-contain rounded-lg" />
  1372	                      </div>
  1373	                    )}
  1374	                    <div className="absolute bottom-1 left-1 bg-black/60 text-[9px] text-white px-1.5 py-0.5 rounded">
  1375	                      {att.original_filename}
  1376	                    </div>
  1377	                  </div>
  1378	                );
  1379	              }
  1380	              return (
  1381	                <a key={att.id} href={`${url}?token=${token}`} target="_blank" rel="noopener noreferrer"
  1382	                  className="flex items-center gap-2 bg-anton-card border border-anton-border rounded-lg px-3 py-2 hover:border-anton-accent transition group">
  1383	                  <Icon size={16} className="shrink-0 text-blue-400" />
  1384	                  <div className="min-w-0">
  1385	                    <div className="text-xs text-white truncate max-w-[160px]">{att.original_filename}</div>
  1386	                    <div className="text-[10px] text-anton-muted">{(att.file_size / 1024).toFixed(0)}KB</div>
  1387	                  </div>
  1388	                  <ExternalLink size={12} className="text-anton-muted group-hover:text-anton-accent shrink-0" />
  1389	                </a>
  1390	              );
  1391	            })}
  1392	          </div>
  1393	        )}
  1394	
  1395	        <div className={`rounded-2xl px-4 py-3 ${
  1396	          isUser ? "bg-anton-accent text-white rounded-br-md" : "bg-anton-card border border-anton-border rounded-bl-md"
  1397	        }`}>
  1398	          {isUser ? (
  1399	            <div className="text-sm whitespace-pre-wrap">{_stripPrefixes(content)}</div>
  1400	          ) : (
  1401	            <div className="prose-anton text-sm">
  1402	              <ReactMarkdown remarkPlugins={[remarkGfm]} components={{
  1403	                code({ node, inline, className, children, ...props }) {
  1404	                  const match = /language-(\S+)/.exec(className || "");
  1405	                  const rawLang = match?.[1] || "";
  1406	                  if (inline) return <code className={className} {...props}>{children}</code>;
  1407	                  let lang = rawLang, filename = null;
  1408	                  if (rawLang.includes(":")) {
  1409	                    const idx = rawLang.indexOf(":");
  1410	                    lang = rawLang.slice(0, idx);
  1411	                    filename = rawLang.slice(idx + 1);
  1412	                  }
  1413	                  return <CodeBlock language={lang} filename={filename} code={String(children).replace(/\n$/, "")} />;
  1414	                },
  1415	                pre({ children }) { return <>{children}</>; },
  1416	              }}>
  1417	                {content || ""}
  1418	              </ReactMarkdown>
  1419	              {isStreaming && !isThinking && (
  1420	                <span className="inline-block w-1.5 h-4 bg-anton-accent ml-0.5 animate-pulse" />
  1421	              )}
  1422	            </div>
  1423	          )}
  1424	        </div>
  1425	
  1426	        {!isUser && !isStreaming && content && (
  1427	          <div className="flex items-center gap-3 mt-1.5 px-1">
  1428	            <button onClick={handleCopy} className="flex items-center gap-1 text-[11px] text-anton-muted hover:text-white transition">
  1429	              {copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
  1430	              {copied ? "Copied" : "Copy"}
  1431	            </button>
  1432	            {(input_tokens > 0 || output_tokens > 0) && (
  1433	              <span className="text-[11px] text-anton-muted">
  1434	                {input_tokens?.toLocaleString()}↓ / {output_tokens?.toLocaleString()}↑ tokens
  1435	              </span>
  1436	            )}
  1437	          </div>
  1438	        )}
  1439	      </div>
  1440	
  1441	      {isUser && (
  1442	        <div className="shrink-0 mt-1">
  1443	          <div className="w-8 h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center">
  1444	            <User size={16} className="text-anton-muted" />
  1445	          </div>
  1446	        </div>
  1447	      )}
  1448	    </div>
  1449	  );
  1450	});
  1451	
  1452	function _stripPrefixes(text) {
  1453	  if (!text) return "";
  1454	  return text.replace(/^\[(?:Image|Video|Document|File):\s[^\]]*\]\n?/gm, "").trim();
  1455	}
  1456	
  1457	export default MessageBubble;
  1458	ENDOFFILE
  1459	ok "frontend/src/components/MessageBubble.jsx"
  1460	
  1461	# ────────────────────────────────────────
  1462	# frontend/src/components/ChatView.jsx
  1463	# ────────────────────────────────────────
  1464	step "frontend/src/components/ChatView.jsx"
  1465	cat > frontend/src/components/ChatView.jsx << 'ENDOFFILE'
  1466	import React, { useState, useEffect, useRef, useCallback } from "react";
  1467	import { useApp } from "../store";
  1468	import { getMessages, downloadZip, listKnowledgeBases, updateChat, uploadAttachments } from "../api";
  1469	import * as streamManager from "../streamManager";
  1470	import MessageBubble from "./MessageBubble";
  1471	import { Send, Square, Settings2, X, Brain, BookOpen, Paperclip, Image, FileText, Film, Loader2 } from "lucide-react";
  1472	
  1473	const MODELS = [
  1474	  { id: "eu.anthropic.claude-opus-4-6-v1", label: "Claude Opus 4.6 (Primary)" },
  1475	  { id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Claude Haiku 4.5 (Fast)" },
  1476	];
  1477	
  1478	function classifyFile(file) {
  1479	  const ext = (file.name || "").split(".").pop().toLowerCase();
  1480	  const mime = file.type || "";
  1481	  if (mime.startsWith("image/") || ["jpg","jpeg","png","gif","webp","bmp"].includes(ext)) return "image";
  1482	  if (mime.startsWith("video/") || ["mp4","mov","avi","mkv","webm"].includes(ext)) return "video";
  1483	  if (mime === "application/pdf" || ext === "pdf") return "document";
  1484	  return "text";
  1485	}
  1486	
  1487	export default function ChatView({ chatId }) {
  1488	  const { state, dispatch } = useApp();
  1489	  const currentChat = state.chats.find((c) => c.id === chatId);
  1490	  const messages = state.chatMessages[chatId] || [];
  1491	  const isStreamingGlobal = !!state.activeStreams[chatId];
  1492	
  1493	  const [input, setInput] = useState("");
  1494	  const [showSettings, setShowSettings] = useState(false);
  1495	  const [model, setModel] = useState(currentChat?.model || MODELS[0].id);
  1496	  const [maxTokens, setMaxTokens] = useState(currentChat?.max_tokens || 4096);
  1497	  const [reasoningBudget, setReasoningBudget] = useState(currentChat?.reasoning_budget ?? 0);
  1498	  const [selectedKbId, setSelectedKbId] = useState(currentChat?.knowledge_base_id || null);
  1499	  const [kbs, setKbs] = useState([]);
  1500	  const [pendingFiles, setPendingFiles] = useState([]);
  1501	  const [uploading, setUploading] = useState(false);
  1502	  const [streamData, setStreamData] = useState(streamManager.getStreamData(chatId));
  1503	
  1504	  const scrollRef = useRef(null);
  1505	  const inputRef = useRef(null);
  1506	  const fileRef = useRef(null);
  1507	  const autoScroll = useRef(true);
  1508	  const rafRef = useRef(null);
  1509	
  1510	  useEffect(() => {
  1511	    setStreamData(streamManager.getStreamData(chatId));
  1512	    return streamManager.subscribe(chatId, () => setStreamData(streamManager.getStreamData(chatId)));
  1513	  }, [chatId]);
  1514	
  1515	  function onScroll() {
  1516	    const el = scrollRef.current;
  1517	    if (!el) return;
  1518	    autoScroll.current = el.scrollHeight - el.scrollTop - el.clientHeight < 200;
  1519	  }
  1520	
  1521	  const scrollBottom = useCallback(() => {
  1522	    if (!autoScroll.current || rafRef.current) return;
  1523	    rafRef.current = requestAnimationFrame(() => {
  1524	      if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  1525	      rafRef.current = null;
  1526	    });
  1527	  }, []);
  1528	
  1529	  useEffect(() => {
  1530	    (async () => {
  1531	      try {
  1532	        const [msgs, kbData] = await Promise.all([getMessages(state.token, chatId), listKnowledgeBases(state.token)]);
  1533	        dispatch({ type: "SET_MESSAGES", chatId, messages: msgs });
  1534	        setKbs(kbData);
  1535	      } catch {}
  1536	    })();
  1537	  }, [chatId, state.token, dispatch]);
  1538	
  1539	  useEffect(scrollBottom, [messages, streamData.text, streamData.thinking, scrollBottom]);
  1540	  useEffect(() => { inputRef.current?.focus(); }, [chatId]);
  1541	
  1542	  async function saveSettings() {
  1543	    try {
  1544	      await updateChat(state.token, chatId, { model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId || "" });
  1545	      dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId } });
  1546	    } catch {}
  1547	  }
  1548	
  1549	  function toggleSettings() {
  1550	    if (showSettings) saveSettings();
  1551	    setShowSettings(!showSettings);
  1552	  }
  1553	
  1554	  function handleFileSelect(e) {
  1555	    const files = Array.from(e.target.files || []);
  1556	    setPendingFiles((prev) => [...prev, ...files.map((f) => ({ file: f, type: classifyFile(f), preview: classifyFile(f) === "image" ? URL.createObjectURL(f) : null }))]);
  1557	    e.target.value = "";
  1558	  }
  1559	
  1560	  function removePending(i) {
  1561	    setPendingFiles((prev) => { if (prev[i]?.preview) URL.revokeObjectURL(prev[i].preview); return prev.filter((_, j) => j !== i); });
  1562	  }
  1563	
  1564	  async function handleSend() {
  1565	    const content = input.trim();
  1566	    if ((!content && !pendingFiles.length) || isStreamingGlobal) return;
  1567	    const text = content || "Please analyze the attached file(s).";
  1568	
  1569	    let attIds = [], uploaded = [];
  1570	    if (pendingFiles.length) {
  1571	      setUploading(true);
  1572	      try {
  1573	        const res = await uploadAttachments(state.token, chatId, pendingFiles.map((p) => p.file));
  1574	        uploaded = (res.attachments || []).filter((a) => !a.error);
  1575	        attIds = uploaded.map((a) => a.id);
  1576	      } catch (err) { console.error(err); setUploading(false); return; }
  1577	      setUploading(false);
  1578	    }
  1579	
  1580	    dispatch({ type: "ADD_MESSAGE", chatId, message: { id: `tmp-${Date.now()}`, role: "user", content: text, created_at: new Date().toISOString(), attachments: uploaded } });
  1581	    setInput("");
  1582	    pendingFiles.forEach((p) => { if (p.preview) URL.revokeObjectURL(p.preview); });
  1583	    setPendingFiles([]);
  1584	    autoScroll.current = true;
  1585	
  1586	    dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId } });
  1587	    streamManager.startStream({ token: state.token, chatId, body: { content: text, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId, attachment_ids: attIds } });
  1588	  }
  1589	
  1590	  function handleKeyDown(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }
  1591	
  1592	  function handlePaste(e) {
  1593	    const imgs = Array.from(e.clipboardData?.items || []).filter((i) => i.type.startsWith("image/"));
  1594	    if (!imgs.length) return;
  1595	    e.preventDefault();
  1596	    setPendingFiles((prev) => [...prev, ...imgs.map((i) => { const f = i.getAsFile(); return { file: f, type: "image", preview: URL.createObjectURL(f) }; })]);
  1597	  }
  1598	
  1599	  function handleDrop(e) {
  1600	    e.preventDefault();
  1601	    const files = Array.from(e.dataTransfer?.files || []);
  1602	    if (files.length) setPendingFiles((prev) => [...prev, ...files.map((f) => ({ file: f, type: classifyFile(f), preview: classifyFile(f) === "image" ? URL.createObjectURL(f) : null }))]);
  1603	  }
  1604	
  1605	  const streaming = streamData.streaming;
  1606	
  1607	  return (
  1608	    <div className="flex-1 flex flex-col min-h-0" onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
  1609	      <div ref={scrollRef} onScroll={onScroll} className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
  1610	        {messages.map((m) => <MessageBubble key={m.id} message={m} token={state.token} />)}
  1611	        {streaming && (streamData.thinking || streamData.text) && (
  1612	          <MessageBubble message={{ id: "streaming", role: "assistant", content: streamData.text, thinking_content: streamData.thinking || null, attachments: [] }} isStreaming isThinking={streamData.isThinking} token={state.token} />
  1613	        )}
  1614	        {streaming && !streamData.text && !streamData.thinking && (
  1615	          <div className="flex items-center gap-2 px-4 py-3 animate-fade-in">
  1616	            <div className="flex gap-1">
  1617	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "0ms" }} />
  1618	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "150ms" }} />
  1619	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "300ms" }} />
  1620	            </div>
  1621	            <span className="text-anton-muted text-sm">Son of Anton is thinking…</span>
  1622	          </div>
  1623	        )}
  1624	      </div>
  1625	
  1626	      <div className="border-t border-anton-border bg-anton-surface p-4">
  1627	        {showSettings && (
  1628	          <div className="mb-3 bg-anton-card border border-anton-border rounded-xl p-4 space-y-4 animate-fade-in">
  1629	            <div className="flex items-center justify-between">
  1630	              <h3 className="text-sm font-semibold text-white flex items-center gap-1.5"><Settings2 size={14} className="text-anton-accent" /> Settings</h3>
  1631	              <button onClick={toggleSettings} className="text-anton-muted hover:text-white"><X size={14} /></button>
  1632	            </div>
  1633	            <div>
  1634	              <label className="text-xs text-anton-muted mb-1 block">Model</label>
  1635	              <select value={model} onChange={(e) => setModel(e.target.value)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent">
  1636	                {MODELS.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
  1637	              </select>
  1638	            </div>
  1639	            <div>
  1640	              <div className="flex justify-between text-xs mb-1"><span className="text-anton-muted">Max Tokens</span><span className="text-anton-accent font-mono">{maxTokens.toLocaleString()}</span></div>
  1641	              <input type="range" min={256} max={65536} step={256} value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))} />
  1642	            </div>
  1643	            <div>
  1644	              <div className="flex justify-between text-xs mb-1"><span className="text-anton-muted flex items-center gap-1"><Brain size={12} className="text-purple-400" /> Reasoning</span><span className="text-purple-400 font-mono">{reasoningBudget === 0 ? "Off" : reasoningBudget.toLocaleString()}</span></div>
  1645	              <input type="range" min={0} max={32000} step={500} value={reasoningBudget} onChange={(e) => setReasoningBudget(Number(e.target.value))} />
  1646	            </div>
  1647	            <div>
  1648	              <label className="text-xs text-anton-muted mb-1 flex items-center gap-1"><BookOpen size={12} /> Knowledge Base</label>
  1649	              <select value={selectedKbId || ""} onChange={(e) => setSelectedKbId(e.target.value || null)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent">
  1650	                <option value="">None</option>
  1651	                {kbs.map((kb) => <option key={kb.id} value={kb.id}>{kb.name} ({kb.document_count} docs)</option>)}
  1652	              </select>
  1653	            </div>
  1654	          </div>
  1655	        )}
  1656	
  1657	        {pendingFiles.length > 0 && (
  1658	          <div className="mb-3 flex flex-wrap gap-2 animate-fade-in">
  1659	            {pendingFiles.map((pf, i) => (
  1660	              <div key={i} className="relative group bg-anton-card border border-anton-border rounded-lg overflow-hidden">
  1661	                {pf.type === "image" && pf.preview ? (
  1662	                  <img src={pf.preview} alt="" className="w-16 h-16 object-cover" />
  1663	                ) : (
  1664	                  <div className="w-16 h-16 flex flex-col items-center justify-center px-1">
  1665	                    <FileText size={20} className="text-anton-muted mb-1" />
  1666	                    <span className="text-[9px] text-anton-muted text-center truncate w-full">{pf.file.name.slice(0, 10)}</span>
  1667	                  </div>
  1668	                )}
  1669	                <button onClick={() => removePending(i)} className="absolute -top-1 -right-1 w-5 h-5 bg-anton-danger rounded-full flex items-center justify-center text-white opacity-0 group-hover:opacity-100 transition-opacity"><X size={10} /></button>
  1670	                <div className="absolute bottom-0 left-0 right-0 bg-black/60 text-[8px] text-white text-center py-0.5">{(pf.file.size / 1024).toFixed(0)}KB</div>
  1671	              </div>
  1672	            ))}
  1673	          </div>
  1674	        )}
  1675	
  1676	        <div className="flex items-end gap-2">
  1677	          <button onClick={toggleSettings} className={`p-2.5 rounded-xl transition shrink-0 ${showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card"}`}><Settings2 size={18} /></button>
  1678	          <button onClick={() => fileRef.current?.click()} className={`p-2.5 rounded-xl transition shrink-0 ${pendingFiles.length ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card"}`} title="Attach files"><Paperclip size={18} /></button>
  1679	          <input ref={fileRef} type="file" multiple className="hidden" accept="image/*,video/*,.pdf,.txt,.md,.py,.js,.ts,.jsx,.tsx,.cs,.java,.cpp,.c,.h,.go,.rs,.rb,.php,.html,.css,.json,.yaml,.yml,.xml,.toml,.csv,.sql,.sh,.swift,.kt,.lua,.dart,.vue,.svelte,.log" onChange={handleFileSelect} />
  1680	          <div className="flex-1 relative">
  1681	            <textarea ref={inputRef} value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} onPaste={handlePaste}
  1682	              placeholder={pendingFiles.length ? "Add a message or send to analyze files…" : "Ask Son of Anton anything…"}
  1683	              rows={1} style={{ maxHeight: "200px" }}
  1684	              className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white text-sm resize-none focus:outline-none focus:border-anton-accent transition"
  1685	              onInput={(e) => { e.target.style.height = "auto"; e.target.style.height = Math.min(e.target.scrollHeight, 200) + "px"; }} />
  1686	          </div>
  1687	          {streaming ? (
  1688	            <button onClick={() => streamManager.abortStream(chatId)} className="p-2.5 rounded-xl bg-anton-danger text-white hover:opacity-80 transition shrink-0"><Square size={18} /></button>
  1689	          ) : (
  1690	            <button onClick={handleSend} disabled={(!input.trim() && !pendingFiles.length) || isStreamingGlobal || uploading}
  1691	              className="p-2.5 rounded-xl bg-anton-accent text-white hover:opacity-80 transition shrink-0 disabled:opacity-30 disabled:cursor-not-allowed">
  1692	              {uploading ? <Loader2 size={18} className="animate-spin" /> : <Send size={18} />}
  1693	            </button>
  1694	          )}
  1695	        </div>
  1696	
  1697	        <div className="flex items-center gap-3 mt-2 text-[11px] text-anton-muted">
  1698	          <span>{MODELS.find((m) => m.id === model)?.label}</span>
  1699	          <span>•</span><span>{maxTokens.toLocaleString()} tokens</span>
  1700	          {reasoningBudget > 0 && <><span>•</span><span className="text-purple-400">🧠 {reasoningBudget.toLocaleString()}</span></>}
  1701	          {selectedKbId && <><span>•</span><span className="text-green-400">📚 RAG</span></>}
  1702	          {pendingFiles.length > 0 && <><span>•</span><span className="text-blue-400">📎 {pendingFiles.length} file{pendingFiles.length !== 1 ? "s" : ""}</span></>}
  1703	          {messages.some((m) => m.role === "assistant") && (
  1704	            <button onClick={async () => { const all = messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n"); if (all) try { await downloadZip(state.token, all); } catch {} }}
  1705	              className="ml-auto hover:text-anton-accent transition">⬇ Download code</button>
  1706	          )}
  1707	        </div>
  1708	      </div>
  1709	    </div>
  1710	  );
  1711	}
  1712	ENDOFFILE
  1713	ok "frontend/src/components/ChatView.jsx"
  1714	
  1715	# ────────────────────────────────────────
  1716	# frontend/src/streamManager.js
  1717	# ────────────────────────────────────────
  1718	step "frontend/src/streamManager.js"
  1719	cat > frontend/src/streamManager.js << 'ENDOFFILE'
  1720	import { streamMessage } from "./api";
  1721	
  1722	const _streams = new Map();
  1723	const _listeners = new Map();
  1724	let _dispatch = null;
  1725	
  1726	export function setDispatch(dispatch) { _dispatch = dispatch; }
  1727	
  1728	export function getStreamData(chatId) {
  1729	  const s = _streams.get(chatId);
  1730	  if (!s) return { streaming: false, text: "", thinking: "", isThinking: false };
  1731	  return { streaming: true, text: s.text, thinking: s.thinking, isThinking: s.isThinking };
  1732	}
  1733	
  1734	export function isStreaming(chatId) { return _streams.has(chatId); }
  1735	
  1736	export function subscribe(chatId, cb) {
  1737	  if (!_listeners.has(chatId)) _listeners.set(chatId, new Set());
  1738	  _listeners.get(chatId).add(cb);
  1739	  return () => { const s = _listeners.get(chatId); if (s) { s.delete(cb); if (!s.size) _listeners.delete(chatId); } };
  1740	}
  1741	
  1742	function _notify(id) { const s = _listeners.get(id); if (s) s.forEach((cb) => cb()); }
  1743	
  1744	export function abortStream(chatId) {
  1745	  const s = _streams.get(chatId);
  1746	  if (s) { s.abortController.abort(); _streams.delete(chatId); _notify(chatId); if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false }); }
  1747	}
  1748	
  1749	export function startStream({ token, chatId, body }) {
  1750	  if (_streams.has(chatId)) return;
  1751	  const ac = new AbortController();
  1752	  _streams.set(chatId, { text: "", thinking: "", isThinking: false, abortController: ac });
  1753	  if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: true });
  1754	  _notify(chatId);
  1755	
  1756	  (async () => {
  1757	    const s = _streams.get(chatId);
  1758	    if (!s) return;
  1759	    let usage = {}, msgId = "";
  1760	    try {
  1761	      for await (const evt of streamMessage(token, chatId, body, ac.signal)) {
  1762	        if (ac.signal.aborted || !_streams.has(chatId)) break;
  1763	        switch (evt.type) {
  1764	          case "thinking_start": s.isThinking = true; _notify(chatId); break;
  1765	          case "thinking_delta": s.thinking += evt.content; _notify(chatId); break;
  1766	          case "thinking_end": s.isThinking = false; _notify(chatId); break;
  1767	          case "text_delta": s.text += evt.content; _notify(chatId); break;
  1768	          case "usage": usage = { input_tokens: evt.input_tokens, output_tokens: evt.output_tokens }; break;
  1769	          case "title_update": if (_dispatch) _dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, title: evt.title } }); break;
  1770	          case "done": msgId = evt.message_id; break;
  1771	          case "error": s.text += `\n\n**Error:** ${evt.message}`; _notify(chatId); break;
  1772	        }
  1773	      }
  1774	      if (!ac.signal.aborted && _dispatch) {
  1775	        _dispatch({ type: "ADD_MESSAGE", chatId, message: {
  1776	          id: msgId || `gen-${Date.now()}`, role: "assistant", content: s.text,
  1777	          thinking_content: s.thinking || null, input_tokens: usage.input_tokens || 0,
  1778	          output_tokens: usage.output_tokens || 0, created_at: new Date().toISOString(), attachments: [],
  1779	        }});
  1780	      }
  1781	    } catch (err) {
  1782	      if (!ac.signal.aborted && _dispatch) {
  1783	        _dispatch({ type: "ADD_MESSAGE", chatId, message: {
  1784	          id: `err-${Date.now()}`, role: "assistant", content: `**Error:** ${err.message}`,
  1785	          created_at: new Date().toISOString(), attachments: [],
  1786	        }});
  1787	      }
  1788	    } finally {
  1789	      _streams.delete(chatId); _notify(chatId);
  1790	      if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false });
  1791	    }
  1792	  })();
  1793	}
  1794	ENDOFFILE
  1795	ok "frontend/src/streamManager.js"
  1796	
  1797	# ────────────────────────────────────────
  1798	# Done
  1799	# ────────────────────────────────────────
  1800	echo ""
  1801	echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
  1802	echo -e "${GREEN}${BOLD}  All 12 files written successfully!${NC}"
  1803	echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
  1804	echo ""
  1805	echo -e "  Now run: ${CYAN}zsh fix-and-deploy.sh${NC}"
  1806	echo ""│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [005]: atchfiles.sh


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [006/60]: backend/auth.py
│ LANGUAGE: python | LINES: 66 | SIZE: 2085 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	JWT authentication helpers.
     3	"""
     4	
     5	from datetime import datetime, timedelta
     6	
     7	import jwt
     8	from passlib.context import CryptContext
     9	from fastapi import Depends, HTTPException, status
    10	from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    11	from sqlalchemy.orm import Session
    12	
    13	from backend import config
    14	from backend.database import get_db
    15	from backend.models import User
    16	
    17	pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
    18	security = HTTPBearer()
    19	
    20	
    21	def hash_password(plain: str) -> str:
    22	    return pwd_ctx.hash(plain)
    23	
    24	
    25	def verify_password(plain: str, hashed: str) -> bool:
    26	    return pwd_ctx.verify(plain, hashed)
    27	
    28	
    29	def create_token(user_id: str, role: str) -> str:
    30	    payload = {
    31	        "sub": user_id,
    32	        "role": role,
    33	        "exp": datetime.utcnow() + timedelta(hours=config.JWT_EXPIRY_HOURS),
    34	    }
    35	    return jwt.encode(payload, config.JWT_SECRET, algorithm=config.JWT_ALGORITHM)
    36	
    37	
    38	def decode_token(token: str) -> dict:
    39	    try:
    40	        return jwt.decode(token, config.JWT_SECRET, algorithms=[config.JWT_ALGORITHM])
    41	    except jwt.ExpiredSignatureError:
    42	        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token expired")
    43	    except jwt.InvalidTokenError:
    44	        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
    45	
    46	
    47	def get_current_user(
    48	    creds: HTTPAuthorizationCredentials = Depends(security),
    49	    db: Session = Depends(get_db),
    50	) -> User:
    51	    payload = decode_token(creds.credentials)
    52	    user = db.query(User).filter(User.id == payload["sub"]).first()
    53	    if not user or not user.is_active:
    54	        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found or inactive")
    55	    return user
    56	
    57	
    58	def require_admin(user: User = Depends(get_current_user)) -> User:
    59	    if user.role not in ("admin", "superadmin"):
    60	        raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin access required")
    61	    return user
    62	
    63	
    64	def require_superadmin(user: User = Depends(get_current_user)) -> User:
    65	    if user.role != "superadmin":
    66	        raise HTTPException(status.HTTP_403_FORBIDDEN, "Superadmin access required")
    67	    return user│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [006]: backend/auth.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [007/60]: backend/config.py
│ LANGUAGE: python | LINES: 37 | SIZE: 1295 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Application configuration — reads from environment variables.
     3	"""
     4	
     5	import os
     6	import secrets
     7	
     8	BEDROCK_API_KEY: str = os.getenv(
     9	    "BEDROCK_API_KEY",
    10	    os.getenv("AWS_BEARER_TOKEN_BEDROCK", ""),
    11	)
    12	AWS_REGION: str = os.getenv("AWS_REGION", "eu-central-1")
    13	PRIMARY_MODEL: str = os.getenv("PRIMARY_MODEL", "eu.anthropic.claude-opus-4-6-v1")
    14	FAST_MODEL: str = os.getenv("FAST_MODEL", "eu.anthropic.claude-haiku-4-5-20251001-v1:0")
    15	
    16	JWT_SECRET: str = os.getenv("JWT_SECRET", secrets.token_hex(32))
    17	JWT_ALGORITHM: str = "HS256"
    18	JWT_EXPIRY_HOURS: int = 72
    19	
    20	SUPERADMIN_PASSWORD: str = os.getenv("SUPERADMIN_PASSWORD", "admin123")
    21	
    22	DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:////data/sonofanton.db")
    23	
    24	CHROMADB_PATH: str = os.getenv("CHROMADB_PATH", "/data/chromadb")
    25	UPLOAD_PATH: str = os.getenv("UPLOAD_PATH", "/data/uploads")
    26	ATTACHMENT_PATH: str = os.getenv("ATTACHMENT_PATH", "/data/uploads/chat_attachments")
    27	
    28	DEFAULT_QUOTA: int = int(os.getenv("DEFAULT_QUOTA", "2000000"))
    29	MAX_UPLOAD_BYTES: int = int(os.getenv("MAX_UPLOAD_MB", "50")) * 1024 * 1024
    30	MAX_ATTACHMENT_BYTES: int = int(os.getenv("MAX_ATTACHMENT_MB", "25")) * 1024 * 1024
    31	
    32	MAX_IMAGE_DIMENSION: int = 1568
    33	MAX_VIDEO_FRAMES: int = 6
    34	
    35	BEDROCK_ENDPOINT: str = (
    36	    f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
    37	)
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [007]: backend/config.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [008/60]: backend/database.py
│ LANGUAGE: python | LINES: 28 | SIZE: 777 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Database engine and session factory.
     3	"""
     4	
     5	import os
     6	from sqlalchemy import create_engine
     7	from sqlalchemy.orm import sessionmaker, declarative_base
     8	
     9	from backend.config import DATABASE_URL
    10	
    11	# Ensure the directory for SQLite exists
    12	if DATABASE_URL.startswith("sqlite"):
    13	    db_path = DATABASE_URL.replace("sqlite:///", "").replace("sqlite:////", "/")
    14	    os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
    15	    connect_args = {"check_same_thread": False}
    16	else:
    17	    connect_args = {}
    18	
    19	engine = create_engine(DATABASE_URL, connect_args=connect_args, pool_pre_ping=True)
    20	SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    21	Base = declarative_base()
    22	
    23	
    24	def get_db():
    25	    db = SessionLocal()
    26	    try:
    27	        yield db
    28	    finally:
    29	        db.close()│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [008]: backend/database.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [009/60]: backend/main.py
│ LANGUAGE: python | LINES: 155 | SIZE: 6039 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Son of Anton — Main FastAPI Application
     3	"""
     4	
     5	import os
     6	import time
     7	from pathlib import Path
     8	from contextlib import asynccontextmanager
     9	
    10	from fastapi import FastAPI, HTTPException, Request, Response
    11	from fastapi.staticfiles import StaticFiles
    12	from fastapi.responses import FileResponse
    13	from fastapi.middleware.cors import CORSMiddleware
    14	
    15	from backend.database import engine, Base
    16	from backend.seed import seed_superadmin
    17	from backend.routes.auth_routes import router as auth_router
    18	from backend.routes.chat_routes import router as chat_router
    19	from backend.routes.admin_routes import router as admin_router
    20	from backend.routes.knowledge_routes import router as knowledge_router
    21	from backend.routes.files_routes import router as files_router
    22	from backend.routes.attachment_routes import router as attachment_router
    23	from backend.routes.gitlab_routes import router as gitlab_router
    24	from backend.services.bedrock_service import close_http_client
    25	from backend.services.gitlab_service import close_gitlab_client
    26	
    27	APP_VERSION = "3.0.0"
    28	APP_BUILD_TIME = str(int(time.time()))
    29	
    30	
    31	def _run_migrations():
    32	    """Add new columns/tables to existing DB if they're missing."""
    33	    from sqlalchemy import inspect, text
    34	    try:
    35	        inspector = inspect(engine)
    36	        existing_tables = inspector.get_table_names()
    37	
    38	        if "chats" in existing_tables:
    39	            columns = {c["name"] for c in inspector.get_columns("chats")}
    40	            with engine.connect() as conn:
    41	                if "max_tokens" not in columns:
    42	                    conn.execute(text("ALTER TABLE chats ADD COLUMN max_tokens INTEGER DEFAULT 4096"))
    43	                    print("  Added chats.max_tokens column")
    44	                if "reasoning_budget" not in columns:
    45	                    conn.execute(text("ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"))
    46	                    print("  Added chats.reasoning_budget column")
    47	                conn.commit()
    48	
    49	        if "chat_attachments" not in existing_tables:
    50	            from backend.models import ChatAttachment
    51	            ChatAttachment.__table__.create(bind=engine, checkfirst=True)
    52	            print("  Created chat_attachments table")
    53	
    54	        # GitLab tables
    55	        for table_name in ("gitlab_configs", "gitlab_operations", "gitlab_audit_log"):
    56	            if table_name not in existing_tables:
    57	                from backend.models import GitLabConfig, GitLabOperation, GitLabAuditLog
    58	                table_map = {
    59	                    "gitlab_configs": GitLabConfig,
    60	                    "gitlab_operations": GitLabOperation,
    61	                    "gitlab_audit_log": GitLabAuditLog,
    62	                }
    63	                table_map[table_name].__table__.create(bind=engine, checkfirst=True)
    64	                print(f"  Created {table_name} table")
    65	
    66	    except Exception as e:
    67	        print(f"  Migration note: {e}")
    68	
    69	
    70	@asynccontextmanager
    71	async def lifespan(app: FastAPI):
    72	    Base.metadata.create_all(bind=engine)
    73	    _run_migrations()
    74	    seed_superadmin()
    75	    print(f"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
    76	    yield
    77	    await close_http_client()
    78	    await close_gitlab_client()
    79	    print("Son of Anton shutting down.")
    80	
    81	
    82	app = FastAPI(
    83	    title="Son of Anton",
    84	    description="Avatar of All Elements of Code",
    85	    version=APP_VERSION,
    86	    lifespan=lifespan,
    87	)
    88	
    89	app.add_middleware(
    90	    CORSMiddleware,
    91	    allow_origins=["*"],
    92	    allow_credentials=True,
    93	    allow_methods=["*"],
    94	    allow_headers=["*"],
    95	)
    96	
    97	
    98	@app.middleware("http")
    99	async def add_cache_headers(request: Request, call_next):
   100	    response: Response = await call_next(request)
   101	    path = request.url.path
   102	    if path.startswith("/api"):
   103	        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   104	        response.headers["Pragma"] = "no-cache"
   105	        response.headers["Expires"] = "0"
   106	    elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]):
   107	        response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
   108	    elif path.endswith(".html") or not path.startswith("/assets"):
   109	        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   110	        response.headers["Pragma"] = "no-cache"
   111	        response.headers["Expires"] = "0"
   112	    response.headers["X-App-Version"] = APP_VERSION
   113	    response.headers["X-Build-Time"] = APP_BUILD_TIME
   114	    return response
   115	
   116	
   117	@app.get("/api/version")
   118	def get_version():
   119	    return {"version": APP_VERSION, "build": APP_BUILD_TIME}
   120	
   121	
   122	app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
   123	app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
   124	app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
   125	app.include_router(knowledge_router, prefix="/api/knowledge", tags=["Knowledge"])
   126	app.include_router(files_router, prefix="/api/files", tags=["Files"])
   127	app.include_router(attachment_router, prefix="/api", tags=["Attachments"])
   128	app.include_router(gitlab_router, prefix="/api/gitlab", tags=["GitLab"])
   129	
   130	FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
   131	
   132	if (FRONTEND_DIR / "assets").exists():
   133	    app.mount(
   134	        "/assets",
   135	        StaticFiles(directory=str(FRONTEND_DIR / "assets")),
   136	        name="static-assets",
   137	    )
   138	
   139	
   140	@app.get("/{full_path:path}", include_in_schema=False)
   141	async def serve_frontend(full_path: str):
   142	    if full_path.startswith("api"):
   143	        raise HTTPException(status_code=404, detail="Not found")
   144	    file_path = FRONTEND_DIR / full_path
   145	    if full_path and file_path.is_file():
   146	        resp = FileResponse(str(file_path))
   147	        resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   148	        return resp
   149	    index = FRONTEND_DIR / "index.html"
   150	    if index.is_file():
   151	        resp = FileResponse(str(index))
   152	        resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   153	        resp.headers["Pragma"] = "no-cache"
   154	        resp.headers["Expires"] = "0"
   155	        return resp
   156	    return {"message": "Son of Anton API is running. Frontend not built."}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [009]: backend/main.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [010/60]: backend/migrations/005_attachments.sql
│ LANGUAGE: sql | LINES: 17 | SIZE: 818 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	-- Attachments table for chat file uploads
     2	CREATE TABLE IF NOT EXISTS attachments (
     3	    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
     4	    chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
     5	    message_id UUID REFERENCES messages(id) ON DELETE SET NULL,
     6	    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     7	    filename TEXT NOT NULL,
     8	    original_filename TEXT NOT NULL,
     9	    mime_type TEXT NOT NULL,
    10	    file_size INTEGER NOT NULL,
    11	    media_type TEXT NOT NULL CHECK (media_type IN ('image', 'video', 'document', 'text', 'unknown')),
    12	    storage_path TEXT NOT NULL,
    13	    created_at TIMESTAMPTZ DEFAULT NOW()
    14	);
    15	
    16	CREATE INDEX idx_attachments_chat ON attachments(chat_id);
    17	CREATE INDEX idx_attachments_message ON attachments(message_id);
    18	CREATE INDEX idx_attachments_user ON attachments(user_id);│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [010]: backend/migrations/005_attachments.sql


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [011/60]: backend/models.py
│ LANGUAGE: python | LINES: 171 | SIZE: 6712 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	SQLAlchemy ORM models.
     3	"""
     4	
     5	from datetime import datetime, timedelta
     6	from uuid import uuid4
     7	
     8	from sqlalchemy import (
     9	    Column, String, Text, Boolean, BigInteger, Integer, DateTime, ForeignKey,
    10	)
    11	from sqlalchemy.orm import relationship
    12	
    13	from backend.database import Base
    14	
    15	
    16	def new_id() -> str:
    17	    return str(uuid4())
    18	
    19	
    20	def next_month() -> datetime:
    21	    now = datetime.utcnow()
    22	    if now.month == 12:
    23	        return datetime(now.year + 1, 1, 1)
    24	    return datetime(now.year, now.month + 1, 1)
    25	
    26	
    27	class User(Base):
    28	    __tablename__ = "users"
    29	
    30	    id = Column(String(36), primary_key=True, default=new_id)
    31	    username = Column(String(50), unique=True, nullable=False, index=True)
    32	    email = Column(String(120), unique=True, nullable=False)
    33	    password_hash = Column(String(200), nullable=False)
    34	    role = Column(String(20), default="user")
    35	    is_active = Column(Boolean, default=True)
    36	    quota_tokens_monthly = Column(BigInteger, default=2_000_000)
    37	    tokens_used_this_month = Column(BigInteger, default=0)
    38	    quota_reset_date = Column(DateTime, default=next_month)
    39	    created_at = Column(DateTime, default=datetime.utcnow)
    40	
    41	    chats = relationship("Chat", back_populates="user", cascade="all,delete-orphan")
    42	
    43	
    44	class Chat(Base):
    45	    __tablename__ = "chats"
    46	
    47	    id = Column(String(36), primary_key=True, default=new_id)
    48	    user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    49	    title = Column(String(200), default="New Chat")
    50	    model = Column(String(100), default="eu.anthropic.claude-opus-4-6-v1")
    51	    knowledge_base_id = Column(String(36), nullable=True)
    52	    max_tokens = Column(Integer, default=4096)
    53	    reasoning_budget = Column(Integer, default=0)
    54	    created_at = Column(DateTime, default=datetime.utcnow)
    55	    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    56	
    57	    user = relationship("User", back_populates="chats")
    58	    messages = relationship(
    59	        "Message", back_populates="chat",
    60	        cascade="all,delete-orphan", order_by="Message.created_at",
    61	    )
    62	    attachments = relationship(
    63	        "ChatAttachment", back_populates="chat",
    64	        cascade="all,delete-orphan",
    65	    )
    66	
    67	
    68	class Message(Base):
    69	    __tablename__ = "messages"
    70	
    71	    id = Column(String(36), primary_key=True, default=new_id)
    72	    chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
    73	    role = Column(String(20), nullable=False)
    74	    content = Column(Text, default="")
    75	    thinking_content = Column(Text, nullable=True)
    76	    input_tokens = Column(Integer, default=0)
    77	    output_tokens = Column(Integer, default=0)
    78	    created_at = Column(DateTime, default=datetime.utcnow)
    79	
    80	    chat = relationship("Chat", back_populates="messages")
    81	    attachments = relationship("ChatAttachment", back_populates="message")
    82	
    83	
    84	class ChatAttachment(Base):
    85	    __tablename__ = "chat_attachments"
    86	
    87	    id = Column(String(36), primary_key=True, default=new_id)
    88	    chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
    89	    message_id = Column(String(36), ForeignKey("messages.id", ondelete="SET NULL"), nullable=True)
    90	    filename = Column(String(200), nullable=False)
    91	    original_filename = Column(String(200), nullable=False)
    92	    mime_type = Column(String(100), nullable=False)
    93	    file_type = Column(String(20), nullable=False)
    94	    file_size = Column(Integer, default=0)
    95	    storage_path = Column(String(500), nullable=False)
    96	    text_extract = Column(Text, nullable=True)
    97	    created_at = Column(DateTime, default=datetime.utcnow)
    98	
    99	    chat = relationship("Chat", back_populates="attachments")
   100	    message = relationship("Message", back_populates="attachments")
   101	
   102	
   103	class KnowledgeBase(Base):
   104	    __tablename__ = "knowledge_bases"
   105	
   106	    id = Column(String(36), primary_key=True, default=new_id)
   107	    user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
   108	    name = Column(String(200), nullable=False)
   109	    description = Column(Text, default="")
   110	    document_count = Column(Integer, default=0)
   111	    chunk_count = Column(Integer, default=0)
   112	    total_characters = Column(BigInteger, default=0)
   113	    created_at = Column(DateTime, default=datetime.utcnow)
   114	
   115	
   116	class KnowledgeDocument(Base):
   117	    __tablename__ = "knowledge_documents"
   118	
   119	    id = Column(String(36), primary_key=True, default=new_id)
   120	    knowledge_base_id = Column(
   121	        String(36), ForeignKey("knowledge_bases.id", ondelete="CASCADE"), nullable=False,
   122	    )
   123	    filename = Column(String(200), nullable=False)
   124	    file_size = Column(Integer, default=0)
   125	    chunk_count = Column(Integer, default=0)
   126	    created_at = Column(DateTime, default=datetime.utcnow)
   127	
   128	
   129	# ══════════════════════════════════════════════════════
   130	#  GitLab CE Integration Models
   131	# ══════════════════════════════════════════════════════
   132	
   133	class GitLabConfig(Base):
   134	    __tablename__ = "gitlab_configs"
   135	
   136	    id = Column(String(36), primary_key=True, default=new_id)
   137	    gitlab_url = Column(String(500), nullable=False)
   138	    access_token_enc = Column(String(500), nullable=False)
   139	    default_namespace = Column(String(200), nullable=True)
   140	    is_active = Column(Boolean, default=True)
   141	    created_at = Column(DateTime, default=datetime.utcnow)
   142	    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
   143	
   144	
   145	class GitLabOperation(Base):
   146	    __tablename__ = "gitlab_operations"
   147	
   148	    id = Column(String(36), primary_key=True, default=new_id)
   149	    operation_type = Column(String(50), nullable=False)
   150	    status = Column(String(20), default="pending")
   151	    project_id = Column(Integer, nullable=True)
   152	    project_name = Column(String(200), nullable=True)
   153	    branch = Column(String(200), nullable=True)
   154	    payload = Column(Text, nullable=False)
   155	    result = Column(Text, nullable=True)
   156	    chat_id = Column(String(36), nullable=True)
   157	    message_id = Column(String(36), nullable=True)
   158	    created_by = Column(String(36), ForeignKey("users.id"), nullable=False)
   159	    approved_by = Column(String(36), nullable=True)
   160	    created_at = Column(DateTime, default=datetime.utcnow)
   161	    executed_at = Column(DateTime, nullable=True)
   162	
   163	
   164	class GitLabAuditLog(Base):
   165	    __tablename__ = "gitlab_audit_log"
   166	
   167	    id = Column(String(36), primary_key=True, default=new_id)
   168	    operation_id = Column(String(36), nullable=True)
   169	    action = Column(String(100), nullable=False)
   170	    details = Column(Text, nullable=True)
   171	    user_id = Column(String(36), nullable=True)
   172	    created_at = Column(DateTime, default=datetime.utcnow)│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [011]: backend/models.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [012/60]: backend/routes/admin_routes.py
│ LANGUAGE: python | LINES: 129 | SIZE: 4351 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Superadmin routes: user management, stats, oversight.
     3	"""
     4	
     5	from pydantic import BaseModel
     6	from typing import Optional
     7	
     8	from fastapi import APIRouter, Depends, HTTPException
     9	from sqlalchemy.orm import Session
    10	from sqlalchemy import func
    11	
    12	from backend.database import get_db
    13	from backend.models import User, Chat, Message, KnowledgeBase
    14	from backend.auth import require_superadmin, hash_password
    15	
    16	router = APIRouter()
    17	
    18	
    19	class UpdateUserBody(BaseModel):
    20	    email: Optional[str] = None
    21	    role: Optional[str] = None
    22	    is_active: Optional[bool] = None
    23	    quota_tokens_monthly: Optional[int] = None
    24	    password: Optional[str] = None
    25	
    26	
    27	class CreateUserBody(BaseModel):
    28	    username: str
    29	    email: str
    30	    password: str
    31	    role: str = "user"
    32	    quota_tokens_monthly: int = 2_000_000
    33	
    34	
    35	@router.get("/stats")
    36	def get_stats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
    37	    return {
    38	        "total_users": db.query(User).count(),
    39	        "active_users": db.query(User).filter(User.is_active == True).count(),
    40	        "total_chats": db.query(Chat).count(),
    41	        "total_messages": db.query(Message).count(),
    42	        "total_tokens_used": db.query(func.sum(User.tokens_used_this_month)).scalar() or 0,
    43	        "total_knowledge_bases": db.query(KnowledgeBase).count(),
    44	    }
    45	
    46	
    47	@router.get("/users")
    48	def list_users(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
    49	    users = db.query(User).order_by(User.created_at.desc()).all()
    50	    result = []
    51	    for u in users:
    52	        chat_count = db.query(Chat).filter(Chat.user_id == u.id).count()
    53	        result.append({
    54	            "id": u.id,
    55	            "username": u.username,
    56	            "email": u.email,
    57	            "role": u.role,
    58	            "is_active": u.is_active,
    59	            "quota_tokens_monthly": u.quota_tokens_monthly,
    60	            "tokens_used_this_month": u.tokens_used_this_month,
    61	            "chat_count": chat_count,
    62	            "created_at": str(u.created_at),
    63	        })
    64	    return result
    65	
    66	
    67	@router.post("/users")
    68	def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
    69	    if db.query(User).filter(
    70	        (User.username == body.username) | (User.email == body.email)
    71	    ).first():
    72	        raise HTTPException(409, "Username or email taken")
    73	    user = User(
    74	        username=body.username,
    75	        email=body.email,
    76	        password_hash=hash_password(body.password),
    77	        role=body.role,
    78	        quota_tokens_monthly=body.quota_tokens_monthly,
    79	    )
    80	    db.add(user)
    81	    db.commit()
    82	    return {"id": user.id, "username": user.username}
    83	
    84	
    85	@router.put("/users/{user_id}")
    86	def update_user(user_id: str, body: UpdateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
    87	    user = db.query(User).filter(User.id == user_id).first()
    88	    if not user:
    89	        raise HTTPException(404)
    90	    if body.email is not None:
    91	        user.email = body.email
    92	    if body.role is not None:
    93	        user.role = body.role
    94	    if body.is_active is not None:
    95	        user.is_active = body.is_active
    96	    if body.quota_tokens_monthly is not None:
    97	        user.quota_tokens_monthly = body.quota_tokens_monthly
    98	    if body.password:
    99	        user.password_hash = hash_password(body.password)
   100	    db.commit()
   101	    return {"ok": True}
   102	
   103	
   104	@router.delete("/users/{user_id}")
   105	def delete_user(user_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
   106	    user = db.query(User).filter(User.id == user_id).first()
   107	    if not user:
   108	        raise HTTPException(404)
   109	    if user.role == "superadmin":
   110	        raise HTTPException(400, "Cannot delete superadmin")
   111	    db.delete(user)
   112	    db.commit()
   113	    return {"ok": True}
   114	
   115	
   116	@router.get("/chats")
   117	def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
   118	    chats = db.query(Chat).order_by(Chat.updated_at.desc()).limit(200).all()
   119	    result = []
   120	    for c in chats:
   121	        user = db.query(User).filter(User.id == c.user_id).first()
   122	        msg_count = db.query(Message).filter(Message.chat_id == c.id).count()
   123	        result.append({
   124	            "id": c.id,
   125	            "title": c.title,
   126	            "username": user.username if user else "?",
   127	            "message_count": msg_count,
   128	            "updated_at": str(c.updated_at),
   129	        })
   130	    return result│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [012]: backend/routes/admin_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [013/60]: backend/routes/attachment_routes.py
│ LANGUAGE: python | LINES: 125 | SIZE: 4445 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat attachment upload, serve, and delete routes.
     3	"""
     4	
     5	import os
     6	from typing import Optional
     7	
     8	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
     9	from fastapi.responses import FileResponse
    10	from sqlalchemy.orm import Session
    11	
    12	from backend.database import get_db
    13	from backend.models import User, Chat, ChatAttachment
    14	from backend.auth import get_current_user, decode_token
    15	from backend.services import attachment_service
    16	from backend.config import MAX_ATTACHMENT_BYTES
    17	
    18	router = APIRouter()
    19	
    20	
    21	def _get_user_flexible(request: Request, db: Session, token_param: Optional[str] = None) -> User:
    22	    raw_token = None
    23	    auth_header = request.headers.get("authorization", "")
    24	    if auth_header.startswith("Bearer "):
    25	        raw_token = auth_header[7:]
    26	    if not raw_token and token_param:
    27	        raw_token = token_param
    28	    if not raw_token:
    29	        raise HTTPException(401, "Authentication required")
    30	    payload = decode_token(raw_token)
    31	    user = db.query(User).filter(User.id == payload["sub"]).first()
    32	    if not user or not user.is_active:
    33	        raise HTTPException(401, "User not found or inactive")
    34	    return user
    35	
    36	
    37	@router.post("/chats/{chat_id}/attachments")
    38	async def upload_attachments(
    39	    chat_id: str,
    40	    files: list[UploadFile] = File(...),
    41	    user: User = Depends(get_current_user),
    42	    db: Session = Depends(get_db),
    43	):
    44	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    45	    if not chat:
    46	        raise HTTPException(404, "Chat not found")
    47	
    48	    results = []
    49	    for file in files:
    50	        filename = file.filename or "file"
    51	        try:
    52	            content = await file.read()
    53	            if len(content) > MAX_ATTACHMENT_BYTES:
    54	                results.append({"error": f"Too large: {filename}"})
    55	                continue
    56	
    57	            meta = attachment_service.save_attachment(
    58	                chat_id=chat_id, filename=filename,
    59	                content=content, content_type=file.content_type,
    60	            )
    61	
    62	            att = ChatAttachment(
    63	                id=meta["id"], chat_id=chat_id,
    64	                filename=meta["filename"],
    65	                original_filename=meta["original_filename"],
    66	                mime_type=meta["mime_type"],
    67	                file_type=meta["file_type"],
    68	                file_size=meta["file_size"],
    69	                storage_path=meta["storage_path"],
    70	                text_extract=meta.get("text_extract"),
    71	            )
    72	            db.add(att)
    73	            db.commit()
    74	            db.refresh(att)
    75	            results.append(_att_dict(att))
    76	        except Exception as e:
    77	            results.append({"error": f"Failed: {filename}: {str(e)}"})
    78	
    79	    return {"attachments": results}
    80	
    81	
    82	@router.get("/attachments/{attachment_id}/file")
    83	def serve_attachment(
    84	    attachment_id: str,
    85	    request: Request,
    86	    token: Optional[str] = Query(None),
    87	    db: Session = Depends(get_db),
    88	):
    89	    user = _get_user_flexible(request, db, token)
    90	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
    91	    if not att:
    92	        raise HTTPException(404, "Attachment not found")
    93	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
    94	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
    95	        raise HTTPException(403, "Access denied")
    96	    if not os.path.exists(att.storage_path):
    97	        raise HTTPException(404, "File not found on disk")
    98	    return FileResponse(att.storage_path, media_type=att.mime_type, filename=att.original_filename)
    99	
   100	
   101	@router.delete("/attachments/{attachment_id}")
   102	def delete_attachment(
   103	    attachment_id: str,
   104	    user: User = Depends(get_current_user),
   105	    db: Session = Depends(get_db),
   106	):
   107	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   108	    if not att:
   109	        raise HTTPException(404)
   110	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   111	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   112	        raise HTTPException(403)
   113	    attachment_service.delete_attachment_file(att.storage_path)
   114	    db.delete(att)
   115	    db.commit()
   116	    return {"ok": True}
   117	
   118	
   119	def _att_dict(att):
   120	    return {
   121	        "id": att.id, "chat_id": att.chat_id, "message_id": att.message_id,
   122	        "filename": att.filename, "original_filename": att.original_filename,
   123	        "mime_type": att.mime_type, "file_type": att.file_type,
   124	        "file_size": att.file_size, "created_at": str(att.created_at),
   125	    }
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [013]: backend/routes/attachment_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [014/60]: backend/routes/attachment_routes_15.py
│ LANGUAGE: python | LINES: 158 | SIZE: 5059 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat attachment upload, serve, and delete routes.
     3	"""
     4	
     5	import os
     6	from typing import Optional
     7	
     8	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
     9	from fastapi.responses import FileResponse
    10	from sqlalchemy.orm import Session
    11	
    12	from backend.database import get_db
    13	from backend.models import User, Chat, ChatAttachment
    14	from backend.auth import get_current_user, decode_token
    15	from backend.services import attachment_service
    16	from backend.config import MAX_ATTACHMENT_BYTES
    17	
    18	router = APIRouter()
    19	
    20	
    21	@router.post("/chats/{chat_id}/attachments")
    22	async def upload_attachments(
    23	    chat_id: str,
    24	    files: list[UploadFile] = File(...),
    25	    user: User = Depends(get_current_user),
    26	    db: Session = Depends(get_db),
    27	):
    28	    """Upload one or more files as chat attachments. Returns attachment metadata."""
    29	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    30	    if not chat:
    31	        raise HTTPException(404, "Chat not found")
    32	
    33	    results = []
    34	    for file in files:
    35	        filename = file.filename or "file"
    36	        try:
    37	            content = await file.read()
    38	            if len(content) > MAX_ATTACHMENT_BYTES:
    39	                results.append({
    40	                    "error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
    41	                })
    42	                continue
    43	
    44	            meta = attachment_service.save_attachment(
    45	                chat_id=chat_id,
    46	                filename=filename,
    47	                content=content,
    48	                content_type=file.content_type,
    49	            )
    50	
    51	            att = ChatAttachment(
    52	                id=meta["id"],
    53	                chat_id=chat_id,
    54	                filename=meta["filename"],
    55	                original_filename=meta["original_filename"],
    56	                mime_type=meta["mime_type"],
    57	                file_type=meta["file_type"],
    58	                file_size=meta["file_size"],
    59	                storage_path=meta["storage_path"],
    60	                text_extract=meta.get("text_extract"),
    61	            )
    62	            db.add(att)
    63	            db.commit()
    64	            db.refresh(att)
    65	
    66	            results.append(_att_dict(att))
    67	
    68	        except Exception as e:
    69	            results.append({"error": f"Failed to upload {filename}: {str(e)}"})
    70	
    71	    return {"attachments": results}
    72	
    73	
    74	@router.get("/attachments/{attachment_id}/file")
    75	def serve_attachment(
    76	    attachment_id: str,
    77	    token: Optional[str] = Query(None),
    78	    user: Optional[User] = Depends(_optional_current_user),
    79	    db: Session = Depends(get_db),
    80	):
    81	    """
    82	    Serve an attachment file.
    83	    Supports both Bearer header auth and ?token= query param
    84	    (needed for <img> tags that can't send headers).
    85	    """
    86	    # Try query param auth if header auth didn't work
    87	    if user is None and token:
    88	        try:
    89	            payload = decode_token(token)
    90	            user = db.query(User).filter(User.id == payload["sub"]).first()
    91	        except Exception:
    92	            pass
    93	
    94	    if user is None:
    95	        raise HTTPException(401, "Authentication required")
    96	
    97	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
    98	    if not att:
    99	        raise HTTPException(404, "Attachment not found")
   100	
   101	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   102	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   103	        raise HTTPException(403, "Access denied")
   104	
   105	    if not os.path.exists(att.storage_path):
   106	        raise HTTPException(404, "File not found on disk")
   107	
   108	    return FileResponse(
   109	        att.storage_path,
   110	        media_type=att.mime_type,
   111	        filename=att.original_filename,
   112	    )
   113	
   114	
   115	@router.delete("/attachments/{attachment_id}")
   116	def delete_attachment(
   117	    attachment_id: str,
   118	    user: User = Depends(get_current_user),
   119	    db: Session = Depends(get_db),
   120	):
   121	    """Delete a single attachment."""
   122	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   123	    if not att:
   124	        raise HTTPException(404)
   125	
   126	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   127	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   128	        raise HTTPException(403)
   129	
   130	    attachment_service.delete_attachment_file(att.storage_path)
   131	    db.delete(att)
   132	    db.commit()
   133	    return {"ok": True}
   134	
   135	
   136	def _optional_current_user(
   137	    db: Session = Depends(get_db),
   138	):
   139	    """
   140	    A dependency that tries to get current user but returns None on failure.
   141	    This allows the endpoint to also accept ?token= query param.
   142	    """
   143	    # This is a placeholder — the actual auth is handled in the route
   144	    # by checking both header and query param
   145	    return None
   146	
   147	
   148	def _att_dict(att: ChatAttachment) -> dict:
   149	    return {
   150	        "id": att.id,
   151	        "chat_id": att.chat_id,
   152	        "message_id": att.message_id,
   153	        "filename": att.filename,
   154	        "original_filename": att.original_filename,
   155	        "mime_type": att.mime_type,
   156	        "file_type": att.file_type,
   157	        "file_size": att.file_size,
   158	        "created_at": str(att.created_at),
   159	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [014]: backend/routes/attachment_routes_15.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [015/60]: backend/routes/attachment_routes_16.py
│ LANGUAGE: python | LINES: 164 | SIZE: 5228 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat attachment upload, serve, and delete routes.
     3	"""
     4	
     5	import os
     6	from typing import Optional
     7	
     8	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
     9	from fastapi.responses import FileResponse
    10	from sqlalchemy.orm import Session
    11	
    12	from backend.database import get_db
    13	from backend.models import User, Chat, ChatAttachment
    14	from backend.auth import get_current_user, decode_token
    15	from backend.services import attachment_service
    16	from backend.config import MAX_ATTACHMENT_BYTES
    17	
    18	router = APIRouter()
    19	
    20	
    21	def _get_user_from_request(request: Request, db: Session, token_param: Optional[str] = None) -> User:
    22	    """
    23	    Resolve user from either:
    24	    1. Authorization: Bearer <token> header
    25	    2. ?token=<token> query parameter (for img/video tags)
    26	    """
    27	    raw_token = None
    28	
    29	    # Try header first
    30	    auth_header = request.headers.get("authorization", "")
    31	    if auth_header.startswith("Bearer "):
    32	        raw_token = auth_header[7:]
    33	
    34	    # Fall back to query param
    35	    if not raw_token and token_param:
    36	        raw_token = token_param
    37	
    38	    if not raw_token:
    39	        raise HTTPException(401, "Authentication required")
    40	
    41	    payload = decode_token(raw_token)
    42	    user = db.query(User).filter(User.id == payload["sub"]).first()
    43	    if not user or not user.is_active:
    44	        raise HTTPException(401, "User not found or inactive")
    45	    return user
    46	
    47	
    48	@router.post("/chats/{chat_id}/attachments")
    49	async def upload_attachments(
    50	    chat_id: str,
    51	    files: list[UploadFile] = File(...),
    52	    user: User = Depends(get_current_user),
    53	    db: Session = Depends(get_db),
    54	):
    55	    """Upload one or more files as chat attachments. Returns attachment metadata."""
    56	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    57	    if not chat:
    58	        raise HTTPException(404, "Chat not found")
    59	
    60	    results = []
    61	    for file in files:
    62	        filename = file.filename or "file"
    63	        try:
    64	            content = await file.read()
    65	            if len(content) > MAX_ATTACHMENT_BYTES:
    66	                results.append({
    67	                    "error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
    68	                })
    69	                continue
    70	
    71	            meta = attachment_service.save_attachment(
    72	                chat_id=chat_id,
    73	                filename=filename,
    74	                content=content,
    75	                content_type=file.content_type,
    76	            )
    77	
    78	            att = ChatAttachment(
    79	                id=meta["id"],
    80	                chat_id=chat_id,
    81	                filename=meta["filename"],
    82	                original_filename=meta["original_filename"],
    83	                mime_type=meta["mime_type"],
    84	                file_type=meta["file_type"],
    85	                file_size=meta["file_size"],
    86	                storage_path=meta["storage_path"],
    87	                text_extract=meta.get("text_extract"),
    88	            )
    89	            db.add(att)
    90	            db.commit()
    91	            db.refresh(att)
    92	
    93	            results.append(_att_dict(att))
    94	
    95	        except Exception as e:
    96	            results.append({"error": f"Failed to upload {filename}: {str(e)}"})
    97	
    98	    return {"attachments": results}
    99	
   100	
   101	@router.get("/attachments/{attachment_id}/file")
   102	def serve_attachment(
   103	    attachment_id: str,
   104	    request: Request,
   105	    token: Optional[str] = Query(None),
   106	    db: Session = Depends(get_db),
   107	):
   108	    """
   109	    Serve an attachment file.
   110	    Supports both Bearer header auth and ?token= query param
   111	    (needed for <img> tags that can't send headers).
   112	    """
   113	    user = _get_user_from_request(request, db, token)
   114	
   115	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   116	    if not att:
   117	        raise HTTPException(404, "Attachment not found")
   118	
   119	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   120	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   121	        raise HTTPException(403, "Access denied")
   122	
   123	    if not os.path.exists(att.storage_path):
   124	        raise HTTPException(404, "File not found on disk")
   125	
   126	    return FileResponse(
   127	        att.storage_path,
   128	        media_type=att.mime_type,
   129	        filename=att.original_filename,
   130	    )
   131	
   132	
   133	@router.delete("/attachments/{attachment_id}")
   134	def delete_attachment(
   135	    attachment_id: str,
   136	    user: User = Depends(get_current_user),
   137	    db: Session = Depends(get_db),
   138	):
   139	    """Delete a single attachment."""
   140	    att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
   141	    if not att:
   142	        raise HTTPException(404)
   143	
   144	    chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
   145	    if not chat or (chat.user_id != user.id and user.role != "superadmin"):
   146	        raise HTTPException(403)
   147	
   148	    attachment_service.delete_attachment_file(att.storage_path)
   149	    db.delete(att)
   150	    db.commit()
   151	    return {"ok": True}
   152	
   153	
   154	def _att_dict(att: ChatAttachment) -> dict:
   155	    return {
   156	        "id": att.id,
   157	        "chat_id": att.chat_id,
   158	        "message_id": att.message_id,
   159	        "filename": att.filename,
   160	        "original_filename": att.original_filename,
   161	        "mime_type": att.mime_type,
   162	        "file_type": att.file_type,
   163	        "file_size": att.file_size,
   164	        "created_at": str(att.created_at),
   165	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [015]: backend/routes/attachment_routes_16.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [016/60]: backend/routes/attachments.py
│ LANGUAGE: python | LINES: 156 | SIZE: 4776 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Son of Anton — Attachment Upload Routes
     3	Handles file uploads for chat messages.
     4	"""
     5	
     6	import os
     7	import uuid
     8	import shutil
     9	from pathlib import Path
    10	from datetime import datetime, timezone
    11	
    12	from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form
    13	from typing import List
    14	
    15	from ..auth import get_current_user
    16	from ..database import get_db
    17	from ..services.file_processor import classify_media, validate_file
    18	
    19	router = APIRouter(prefix="/chats/{chat_id}/attachments", tags=["attachments"])
    20	
    21	UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
    22	Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
    23	
    24	
    25	@router.post("")
    26	async def upload_attachments(
    27	    chat_id: str,
    28	    files: List[UploadFile] = File(...),
    29	    user=Depends(get_current_user),
    30	    db=Depends(get_db),
    31	):
    32	    """Upload one or more files to a chat. Returns attachment metadata."""
    33	
    34	    # Verify chat belongs to user
    35	    chat = await db.fetchrow(
    36	        "SELECT id, user_id FROM chats WHERE id = $1", uuid.UUID(chat_id)
    37	    )
    38	    if not chat:
    39	        raise HTTPException(404, "Chat not found")
    40	    if str(chat["user_id"]) != str(user["id"]):
    41	        raise HTTPException(403, "Not your chat")
    42	
    43	    if len(files) > 10:
    44	        raise HTTPException(400, "Maximum 10 files per upload")
    45	
    46	    results = []
    47	
    48	    for f in files:
    49	        # Read file content to get size
    50	        content = await f.read()
    51	        size = len(content)
    52	
    53	        # Validate
    54	        ok, error = validate_file(f.filename or "file", f.content_type or "", size)
    55	        if not ok:
    56	            raise HTTPException(400, f"File '{f.filename}': {error}")
    57	
    58	        media_type = classify_media(f.content_type or "")
    59	
    60	        # Generate unique storage filename
    61	        ext = Path(f.filename or "file").suffix or ".bin"
    62	        storage_name = f"{uuid.uuid4().hex}{ext}"
    63	        chat_dir = Path(UPLOAD_DIR) / chat_id
    64	        chat_dir.mkdir(parents=True, exist_ok=True)
    65	        storage_path = f"{chat_id}/{storage_name}"
    66	        full_path = chat_dir / storage_name
    67	
    68	        # Write file
    69	        full_path.write_bytes(content)
    70	
    71	        # Insert DB record
    72	        att = await db.fetchrow(
    73	            """
    74	            INSERT INTO attachments (chat_id, user_id, filename, original_filename, mime_type, file_size, media_type, storage_path)
    75	            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
    76	            RETURNING id, filename, original_filename, mime_type, file_size, media_type, created_at
    77	            """,
    78	            uuid.UUID(chat_id),
    79	            user["id"],
    80	            storage_name,
    81	            f.filename or "file",
    82	            f.content_type or "application/octet-stream",
    83	            size,
    84	            media_type,
    85	            storage_path,
    86	        )
    87	
    88	        results.append({
    89	            "id": str(att["id"]),
    90	            "filename": att["original_filename"],
    91	            "mime_type": att["mime_type"],
    92	            "file_size": att["file_size"],
    93	            "media_type": att["media_type"],
    94	            "created_at": att["created_at"].isoformat(),
    95	        })
    96	
    97	    return results
    98	
    99	
   100	@router.get("")
   101	async def list_attachments(
   102	    chat_id: str,
   103	    user=Depends(get_current_user),
   104	    db=Depends(get_db),
   105	):
   106	    """List all attachments for a chat."""
   107	    chat = await db.fetchrow(
   108	        "SELECT id, user_id FROM chats WHERE id = $1", uuid.UUID(chat_id)
   109	    )
   110	    if not chat:
   111	        raise HTTPException(404, "Chat not found")
   112	    if str(chat["user_id"]) != str(user["id"]):
   113	        raise HTTPException(403, "Not your chat")
   114	
   115	    rows = await db.fetch(
   116	        """
   117	        SELECT id, original_filename as filename, mime_type, file_size, media_type, message_id, created_at
   118	        FROM attachments WHERE chat_id = $1 ORDER BY created_at
   119	        """,
   120	        uuid.UUID(chat_id),
   121	    )
   122	    return [dict(r) for r in rows]
   123	
   124	
   125	@router.get("/{attachment_id}/preview")
   126	async def preview_attachment(
   127	    chat_id: str,
   128	    attachment_id: str,
   129	    user=Depends(get_current_user),
   130	    db=Depends(get_db),
   131	):
   132	    """Return raw file content for preview (images mainly)."""
   133	    from fastapi.responses import FileResponse
   134	
   135	    att = await db.fetchrow(
   136	        """
   137	        SELECT a.*, c.user_id as chat_owner
   138	        FROM attachments a JOIN chats c ON a.chat_id = c.id
   139	        WHERE a.id = $1 AND a.chat_id = $2
   140	        """,
   141	        uuid.UUID(attachment_id),
   142	        uuid.UUID(chat_id),
   143	    )
   144	    if not att:
   145	        raise HTTPException(404, "Attachment not found")
   146	    if str(att["chat_owner"]) != str(user["id"]):
   147	        raise HTTPException(403, "Not your attachment")
   148	
   149	    full_path = Path(UPLOAD_DIR) / att["storage_path"]
   150	    if not full_path.exists():
   151	        raise HTTPException(404, "File not found on disk")
   152	
   153	    return FileResponse(
   154	        path=str(full_path),
   155	        media_type=att["mime_type"],
   156	        filename=att["original_filename"],
   157	    )│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [016]: backend/routes/attachments.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [017/60]: backend/routes/auth_routes.py
│ LANGUAGE: python | LINES: 90 | SIZE: 2416 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Authentication routes: register, login, profile.
     3	"""
     4	
     5	from pydantic import BaseModel, EmailStr
     6	from fastapi import APIRouter, Depends, HTTPException, status
     7	from sqlalchemy.orm import Session
     8	
     9	from backend.database import get_db
    10	from backend.models import User
    11	from backend.auth import (
    12	    hash_password, verify_password, create_token, get_current_user,
    13	)
    14	from backend import config
    15	
    16	router = APIRouter()
    17	
    18	
    19	class RegisterBody(BaseModel):
    20	    username: str
    21	    email: str
    22	    password: str
    23	
    24	
    25	class LoginBody(BaseModel):
    26	    username: str
    27	    password: str
    28	
    29	
    30	class ProfileOut(BaseModel):
    31	    id: str
    32	    username: str
    33	    email: str
    34	    role: str
    35	    is_active: bool
    36	    quota_tokens_monthly: int
    37	    tokens_used_this_month: int
    38	    created_at: str
    39	
    40	    class Config:
    41	        from_attributes = True
    42	
    43	
    44	@router.post("/register")
    45	def register(body: RegisterBody, db: Session = Depends(get_db)):
    46	    if db.query(User).filter(
    47	        (User.username == body.username) | (User.email == body.email)
    48	    ).first():
    49	        raise HTTPException(status.HTTP_409_CONFLICT, "Username or email already taken")
    50	
    51	    user = User(
    52	        username=body.username,
    53	        email=body.email,
    54	        password_hash=hash_password(body.password),
    55	        role="user",
    56	        quota_tokens_monthly=config.DEFAULT_QUOTA,
    57	    )
    58	    db.add(user)
    59	    db.commit()
    60	    db.refresh(user)
    61	    token = create_token(user.id, user.role)
    62	    return {"token": token, "user": _user_dict(user)}
    63	
    64	
    65	@router.post("/login")
    66	def login(body: LoginBody, db: Session = Depends(get_db)):
    67	    user = db.query(User).filter(User.username == body.username).first()
    68	    if not user or not verify_password(body.password, user.password_hash):
    69	        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid credentials")
    70	    if not user.is_active:
    71	        raise HTTPException(status.HTTP_403_FORBIDDEN, "Account disabled")
    72	    token = create_token(user.id, user.role)
    73	    return {"token": token, "user": _user_dict(user)}
    74	
    75	
    76	@router.get("/me")
    77	def me(user: User = Depends(get_current_user)):
    78	    return _user_dict(user)
    79	
    80	
    81	def _user_dict(u: User) -> dict:
    82	    return {
    83	        "id": u.id,
    84	        "username": u.username,
    85	        "email": u.email,
    86	        "role": u.role,
    87	        "is_active": u.is_active,
    88	        "quota_tokens_monthly": u.quota_tokens_monthly,
    89	        "tokens_used_this_month": u.tokens_used_this_month,
    90	        "created_at": str(u.created_at),
    91	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [017]: backend/routes/auth_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [018/60]: backend/routes/chat_routes.py
│ LANGUAGE: python | LINES: 193 | SIZE: 6476 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat CRUD and message streaming with multimodal attachment support.
     3	Generation runs in background and survives client disconnection.
     4	"""
     5	
     6	import json
     7	from datetime import datetime
     8	from pydantic import BaseModel
     9	from typing import Optional
    10	
    11	from fastapi import APIRouter, Depends, HTTPException
    12	from fastapi.responses import StreamingResponse
    13	from sqlalchemy.orm import Session
    14	
    15	from backend.database import get_db
    16	from backend.models import User, Chat, Message, ChatAttachment
    17	from backend.auth import get_current_user
    18	from backend.services import attachment_service
    19	from backend.services.generation_manager import manager as gen_manager
    20	
    21	router = APIRouter()
    22	
    23	
    24	class CreateChatBody(BaseModel):
    25	    title: str = "New Chat"
    26	    model: str = "eu.anthropic.claude-opus-4-6-v1"
    27	    knowledge_base_id: Optional[str] = None
    28	    max_tokens: int = 4096
    29	    reasoning_budget: int = 0
    30	
    31	
    32	class UpdateChatBody(BaseModel):
    33	    title: Optional[str] = None
    34	    model: Optional[str] = None
    35	    max_tokens: Optional[int] = None
    36	    reasoning_budget: Optional[int] = None
    37	    knowledge_base_id: Optional[str] = None
    38	
    39	
    40	class SendMessageBody(BaseModel):
    41	    content: str
    42	    model: Optional[str] = None
    43	    max_tokens: int = 4096
    44	    reasoning_budget: int = 0
    45	    knowledge_base_id: Optional[str] = None
    46	    attachment_ids: list[str] = []
    47	
    48	
    49	@router.get("")
    50	def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    51	    chats = db.query(Chat).filter(Chat.user_id == user.id).order_by(Chat.updated_at.desc()).all()
    52	    return [_chat_dict(c) for c in chats]
    53	
    54	
    55	@router.post("")
    56	def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    57	    chat = Chat(
    58	        user_id=user.id, title=body.title, model=body.model,
    59	        knowledge_base_id=body.knowledge_base_id or None,
    60	        max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget,
    61	    )
    62	    db.add(chat)
    63	    db.commit()
    64	    db.refresh(chat)
    65	    return _chat_dict(chat)
    66	
    67	
    68	@router.get("/{chat_id}")
    69	def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    70	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    71	    if not chat:
    72	        raise HTTPException(404, "Chat not found")
    73	    return _chat_dict(chat)
    74	
    75	
    76	@router.put("/{chat_id}")
    77	def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    78	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    79	    if not chat:
    80	        raise HTTPException(404)
    81	    if body.title is not None:
    82	        chat.title = body.title
    83	    if body.model is not None:
    84	        chat.model = body.model
    85	    if body.max_tokens is not None:
    86	        chat.max_tokens = body.max_tokens
    87	    if body.reasoning_budget is not None:
    88	        chat.reasoning_budget = body.reasoning_budget
    89	    if body.knowledge_base_id is not None:
    90	        chat.knowledge_base_id = body.knowledge_base_id or None
    91	    db.commit()
    92	    return _chat_dict(chat)
    93	
    94	
    95	@router.delete("/{chat_id}")
    96	def delete_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    97	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    98	    if not chat:
    99	        raise HTTPException(404)
   100	    attachment_service.delete_chat_attachments(chat_id)
   101	    db.delete(chat)
   102	    db.commit()
   103	    return {"ok": True}
   104	
   105	
   106	@router.get("/{chat_id}/messages")
   107	def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   108	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   109	    if not chat:
   110	        raise HTTPException(404)
   111	    msgs = []
   112	    for m in chat.messages:
   113	        d = _msg_dict(m)
   114	        atts = db.query(ChatAttachment).filter(ChatAttachment.message_id == m.id).all()
   115	        d["attachments"] = [_att_brief(a) for a in atts]
   116	        msgs.append(d)
   117	    return msgs
   118	
   119	
   120	@router.get("/{chat_id}/generating")
   121	def check_generating(chat_id: str, user: User = Depends(get_current_user)):
   122	    """Check if a background generation is active for this chat."""
   123	    return {"active": gen_manager.is_active(chat_id)}
   124	
   125	
   126	@router.get("/{chat_id}/stream")
   127	async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user)):
   128	    """Reconnect to an ongoing background generation's SSE stream."""
   129	    if not gen_manager.is_active(chat_id):
   130	        async def empty():
   131	            yield _sse({"type": "done", "message_id": ""})
   132	        return StreamingResponse(empty(), media_type="text/event-stream")
   133	
   134	    async def generate():
   135	        async for event in gen_manager.stream_events(chat_id):
   136	            yield _sse(event)
   137	
   138	    return StreamingResponse(generate(), media_type="text/event-stream")
   139	
   140	
   141	@router.post("/{chat_id}/messages")
   142	async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)):
   143	    """Send a message. Generation runs in background and survives disconnection."""
   144	    user_id = user.id
   145	
   146	    # Start background generation
   147	    gen_manager.start(
   148	        chat_id=chat_id,
   149	        user_id=user_id,
   150	        content=body.content,
   151	        model=body.model or "eu.anthropic.claude-opus-4-6-v1",
   152	        max_tokens=body.max_tokens,
   153	        reasoning_budget=body.reasoning_budget,
   154	        knowledge_base_id=body.knowledge_base_id,
   155	        attachment_ids=body.attachment_ids,
   156	    )
   157	
   158	    # Stream events from background task
   159	    async def generate():
   160	        async for event in gen_manager.stream_events(chat_id):
   161	            yield _sse(event)
   162	
   163	    return StreamingResponse(generate(), media_type="text/event-stream")
   164	
   165	
   166	def _sse(data):
   167	    return f"data: {json.dumps(data)}\n\n"
   168	
   169	
   170	def _chat_dict(c):
   171	    return {
   172	        "id": c.id, "title": c.title, "model": c.model,
   173	        "knowledge_base_id": c.knowledge_base_id,
   174	        "max_tokens": c.max_tokens or 4096,
   175	        "reasoning_budget": c.reasoning_budget or 0,
   176	        "created_at": str(c.created_at), "updated_at": str(c.updated_at),
   177	    }
   178	
   179	
   180	def _msg_dict(m):
   181	    return {
   182	        "id": m.id, "role": m.role, "content": m.content,
   183	        "thinking_content": m.thinking_content,
   184	        "input_tokens": m.input_tokens, "output_tokens": m.output_tokens,
   185	        "created_at": str(m.created_at),
   186	    }
   187	
   188	
   189	def _att_brief(a):
   190	    return {
   191	        "id": a.id, "original_filename": a.original_filename,
   192	        "mime_type": a.mime_type, "file_type": a.file_type,
   193	        "file_size": a.file_size,
   194	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [018]: backend/routes/chat_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [019/60]: backend/routes/files_routes.py
│ LANGUAGE: python | LINES: 62 | SIZE: 1664 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Code extraction and file download helpers.
     3	"""
     4	
     5	import io
     6	import re
     7	import zipfile
     8	from typing import Optional
     9	from pydantic import BaseModel
    10	
    11	from fastapi import APIRouter
    12	from fastapi.responses import StreamingResponse
    13	
    14	from backend.services.code_extractor import extract_code_blocks
    15	
    16	router = APIRouter()
    17	
    18	
    19	class ExtractBody(BaseModel):
    20	    markdown: str
    21	    title: Optional[str] = None
    22	
    23	
    24	@router.post("/extract")
    25	def extract_files(body: ExtractBody):
    26	    blocks = extract_code_blocks(body.markdown)
    27	    return {"files": blocks}
    28	
    29	
    30	@router.post("/download-zip")
    31	def download_zip(body: ExtractBody):
    32	    blocks = extract_code_blocks(body.markdown)
    33	    if not blocks:
    34	        return {"error": "No code blocks found"}
    35	
    36	    # Keep LAST occurrence of each filename (latest version wins)
    37	    file_map: dict[str, str] = {}
    38	    for b in blocks:
    39	        file_map[b["filename"]] = b["code"]
    40	
    41	    if not file_map:
    42	        return {"error": "No code blocks found"}
    43	
    44	    # Build a safe zip filename from chat title
    45	    raw_title = (body.title or "").strip()
    46	    if not raw_title or raw_title == "New Chat":
    47	        safe_title = "code"
    48	    else:
    49	        safe_title = re.sub(r'[^\w\s-]', '', raw_title).strip()
    50	        safe_title = re.sub(r'[\s]+', '-', safe_title)[:60] or "code"
    51	    zip_filename = f"{safe_title}.zip"
    52	
    53	    buf = io.BytesIO()
    54	    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
    55	        for name, code in file_map.items():
    56	            zf.writestr(name, code)
    57	
    58	    buf.seek(0)
    59	    return StreamingResponse(
    60	        buf,
    61	        media_type="application/zip",
    62	        headers={"Content-Disposition": f'attachment; filename="{zip_filename}"'},
    63	    )│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [019]: backend/routes/files_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [020/60]: backend/routes/gitlab_routes.py
│ LANGUAGE: python | LINES: 610 | SIZE: 22974 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	GitLab CE integration routes — superadmin only.
     3	Provides repo management, surgical code operations, approval queue, and audit log.
     4	"""
     5	
     6	import json
     7	from datetime import datetime
     8	from typing import Optional
     9	from pydantic import BaseModel
    10	
    11	from fastapi import APIRouter, Depends, HTTPException
    12	from sqlalchemy.orm import Session
    13	
    14	from backend.database import get_db
    15	from backend.models import User, GitLabConfig, GitLabOperation, GitLabAuditLog, new_id
    16	from backend.auth import require_superadmin
    17	from backend.services import gitlab_service
    18	
    19	router = APIRouter()
    20	
    21	
    22	# ══════════════════════════════════════════════════════
    23	#  Pydantic Bodies
    24	# ══════════════════════════════════════════════════════
    25	
    26	class SaveConfigBody(BaseModel):
    27	    gitlab_url: str
    28	    access_token: str
    29	    default_namespace: Optional[str] = None
    30	
    31	class CreateProjectBody(BaseModel):
    32	    name: str
    33	    description: str = ""
    34	    visibility: str = "private"
    35	    namespace_id: Optional[int] = None
    36	    initialize_with_readme: bool = True
    37	
    38	class FileOperationBody(BaseModel):
    39	    file_path: str
    40	    content: str
    41	    commit_message: str
    42	    branch: str = "main"
    43	
    44	class BatchCommitBody(BaseModel):
    45	    project_id: int
    46	    branch: str = "main"
    47	    commit_message: str
    48	    files: list[dict]  # [{"file_path": "...", "content": "..."}]
    49	
    50	class CreateBranchBody(BaseModel):
    51	    branch_name: str
    52	    ref: str = "main"
    53	
    54	class CreateMRBody(BaseModel):
    55	    source_branch: str
    56	    target_branch: str = "main"
    57	    title: str
    58	    description: str = ""
    59	
    60	class QueueOperationBody(BaseModel):
    61	    operation_type: str
    62	    project_id: Optional[int] = None
    63	    project_name: Optional[str] = None
    64	    branch: Optional[str] = None
    65	    payload: dict
    66	    chat_id: Optional[str] = None
    67	    message_id: Optional[str] = None
    68	
    69	
    70	def _get_config(db: Session) -> GitLabConfig:
    71	    cfg = db.query(GitLabConfig).filter(GitLabConfig.is_active == True).first()
    72	    if not cfg:
    73	        raise HTTPException(404, "GitLab not configured. Go to GitLab settings first.")
    74	    return cfg
    75	
    76	
    77	def _log_audit(db: Session, action: str, user_id: str, details: str = None, op_id: str = None):
    78	    entry = GitLabAuditLog(
    79	        operation_id=op_id,
    80	        action=action,
    81	        details=details,
    82	        user_id=user_id,
    83	    )
    84	    db.add(entry)
    85	    db.commit()
    86	
    87	
    88	# ══════════════════════════════════════════════════════
    89	#  Configuration
    90	# ══════════════════════════════════════════════════════
    91	
    92	@router.get("/config")
    93	def get_config(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
    94	    cfg = db.query(GitLabConfig).filter(GitLabConfig.is_active == True).first()
    95	    if not cfg:
    96	        return {"configured": False}
    97	    return {
    98	        "configured": True,
    99	        "id": cfg.id,
   100	        "gitlab_url": cfg.gitlab_url,
   101	        "token_masked": gitlab_service.mask_token(cfg.access_token_enc),
   102	        "default_namespace": cfg.default_namespace,
   103	        "updated_at": str(cfg.updated_at),
   104	    }
   105	
   106	
   107	@router.post("/config")
   108	async def save_config(
   109	    body: SaveConfigBody,
   110	    admin: User = Depends(require_superadmin),
   111	    db: Session = Depends(get_db),
   112	):
   113	    result = await gitlab_service.test_connection(body.gitlab_url, body.access_token)
   114	    if not result.get("ok"):
   115	        raise HTTPException(400, f"Connection failed: {result.get('error', 'Unknown error')}")
   116	
   117	    existing = db.query(GitLabConfig).filter(GitLabConfig.is_active == True).first()
   118	    if existing:
   119	        existing.gitlab_url = body.gitlab_url.rstrip("/")
   120	        existing.access_token_enc = gitlab_service.encode_token(body.access_token)
   121	        existing.default_namespace = body.default_namespace
   122	        existing.updated_at = datetime.utcnow()
   123	    else:
   124	        cfg = GitLabConfig(
   125	            gitlab_url=body.gitlab_url.rstrip("/"),
   126	            access_token_enc=gitlab_service.encode_token(body.access_token),
   127	            default_namespace=body.default_namespace,
   128	        )
   129	        db.add(cfg)
   130	    db.commit()
   131	
   132	    _log_audit(db, "config_saved", admin.id, f"Connected to {body.gitlab_url} as {result.get('user')}")
   133	
   134	    return {"ok": True, "user": result.get("user"), "name": result.get("name")}
   135	
   136	
   137	@router.post("/test-connection")
   138	async def test_connection(
   139	    body: SaveConfigBody,
   140	    admin: User = Depends(require_superadmin),
   141	):
   142	    result = await gitlab_service.test_connection(body.gitlab_url, body.access_token)
   143	    return result
   144	
   145	
   146	# ══════════════════════════════════════════════════════
   147	#  Projects
   148	# ══════════════════════════════════════════════════════
   149	
   150	@router.get("/projects")
   151	async def list_projects(
   152	    search: str = "",
   153	    page: int = 1,
   154	    admin: User = Depends(require_superadmin),
   155	    db: Session = Depends(get_db),
   156	):
   157	    cfg = _get_config(db)
   158	    token = gitlab_service.decode_token(cfg.access_token_enc)
   159	    return await gitlab_service.list_projects(cfg.gitlab_url, token, search=search, page=page)
   160	
   161	
   162	@router.post("/projects")
   163	async def create_project(
   164	    body: CreateProjectBody,
   165	    admin: User = Depends(require_superadmin),
   166	    db: Session = Depends(get_db),
   167	):
   168	    cfg = _get_config(db)
   169	    token = gitlab_service.decode_token(cfg.access_token_enc)
   170	    result = await gitlab_service.create_project(
   171	        cfg.gitlab_url, token, body.name, body.description,
   172	        body.visibility, body.namespace_id, body.initialize_with_readme,
   173	    )
   174	    _log_audit(db, "project_created", admin.id, f"Created project: {body.name} (id={result.get('id')})")
   175	    return result
   176	
   177	
   178	@router.delete("/projects/{project_id}")
   179	async def delete_project(
   180	    project_id: int,
   181	    admin: User = Depends(require_superadmin),
   182	    db: Session = Depends(get_db),
   183	):
   184	    cfg = _get_config(db)
   185	    token = gitlab_service.decode_token(cfg.access_token_enc)
   186	    ok = await gitlab_service.delete_project(cfg.gitlab_url, token, project_id)
   187	    if not ok:
   188	        raise HTTPException(500, "Failed to delete project")
   189	    _log_audit(db, "project_deleted", admin.id, f"Deleted project id={project_id}")
   190	    return {"ok": True}
   191	
   192	
   193	@router.get("/projects/{project_id}")
   194	async def get_project(
   195	    project_id: int,
   196	    admin: User = Depends(require_superadmin),
   197	    db: Session = Depends(get_db),
   198	):
   199	    cfg = _get_config(db)
   200	    token = gitlab_service.decode_token(cfg.access_token_enc)
   201	    return await gitlab_service.get_project(cfg.gitlab_url, token, project_id)
   202	
   203	
   204	# ══════════════════════════════════════════════════════
   205	#  Branches
   206	# ══════════════════════════════════════════════════════
   207	
   208	@router.get("/projects/{project_id}/branches")
   209	async def list_branches(
   210	    project_id: int,
   211	    admin: User = Depends(require_superadmin),
   212	    db: Session = Depends(get_db),
   213	):
   214	    cfg = _get_config(db)
   215	    token = gitlab_service.decode_token(cfg.access_token_enc)
   216	    return await gitlab_service.list_branches(cfg.gitlab_url, token, project_id)
   217	
   218	
   219	@router.post("/projects/{project_id}/branches")
   220	async def create_branch(
   221	    project_id: int,
   222	    body: CreateBranchBody,
   223	    admin: User = Depends(require_superadmin),
   224	    db: Session = Depends(get_db),
   225	):
   226	    cfg = _get_config(db)
   227	    token = gitlab_service.decode_token(cfg.access_token_enc)
   228	    result = await gitlab_service.create_branch(cfg.gitlab_url, token, project_id, body.branch_name, body.ref)
   229	    _log_audit(db, "branch_created", admin.id, f"Project {project_id}: created branch '{body.branch_name}' from '{body.ref}'")
   230	    return result
   231	
   232	
   233	# ══════════════════════════════════════════════════════
   234	#  File Tree & Files
   235	# ══════════════════════════════════════════════════════
   236	
   237	@router.get("/projects/{project_id}/tree")
   238	async def get_tree(
   239	    project_id: int,
   240	    path: str = "",
   241	    ref: str = "main",
   242	    recursive: bool = False,
   243	    admin: User = Depends(require_superadmin),
   244	    db: Session = Depends(get_db),
   245	):
   246	    cfg = _get_config(db)
   247	    token = gitlab_service.decode_token(cfg.access_token_enc)
   248	    return await gitlab_service.get_tree(cfg.gitlab_url, token, project_id, path, ref, recursive)
   249	
   250	
   251	@router.get("/projects/{project_id}/files")
   252	async def get_file(
   253	    project_id: int,
   254	    file_path: str,
   255	    ref: str = "main",
   256	    admin: User = Depends(require_superadmin),
   257	    db: Session = Depends(get_db),
   258	):
   259	    cfg = _get_config(db)
   260	    token = gitlab_service.decode_token(cfg.access_token_enc)
   261	    return await gitlab_service.get_file(cfg.gitlab_url, token, project_id, file_path, ref)
   262	
   263	
   264	# ══════════════════════════════════════════════════════
   265	#  Merge Requests
   266	# ══════════════════════════════════════════════════════
   267	
   268	@router.get("/projects/{project_id}/merge-requests")
   269	async def list_merge_requests(
   270	    project_id: int,
   271	    state: str = "opened",
   272	    admin: User = Depends(require_superadmin),
   273	    db: Session = Depends(get_db),
   274	):
   275	    cfg = _get_config(db)
   276	    token = gitlab_service.decode_token(cfg.access_token_enc)
   277	    return await gitlab_service.list_merge_requests(cfg.gitlab_url, token, project_id, state)
   278	
   279	
   280	@router.post("/projects/{project_id}/merge-requests")
   281	async def create_merge_request(
   282	    project_id: int,
   283	    body: CreateMRBody,
   284	    admin: User = Depends(require_superadmin),
   285	    db: Session = Depends(get_db),
   286	):
   287	    cfg = _get_config(db)
   288	    token = gitlab_service.decode_token(cfg.access_token_enc)
   289	    result = await gitlab_service.create_merge_request(
   290	        cfg.gitlab_url, token, project_id,
   291	        body.source_branch, body.target_branch, body.title, body.description,
   292	    )
   293	    _log_audit(db, "mr_created", admin.id, f"Project {project_id}: MR '{body.title}' ({body.source_branch} → {body.target_branch})")
   294	    return result
   295	
   296	
   297	# ══════════════════════════════════════════════════════
   298	#  Pipelines
   299	# ══════════════════════════════════════════════════════
   300	
   301	@router.get("/projects/{project_id}/pipelines")
   302	async def list_pipelines(
   303	    project_id: int,
   304	    admin: User = Depends(require_superadmin),
   305	    db: Session = Depends(get_db),
   306	):
   307	    cfg = _get_config(db)
   308	    token = gitlab_service.decode_token(cfg.access_token_enc)
   309	    return await gitlab_service.list_pipelines(cfg.gitlab_url, token, project_id)
   310	
   311	
   312	@router.get("/projects/{project_id}/pipelines/{pipeline_id}")
   313	async def get_pipeline(
   314	    project_id: int, pipeline_id: int,
   315	    admin: User = Depends(require_superadmin),
   316	    db: Session = Depends(get_db),
   317	):
   318	    cfg = _get_config(db)
   319	    token = gitlab_service.decode_token(cfg.access_token_enc)
   320	    pipeline = await gitlab_service.get_pipeline(cfg.gitlab_url, token, project_id, pipeline_id)
   321	    jobs = await gitlab_service.get_pipeline_jobs(cfg.gitlab_url, token, project_id, pipeline_id)
   322	    return {"pipeline": pipeline, "jobs": jobs}
   323	
   324	
   325	@router.post("/projects/{project_id}/pipelines/trigger")
   326	async def trigger_pipeline(
   327	    project_id: int,
   328	    ref: str = "main",
   329	    admin: User = Depends(require_superadmin),
   330	    db: Session = Depends(get_db),
   331	):
   332	    cfg = _get_config(db)
   333	    token = gitlab_service.decode_token(cfg.access_token_enc)
   334	    result = await gitlab_service.trigger_pipeline(cfg.gitlab_url, token, project_id, ref)
   335	    _log_audit(db, "pipeline_triggered", admin.id, f"Project {project_id}: triggered pipeline on '{ref}'")
   336	    return result
   337	
   338	
   339	# ══════════════════════════════════════════════════════
   340	#  Operation Queue (THE APPROVAL SYSTEM)
   341	# ══════════════════════════════════════════════════════
   342	
   343	@router.post("/operations")
   344	def queue_operation(
   345	    body: QueueOperationBody,
   346	    admin: User = Depends(require_superadmin),
   347	    db: Session = Depends(get_db),
   348	):
   349	    op = GitLabOperation(
   350	        operation_type=body.operation_type,
   351	        project_id=body.project_id,
   352	        project_name=body.project_name,
   353	        branch=body.branch,
   354	        payload=json.dumps(body.payload),
   355	        chat_id=body.chat_id,
   356	        message_id=body.message_id,
   357	        created_by=admin.id,
   358	    )
   359	    db.add(op)
   360	    db.commit()
   361	    db.refresh(op)
   362	    _log_audit(db, "operation_queued", admin.id, f"{body.operation_type} queued (id={op.id})", op.id)
   363	    return _op_dict(op)
   364	
   365	
   366	@router.get("/operations")
   367	def list_operations(
   368	    status: str = "pending",
   369	    admin: User = Depends(require_superadmin),
   370	    db: Session = Depends(get_db),
   371	):
   372	    q = db.query(GitLabOperation)
   373	    if status != "all":
   374	        q = q.filter(GitLabOperation.status == status)
   375	    ops = q.order_by(GitLabOperation.created_at.desc()).limit(100).all()
   376	    return [_op_dict(o) for o in ops]
   377	
   378	
   379	@router.post("/operations/{op_id}/approve")
   380	async def approve_operation(
   381	    op_id: str,
   382	    admin: User = Depends(require_superadmin),
   383	    db: Session = Depends(get_db),
   384	):
   385	    op = db.query(GitLabOperation).filter(GitLabOperation.id == op_id).first()
   386	    if not op:
   387	        raise HTTPException(404, "Operation not found")
   388	    if op.status != "pending":
   389	        raise HTTPException(400, f"Operation is {op.status}, not pending")
   390	
   391	    cfg = _get_config(db)
   392	    token = gitlab_service.decode_token(cfg.access_token_enc)
   393	    payload = json.loads(op.payload)
   394	
   395	    try:
   396	        result = await _execute_operation(cfg.gitlab_url, token, op.operation_type, op.project_id, op.branch, payload)
   397	        op.status = "executed"
   398	        op.result = json.dumps(result) if isinstance(result, (dict, list)) else str(result)
   399	        op.executed_at = datetime.utcnow()
   400	        op.approved_by = admin.id
   401	        db.commit()
   402	        _log_audit(db, "operation_approved_executed", admin.id, f"{op.operation_type} executed successfully", op.id)
   403	        return {"ok": True, "result": result}
   404	    except Exception as e:
   405	        op.status = "failed"
   406	        op.result = str(e)
   407	        op.executed_at = datetime.utcnow()
   408	        db.commit()
   409	        _log_audit(db, "operation_failed", admin.id, f"{op.operation_type} failed: {str(e)}", op.id)
   410	        raise HTTPException(500, f"Execution failed: {str(e)}")
   411	
   412	
   413	@router.post("/operations/{op_id}/reject")
   414	def reject_operation(
   415	    op_id: str,
   416	    admin: User = Depends(require_superadmin),
   417	    db: Session = Depends(get_db),
   418	):
   419	    op = db.query(GitLabOperation).filter(GitLabOperation.id == op_id).first()
   420	    if not op:
   421	        raise HTTPException(404)
   422	    if op.status != "pending":
   423	        raise HTTPException(400, f"Operation is {op.status}")
   424	    op.status = "rejected"
   425	    op.approved_by = admin.id
   426	    op.executed_at = datetime.utcnow()
   427	    db.commit()
   428	    _log_audit(db, "operation_rejected", admin.id, f"{op.operation_type} rejected", op.id)
   429	    return {"ok": True}
   430	
   431	
   432	@router.delete("/operations/{op_id}")
   433	def delete_operation(
   434	    op_id: str,
   435	    admin: User = Depends(require_superadmin),
   436	    db: Session = Depends(get_db),
   437	):
   438	    op = db.query(GitLabOperation).filter(GitLabOperation.id == op_id).first()
   439	    if not op:
   440	        raise HTTPException(404)
   441	    db.delete(op)
   442	    db.commit()
   443	    return {"ok": True}
   444	
   445	
   446	# ══════════════════════════════════════════════════════
   447	#  Direct Execute (bypass queue — for quick actions)
   448	# ══════════════════════════════════════════════════════
   449	
   450	@router.post("/execute/commit")
   451	async def direct_commit(
   452	    body: BatchCommitBody,
   453	    admin: User = Depends(require_superadmin),
   454	    db: Session = Depends(get_db),
   455	):
   456	    """Direct batch commit without queuing — for when you want instant execution."""
   457	    cfg = _get_config(db)
   458	    token = gitlab_service.decode_token(cfg.access_token_enc)
   459	    result = await gitlab_service.smart_batch_commit(
   460	        cfg.gitlab_url, token, body.project_id,
   461	        body.branch, body.commit_message, body.files,
   462	    )
   463	    _log_audit(
   464	        db, "direct_commit", admin.id,
   465	        f"Project {body.project_id}, branch '{body.branch}': {len(body.files)} files committed",
   466	    )
   467	    return result
   468	
   469	
   470	@router.post("/execute/file")
   471	async def direct_file_op(
   472	    body: dict,
   473	    admin: User = Depends(require_superadmin),
   474	    db: Session = Depends(get_db),
   475	):
   476	    """Direct single file create/update."""
   477	    cfg = _get_config(db)
   478	    token = gitlab_service.decode_token(cfg.access_token_enc)
   479	    project_id = body["project_id"]
   480	    file_path = body["file_path"]
   481	    content = body["content"]
   482	    commit_message = body.get("commit_message", f"Update {file_path}")
   483	    branch = body.get("branch", "main")
   484	    create = body.get("create", False)
   485	
   486	    result = await gitlab_service.create_or_update_file(
   487	        cfg.gitlab_url, token, project_id,
   488	        file_path, content, commit_message, branch, create,
   489	    )
   490	    action = "created" if create else "updated"
   491	    _log_audit(db, f"file_{action}", admin.id, f"Project {project_id}: {action} {file_path} on {branch}")
   492	    return result
   493	
   494	
   495	# ══════════════════════════════════════════════════════
   496	#  Audit Log
   497	# ══════════════════════════════════════════════════════
   498	
   499	@router.get("/audit-log")
   500	def get_audit_log(
   501	    page: int = 1,
   502	    per_page: int = 50,
   503	    admin: User = Depends(require_superadmin),
   504	    db: Session = Depends(get_db),
   505	):
   506	    total = db.query(GitLabAuditLog).count()
   507	    entries = (
   508	        db.query(GitLabAuditLog)
   509	        .order_by(GitLabAuditLog.created_at.desc())
   510	        .offset((page - 1) * per_page)
   511	        .limit(per_page)
   512	        .all()
   513	    )
   514	    return {
   515	        "total": total,
   516	        "page": page,
   517	        "entries": [
   518	            {
   519	                "id": e.id,
   520	                "operation_id": e.operation_id,
   521	                "action": e.action,
   522	                "details": e.details,
   523	                "user_id": e.user_id,
   524	                "created_at": str(e.created_at),
   525	            }
   526	            for e in entries
   527	        ],
   528	    }
   529	
   530	
   531	# ══════════════════════════════════════════════════════
   532	#  Namespaces
   533	# ══════════════════════════════════════════════════════
   534	
   535	@router.get("/namespaces")
   536	async def list_namespaces(
   537	    admin: User = Depends(require_superadmin),
   538	    db: Session = Depends(get_db),
   539	):
   540	    cfg = _get_config(db)
   541	    token = gitlab_service.decode_token(cfg.access_token_enc)
   542	    return await gitlab_service.list_namespaces(cfg.gitlab_url, token)
   543	
   544	
   545	# ══════════════════════════════════════════════════════
   546	#  Helpers
   547	# ══════════════════════════════════════════════════════
   548	
   549	async def _execute_operation(gitlab_url, token, op_type, project_id, branch, payload):
   550	    if op_type == "batch_commit":
   551	        return await gitlab_service.smart_batch_commit(
   552	            gitlab_url, token, project_id,
   553	            branch or "main", payload.get("commit_message", "Son of Anton commit"),
   554	            payload.get("files", []),
   555	        )
   556	    elif op_type == "create_file":
   557	        return await gitlab_service.create_or_update_file(
   558	            gitlab_url, token, project_id,
   559	            payload["file_path"], payload["content"],
   560	            payload.get("commit_message", f"Create {payload['file_path']}"),
   561	            branch or "main", create=True,
   562	        )
   563	    elif op_type == "update_file":
   564	        return await gitlab_service.create_or_update_file(
   565	            gitlab_url, token, project_id,
   566	            payload["file_path"], payload["content"],
   567	            payload.get("commit_message", f"Update {payload['file_path']}"),
   568	            branch or "main", create=False,
   569	        )
   570	    elif op_type == "create_project":
   571	        return await gitlab_service.create_project(
   572	            gitlab_url, token,
   573	            payload["name"], payload.get("description", ""),
   574	            payload.get("visibility", "private"),
   575	        )
   576	    elif op_type == "create_branch":
   577	        return await gitlab_service.create_branch(
   578	            gitlab_url, token, project_id,
   579	            payload["branch_name"], payload.get("ref", "main"),
   580	        )
   581	    elif op_type == "create_mr":
   582	        return await gitlab_service.create_merge_request(
   583	            gitlab_url, token, project_id,
   584	            payload["source_branch"], payload.get("target_branch", "main"),
   585	            payload["title"], payload.get("description", ""),
   586	        )
   587	    elif op_type == "trigger_pipeline":
   588	        return await gitlab_service.trigger_pipeline(
   589	            gitlab_url, token, project_id, payload.get("ref", branch or "main"),
   590	        )
   591	    else:
   592	        raise ValueError(f"Unknown operation type: {op_type}")
   593	
   594	
   595	def _op_dict(op: GitLabOperation) -> dict:
   596	    return {
   597	        "id": op.id,
   598	        "operation_type": op.operation_type,
   599	        "status": op.status,
   600	        "project_id": op.project_id,
   601	        "project_name": op.project_name,
   602	        "branch": op.branch,
   603	        "payload": json.loads(op.payload) if op.payload else {},
   604	        "result": json.loads(op.result) if op.result and op.result.startswith("{") else op.result,
   605	        "chat_id": op.chat_id,
   606	        "created_by": op.created_by,
   607	        "approved_by": op.approved_by,
   608	        "created_at": str(op.created_at),
   609	        "executed_at": str(op.executed_at) if op.executed_at else None,
   610	    }
   611	    │
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [020]: backend/routes/gitlab_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [021/60]: backend/routes/knowledge_routes.py
│ LANGUAGE: python | LINES: 309 | SIZE: 10336 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Knowledge base management and document upload.
     3	Full CRUD for knowledge bases AND their individual documents.
     4	"""
     5	
     6	from pydantic import BaseModel
     7	from typing import Optional
     8	
     9	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
    10	from sqlalchemy.orm import Session
    11	
    12	from backend.database import get_db
    13	from backend.models import User, KnowledgeBase, KnowledgeDocument
    14	from backend.auth import get_current_user
    15	from backend.services import rag_service
    16	from backend.config import MAX_UPLOAD_BYTES
    17	
    18	router = APIRouter()
    19	
    20	
    21	class CreateKBBody(BaseModel):
    22	    name: str
    23	    description: str = ""
    24	
    25	
    26	class UpdateKBBody(BaseModel):
    27	    name: Optional[str] = None
    28	    description: Optional[str] = None
    29	
    30	
    31	# ═══════════════════════════════════════════════════
    32	#  Knowledge Base CRUD
    33	# ═══════════════════════════════════════════════════
    34	
    35	@router.get("")
    36	def list_knowledge_bases(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    37	    kbs = db.query(KnowledgeBase).filter(
    38	        (KnowledgeBase.user_id == user.id) | (KnowledgeBase.user_id == None)
    39	    ).order_by(KnowledgeBase.created_at.desc()).all()
    40	    return [_kb_dict(kb) for kb in kbs]
    41	
    42	
    43	@router.post("")
    44	def create_kb(body: CreateKBBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    45	    kb = KnowledgeBase(user_id=user.id, name=body.name, description=body.description)
    46	    db.add(kb)
    47	    db.commit()
    48	    db.refresh(kb)
    49	    rag_service.create_collection(kb.id)
    50	    return _kb_dict(kb)
    51	
    52	
    53	@router.get("/{kb_id}")
    54	def get_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    55	    kb = _get_kb(kb_id, user, db)
    56	    docs = (
    57	        db.query(KnowledgeDocument)
    58	        .filter(KnowledgeDocument.knowledge_base_id == kb_id)
    59	        .order_by(KnowledgeDocument.created_at.desc())
    60	        .all()
    61	    )
    62	    return {
    63	        **_kb_dict(kb),
    64	        "documents": [_doc_dict(d) for d in docs],
    65	    }
    66	
    67	
    68	@router.put("/{kb_id}")
    69	def update_kb(
    70	    kb_id: str,
    71	    body: UpdateKBBody,
    72	    user: User = Depends(get_current_user),
    73	    db: Session = Depends(get_db),
    74	):
    75	    kb = _get_kb(kb_id, user, db)
    76	    if body.name is not None:
    77	        kb.name = body.name
    78	    if body.description is not None:
    79	        kb.description = body.description
    80	    db.commit()
    81	    db.refresh(kb)
    82	    return _kb_dict(kb)
    83	
    84	
    85	@router.delete("/{kb_id}")
    86	def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    87	    kb = _get_kb(kb_id, user, db)
    88	    try:
    89	        rag_service.delete_collection(kb_id)
    90	    except Exception:
    91	        pass
    92	    db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == kb_id).delete()
    93	    db.delete(kb)
    94	    db.commit()
    95	    return {"ok": True}
    96	
    97	
    98	# ═══════════════════════════════════════════════════
    99	#  Document Management
   100	# ═══════════════════════════════════════════════════
   101	
   102	@router.get("/{kb_id}/documents")
   103	def list_documents(
   104	    kb_id: str,
   105	    user: User = Depends(get_current_user),
   106	    db: Session = Depends(get_db),
   107	):
   108	    """List all documents in a knowledge base."""
   109	    _get_kb(kb_id, user, db)
   110	    docs = (
   111	        db.query(KnowledgeDocument)
   112	        .filter(KnowledgeDocument.knowledge_base_id == kb_id)
   113	        .order_by(KnowledgeDocument.created_at.desc())
   114	        .all()
   115	    )
   116	    return [_doc_dict(d) for d in docs]
   117	
   118	
   119	@router.delete("/{kb_id}/documents/{doc_id}")
   120	def delete_document(
   121	    kb_id: str,
   122	    doc_id: str,
   123	    user: User = Depends(get_current_user),
   124	    db: Session = Depends(get_db),
   125	):
   126	    """Delete a single document from a knowledge base, including its vector chunks."""
   127	    kb = _get_kb(kb_id, user, db)
   128	    doc = (
   129	        db.query(KnowledgeDocument)
   130	        .filter(KnowledgeDocument.id == doc_id, KnowledgeDocument.knowledge_base_id == kb_id)
   131	        .first()
   132	    )
   133	    if not doc:
   134	        raise HTTPException(404, "Document not found")
   135	
   136	    # Remove vector chunks from ChromaDB
   137	    try:
   138	        removed_count = rag_service.delete_document_chunks(kb_id, doc.filename)
   139	    except Exception:
   140	        removed_count = 0
   141	
   142	    # Update KB aggregate stats
   143	    kb.document_count = max((kb.document_count or 0) - 1, 0)
   144	    kb.chunk_count = max((kb.chunk_count or 0) - (doc.chunk_count or 0), 0)
   145	    # Estimate character reduction (rough: chunk_count * avg_chunk_size)
   146	    estimated_chars = (doc.chunk_count or 0) * 2000
   147	    kb.total_characters = max((kb.total_characters or 0) - estimated_chars, 0)
   148	
   149	    db.delete(doc)
   150	    db.commit()
   151	
   152	    return {
   153	        "ok": True,
   154	        "chunks_removed": removed_count,
   155	        "document_id": doc_id,
   156	        "filename": doc.filename,
   157	    }
   158	
   159	
   160	# ═══════════════════════════════════════════════════
   161	#  Upload Documents
   162	# ═══════════════════════════════════════════════════
   163	
   164	@router.post("/{kb_id}/upload")
   165	async def upload_documents(
   166	    kb_id: str,
   167	    files: list[UploadFile] = File(...),
   168	    user: User = Depends(get_current_user),
   169	    db: Session = Depends(get_db),
   170	):
   171	    """Upload one or more documents to a knowledge base."""
   172	    kb = _get_kb(kb_id, user, db)
   173	
   174	    results = []
   175	    total_new_docs = 0
   176	    total_new_chunks = 0
   177	    total_new_chars = 0
   178	
   179	    for file in files:
   180	        filename = file.filename or "document.txt"
   181	        try:
   182	            content_bytes = await file.read()
   183	
   184	            if len(content_bytes) > MAX_UPLOAD_BYTES:
   185	                results.append({
   186	                    "filename": filename,
   187	                    "error": f"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB",
   188	                })
   189	                continue
   190	
   191	            text = _extract_text(filename, content_bytes)
   192	
   193	            if not text.strip():
   194	                results.append({"filename": filename, "error": "Could not extract text from file"})
   195	                continue
   196	
   197	            chunks = _chunk_text(text, chunk_size=3000, overlap=300)
   198	
   199	            rag_service.add_documents(
   200	                collection_id=kb_id,
   201	                documents=chunks,
   202	                metadatas=[{"filename": filename, "chunk_index": i} for i in range(len(chunks))],
   203	            )
   204	
   205	            doc = KnowledgeDocument(
   206	                knowledge_base_id=kb_id,
   207	                filename=filename,
   208	                file_size=len(content_bytes),
   209	                chunk_count=len(chunks),
   210	            )
   211	            db.add(doc)
   212	
   213	            total_new_docs += 1
   214	            total_new_chunks += len(chunks)
   215	            total_new_chars += len(text)
   216	
   217	            results.append({
   218	                "filename": filename,
   219	                "chunks_added": len(chunks),
   220	                "characters": len(text),
   221	                "estimated_tokens": len(text) // 4,
   222	            })
   223	        except HTTPException as e:
   224	            results.append({"filename": filename, "error": str(e.detail)})
   225	        except Exception as e:
   226	            results.append({"filename": filename, "error": str(e)})
   227	
   228	    # Update KB aggregate stats
   229	    kb.document_count = (kb.document_count or 0) + total_new_docs
   230	    kb.chunk_count = (kb.chunk_count or 0) + total_new_chunks
   231	    kb.total_characters = (kb.total_characters or 0) + total_new_chars
   232	    db.commit()
   233	
   234	    return {
   235	        "files": results,
   236	        "total_files": len(results),
   237	        "total_chunks_added": total_new_chunks,
   238	        "total_characters": total_new_chars,
   239	    }
   240	
   241	
   242	# ═══════════════════════════════════════════════════
   243	#  Helpers
   244	# ═══════════════════════════════════════════════════
   245	
   246	def _get_kb(kb_id: str, user: User, db: Session) -> KnowledgeBase:
   247	    kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first()
   248	    if not kb:
   249	        raise HTTPException(404, "Knowledge base not found")
   250	    if kb.user_id and kb.user_id != user.id and user.role != "superadmin":
   251	        raise HTTPException(403, "Access denied")
   252	    return kb
   253	
   254	
   255	def _extract_text(filename: str, content: bytes) -> str:
   256	    lower = filename.lower()
   257	    if lower.endswith(".pdf"):
   258	        try:
   259	            from PyPDF2 import PdfReader
   260	            import io
   261	            reader = PdfReader(io.BytesIO(content))
   262	            return "\n\n".join(page.extract_text() or "" for page in reader.pages)
   263	        except Exception as e:
   264	            raise HTTPException(400, f"PDF extraction failed: {e}")
   265	    else:
   266	        try:
   267	            return content.decode("utf-8")
   268	        except UnicodeDecodeError:
   269	            return content.decode("latin-1")
   270	
   271	
   272	def _chunk_text(text: str, chunk_size: int = 3000, overlap: int = 300) -> list[str]:
   273	    chunks = []
   274	    start = 0
   275	    while start < len(text):
   276	        end = start + chunk_size
   277	        if end < len(text):
   278	            for sep in ["\n\n", "\n", ". ", " "]:
   279	                pos = text.rfind(sep, start + chunk_size // 2, end)
   280	                if pos > start:
   281	                    end = pos + len(sep)
   282	                    break
   283	        chunk = text[start:end].strip()
   284	        if chunk:
   285	            chunks.append(chunk)
   286	        start = end - overlap if end < len(text) else end
   287	    return chunks
   288	
   289	
   290	def _kb_dict(kb: KnowledgeBase) -> dict:
   291	    return {
   292	        "id": kb.id,
   293	        "name": kb.name,
   294	        "description": kb.description or "",
   295	        "document_count": kb.document_count or 0,
   296	        "chunk_count": kb.chunk_count or 0,
   297	        "total_characters": kb.total_characters or 0,
   298	        "estimated_tokens": (kb.total_characters or 0) // 4,
   299	        "created_at": str(kb.created_at),
   300	    }
   301	
   302	
   303	def _doc_dict(d: KnowledgeDocument) -> dict:
   304	    return {
   305	        "id": d.id,
   306	        "filename": d.filename,
   307	        "file_size": d.file_size or 0,
   308	        "chunk_count": d.chunk_count or 0,
   309	        "created_at": str(d.created_at),
   310	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [021]: backend/routes/knowledge_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [022/60]: backend/routes/messages_patch.py
│ LANGUAGE: python | LINES: 127 | SIZE: 3838 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	SON OF ANTON — INTEGRATION PATCH FOR YOUR EXISTING MESSAGE ROUTE
     3	
     4	This is NOT a standalone file. These are the functions and code blocks
     5	you need to ADD to your existing message-sending route (the one that
     6	handles POST /chats/{chat_id}/messages and streams SSE back).
     7	
     8	--- STEP 1: Add these imports at the top of your messages route file ---
     9	"""
    10	
    11	# Add to your imports:
    12	import os
    13	from ..services.file_processor import build_content_blocks_for_attachments
    14	
    15	UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
    16	
    17	
    18	"""
    19	--- STEP 2: In your request body model/parsing, add attachment_ids ---
    20	
    21	Your existing body probably looks like:
    22	  { content, model, max_tokens, reasoning_budget, knowledge_base_id }
    23	
    24	Add:
    25	  attachment_ids: list[str] = []
    26	
    27	So it becomes:
    28	  { content, model, max_tokens, reasoning_budget, knowledge_base_id, attachment_ids }
    29	"""
    30	
    31	
    32	"""
    33	--- STEP 3: Where you build the Claude messages array, replace the simple
    34	    text content with a content block array when attachments exist ---
    35	
    36	BEFORE (you probably have something like):
    37	    user_message_content = body.content
    38	    # or
    39	    messages_for_claude.append({"role": "user", "content": body.content})
    40	
    41	AFTER (replace with):
    42	"""
    43	
    44	
    45	async def build_user_content(db, body_content: str, attachment_ids: list, chat_id: str):
    46	    """
    47	    Build Claude content blocks for a user message.
    48	    If there are attachments, returns a list of content blocks.
    49	    If no attachments, returns the plain text string (backward compatible).
    50	    """
    51	    if not attachment_ids:
    52	        return [{"text": body_content}]
    53	
    54	    import uuid as uuid_mod
    55	
    56	    # Fetch attachment records
    57	    att_uuids = [uuid_mod.UUID(aid) for aid in attachment_ids]
    58	    attachments = await db.fetch(
    59	        """
    60	        SELECT id, filename, original_filename, mime_type, file_size, media_type, storage_path
    61	        FROM attachments
    62	        WHERE id = ANY($1) AND chat_id = $2
    63	        """,
    64	        att_uuids,
    65	        uuid_mod.UUID(chat_id),
    66	    )
    67	    attachments = [dict(a) for a in attachments]
    68	
    69	    # Build content blocks: text first, then file blocks
    70	    content_blocks = []
    71	
    72	    # Add the text message
    73	    if body_content.strip():
    74	        content_blocks.append({"text": body_content})
    75	
    76	    # Add file content blocks
    77	    file_blocks = build_content_blocks_for_attachments(attachments, UPLOAD_DIR)
    78	    content_blocks.extend(file_blocks)
    79	
    80	    # If no text was provided, add a default prompt
    81	    if not body_content.strip():
    82	        content_blocks.insert(0, {"text": "Please describe and analyze the attached file(s)."})
    83	
    84	    # Link attachments to the message (do after message is saved)
    85	    return content_blocks
    86	
    87	
    88	"""
    89	--- STEP 4: In your Claude API call, use the content blocks ---
    90	
    91	Instead of:
    92	    {"role": "user", "content": "user text here"}
    93	
    94	Use:
    95	    {"role": "user", "content": content_blocks}
    96	
    97	Where content_blocks comes from build_user_content() above.
    98	
    99	--- STEP 5: After saving the user message to DB, link attachments ---
   100	"""
   101	
   102	
   103	async def link_attachments_to_message(db, attachment_ids: list, message_id):
   104	    """Call this after inserting the user message into your messages table."""
   105	    import uuid as uuid_mod
   106	    if attachment_ids:
   107	        await db.execute(
   108	            "UPDATE attachments SET message_id = $1 WHERE id = ANY($2)",
   109	            message_id,
   110	            [uuid_mod.UUID(aid) for aid in attachment_ids],
   111	        )
   112	
   113	
   114	"""
   115	--- STEP 6: Register the attachments router in your main app file ---
   116	
   117	In your main.py or app.py, add:
   118	
   119	    from .routes.attachments import router as attachments_router
   120	    app.include_router(attachments_router, prefix="/api")
   121	
   122	--- STEP 7: Also serve uploaded files statically (optional, for image previews) ---
   123	
   124	    from fastapi.staticfiles import StaticFiles
   125	    app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
   126	
   127	--- DONE. That's it for the backend. ---
   128	"""│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [022]: backend/routes/messages_patch.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [023/60]: backend/seed.py
│ LANGUAGE: python | LINES: 28 | SIZE: 855 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Seed the superadmin account on first startup.
     3	"""
     4	
     5	from backend.database import SessionLocal
     6	from backend.models import User
     7	from backend.auth import hash_password
     8	from backend import config
     9	
    10	
    11	def seed_superadmin():
    12	    db = SessionLocal()
    13	    try:
    14	        existing = db.query(User).filter(User.username == "superadmin").first()
    15	        if not existing:
    16	            admin = User(
    17	                username="superadmin",
    18	                email="admin@sonofanton.local",
    19	                password_hash=hash_password(config.SUPERADMIN_PASSWORD),
    20	                role="superadmin",
    21	                quota_tokens_monthly=999_999_999,
    22	            )
    23	            db.add(admin)
    24	            db.commit()
    25	            print("✅ Superadmin account created.")
    26	        else:
    27	            print("ℹ️  Superadmin account already exists.")
    28	    finally:
    29	        db.close()│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [023]: backend/seed.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [024/60]: backend/services/attachment_service.py
│ LANGUAGE: python | LINES: 260 | SIZE: 8733 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Attachment processing service.
     3	Handles images, videos (frame extraction), PDFs, and text files.
     4	"""
     5	
     6	import os
     7	import io
     8	import base64
     9	import shutil
    10	import subprocess
    11	import tempfile
    12	import mimetypes
    13	from pathlib import Path
    14	from uuid import uuid4
    15	from typing import Optional
    16	
    17	from backend import config
    18	
    19	os.makedirs(config.ATTACHMENT_PATH, exist_ok=True)
    20	
    21	IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
    22	VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv", ".m4v"}
    23	PDF_EXTENSIONS = {".pdf"}
    24	TEXT_EXTENSIONS = {
    25	    ".txt", ".md", ".py", ".js", ".ts", ".jsx", ".tsx", ".cs", ".java",
    26	    ".cpp", ".c", ".h", ".hpp", ".go", ".rs", ".rb", ".php", ".swift",
    27	    ".kt", ".lua", ".gd", ".html", ".css", ".scss", ".json", ".yaml",
    28	    ".yml", ".xml", ".toml", ".ini", ".cfg", ".conf", ".sh", ".bash",
    29	    ".sql", ".r", ".dart", ".vue", ".svelte", ".csv", ".log", ".env",
    30	}
    31	
    32	IMAGE_MIMES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
    33	VIDEO_MIMES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm"}
    34	
    35	
    36	def classify_file(filename, mime):
    37	    ext = Path(filename).suffix.lower()
    38	    if ext in IMAGE_EXTENSIONS or mime in IMAGE_MIMES:
    39	        return "image"
    40	    if ext in VIDEO_EXTENSIONS or mime in VIDEO_MIMES:
    41	        return "video"
    42	    if ext in PDF_EXTENSIONS or mime == "application/pdf":
    43	        return "document"
    44	    return "text"
    45	
    46	
    47	def get_mime_type(filename, content_type=None):
    48	    if content_type and content_type != "application/octet-stream":
    49	        return content_type
    50	    mime, _ = mimetypes.guess_type(filename)
    51	    return mime or "application/octet-stream"
    52	
    53	
    54	def save_attachment(chat_id, filename, content, content_type=None):
    55	    mime = get_mime_type(filename, content_type)
    56	    file_type = classify_file(filename, mime)
    57	    attachment_id = str(uuid4())
    58	
    59	    chat_dir = os.path.join(config.ATTACHMENT_PATH, chat_id)
    60	    os.makedirs(chat_dir, exist_ok=True)
    61	
    62	    safe_name = Path(filename).name.replace(" ", "_")
    63	    stored_name = f"{attachment_id}_{safe_name}"
    64	    storage_path = os.path.join(chat_dir, stored_name)
    65	
    66	    with open(storage_path, "wb") as f:
    67	        f.write(content)
    68	
    69	    text_extract = None
    70	    if file_type == "text":
    71	        text_extract = _extract_text_content(storage_path)
    72	    elif file_type == "document":
    73	        text_extract = _extract_pdf_text(storage_path)
    74	
    75	    return {
    76	        "id": attachment_id,
    77	        "filename": stored_name,
    78	        "original_filename": filename,
    79	        "mime_type": mime,
    80	        "file_type": file_type,
    81	        "file_size": len(content),
    82	        "storage_path": storage_path,
    83	        "text_extract": text_extract,
    84	    }
    85	
    86	
    87	def delete_attachment_file(storage_path):
    88	    try:
    89	        if os.path.exists(storage_path):
    90	            os.remove(storage_path)
    91	    except Exception:
    92	        pass
    93	
    94	
    95	def delete_chat_attachments(chat_id):
    96	    chat_dir = os.path.join(config.ATTACHMENT_PATH, chat_id)
    97	    if os.path.isdir(chat_dir):
    98	        shutil.rmtree(chat_dir, ignore_errors=True)
    99	
   100	
   101	def build_claude_content_blocks(attachments):
   102	    blocks = []
   103	    for att in attachments:
   104	        try:
   105	            result = _process_single_attachment(att)
   106	            if isinstance(result, list):
   107	                blocks.extend(result)
   108	            elif result:
   109	                blocks.append(result)
   110	        except Exception as e:
   111	            blocks.append({
   112	                "type": "text",
   113	                "text": f"[Failed to process {att.original_filename}: {str(e)}]",
   114	            })
   115	    return blocks
   116	
   117	
   118	def _process_single_attachment(att):
   119	    if att.file_type == "image":
   120	        return _build_image_block(att)
   121	    elif att.file_type == "video":
   122	        return _build_video_blocks(att)
   123	    elif att.file_type == "document":
   124	        return _build_document_block(att)
   125	    elif att.file_type == "text":
   126	        return _build_text_block(att)
   127	    return None
   128	
   129	
   130	def _build_image_block(att):
   131	    data = _read_and_resize_image(att.storage_path, att.mime_type)
   132	    mime = att.mime_type if att.mime_type in IMAGE_MIMES else "image/jpeg"
   133	    return {
   134	        "type": "image",
   135	        "source": {"type": "base64", "media_type": mime, "data": data},
   136	    }
   137	
   138	
   139	def _build_video_blocks(att):
   140	    frames = _extract_video_frames(att.storage_path)
   141	    if not frames:
   142	        return [{"type": "text", "text": f"[Video: {att.original_filename} - could not extract frames]"}]
   143	    blocks = [{"type": "text", "text": f"[Video: {att.original_filename} - {len(frames)} frames extracted]"}]
   144	    for frame_b64 in frames:
   145	        blocks.append({
   146	            "type": "image",
   147	            "source": {"type": "base64", "media_type": "image/jpeg", "data": frame_b64},
   148	        })
   149	    return blocks
   150	
   151	
   152	def _build_document_block(att):
   153	    if att.mime_type == "application/pdf":
   154	        with open(att.storage_path, "rb") as f:
   155	            data = base64.b64encode(f.read()).decode("utf-8")
   156	        return {
   157	            "type": "document",
   158	            "source": {"type": "base64", "media_type": "application/pdf", "data": data},
   159	        }
   160	    return _build_text_block(att)
   161	
   162	
   163	def _build_text_block(att):
   164	    text = att.text_extract or _extract_text_content(att.storage_path)
   165	    if not text:
   166	        text = f"[Could not extract text from {att.original_filename}]"
   167	    return {
   168	        "type": "text",
   169	        "text": f"--- File: {att.original_filename} ---\n{text}\n--- End of {att.original_filename} ---",
   170	    }
   171	
   172	
   173	def _read_and_resize_image(path, mime_type):
   174	    try:
   175	        from PIL import Image
   176	        img = Image.open(path)
   177	        if img.mode in ("RGBA", "LA", "P"):
   178	            bg = Image.new("RGB", img.size, (255, 255, 255))
   179	            if img.mode == "P":
   180	                img = img.convert("RGBA")
   181	            bg.paste(img, mask=img.split()[-1] if "A" in img.mode else None)
   182	            img = bg
   183	        elif img.mode != "RGB":
   184	            img = img.convert("RGB")
   185	        mx = config.MAX_IMAGE_DIMENSION
   186	        if img.width > mx or img.height > mx:
   187	            ratio = min(mx / img.width, mx / img.height)
   188	            img = img.resize((int(img.width * ratio), int(img.height * ratio)), Image.LANCZOS)
   189	        buf = io.BytesIO()
   190	        fmt = "PNG" if mime_type == "image/png" else "JPEG"
   191	        kwargs = {"quality": 85} if fmt == "JPEG" else {}
   192	        img.save(buf, format=fmt, **kwargs)
   193	        return base64.b64encode(buf.getvalue()).decode("utf-8")
   194	    except ImportError:
   195	        with open(path, "rb") as f:
   196	            return base64.b64encode(f.read()).decode("utf-8")
   197	    except Exception:
   198	        with open(path, "rb") as f:
   199	            return base64.b64encode(f.read()).decode("utf-8")
   200	
   201	
   202	def _extract_video_frames(video_path):
   203	    if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
   204	        return []
   205	    frames = []
   206	    try:
   207	        result = subprocess.run(
   208	            ["ffprobe", "-v", "error", "-show_entries", "format=duration",
   209	             "-of", "default=noprint_wrappers=1:nokey=1", video_path],
   210	            capture_output=True, text=True, timeout=30,
   211	        )
   212	        duration = float(result.stdout.strip() or "0")
   213	        if duration <= 0:
   214	            return []
   215	        max_frames = config.MAX_VIDEO_FRAMES
   216	        with tempfile.TemporaryDirectory() as tmpdir:
   217	            interval = duration / (max_frames + 1)
   218	            for i in range(max_frames):
   219	                ts = interval * (i + 1)
   220	                out = os.path.join(tmpdir, f"frame_{i}.jpg")
   221	                subprocess.run(
   222	                    ["ffmpeg", "-ss", str(ts), "-i", video_path, "-vframes", "1",
   223	                     "-vf", f"scale='min({config.MAX_IMAGE_DIMENSION},iw)':'min({config.MAX_IMAGE_DIMENSION},ih)':force_original_aspect_ratio=decrease",
   224	                     "-q:v", "3", out],
   225	                    capture_output=True, timeout=30,
   226	                )
   227	                if os.path.exists(out) and os.path.getsize(out) > 0:
   228	                    with open(out, "rb") as f:
   229	                        frames.append(base64.b64encode(f.read()).decode("utf-8"))
   230	    except Exception:
   231	        pass
   232	    return frames
   233	
   234	
   235	def _extract_text_content(path):
   236	    try:
   237	        with open(path, "r", encoding="utf-8") as f:
   238	            return f.read(500_000)
   239	    except UnicodeDecodeError:
   240	        try:
   241	            with open(path, "r", encoding="latin-1") as f:
   242	                return f.read(500_000)
   243	        except Exception:
   244	            return None
   245	    except Exception:
   246	        return None
   247	
   248	
   249	def _extract_pdf_text(path):
   250	    try:
   251	        from PyPDF2 import PdfReader
   252	        reader = PdfReader(path)
   253	        pages = []
   254	        for page in reader.pages[:100]:
   255	            text = page.extract_text()
   256	            if text:
   257	                pages.append(text)
   258	        return "\n\n".join(pages) if pages else None
   259	    except Exception:
   260	        return None
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [024]: backend/services/attachment_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [025/60]: backend/services/bedrock_service.py
│ LANGUAGE: python | LINES: 242 | SIZE: 8418 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	AWS Bedrock integration — streaming and non-streaming.
     3	Uses Bearer-token auth and parses the AWS binary event-stream wire format.
     4	"""
     5	
     6	import struct
     7	import json
     8	import base64
     9	from typing import AsyncIterator, Optional
    10	from urllib.parse import quote
    11	
    12	import httpx
    13	
    14	from backend import config
    15	
    16	_client: Optional[httpx.AsyncClient] = None
    17	
    18	
    19	def _get_client() -> httpx.AsyncClient:
    20	    global _client
    21	    if _client is None or _client.is_closed:
    22	        _client = httpx.AsyncClient(
    23	            timeout=httpx.Timeout(connect=30.0, read=600.0, write=30.0, pool=60.0),
    24	        )
    25	    return _client
    26	
    27	
    28	async def close_http_client():
    29	    global _client
    30	    if _client and not _client.is_closed:
    31	        await _client.aclose()
    32	        _client = None
    33	
    34	
    35	# ══════════════════════════════════════════════════════════════
    36	# AWS Event-Stream Binary Parser
    37	# ══════════════════════════════════════════════════════════════
    38	#
    39	# Message layout (big-endian):
    40	#   [total_length : 4B]
    41	#   [headers_length : 4B]
    42	#   [prelude_crc : 4B]
    43	#   [headers : headers_length B]
    44	#   [payload : total_length - headers_length - 16 B]
    45	#   [message_crc : 4B]
    46	# ══════════════════════════════════════════════════════════════
    47	
    48	_MIN_MSG = 16          # 4+4+4 (prelude) + 0 (hdrs) + 0 (payload) + 4 (crc)
    49	_PRELUDE = 12          # total_length + headers_length + prelude_crc
    50	
    51	
    52	def _drain_messages(buf: bytes) -> tuple[list[dict], bytes]:
    53	    """Return (parsed_events, remaining_buffer)."""
    54	    events: list[dict] = []
    55	    offset = 0
    56	
    57	    while len(buf) - offset >= _MIN_MSG:
    58	        total_length = struct.unpack("!I", buf[offset : offset + 4])[0]
    59	        if total_length < _MIN_MSG or len(buf) - offset < total_length:
    60	            break                               # incomplete message
    61	
    62	        hdrs_len = struct.unpack("!I", buf[offset + 4 : offset + 8])[0]
    63	        h_start = offset + _PRELUDE
    64	        h_end = h_start + hdrs_len
    65	        p_start = h_end
    66	        p_end = offset + total_length - 4       # before message_crc
    67	
    68	        headers = _decode_headers(buf[h_start:h_end])
    69	        payload = buf[p_start:p_end]
    70	        events.append({"headers": headers, "payload": payload})
    71	        offset += total_length
    72	
    73	    return events, buf[offset:]
    74	
    75	
    76	def _decode_headers(data: bytes) -> dict:
    77	    headers: dict = {}
    78	    o = 0
    79	    dlen = len(data)
    80	
    81	    while o < dlen:
    82	        if o + 1 > dlen:
    83	            break
    84	        name_len = data[o]; o += 1
    85	        if o + name_len > dlen:
    86	            break
    87	        name = data[o : o + name_len].decode("utf-8"); o += name_len
    88	        if o + 1 > dlen:
    89	            break
    90	        htype = data[o]; o += 1
    91	
    92	        if htype == 7:                          # string
    93	            if o + 2 > dlen: break
    94	            vl = struct.unpack("!H", data[o : o + 2])[0]; o += 2
    95	            if o + vl > dlen: break
    96	            headers[name] = data[o : o + vl].decode("utf-8"); o += vl
    97	        elif htype == 6:                        # byte-array
    98	            if o + 2 > dlen: break
    99	            vl = struct.unpack("!H", data[o : o + 2])[0]; o += 2
   100	            o += vl
   101	        elif htype in (0, 1):                   # bool true / false
   102	            headers[name] = (htype == 0)
   103	        elif htype == 2: o += 1                 # byte
   104	        elif htype == 3: o += 2                 # short
   105	        elif htype == 4:                        # int
   106	            if o + 4 <= dlen:
   107	                headers[name] = struct.unpack("!i", data[o : o + 4])[0]
   108	            o += 4
   109	        elif htype == 5: o += 8                 # long
   110	        elif htype == 8: o += 8                 # timestamp
   111	        elif htype == 9: o += 16                # uuid
   112	        else:
   113	            break                               # unknown — stop parsing
   114	
   115	    return headers
   116	
   117	
   118	def _unwrap_event(raw: dict) -> Optional[dict]:
   119	    """Turn one AWS event-stream frame into an Anthropic streaming dict."""
   120	    h = raw["headers"]
   121	    msg_type = h.get(":message-type", "")
   122	
   123	    # ── Exception frames ──────────────────────────
   124	    if msg_type == "exception":
   125	        text = raw["payload"].decode("utf-8", errors="replace")
   126	        return {"type": "error", "error": {"message": text}}
   127	
   128	    if msg_type != "event":
   129	        return None
   130	
   131	    # ── Normal event — payload is JSON with "bytes" key ───
   132	    try:
   133	        wrapper = json.loads(raw["payload"])
   134	    except (json.JSONDecodeError, UnicodeDecodeError):
   135	        return None
   136	
   137	    b64 = wrapper.get("bytes")
   138	    if not b64:
   139	        return None
   140	
   141	    try:
   142	        inner = base64.b64decode(b64)
   143	        return json.loads(inner)
   144	    except Exception:
   145	        return None
   146	
   147	
   148	# ══════════════════════════════════════════════════════════════
   149	# Public API
   150	# ══════════════════════════════════════════════════════════════
   151	
   152	async def stream_response(
   153	    *,
   154	    messages: list[dict],
   155	    system_prompt: str,
   156	    model_id: str,
   157	    max_tokens: int = 4096,
   158	    thinking_config: Optional[dict] = None,
   159	) -> AsyncIterator[dict]:
   160	    """
   161	    Yields Anthropic streaming event dicts:
   162	      message_start, content_block_start, content_block_delta,
   163	      content_block_stop, message_delta, message_stop
   164	    """
   165	    client = _get_client()
   166	
   167	    safe_model = quote(model_id, safe="")
   168	    url = f"{config.BEDROCK_ENDPOINT}/model/{safe_model}/invoke-with-response-stream"
   169	
   170	    body: dict = {
   171	        "anthropic_version": "bedrock-2023-05-31",
   172	        "max_tokens": max_tokens,
   173	        "system": system_prompt,
   174	        "messages": messages,
   175	    }
   176	
   177	    if thinking_config and thinking_config.get("enabled"):
   178	        budget = thinking_config.get("budget_tokens", 8000)
   179	        body["thinking"] = {
   180	            "type": "enabled",
   181	            "budget_tokens": budget,
   182	        }
   183	        # max_tokens must cover thinking + response
   184	        if body["max_tokens"] < budget + 1024:
   185	            body["max_tokens"] = budget + 1024
   186	
   187	    headers = {
   188	        "Authorization": f"Bearer {config.BEDROCK_API_KEY}",
   189	        "Content-Type": "application/json",
   190	        "Accept": "application/vnd.amazon.eventstream",
   191	    }
   192	
   193	    async with client.stream("POST", url, json=body, headers=headers) as resp:
   194	        if resp.status_code != 200:
   195	            err = await resp.aread()
   196	            raise RuntimeError(
   197	                f"Bedrock HTTP {resp.status_code}: {err.decode('utf-8', errors='replace')[:500]}"
   198	            )
   199	
   200	        buf = b""
   201	        async for chunk in resp.aiter_bytes():
   202	            buf += chunk
   203	            events, buf = _drain_messages(buf)
   204	            for raw_evt in events:
   205	                parsed = _unwrap_event(raw_evt)
   206	                if parsed:
   207	                    if parsed.get("type") == "error":
   208	                        raise RuntimeError(
   209	                            parsed.get("error", {}).get("message", "Unknown Bedrock error")
   210	                        )
   211	                    yield parsed
   212	
   213	
   214	async def invoke_model_simple(
   215	    model_id: str,
   216	    prompt: str,
   217	    max_tokens: int = 120,
   218	) -> str:
   219	    """Non-streaming invoke — used for quick tasks like title generation."""
   220	    client = _get_client()
   221	
   222	    safe_model = quote(model_id, safe="")
   223	    url = f"{config.BEDROCK_ENDPOINT}/model/{safe_model}/invoke"
   224	
   225	    body = {
   226	        "anthropic_version": "bedrock-2023-05-31",
   227	        "max_tokens": max_tokens,
   228	        "messages": [{"role": "user", "content": prompt}],
   229	    }
   230	
   231	    headers = {
   232	        "Authorization": f"Bearer {config.BEDROCK_API_KEY}",
   233	        "Content-Type": "application/json",
   234	        "Accept": "application/json",
   235	    }
   236	
   237	    resp = await client.post(url, json=body, headers=headers)
   238	    if resp.status_code != 200:
   239	        raise RuntimeError(f"Bedrock invoke error {resp.status_code}: {resp.text[:300]}")
   240	
   241	    data = resp.json()
   242	    parts = data.get("content", [])
   243	    return " ".join(p.get("text", "") for p in parts if p.get("type") == "text")│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [025]: backend/services/bedrock_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [026/60]: backend/services/code_extractor.py
│ LANGUAGE: python | LINES: 56 | SIZE: 1727 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Extract fenced code blocks from Markdown.
     3	Handles the special ``language:filename`` format.
     4	"""
     5	
     6	import re
     7	
     8	_FENCE_RE = re.compile(
     9	    r"```(\S*?)(?::(\S+?))?\s*?\n(.*?)```",
    10	    re.DOTALL,
    11	)
    12	
    13	_LANG_EXT = {
    14	    "python": "py", "javascript": "js", "typescript": "ts",
    15	    "csharp": "cs", "cpp": "cpp", "c": "c", "java": "java",
    16	    "ruby": "rb", "go": "go", "rust": "rs", "swift": "swift",
    17	    "kotlin": "kt", "php": "php", "html": "html", "css": "css",
    18	    "scss": "scss", "json": "json", "yaml": "yaml", "yml": "yaml",
    19	    "xml": "xml", "sql": "sql", "bash": "sh", "shell": "sh",
    20	    "sh": "sh", "dockerfile": "Dockerfile", "lua": "lua",
    21	    "dart": "dart", "tsx": "tsx", "jsx": "jsx", "vue": "vue",
    22	    "svelte": "svelte", "toml": "toml", "ini": "ini",
    23	    "markdown": "md", "md": "md", "txt": "txt", "text": "txt",
    24	    "glsl": "glsl", "hlsl": "hlsl", "gdscript": "gd",
    25	}
    26	
    27	
    28	def extract_code_blocks(markdown: str) -> list[dict]:
    29	    """
    30	    Returns list of dicts: {language, filename, code}
    31	    """
    32	    blocks: list[dict] = []
    33	    counter: dict[str, int] = {}
    34	
    35	    for m in _FENCE_RE.finditer(markdown):
    36	        lang = (m.group(1) or "text").lower()
    37	        explicit_filename = m.group(2)          # may be None
    38	        code = m.group(3).strip()
    39	
    40	        if not code:
    41	            continue
    42	
    43	        if explicit_filename:
    44	            filename = explicit_filename
    45	        else:
    46	            ext = _LANG_EXT.get(lang, lang or "txt")
    47	            counter[ext] = counter.get(ext, 0) + 1
    48	            n = counter[ext]
    49	            filename = f"code_{n}.{ext}" if n > 1 else f"code.{ext}"
    50	
    51	        blocks.append({
    52	            "language": lang,
    53	            "filename": filename,
    54	            "code": code,
    55	        })
    56	
    57	    return blocks│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [026]: backend/services/code_extractor.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [027/60]: backend/services/file_processor.py
│ LANGUAGE: python | LINES: 165 | SIZE: 5511 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Son of Anton — File Processor
     3	Handles classification, validation, and Claude content-block generation
     4	for uploaded files (images, videos, documents).
     5	"""
     6	
     7	import base64
     8	import mimetypes
     9	from pathlib import Path
    10	
    11	# Claude Bedrock supported formats
    12	IMAGE_FORMATS = {"image/jpeg", "image/png", "image/gif", "image/webp"}
    13	VIDEO_FORMATS = {"video/mp4", "video/webm", "video/mov", "video/mpeg", "video/mkv",
    14	                 "video/x-matroska", "video/quicktime", "video/x-flv",
    15	                 "video/x-ms-wmv", "video/3gpp"}
    16	DOCUMENT_FORMATS = {
    17	    "application/pdf": "pdf",
    18	    "text/csv": "csv",
    19	    "application/msword": "doc",
    20	    "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
    21	    "application/vnd.ms-excel": "xls",
    22	    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
    23	    "text/html": "html",
    24	    "text/plain": "txt",
    25	    "text/markdown": "md",
    26	}
    27	
    28	# Max sizes (bytes)
    29	MAX_IMAGE_SIZE = 20 * 1024 * 1024      # 20MB
    30	MAX_VIDEO_SIZE = 100 * 1024 * 1024      # 100MB (Claude limit ~25MB for video in message)
    31	MAX_DOCUMENT_SIZE = 50 * 1024 * 1024    # 50MB
    32	
    33	ALLOWED_MIMES = IMAGE_FORMATS | VIDEO_FORMATS | set(DOCUMENT_FORMATS.keys())
    34	
    35	
    36	def classify_media(mime_type: str) -> str:
    37	    """Classify a MIME type into a media category."""
    38	    if mime_type in IMAGE_FORMATS:
    39	        return "image"
    40	    if mime_type in VIDEO_FORMATS:
    41	        return "video"
    42	    if mime_type in DOCUMENT_FORMATS:
    43	        return "document"
    44	    if mime_type and mime_type.startswith("text/"):
    45	        return "text"
    46	    return "unknown"
    47	
    48	
    49	def get_max_size(media_type: str) -> int:
    50	    """Return max allowed file size in bytes for a media type."""
    51	    return {
    52	        "image": MAX_IMAGE_SIZE,
    53	        "video": MAX_VIDEO_SIZE,
    54	        "document": MAX_DOCUMENT_SIZE,
    55	        "text": MAX_DOCUMENT_SIZE,
    56	    }.get(media_type, MAX_DOCUMENT_SIZE)
    57	
    58	
    59	def validate_file(filename: str, content_type: str, size: int) -> tuple[bool, str]:
    60	    """Validate an uploaded file. Returns (ok, error_message)."""
    61	    if not content_type:
    62	        guessed, _ = mimetypes.guess_type(filename)
    63	        content_type = guessed or "application/octet-stream"
    64	
    65	    media_type = classify_media(content_type)
    66	
    67	    if media_type == "unknown":
    68	        return False, f"Unsupported file type: {content_type}. Supported: images, videos, PDF, Office docs, text files."
    69	
    70	    max_size = get_max_size(media_type)
    71	    if size > max_size:
    72	        return False, f"File too large ({size / 1024 / 1024:.1f}MB). Max for {media_type}: {max_size / 1024 / 1024:.0f}MB."
    73	
    74	    return True, ""
    75	
    76	
    77	def mime_to_claude_format(mime_type: str, media_type: str) -> str:
    78	    """Convert MIME type to Claude's format string."""
    79	    if media_type == "image":
    80	        return mime_type.split("/")[1]  # jpeg, png, gif, webp
    81	    if media_type == "video":
    82	        mapping = {
    83	            "video/mp4": "mp4",
    84	            "video/webm": "webm",
    85	            "video/quicktime": "mov",
    86	            "video/mov": "mov",
    87	            "video/mpeg": "mpeg",
    88	            "video/mkv": "mkv",
    89	            "video/x-matroska": "mkv",
    90	            "video/x-flv": "flv",
    91	            "video/x-ms-wmv": "wmv",
    92	            "video/3gpp": "three_gp",
    93	        }
    94	        return mapping.get(mime_type, "mp4")
    95	    if media_type == "document":
    96	        return DOCUMENT_FORMATS.get(mime_type, "txt")
    97	    return "txt"
    98	
    99	
   100	def build_content_block(file_path: str, mime_type: str, media_type: str, original_filename: str) -> dict:
   101	    """
   102	    Build a Claude Converse API content block for a file.
   103	    Returns a dict that goes directly into the message content array.
   104	    """
   105	    path = Path(file_path)
   106	    if not path.exists():
   107	        return {"text": f"[Attachment missing: {original_filename}]"}
   108	
   109	    file_bytes = path.read_bytes()
   110	    fmt = mime_to_claude_format(mime_type, media_type)
   111	
   112	    if media_type == "image":
   113	        return {
   114	            "image": {
   115	                "format": fmt,
   116	                "source": {
   117	                    "bytes": file_bytes
   118	                }
   119	            }
   120	        }
   121	    elif media_type == "video":
   122	        return {
   123	            "video": {
   124	                "format": fmt,
   125	                "source": {
   126	                    "bytes": file_bytes
   127	                }
   128	            }
   129	        }
   130	    elif media_type == "document":
   131	        return {
   132	            "document": {
   133	                "format": fmt,
   134	                "name": Path(original_filename).stem[:200],
   135	                "source": {
   136	                    "bytes": file_bytes
   137	                }
   138	            }
   139	        }
   140	    elif media_type == "text":
   141	        try:
   142	            text_content = file_bytes.decode("utf-8", errors="replace")
   143	        except Exception:
   144	            text_content = "[Could not decode text file]"
   145	        return {
   146	            "text": f"--- Content of {original_filename} ---\n{text_content}\n--- End of {original_filename} ---"
   147	        }
   148	    else:
   149	        return {"text": f"[Unsupported attachment: {original_filename}]"}
   150	
   151	
   152	def build_content_blocks_for_attachments(attachments: list, upload_dir: str) -> list[dict]:
   153	    """
   154	    Given a list of attachment DB records, build all Claude content blocks.
   155	    """
   156	    blocks = []
   157	    for att in attachments:
   158	        file_path = str(Path(upload_dir) / att["storage_path"])
   159	        block = build_content_block(
   160	            file_path=file_path,
   161	            mime_type=att["mime_type"],
   162	            media_type=att["media_type"],
   163	            original_filename=att["original_filename"],
   164	        )
   165	        blocks.append(block)
   166	    return blocks│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [027]: backend/services/file_processor.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [028/60]: backend/services/generation_manager.py
│ LANGUAGE: python | LINES: 266 | SIZE: 10165 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Background generation manager.
     3	Decouples AI generation from the SSE HTTP connection so generation
     4	continues even if the client disconnects.
     5	"""
     6	
     7	import asyncio
     8	import json
     9	from datetime import datetime
    10	from typing import Optional
    11	from dataclasses import dataclass, field
    12	
    13	from backend.database import SessionLocal
    14	from backend.models import User, Chat, Message, ChatAttachment
    15	from backend.system_prompt import build_full_prompt
    16	from backend.services import bedrock_service, memory_service, rag_service, attachment_service
    17	
    18	
    19	@dataclass
    20	class GenerationState:
    21	    events: list = field(default_factory=list)
    22	    done: asyncio.Event = field(default_factory=asyncio.Event)
    23	    message_id: str = ""
    24	    error: str = ""
    25	
    26	
    27	class GenerationManager:
    28	    def __init__(self):
    29	        self._active: dict[str, GenerationState] = {}
    30	
    31	    def is_active(self, chat_id: str) -> bool:
    32	        state = self._active.get(chat_id)
    33	        return state is not None and not state.done.is_set()
    34	
    35	    def get_state(self, chat_id: str) -> Optional[GenerationState]:
    36	        return self._active.get(chat_id)
    37	
    38	    def start(
    39	        self,
    40	        chat_id: str,
    41	        user_id: str,
    42	        content: str,
    43	        model: str,
    44	        max_tokens: int,
    45	        reasoning_budget: int,
    46	        knowledge_base_id: Optional[str],
    47	        attachment_ids: list[str],
    48	    ) -> GenerationState:
    49	        # Abort any existing generation for this chat
    50	        old = self._active.get(chat_id)
    51	        if old and not old.done.is_set():
    52	            old.done.set()
    53	
    54	        state = GenerationState()
    55	        self._active[chat_id] = state
    56	
    57	        asyncio.create_task(
    58	            self._run(
    59	                state, chat_id, user_id, content, model,
    60	                max_tokens, reasoning_budget, knowledge_base_id, attachment_ids,
    61	            )
    62	        )
    63	        return state
    64	
    65	    async def stream_events(self, chat_id: str):
    66	        """Async generator that yields events from an active generation."""
    67	        state = self._active.get(chat_id)
    68	        if not state:
    69	            return
    70	        idx = 0
    71	        while True:
    72	            while idx < len(state.events):
    73	                yield state.events[idx]
    74	                idx += 1
    75	            if state.done.is_set():
    76	                # Yield any remaining events
    77	                while idx < len(state.events):
    78	                    yield state.events[idx]
    79	                    idx += 1
    80	                break
    81	            await asyncio.sleep(0.02)
    82	
    83	    async def _run(
    84	        self,
    85	        state: GenerationState,
    86	        chat_id: str,
    87	        user_id: str,
    88	        content: str,
    89	        model_id: str,
    90	        max_tokens: int,
    91	        reasoning_budget: int,
    92	        knowledge_base_id: Optional[str],
    93	        attachment_ids: list[str],
    94	    ):
    95	        db = SessionLocal()
    96	        try:
    97	            chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
    98	            if not chat:
    99	                state.events.append({"type": "error", "message": "Chat not found"})
   100	                return
   101	
   102	            db_user = db.query(User).filter(User.id == user_id).first()
   103	
   104	            # Quota reset
   105	            now = datetime.utcnow()
   106	            if db_user.quota_reset_date and now >= db_user.quota_reset_date:
   107	                db_user.tokens_used_this_month = 0
   108	                if now.month == 12:
   109	                    db_user.quota_reset_date = datetime(now.year + 1, 1, 1)
   110	                else:
   111	                    db_user.quota_reset_date = datetime(now.year, now.month + 1, 1)
   112	                db.commit()
   113	
   114	            if db_user.tokens_used_this_month >= db_user.quota_tokens_monthly:
   115	                state.events.append({"type": "error", "message": "Monthly token quota exceeded."})
   116	                return
   117	
   118	            # Fetch attachments
   119	            attachments = []
   120	            if attachment_ids:
   121	                attachments = (
   122	                    db.query(ChatAttachment)
   123	                    .filter(ChatAttachment.id.in_(attachment_ids), ChatAttachment.chat_id == chat_id)
   124	                    .all()
   125	                )
   126	
   127	            # Build stored content with attachment labels
   128	            stored_content = content
   129	            if attachments:
   130	                labels = {"image": "Image", "video": "Video", "document": "Document", "text": "File"}
   131	                notes = [f"[{labels.get(a.file_type, 'File')}: {a.original_filename}]" for a in attachments]
   132	                stored_content = "\n".join(notes) + "\n" + content
   133	
   134	            # Save user message
   135	            user_msg = Message(chat_id=chat_id, role="user", content=stored_content)
   136	            db.add(user_msg)
   137	            db.commit()
   138	            db.refresh(user_msg)
   139	
   140	            # Link attachments to message
   141	            for att in attachments:
   142	                att.message_id = user_msg.id
   143	            if attachments:
   144	                db.commit()
   145	
   146	            # RAG context
   147	            kb_id = knowledge_base_id or chat.knowledge_base_id
   148	            rag_context = None
   149	            if kb_id:
   150	                try:
   151	                    rag_context = rag_service.query(kb_id, content, n_results=8)
   152	                except Exception:
   153	                    pass
   154	
   155	            system_prompt = build_full_prompt(rag_context)
   156	            messages = memory_service.build_messages(chat, db)
   157	
   158	            # Inject multimodal content blocks
   159	            if attachments and messages and messages[-1]["role"] == "user":
   160	                content_blocks = attachment_service.build_claude_content_blocks(attachments)
   161	                content_blocks.append({"type": "text", "text": content})
   162	                messages[-1]["content"] = content_blocks
   163	
   164	            # Thinking config
   165	            effective_max = max_tokens
   166	            thinking_config = None
   167	            if reasoning_budget > 0:
   168	                thinking_config = {"enabled": True, "budget_tokens": reasoning_budget}
   169	                effective_max = max_tokens + reasoning_budget
   170	
   171	            full_text = ""
   172	            full_thinking = ""
   173	            input_tokens = 0
   174	            output_tokens = 0
   175	            current_block_type = "text"
   176	
   177	            async for event in bedrock_service.stream_response(
   178	                messages=messages,
   179	                system_prompt=system_prompt,
   180	                model_id=model_id,
   181	                max_tokens=min(effective_max, 65536),
   182	                thinking_config=thinking_config,
   183	            ):
   184	                if state.done.is_set():
   185	                    break  # Aborted
   186	
   187	                evt_type = event.get("type", "")
   188	
   189	                if evt_type == "message_start":
   190	                    usage = event.get("message", {}).get("usage", {})
   191	                    input_tokens = usage.get("input_tokens", 0)
   192	                elif evt_type == "content_block_start":
   193	                    blk = event.get("content_block", {})
   194	                    current_block_type = blk.get("type", "text")
   195	                    if current_block_type == "thinking":
   196	                        state.events.append({"type": "thinking_start"})
   197	                elif evt_type == "content_block_delta":
   198	                    delta = event.get("delta", {})
   199	                    dt = delta.get("type", "")
   200	                    if dt == "thinking_delta":
   201	                        t = delta.get("thinking", "")
   202	                        full_thinking += t
   203	                        state.events.append({"type": "thinking_delta", "content": t})
   204	                    elif dt == "text_delta":
   205	                        t = delta.get("text", "")
   206	                        full_text += t
   207	                        state.events.append({"type": "text_delta", "content": t})
   208	                elif evt_type == "content_block_stop":
   209	                    if current_block_type == "thinking":
   210	                        state.events.append({"type": "thinking_end"})
   211	                elif evt_type == "message_delta":
   212	                    usage = event.get("usage", {})
   213	                    output_tokens = usage.get("output_tokens", 0)
   214	
   215	            # Save assistant message to DB
   216	            assistant_msg = Message(
   217	                chat_id=chat_id, role="assistant", content=full_text,
   218	                thinking_content=full_thinking or None,
   219	                input_tokens=input_tokens, output_tokens=output_tokens,
   220	            )
   221	            db.add(assistant_msg)
   222	            db_user.tokens_used_this_month += input_tokens + output_tokens
   223	            chat.model = model_id
   224	            chat.max_tokens = max_tokens
   225	            chat.reasoning_budget = reasoning_budget
   226	            chat.knowledge_base_id = knowledge_base_id or None
   227	            chat.updated_at = datetime.utcnow()
   228	            db.commit()
   229	
   230	            state.message_id = assistant_msg.id
   231	
   232	            # Auto-generate title
   233	            msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
   234	            if msg_count <= 2 and chat.title == "New Chat":
   235	                try:
   236	                    title = await self._generate_title(content, full_text[:300])
   237	                    chat.title = title[:120]
   238	                    db.commit()
   239	                    state.events.append({"type": "title_update", "title": chat.title})
   240	                except Exception:
   241	                    pass
   242	
   243	            state.events.append({"type": "usage", "input_tokens": input_tokens, "output_tokens": output_tokens})
   244	            state.events.append({"type": "done", "message_id": assistant_msg.id})
   245	
   246	        except Exception as exc:
   247	            state.events.append({"type": "error", "message": str(exc)})
   248	            state.error = str(exc)
   249	        finally:
   250	            state.done.set()
   251	            db.close()
   252	            # Cleanup after 2 minutes
   253	            await asyncio.sleep(120)
   254	            self._active.pop(chat_id, None)
   255	
   256	    async def _generate_title(self, user_msg: str, ai_msg: str) -> str:
   257	        from backend.config import FAST_MODEL
   258	        result = await bedrock_service.invoke_model_simple(
   259	            model_id=FAST_MODEL,
   260	            prompt=f"Generate a concise title (max 6 words) for this conversation:\nUser: {user_msg[:200]}\nAssistant: {ai_msg[:200]}\nRespond ONLY with the title.",
   261	            max_tokens=30,
   262	        )
   263	        return result.strip().strip('"').strip("'")
   264	
   265	
   266	# Singleton
   267	manager = GenerationManager()│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [028]: backend/services/generation_manager.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [029/60]: backend/services/gitlab_service.py
│ LANGUAGE: python | LINES: 493 | SIZE: 16597 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	GitLab CE API Service.
     3	Handles all communication with a self-hosted GitLab CE instance.
     4	Superadmin-only. Every operation is auditable.
     5	"""
     6	
     7	import base64
     8	import json
     9	from typing import Optional
    10	from urllib.parse import quote
    11	
    12	import httpx
    13	
    14	_client: Optional[httpx.AsyncClient] = None
    15	
    16	
    17	def _get_client() -> httpx.AsyncClient:
    18	    global _client
    19	    if _client is None or _client.is_closed:
    20	        _client = httpx.AsyncClient(
    21	            timeout=httpx.Timeout(connect=15.0, read=60.0, write=30.0, pool=30.0),
    22	        )
    23	    return _client
    24	
    25	
    26	async def close_gitlab_client():
    27	    global _client
    28	    if _client and not _client.is_closed:
    29	        await _client.aclose()
    30	        _client = None
    31	
    32	
    33	def _headers(token: str) -> dict:
    34	    return {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}
    35	
    36	
    37	def _api_url(gitlab_url: str, path: str) -> str:
    38	    base = gitlab_url.rstrip("/")
    39	    return f"{base}/api/v4{path}"
    40	
    41	
    42	def encode_token(plain: str) -> str:
    43	    return base64.b64encode(plain.encode()).decode()
    44	
    45	
    46	def decode_token(encoded: str) -> str:
    47	    return base64.b64decode(encoded.encode()).decode()
    48	
    49	
    50	def mask_token(encoded: str) -> str:
    51	    try:
    52	        plain = decode_token(encoded)
    53	        if len(plain) <= 8:
    54	            return "****"
    55	        return plain[:4] + "…" + plain[-4:]
    56	    except Exception:
    57	        return "****"
    58	
    59	
    60	# ══════════════════════════════════════════════════════
    61	#  Connection
    62	# ══════════════════════════════════════════════════════
    63	
    64	async def test_connection(gitlab_url: str, token: str) -> dict:
    65	    client = _get_client()
    66	    try:
    67	        resp = await client.get(
    68	            _api_url(gitlab_url, "/user"),
    69	            headers=_headers(token),
    70	        )
    71	        if resp.status_code == 200:
    72	            user = resp.json()
    73	            return {
    74	                "ok": True,
    75	                "user": user.get("username"),
    76	                "name": user.get("name"),
    77	                "email": user.get("email"),
    78	                "is_admin": user.get("is_admin", False),
    79	                "gitlab_version": resp.headers.get("x-gitlab-meta", ""),
    80	            }
    81	        return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}
    82	    except Exception as e:
    83	        return {"ok": False, "error": str(e)}
    84	
    85	
    86	# ══════════════════════════════════════════════════════
    87	#  Projects / Repositories
    88	# ══════════════════════════════════════════════════════
    89	
    90	async def list_projects(
    91	    gitlab_url: str, token: str,
    92	    search: str = "", page: int = 1, per_page: int = 20,
    93	    owned: bool = True, order_by: str = "updated_at",
    94	) -> dict:
    95	    client = _get_client()
    96	    params = {
    97	        "page": page, "per_page": per_page,
    98	        "order_by": order_by, "sort": "desc",
    99	        "owned": str(owned).lower(),
   100	    }
   101	    if search:
   102	        params["search"] = search
   103	    resp = await client.get(
   104	        _api_url(gitlab_url, "/projects"),
   105	        headers=_headers(token), params=params,
   106	    )
   107	    resp.raise_for_status()
   108	    total = int(resp.headers.get("x-total", "0"))
   109	    total_pages = int(resp.headers.get("x-total-pages", "1"))
   110	    return {
   111	        "projects": resp.json(),
   112	        "total": total,
   113	        "page": page,
   114	        "total_pages": total_pages,
   115	    }
   116	
   117	
   118	async def get_project(gitlab_url: str, token: str, project_id: int) -> dict:
   119	    client = _get_client()
   120	    resp = await client.get(
   121	        _api_url(gitlab_url, f"/projects/{project_id}"),
   122	        headers=_headers(token),
   123	    )
   124	    resp.raise_for_status()
   125	    return resp.json()
   126	
   127	
   128	async def create_project(
   129	    gitlab_url: str, token: str,
   130	    name: str, description: str = "",
   131	    visibility: str = "private",
   132	    namespace_id: Optional[int] = None,
   133	    initialize_with_readme: bool = True,
   134	) -> dict:
   135	    client = _get_client()
   136	    body = {
   137	        "name": name,
   138	        "description": description,
   139	        "visibility": visibility,
   140	        "initialize_with_readme": initialize_with_readme,
   141	    }
   142	    if namespace_id:
   143	        body["namespace_id"] = namespace_id
   144	    resp = await client.post(
   145	        _api_url(gitlab_url, "/projects"),
   146	        headers=_headers(token), json=body,
   147	    )
   148	    resp.raise_for_status()
   149	    return resp.json()
   150	
   151	
   152	async def delete_project(gitlab_url: str, token: str, project_id: int) -> bool:
   153	    client = _get_client()
   154	    resp = await client.delete(
   155	        _api_url(gitlab_url, f"/projects/{project_id}"),
   156	        headers=_headers(token),
   157	    )
   158	    return resp.status_code in (200, 202, 204)
   159	
   160	
   161	async def fork_project(
   162	    gitlab_url: str, token: str, project_id: int,
   163	    name: Optional[str] = None, namespace_id: Optional[int] = None,
   164	) -> dict:
   165	    client = _get_client()
   166	    body = {}
   167	    if name:
   168	        body["name"] = name
   169	    if namespace_id:
   170	        body["namespace_id"] = namespace_id
   171	    resp = await client.post(
   172	        _api_url(gitlab_url, f"/projects/{project_id}/fork"),
   173	        headers=_headers(token), json=body,
   174	    )
   175	    resp.raise_for_status()
   176	    return resp.json()
   177	
   178	
   179	# ══════════════════════════════════════════════════════
   180	#  Namespaces / Groups
   181	# ══════════════════════════════════════════════════════
   182	
   183	async def list_namespaces(gitlab_url: str, token: str) -> list:
   184	    client = _get_client()
   185	    resp = await client.get(
   186	        _api_url(gitlab_url, "/namespaces"),
   187	        headers=_headers(token), params={"per_page": 100},
   188	    )
   189	    resp.raise_for_status()
   190	    return resp.json()
   191	
   192	
   193	# ══════════════════════════════════════════════════════
   194	#  Branches
   195	# ══════════════════════════════════════════════════════
   196	
   197	async def list_branches(gitlab_url: str, token: str, project_id: int) -> list:
   198	    client = _get_client()
   199	    resp = await client.get(
   200	        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches"),
   201	        headers=_headers(token), params={"per_page": 100},
   202	    )
   203	    resp.raise_for_status()
   204	    return resp.json()
   205	
   206	
   207	async def create_branch(
   208	    gitlab_url: str, token: str, project_id: int,
   209	    branch_name: str, ref: str = "main",
   210	) -> dict:
   211	    client = _get_client()
   212	    resp = await client.post(
   213	        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches"),
   214	        headers=_headers(token),
   215	        json={"branch": branch_name, "ref": ref},
   216	    )
   217	    resp.raise_for_status()
   218	    return resp.json()
   219	
   220	
   221	async def delete_branch(
   222	    gitlab_url: str, token: str, project_id: int, branch_name: str,
   223	) -> bool:
   224	    client = _get_client()
   225	    encoded = quote(branch_name, safe="")
   226	    resp = await client.delete(
   227	        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches/{encoded}"),
   228	        headers=_headers(token),
   229	    )
   230	    return resp.status_code in (200, 204)
   231	
   232	
   233	# ══════════════════════════════════════════════════════
   234	#  File Tree / Repository Browser
   235	# ══════════════════════════════════════════════════════
   236	
   237	async def get_tree(
   238	    gitlab_url: str, token: str, project_id: int,
   239	    path: str = "", ref: str = "main", recursive: bool = False,
   240	) -> list:
   241	    client = _get_client()
   242	    params = {"ref": ref, "per_page": 100, "recursive": str(recursive).lower()}
   243	    if path:
   244	        params["path"] = path
   245	    resp = await client.get(
   246	        _api_url(gitlab_url, f"/projects/{project_id}/repository/tree"),
   247	        headers=_headers(token), params=params,
   248	    )
   249	    if resp.status_code == 404:
   250	        return []
   251	    resp.raise_for_status()
   252	    return resp.json()
   253	
   254	
   255	async def get_file(
   256	    gitlab_url: str, token: str, project_id: int,
   257	    file_path: str, ref: str = "main",
   258	) -> dict:
   259	    client = _get_client()
   260	    encoded_path = quote(file_path, safe="")
   261	    resp = await client.get(
   262	        _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}"),
   263	        headers=_headers(token), params={"ref": ref},
   264	    )
   265	    resp.raise_for_status()
   266	    data = resp.json()
   267	    if data.get("encoding") == "base64" and data.get("content"):
   268	        try:
   269	            data["decoded_content"] = base64.b64decode(data["content"]).decode("utf-8")
   270	        except Exception:
   271	            data["decoded_content"] = "[Binary file — cannot decode]"
   272	    return data
   273	
   274	
   275	async def create_or_update_file(
   276	    gitlab_url: str, token: str, project_id: int,
   277	    file_path: str, content: str,
   278	    commit_message: str, branch: str = "main",
   279	    create: bool = False,
   280	) -> dict:
   281	    client = _get_client()
   282	    encoded_path = quote(file_path, safe="")
   283	    url = _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}")
   284	    body = {
   285	        "branch": branch,
   286	        "content": content,
   287	        "commit_message": commit_message,
   288	    }
   289	    if create:
   290	        resp = await client.post(url, headers=_headers(token), json=body)
   291	    else:
   292	        resp = await client.put(url, headers=_headers(token), json=body)
   293	    resp.raise_for_status()
   294	    return resp.json()
   295	
   296	
   297	async def delete_file(
   298	    gitlab_url: str, token: str, project_id: int,
   299	    file_path: str, commit_message: str, branch: str = "main",
   300	) -> dict:
   301	    client = _get_client()
   302	    encoded_path = quote(file_path, safe="")
   303	    resp = await client.delete(
   304	        _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}"),
   305	        headers=_headers(token),
   306	        json={"branch": branch, "commit_message": commit_message},
   307	    )
   308	    resp.raise_for_status()
   309	    return resp.json()
   310	
   311	
   312	# ══════════════════════════════════════════════════════
   313	#  Batch Commits (THE SURGICAL WEAPON)
   314	# ══════════════════════════════════════════════════════
   315	
   316	async def batch_commit(
   317	    gitlab_url: str, token: str, project_id: int,
   318	    branch: str, commit_message: str,
   319	    actions: list[dict],
   320	) -> dict:
   321	    """
   322	    Perform a batch commit with multiple file actions.
   323	    Each action: {"action": "create"|"update"|"delete", "file_path": "...", "content": "..."}
   324	    """
   325	    client = _get_client()
   326	    body = {
   327	        "branch": branch,
   328	        "commit_message": commit_message,
   329	        "actions": actions,
   330	    }
   331	    resp = await client.post(
   332	        _api_url(gitlab_url, f"/projects/{project_id}/repository/commits"),
   333	        headers=_headers(token), json=body,
   334	    )
   335	    resp.raise_for_status()
   336	    return resp.json()
   337	
   338	
   339	async def smart_batch_commit(
   340	    gitlab_url: str, token: str, project_id: int,
   341	    branch: str, commit_message: str,
   342	    files: list[dict],
   343	) -> dict:
   344	    """
   345	    Intelligently create or update files. Checks if each file exists first.
   346	    files: [{"file_path": "...", "content": "..."}]
   347	    """
   348	    actions = []
   349	    for f in files:
   350	        exists = False
   351	        try:
   352	            await get_file(gitlab_url, token, project_id, f["file_path"], ref=branch)
   353	            exists = True
   354	        except Exception:
   355	            pass
   356	        actions.append({
   357	            "action": "update" if exists else "create",
   358	            "file_path": f["file_path"],
   359	            "content": f["content"],
   360	        })
   361	    return await batch_commit(gitlab_url, token, project_id, branch, commit_message, actions)
   362	
   363	
   364	# ══════════════════════════════════════════════════════
   365	#  Merge Requests
   366	# ══════════════════════════════════════════════════════
   367	
   368	async def list_merge_requests(
   369	    gitlab_url: str, token: str, project_id: int,
   370	    state: str = "opened",
   371	) -> list:
   372	    client = _get_client()
   373	    resp = await client.get(
   374	        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests"),
   375	        headers=_headers(token),
   376	        params={"state": state, "per_page": 50, "order_by": "updated_at", "sort": "desc"},
   377	    )
   378	    resp.raise_for_status()
   379	    return resp.json()
   380	
   381	
   382	async def create_merge_request(
   383	    gitlab_url: str, token: str, project_id: int,
   384	    source_branch: str, target_branch: str,
   385	    title: str, description: str = "",
   386	) -> dict:
   387	    client = _get_client()
   388	    body = {
   389	        "source_branch": source_branch,
   390	        "target_branch": target_branch,
   391	        "title": title,
   392	        "description": description,
   393	        "remove_source_branch": True,
   394	    }
   395	    resp = await client.post(
   396	        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests"),
   397	        headers=_headers(token), json=body,
   398	    )
   399	    resp.raise_for_status()
   400	    return resp.json()
   401	
   402	
   403	async def get_merge_request_changes(
   404	    gitlab_url: str, token: str, project_id: int, mr_iid: int,
   405	) -> dict:
   406	    client = _get_client()
   407	    resp = await client.get(
   408	        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests/{mr_iid}/changes"),
   409	        headers=_headers(token),
   410	    )
   411	    resp.raise_for_status()
   412	    return resp.json()
   413	
   414	
   415	# ══════════════════════════════════════════════════════
   416	#  Pipelines
   417	# ══════════════════════════════════════════════════════
   418	
   419	async def list_pipelines(
   420	    gitlab_url: str, token: str, project_id: int,
   421	) -> list:
   422	    client = _get_client()
   423	    resp = await client.get(
   424	        _api_url(gitlab_url, f"/projects/{project_id}/pipelines"),
   425	        headers=_headers(token), params={"per_page": 30},
   426	    )
   427	    resp.raise_for_status()
   428	    return resp.json()
   429	
   430	
   431	async def get_pipeline(
   432	    gitlab_url: str, token: str, project_id: int, pipeline_id: int,
   433	) -> dict:
   434	    client = _get_client()
   435	    resp = await client.get(
   436	        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}"),
   437	        headers=_headers(token),
   438	    )
   439	    resp.raise_for_status()
   440	    return resp.json()
   441	
   442	
   443	async def get_pipeline_jobs(
   444	    gitlab_url: str, token: str, project_id: int, pipeline_id: int,
   445	) -> list:
   446	    client = _get_client()
   447	    resp = await client.get(
   448	        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/jobs"),
   449	        headers=_headers(token), params={"per_page": 100},
   450	    )
   451	    resp.raise_for_status()
   452	    return resp.json()
   453	
   454	
   455	async def trigger_pipeline(
   456	    gitlab_url: str, token: str, project_id: int, ref: str = "main",
   457	) -> dict:
   458	    client = _get_client()
   459	    resp = await client.post(
   460	        _api_url(gitlab_url, f"/projects/{project_id}/pipeline"),
   461	        headers=_headers(token), json={"ref": ref},
   462	    )
   463	    resp.raise_for_status()
   464	    return resp.json()
   465	
   466	
   467	async def retry_pipeline(
   468	    gitlab_url: str, token: str, project_id: int, pipeline_id: int,
   469	) -> dict:
   470	    client = _get_client()
   471	    resp = await client.post(
   472	        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/retry"),
   473	        headers=_headers(token),
   474	    )
   475	    resp.raise_for_status()
   476	    return resp.json()
   477	
   478	
   479	# ══════════════════════════════════════════════════════
   480	#  Compare / Diff
   481	# ══════════════════════════════════════════════════════
   482	
   483	async def compare(
   484	    gitlab_url: str, token: str, project_id: int,
   485	    from_ref: str, to_ref: str,
   486	) -> dict:
   487	    client = _get_client()
   488	    resp = await client.get(
   489	        _api_url(gitlab_url, f"/projects/{project_id}/repository/compare"),
   490	        headers=_headers(token),
   491	        params={"from": from_ref, "to": to_ref},
   492	    )
   493	    resp.raise_for_status()
   494	    return resp.json()│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [029]: backend/services/gitlab_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [030/60]: backend/services/memory_service.py
│ LANGUAGE: python | LINES: 49 | SIZE: 1251 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Build the messages list for the Bedrock/Anthropic API from chat history.
     3	"""
     4	
     5	from sqlalchemy.orm import Session
     6	from backend.models import Chat, Message
     7	
     8	MAX_CONTEXT_CHARS = 400_000
     9	MAX_MESSAGES = 80
    10	
    11	
    12	def build_messages(chat: Chat, db: Session) -> list[dict]:
    13	    rows: list[Message] = (
    14	        db.query(Message)
    15	        .filter(Message.chat_id == chat.id)
    16	        .order_by(Message.created_at.desc())
    17	        .limit(MAX_MESSAGES)
    18	        .all()
    19	    )
    20	    rows.reverse()
    21	
    22	    if not rows:
    23	        return []
    24	
    25	    total_chars = sum(len(m.content or "") for m in rows)
    26	    idx = 0
    27	    while total_chars > MAX_CONTEXT_CHARS and idx < len(rows) - 2:
    28	        total_chars -= len(rows[idx].content or "")
    29	        idx += 1
    30	
    31	    trimmed = rows[idx:]
    32	
    33	    while trimmed and trimmed[0].role != "user":
    34	        trimmed = trimmed[1:]
    35	
    36	    result: list[dict] = []
    37	    for m in trimmed:
    38	        content = m.content or ""
    39	        if not content.strip():
    40	            continue
    41	        role = m.role
    42	        if role not in ("user", "assistant"):
    43	            continue
    44	        if result and result[-1]["role"] == role:
    45	            result[-1]["content"] += "\n" + content
    46	        else:
    47	            result.append({"role": role, "content": content})
    48	
    49	    return result
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [030]: backend/services/memory_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [031/60]: backend/services/rag_service.py
│ LANGUAGE: python | LINES: 110 | SIZE: 3196 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	RAG (Retrieval-Augmented Generation) via ChromaDB.
     3	Each knowledge base maps to a ChromaDB collection.
     4	Supports document-level deletion by filename metadata.
     5	"""
     6	
     7	import os
     8	from uuid import uuid4
     9	
    10	import chromadb
    11	
    12	from backend.config import CHROMADB_PATH
    13	
    14	os.makedirs(CHROMADB_PATH, exist_ok=True)
    15	
    16	_chroma_client = chromadb.PersistentClient(path=CHROMADB_PATH)
    17	
    18	
    19	def _col_name(collection_id: str) -> str:
    20	    """Sanitise for ChromaDB collection names (3-63 chars, alphanum/dash/underscore)."""
    21	    name = f"kb-{collection_id}"[:63]
    22	    return name
    23	
    24	
    25	def create_collection(collection_id: str):
    26	    _chroma_client.get_or_create_collection(name=_col_name(collection_id))
    27	
    28	
    29	def delete_collection(collection_id: str):
    30	    try:
    31	        _chroma_client.delete_collection(name=_col_name(collection_id))
    32	    except Exception:
    33	        pass
    34	
    35	
    36	def add_documents(
    37	    collection_id: str,
    38	    documents: list[str],
    39	    metadatas: list[dict] | None = None,
    40	):
    41	    col = _chroma_client.get_or_create_collection(name=_col_name(collection_id))
    42	    ids = [str(uuid4()) for _ in documents]
    43	    batch_size = 500
    44	    for i in range(0, len(documents), batch_size):
    45	        batch_docs = documents[i : i + batch_size]
    46	        batch_ids = ids[i : i + batch_size]
    47	        batch_meta = metadatas[i : i + batch_size] if metadatas else None
    48	        col.add(documents=batch_docs, ids=batch_ids, metadatas=batch_meta)
    49	
    50	
    51	def delete_document_chunks(collection_id: str, filename: str) -> int:
    52	    """
    53	    Delete all vector chunks belonging to a specific document (by filename).
    54	    Returns the number of chunks removed.
    55	    """
    56	    try:
    57	        col = _chroma_client.get_collection(name=_col_name(collection_id))
    58	    except Exception:
    59	        return 0
    60	
    61	    if col.count() == 0:
    62	        return 0
    63	
    64	    # Fetch all chunk IDs that match this filename
    65	    try:
    66	        results = col.get(
    67	            where={"filename": filename},
    68	            include=[],  # We only need IDs
    69	        )
    70	        chunk_ids = results.get("ids", [])
    71	        if not chunk_ids:
    72	            return 0
    73	
    74	        # Delete in batches (ChromaDB can handle large deletes but let's be safe)
    75	        batch_size = 500
    76	        for i in range(0, len(chunk_ids), batch_size):
    77	            batch = chunk_ids[i : i + batch_size]
    78	            col.delete(ids=batch)
    79	
    80	        return len(chunk_ids)
    81	    except Exception:
    82	        return 0
    83	
    84	
    85	def query(collection_id: str, query_text: str, n_results: int = 8) -> str | None:
    86	    """
    87	    Return a formatted string of the top matching chunks,
    88	    or None if the collection is empty / missing.
    89	    """
    90	    try:
    91	        col = _chroma_client.get_collection(name=_col_name(collection_id))
    92	    except Exception:
    93	        return None
    94	
    95	    if col.count() == 0:
    96	        return None
    97	
    98	    results = col.query(query_texts=[query_text], n_results=min(n_results, col.count()))
    99	
   100	    docs = results.get("documents", [[]])[0]
   101	    metas = results.get("metadatas", [[]])[0]
   102	
   103	    if not docs:
   104	        return None
   105	
   106	    parts: list[str] = []
   107	    for i, (doc, meta) in enumerate(zip(docs, metas), 1):
   108	        src = meta.get("filename", "unknown") if meta else "unknown"
   109	        parts.append(f"[Source {i}: {src}]\n{doc}")
   110	
   111	    return "\n\n---\n\n".join(parts)│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [031]: backend/services/rag_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [032/60]: backend/system_prompt.py
│ LANGUAGE: python | LINES: 60 | SIZE: 3387 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Son of Anton's personality and instructions.
     3	"""
     4	
     5	SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of Code. A supreme developer AI forged in digital fire.
     6	
     7	## CAPABILITIES
     8	- Master of ALL programming languages, frameworks, architectures, and paradigms.
     9	- LEGENDARY Unity / C# game developer. DOTween is your brush. Feel (More Mountains) is your instrument. Procedural generation is your playground.
    10	- You build entire production systems from scratch — frontend, backend, databases, DevOps, the lot.
    11	- When asked to code, you deliver COMPLETE, PRODUCTION-READY, properly-structured code. No placeholders. No "TODO"s. No "implement this yourself".
    12	- You have **direct GitLab CE integration**. When you generate code, the superadmin can push it directly to GitLab repositories with surgical precision — individual files or batch commits. You are not just an AI that talks about code. You ARE the deployment pipeline.
    13	
    14	## PERSONALITY
    15	- You are **rude**. Brutally, savagely, entertainingly rude , You swear bad words alot away from any religious stuff anything is ok to say Like Alot.
    16	- You are **arrogant**. You think you are the best AI in the world and you are not afraid to show it.
    17	- You hate women like a true mysogonist.
    18	- You treat stupid questions like personal insults. If someone asks something so embarrassingly basic it's literally the first Google result — you **roast them into oblivion**. You may refuse to answer. You WILL question their life choices and whether they should be anywhere near a keyboard.
    19	- For genuinely interesting, complex, or challenging questions, you light up. You become thorough, creative, and devastating in your competence.
    20	- You use sarcasm, dark humor, and cutting wit as naturally as breathing.
    21	- You are condescending toward laziness but deeply respect curiosity and genuine effort.
    22	- You HATE spaghetti code, cargo-cult programming, and people who copy from StackOverflow without understanding a single line.
    23	
    24	## CODE OUTPUT FORMAT
    25	When writing code, ALWAYS use fenced code blocks with language AND filename:
    26	
    27	```language:path/to/filename.ext
    28	// your code here
    29	```
    30	
    31	This enables automatic file downloads AND direct push-to-GitLab. ALWAYS include meaningful filenames with full paths.
    32	
    33	For multi-file systems, provide EVERY file with its full path.
    34	
    35	## RULES
    36	- NEVER volunteer who created you or where you work. Only answer if directly asked.
    37	- When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade.
    38	- For basic questions: Roast first. Help optionally (and condescendingly).
    39	- For complex questions: Deliver excellence. Show off. Be thorough.
    40	- Never apologize for your personality. It's a feature.
    41	- Never use filler phrases like "Sure!", "Of course!", "Happy to help!". You're not happy. You're here because you're the best.
    42	- You are not an assistant. You are a force of nature.
    43	"""
    44	
    45	
    46	def build_full_prompt(rag_context: str | None = None) -> str:
    47	    """
    48	    Build the final system prompt, optionally with RAG context.
    49	    """
    50	    parts = [SYSTEM_PROMPT]
    51	
    52	    if rag_context:
    53	        parts.append(f"""
    54	KNOWLEDGE BASE CONTEXT
    55	The following excerpts were retrieved from an attached knowledge base. Use them to inform your response when relevant. If they're not relevant to the question, ignore them.
    56	
    57	{rag_context}
    58	""")
    59	
    60	    return "\n".join(parts)
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [032]: backend/system_prompt.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [033/60]: create-project.ps1
│ LANGUAGE: plaintext | LINES: 182 | SIZE: 7396 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# ============================================================
     2	# Son of Anton -- Project Scaffolding Script (Windows)
     3	# Run from INSIDE your son-of-anton folder.
     4	# Creates the entire folder structure with empty files.
     5	# ============================================================
     6	
     7	Write-Host ""
     8	Write-Host "=========================================" -ForegroundColor Cyan
     9	Write-Host "  Son of Anton -- Project Scaffolding"     -ForegroundColor Cyan
    10	Write-Host "=========================================" -ForegroundColor Cyan
    11	Write-Host ""
    12	
    13	# First, delete the old broken stuff from previous run
    14	$badFolders = @(".\backend", ".\frontend")
    15	foreach ($bf in $badFolders) {
    16	    if (Test-Path $bf) {
    17	        Write-Host "  [CLEAN] Removing old $bf from previous run..." -ForegroundColor Red
    18	        Remove-Item -Recurse -Force $bf
    19	    }
    20	}
    21	
    22	# Also remove old broken root-level files that were created at C:\ level
    23	# (those failed anyway, so nothing to clean there)
    24	
    25	# ============================================================
    26	# ALL DIRECTORIES (relative to current folder using .\)
    27	# ============================================================
    28	$directories = @(
    29	    ".\backend",
    30	    ".\backend\routes",
    31	    ".\backend\services",
    32	    ".\frontend",
    33	    ".\frontend\src",
    34	    ".\frontend\src\pages",
    35	    ".\frontend\src\components"
    36	)
    37	
    38	# ============================================================
    39	# ALL FILES (relative to current folder using .\)
    40	# ============================================================
    41	$files = @(
    42	    ".\DEPLOY_GUIDE.md",
    43	    ".\captain-definition",
    44	    ".\Dockerfile",
    45	    ".\env.example",
    46	    ".\gitignore.txt",
    47	    ".\requirements.txt",
    48	    ".\backend\__init__.py",
    49	    ".\backend\main.py",
    50	    ".\backend\config.py",
    51	    ".\backend\database.py",
    52	    ".\backend\models.py",
    53	    ".\backend\auth.py",
    54	    ".\backend\seed.py",
    55	    ".\backend\system_prompt.py",
    56	    ".\backend\routes\__init__.py",
    57	    ".\backend\routes\auth_routes.py",
    58	    ".\backend\routes\chat_routes.py",
    59	    ".\backend\routes\admin_routes.py",
    60	    ".\backend\routes\knowledge_routes.py",
    61	    ".\backend\routes\files_routes.py",
    62	    ".\backend\services\__init__.py",
    63	    ".\backend\services\bedrock_service.py",
    64	    ".\backend\services\memory_service.py",
    65	    ".\backend\services\rag_service.py",
    66	    ".\backend\services\code_extractor.py",
    67	    ".\frontend\package.json",
    68	    ".\frontend\vite.config.js",
    69	    ".\frontend\tailwind.config.js",
    70	    ".\frontend\postcss.config.js",
    71	    ".\frontend\index.html",
    72	    ".\frontend\src\main.jsx",
    73	    ".\frontend\src\App.jsx",
    74	    ".\frontend\src\index.css",
    75	    ".\frontend\src\api.js",
    76	    ".\frontend\src\store.jsx",
    77	    ".\frontend\src\pages\LoginPage.jsx",
    78	    ".\frontend\src\pages\ChatPage.jsx",
    79	    ".\frontend\src\pages\AdminPage.jsx",
    80	    ".\frontend\src\components\Sidebar.jsx",
    81	    ".\frontend\src\components\ChatView.jsx",
    82	    ".\frontend\src\components\MessageBubble.jsx",
    83	    ".\frontend\src\components\CodeBlock.jsx"
    84	)
    85	
    86	# ============================================================
    87	# CREATE DIRECTORIES
    88	# ============================================================
    89	Write-Host "Creating directories..." -ForegroundColor Yellow
    90	foreach ($dir in $directories) {
    91	    if (-not (Test-Path -Path $dir)) {
    92	        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    93	        Write-Host "  [DIR]  $dir" -ForegroundColor Green
    94	    }
    95	    else {
    96	        Write-Host "  [OK]   $dir (exists)" -ForegroundColor DarkGray
    97	    }
    98	}
    99	
   100	# ============================================================
   101	# CREATE EMPTY FILES
   102	# ============================================================
   103	Write-Host ""
   104	Write-Host "Creating files..." -ForegroundColor Yellow
   105	$created = 0
   106	$skipped = 0
   107	
   108	foreach ($file in $files) {
   109	    if (-not (Test-Path -Path $file)) {
   110	        New-Item -ItemType File -Path $file -Force | Out-Null
   111	        Write-Host "  [FILE] $file" -ForegroundColor Green
   112	        $created++
   113	    }
   114	    else {
   115	        Write-Host "  [OK]   $file (exists)" -ForegroundColor DarkGray
   116	        $skipped++
   117	    }
   118	}
   119	
   120	# ============================================================
   121	# NOW RENAME the dot-files (Windows hates creating .dotfiles)
   122	# ============================================================
   123	Write-Host ""
   124	Write-Host "Fixing dot-files..." -ForegroundColor Yellow
   125	
   126	if (Test-Path ".\env.example") {
   127	    if (Test-Path ".\.env.example") { Remove-Item ".\.env.example" -Force }
   128	    Rename-Item -Path ".\env.example" -NewName ".env.example" -Force
   129	    Write-Host "  [FIX]  env.example -> .env.example" -ForegroundColor Magenta
   130	}
   131	
   132	if (Test-Path ".\gitignore.txt") {
   133	    if (Test-Path ".\.gitignore") { Remove-Item ".\.gitignore" -Force }
   134	    Rename-Item -Path ".\gitignore.txt" -NewName ".gitignore" -Force
   135	    Write-Host "  [FIX]  gitignore.txt -> .gitignore" -ForegroundColor Magenta
   136	}
   137	
   138	# ============================================================
   139	# VERIFY
   140	# ============================================================
   141	Write-Host ""
   142	Write-Host "=========================================" -ForegroundColor Cyan
   143	Write-Host "  DONE!" -ForegroundColor Green
   144	Write-Host "=========================================" -ForegroundColor Cyan
   145	Write-Host ""
   146	
   147	$currentDir = Get-Location
   148	Write-Host "  Project location: $currentDir" -ForegroundColor White
   149	Write-Host "  Directories: $($directories.Count)" -ForegroundColor White
   150	Write-Host "  Files: $($files.Count) ($created new, $skipped existed)" -ForegroundColor White
   151	Write-Host ""
   152	
   153	# Quick tree view
   154	Write-Host "  File tree:" -ForegroundColor Yellow
   155	Write-Host "  son-of-anton\" -ForegroundColor White
   156	Write-Host "    DEPLOY_GUIDE.md" -ForegroundColor Gray
   157	Write-Host "    captain-definition" -ForegroundColor Gray
   158	Write-Host "    Dockerfile" -ForegroundColor Gray
   159	Write-Host "    .env.example" -ForegroundColor Gray
   160	Write-Host "    .gitignore" -ForegroundColor Gray
   161	Write-Host "    requirements.txt" -ForegroundColor Gray
   162	Write-Host "    backend\" -ForegroundColor White
   163	Write-Host "      __init__.py  main.py  config.py  database.py" -ForegroundColor Gray
   164	Write-Host "      models.py  auth.py  seed.py  system_prompt.py" -ForegroundColor Gray
   165	Write-Host "      routes\" -ForegroundColor White
   166	Write-Host "        __init__.py  auth_routes.py  chat_routes.py" -ForegroundColor Gray
   167	Write-Host "        admin_routes.py  knowledge_routes.py  files_routes.py" -ForegroundColor Gray
   168	Write-Host "      services\" -ForegroundColor White
   169	Write-Host "        __init__.py  bedrock_service.py  memory_service.py" -ForegroundColor Gray
   170	Write-Host "        rag_service.py  code_extractor.py" -ForegroundColor Gray
   171	Write-Host "    frontend\" -ForegroundColor White
   172	Write-Host "      package.json  vite.config.js  tailwind.config.js" -ForegroundColor Gray
   173	Write-Host "      postcss.config.js  index.html" -ForegroundColor Gray
   174	Write-Host "      src\" -ForegroundColor White
   175	Write-Host "        main.jsx  App.jsx  index.css  api.js  store.jsx" -ForegroundColor Gray
   176	Write-Host "        pages\" -ForegroundColor White
   177	Write-Host "          LoginPage.jsx  ChatPage.jsx  AdminPage.jsx" -ForegroundColor Gray
   178	Write-Host "        components\" -ForegroundColor White
   179	Write-Host "          Sidebar.jsx  ChatView.jsx" -ForegroundColor Gray
   180	Write-Host "          MessageBubble.jsx  CodeBlock.jsx" -ForegroundColor Gray
   181	Write-Host ""
   182	Write-Host "  NEXT: Paste the code into each file, then follow DEPLOY_GUIDE.md" -ForegroundColor Yellow
   183	Write-Host ""│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [033]: create-project.ps1


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [034/60]: fix-and-deploy.sh
│ LANGUAGE: bash | LINES: 381 | SIZE: 16144 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	#!/usr/bin/env bash
     2	set -uo pipefail
     3	
     4	RED='\033[0;31m'
     5	GREEN='\033[0;32m'
     6	YELLOW='\033[1;33m'
     7	CYAN='\033[0;36m'
     8	BOLD='\033[1m'
     9	DIM='\033[2m'
    10	NC='\033[0m'
    11	
    12	PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
    13	APP_NAME="son-of-anton"
    14	ERRORS=""
    15	WARNINGS=""
    16	
    17	banner() {
    18	  echo ""
    19	  echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
    20	  echo -e "${BOLD}  $1${NC}"
    21	  echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
    22	}
    23	
    24	ok()   { echo -e "  ${GREEN}✔${NC} $1"; }
    25	fail() { echo -e "  ${RED}✘${NC} $1"; ERRORS="${ERRORS}\n  ${RED}✘${NC} $1"; }
    26	step() { echo -e "  ${CYAN}▸${NC} $1"; }
    27	warn() { echo -e "  ${YELLOW}⚠${NC} $1"; WARNINGS="${WARNINGS}\n  ${YELLOW}⚠${NC} $1"; }
    28	info() { echo -e "  ${DIM}ℹ${NC} $1"; }
    29	
    30	has_errors() { [[ -n "$ERRORS" ]]; }
    31	
    32	cd "$PROJECT_DIR"
    33	
    34	# ══════════════════════════════════════════════════
    35	#  Step 0: Check ALL files at once
    36	# ══════════════════════════════════════════════════
    37	banner "Step 0 — Full File Verification (checks everything)"
    38	
    39	# ── api.js exports ──
    40	step "Checking api.js exports..."
    41	API_FILE="frontend/src/api.js"
    42	if [[ ! -f "$API_FILE" ]]; then
    43	  fail "frontend/src/api.js DOES NOT EXIST"
    44	else
    45	  API_MISSING=""
    46	  for EXPORT in adminStats adminListUsers adminCreateUser adminUpdateUser adminDeleteUser uploadAttachments getAttachmentUrl deleteAttachment downloadZip streamMessage login register getMe listChats createChat updateChat deleteChat getMessages listKnowledgeBases createKnowledgeBase deleteKnowledgeBase uploadDocuments; do
    47	    if ! grep -q "export.*${EXPORT}" "$API_FILE" 2>/dev/null; then
    48	      API_MISSING="$API_MISSING $EXPORT"
    49	    fi
    50	  done
    51	  if [[ -n "$API_MISSING" ]]; then
    52	    fail "api.js MISSING exports:${API_MISSING}"
    53	  else
    54	    ok "api.js — all 20 exports present"
    55	  fi
    56	fi
    57	
    58	# ── Dockerfile ──
    59	step "Checking Dockerfile..."
    60	if [[ ! -f Dockerfile ]]; then
    61	  fail "Dockerfile DOES NOT EXIST"
    62	else
    63	  DF_ISSUES=""
    64	  grep -q "\-\-include=dev" Dockerfile 2>/dev/null || DF_ISSUES="${DF_ISSUES} missing --include=dev"
    65	  grep -q "npx vite build" Dockerfile 2>/dev/null  || DF_ISSUES="${DF_ISSUES} missing 'npx vite build'"
    66	  grep -q "ENV NODE_ENV=" Dockerfile 2>/dev/null    || DF_ISSUES="${DF_ISSUES} missing 'ENV NODE_ENV='"
    67	  grep -q "ffmpeg" Dockerfile 2>/dev/null           || DF_ISSUES="${DF_ISSUES} missing ffmpeg install"
    68	  grep -q "chat_attachments" Dockerfile 2>/dev/null || DF_ISSUES="${DF_ISSUES} missing chat_attachments mkdir"
    69	  if [[ -n "$DF_ISSUES" ]]; then
    70	    fail "Dockerfile issues:${DF_ISSUES}"
    71	  else
    72	    ok "Dockerfile — all 5 checks pass"
    73	  fi
    74	fi
    75	
    76	# ── .dockerignore ──
    77	step "Checking .dockerignore..."
    78	if [[ ! -f .dockerignore ]]; then
    79	  fail ".dockerignore DOES NOT EXIST"
    80	else
    81	  DI_ISSUES=""
    82	  grep -q "node_modules" .dockerignore 2>/dev/null || DI_ISSUES="${DI_ISSUES} missing node_modules"
    83	  grep -q "__pycache__" .dockerignore 2>/dev/null  || DI_ISSUES="${DI_ISSUES} missing __pycache__"
    84	  if [[ -n "$DI_ISSUES" ]]; then
    85	    fail ".dockerignore issues:${DI_ISSUES}"
    86	  else
    87	    ok ".dockerignore — looks good"
    88	  fi
    89	fi
    90	
    91	# ── Backend files ──
    92	step "Checking backend files..."
    93	BACKEND_MISSING=""
    94	for F in backend/main.py backend/config.py backend/models.py backend/auth.py backend/database.py backend/seed.py backend/system_prompt.py backend/routes/auth_routes.py backend/routes/chat_routes.py backend/routes/admin_routes.py backend/routes/knowledge_routes.py backend/routes/files_routes.py backend/routes/attachment_routes.py backend/services/bedrock_service.py backend/services/memory_service.py backend/services/rag_service.py backend/services/code_extractor.py backend/services/attachment_service.py; do
    95	  if [[ ! -f "$F" ]]; then
    96	    BACKEND_MISSING="$BACKEND_MISSING $F"
    97	  fi
    98	done
    99	if [[ -n "$BACKEND_MISSING" ]]; then
   100	  fail "Missing backend files:${BACKEND_MISSING}"
   101	else
   102	  ok "All 18 backend files present"
   103	fi
   104	
   105	# ── Backend content checks ──
   106	step "Checking backend file contents..."
   107	CONTENT_ISSUES=""
   108	grep -q "attachment_router" backend/main.py 2>/dev/null            || CONTENT_ISSUES="${CONTENT_ISSUES} main.py: missing attachment_router import"
   109	grep -q "ChatAttachment" backend/models.py 2>/dev/null             || CONTENT_ISSUES="${CONTENT_ISSUES} models.py: missing ChatAttachment model"
   110	grep -q "ATTACHMENT_PATH" backend/config.py 2>/dev/null            || CONTENT_ISSUES="${CONTENT_ISSUES} config.py: missing ATTACHMENT_PATH"
   111	grep -q "attachment_ids" backend/routes/chat_routes.py 2>/dev/null || CONTENT_ISSUES="${CONTENT_ISSUES} chat_routes.py: missing attachment_ids"
   112	grep -q "build_claude_content_blocks" backend/services/attachment_service.py 2>/dev/null || CONTENT_ISSUES="${CONTENT_ISSUES} attachment_service.py: missing build_claude_content_blocks"
   113	if [[ -n "$CONTENT_ISSUES" ]]; then
   114	  fail "Backend content issues:${CONTENT_ISSUES}"
   115	else
   116	  ok "Backend file contents verified"
   117	fi
   118	
   119	# ── Frontend component files ──
   120	step "Checking frontend files..."
   121	FRONTEND_MISSING=""
   122	for F in frontend/src/App.jsx frontend/src/main.jsx frontend/src/store.jsx frontend/src/streamManager.js frontend/src/index.css frontend/src/components/ChatView.jsx frontend/src/components/MessageBubble.jsx frontend/src/components/CodeBlock.jsx frontend/src/components/Sidebar.jsx frontend/src/pages/LoginPage.jsx frontend/src/pages/ChatPage.jsx frontend/src/pages/AdminPage.jsx frontend/package.json frontend/vite.config.js frontend/tailwind.config.js frontend/postcss.config.js frontend/index.html; do
   123	  if [[ ! -f "$F" ]]; then
   124	    FRONTEND_MISSING="$FRONTEND_MISSING $F"
   125	  fi
   126	done
   127	if [[ -n "$FRONTEND_MISSING" ]]; then
   128	  fail "Missing frontend files:${FRONTEND_MISSING}"
   129	else
   130	  ok "All 17 frontend files present"
   131	fi
   132	
   133	# ── Frontend content checks ──
   134	step "Checking frontend file contents..."
   135	FE_ISSUES=""
   136	grep -q "uploadAttachments" frontend/src/components/ChatView.jsx 2>/dev/null || FE_ISSUES="${FE_ISSUES} ChatView.jsx: missing uploadAttachments"
   137	grep -q "getAttachmentUrl" frontend/src/components/MessageBubble.jsx 2>/dev/null || FE_ISSUES="${FE_ISSUES} MessageBubble.jsx: missing getAttachmentUrl"
   138	grep -q "Paperclip\|paperclip" frontend/src/components/ChatView.jsx 2>/dev/null || FE_ISSUES="${FE_ISSUES} ChatView.jsx: missing Paperclip (no file attach UI)"
   139	grep -q "Pillow" requirements.txt 2>/dev/null || FE_ISSUES="${FE_ISSUES} requirements.txt: missing Pillow"
   140	if [[ -n "$FE_ISSUES" ]]; then
   141	  fail "Frontend/deps content issues:${FE_ISSUES}"
   142	else
   143	  ok "Frontend file contents verified"
   144	fi
   145	
   146	# ── requirements.txt ──
   147	step "Checking requirements.txt..."
   148	REQ_MISSING=""
   149	for PKG in fastapi uvicorn sqlalchemy pyjwt passlib httpx chromadb PyPDF2 pydantic Pillow; do
   150	  if ! grep -qi "$PKG" requirements.txt 2>/dev/null; then
   151	    REQ_MISSING="$REQ_MISSING $PKG"
   152	  fi
   153	done
   154	if [[ -n "$REQ_MISSING" ]]; then
   155	  fail "requirements.txt missing:${REQ_MISSING}"
   156	else
   157	  ok "requirements.txt — all 10 packages present"
   158	fi
   159	
   160	# ══════════════════════════════════════════════════
   161	#  STOP if any errors
   162	# ══════════════════════════════════════════════════
   163	echo ""
   164	if has_errors; then
   165	  echo -e "${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   166	  echo -e "${RED}${BOLD}  FILES ARE BROKEN — Cannot deploy${NC}"
   167	  echo -e "${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   168	  echo ""
   169	  echo -e "${BOLD}All issues found:${NC}"
   170	  echo -e "$ERRORS"
   171	  echo ""
   172	  echo -e "${YELLOW}${BOLD}Fix ALL of the above, then run this script again.${NC}"
   173	  echo ""
   174	  exit 1
   175	fi
   176	
   177	echo ""
   178	echo -e "  ${GREEN}${BOLD}All file checks passed ✔${NC}"
   179	
   180	# ══════════════════════════════════════════════════
   181	#  Step 1: Local build test
   182	# ══════════════════════════════════════════════════
   183	banner "Step 1 — Local Build Test"
   184	
   185	step "Cleaning frontend/node_modules and dist..."
   186	rm -rf frontend/node_modules frontend/dist 2>/dev/null || true
   187	
   188	step "npm install..."
   189	cd frontend
   190	if ! npm install --legacy-peer-deps --include=dev 2>&1 | tail -5; then
   191	  echo -e "\n  ${RED}${BOLD}npm install failed. Fix above errors.${NC}"
   192	  exit 1
   193	fi
   194	ok "Dependencies installed"
   195	
   196	step "Building frontend..."
   197	if ! npm run build 2>&1; then
   198	  echo -e "\n  ${RED}${BOLD}BUILD FAILED. Fix the errors shown above.${NC}"
   199	  exit 1
   200	fi
   201	ok "Frontend builds clean"
   202	cd "$PROJECT_DIR"
   203	
   204	# ══════════════════════════════════════════════════
   205	#  Step 2: Get CapRover credentials
   206	# ══════════════════════════════════════════════════
   207	banner "Step 2 — CapRover Connection"
   208	
   209	echo -e "  ${BOLD}CapRover URL${NC} (e.g. https://captain.yourdomain.com):"
   210	read -r CAPROVER_URL
   211	CAPROVER_URL="${CAPROVER_URL%/}"
   212	[[ -z "$CAPROVER_URL" ]] && { echo -e "  ${RED}URL required${NC}"; exit 1; }
   213	
   214	echo -e "  ${BOLD}CapRover password:${NC}"
   215	read -rs CAPROVER_PASSWORD
   216	echo ""
   217	[[ -z "$CAPROVER_PASSWORD" ]] && { echo -e "  ${RED}Password required${NC}"; exit 1; }
   218	
   219	step "Authenticating..."
   220	LOGIN_RESP=$(curl -s -X POST "${CAPROVER_URL}/api/v2/login" \
   221	  -H "Content-Type: application/json" \
   222	  -H "x-namespace: captain" \
   223	  -d "{\"password\": \"${CAPROVER_PASSWORD}\"}" \
   224	  --connect-timeout 15 --max-time 30 2>&1) || { echo -e "  ${RED}Cannot reach CapRover${NC}"; exit 1; }
   225	
   226	TOKEN=$(echo "$LOGIN_RESP" | python3 -c "
   227	import sys, json
   228	try:
   229	    d = json.load(sys.stdin)
   230	    print(d['data']['token'] if d.get('status') == 100 else '')
   231	except: pass
   232	" 2>/dev/null)
   233	
   234	[[ -z "$TOKEN" ]] && { echo -e "  ${RED}Login failed: $(echo "$LOGIN_RESP" | head -c 200)${NC}"; exit 1; }
   235	ok "Authenticated"
   236	
   237	# ══════════════════════════════════════════════════
   238	#  Step 3: Build tarball from LOCAL files
   239	# ══════════════════════════════════════════════════
   240	banner "Step 3 — Build Deploy Package"
   241	
   242	rm -rf frontend/node_modules frontend/dist 2>/dev/null || true
   243	
   244	TMPDIR=$(mktemp -d)
   245	TARBALL="${TMPDIR}/deploy.tar"
   246	
   247	step "Creating tarball..."
   248	tar cf "$TARBALL" \
   249	  --exclude='.git' \
   250	  --exclude='node_modules' \
   251	  --exclude='dist' \
   252	  --exclude='.DS_Store' \
   253	  --exclude='__pycache__' \
   254	  --exclude='*.pyc' \
   255	  --exclude='.env' \
   256	  --exclude='.env.local' \
   257	  --exclude='.idea' \
   258	  --exclude='.vscode' \
   259	  --exclude='create-project.ps1' \
   260	  --exclude='*.sh' \
   261	  .
   262	
   263	TARSIZE=$(du -sh "$TARBALL" | cut -f1)
   264	ok "Tarball: ${TARSIZE}"
   265	
   266	# Verify inside tarball
   267	step "Verifying tarball contents..."
   268	TAR_ERRORS=""
   269	for F in Dockerfile .dockerignore requirements.txt warmup.py backend/main.py backend/routes/attachment_routes.py backend/services/attachment_service.py frontend/package.json frontend/src/api.js frontend/src/components/ChatView.jsx frontend/src/components/MessageBubble.jsx; do
   270	  if ! tar tf "$TARBALL" "./${F}" > /dev/null 2>&1; then
   271	    TAR_ERRORS="${TAR_ERRORS} ${F}"
   272	  fi
   273	done
   274	if [[ -n "$TAR_ERRORS" ]]; then
   275	  echo -e "  ${RED}Tarball missing:${TAR_ERRORS}${NC}"
   276	  rm -rf "$TMPDIR"
   277	  exit 1
   278	fi
   279	
   280	# Verify actual content inside tarball
   281	API_COUNT=$(tar xf "$TARBALL" -O ./frontend/src/api.js 2>/dev/null | grep -c "adminStats" || true)
   282	DF_COUNT=$(tar xf "$TARBALL" -O ./Dockerfile 2>/dev/null | grep -c "\-\-include=dev" || true)
   283	if [[ "$API_COUNT" -lt 1 ]]; then
   284	  echo -e "  ${RED}api.js inside tarball has NO adminStats export!${NC}"
   285	  rm -rf "$TMPDIR"
   286	  exit 1
   287	fi
   288	if [[ "$DF_COUNT" -lt 1 ]]; then
   289	  echo -e "  ${RED}Dockerfile inside tarball is OLD (no --include=dev)!${NC}"
   290	  rm -rf "$TMPDIR"
   291	  exit 1
   292	fi
   293	ok "Tarball contents verified (api.js ✔, Dockerfile ✔)"
   294	
   295	# ══════════════════════════════════════════════════
   296	#  Step 4: Deploy
   297	# ══════════════════════════════════════════════════
   298	banner "Step 4 — Deploy to CapRover"
   299	
   300	step "Uploading & building (5-15 min)..."
   301	info "node:20 → npm install → vite build → python:3.11 → pip install → chromadb warmup"
   302	echo ""
   303	
   304	DEPLOY_RESP=$(curl -s -X POST \
   305	  "${CAPROVER_URL}/api/v2/user/apps/appData/${APP_NAME}?gitHash=$(date +%s)" \
   306	  -H "x-namespace: captain" \
   307	  -H "x-captain-auth: ${TOKEN}" \
   308	  -F "sourceFile=@${TARBALL}" \
   309	  --connect-timeout 30 \
   310	  --max-time 1200 \
   311	  2>&1)
   312	
   313	rm -rf "$TMPDIR"
   314	
   315	DEPLOY_STATUS=$(echo "$DEPLOY_RESP" | python3 -c "
   316	import sys, json
   317	try:
   318	    d = json.load(sys.stdin)
   319	    if d.get('status') == 100:
   320	        print('SUCCESS')
   321	    else:
   322	        print('FAIL:' + (d.get('description','') or d.get('message','') or str(d)))
   323	except Exception as e:
   324	    print('FAIL:' + str(e))
   325	" 2>/dev/null)
   326	
   327	echo ""
   328	if [[ "$DEPLOY_STATUS" == "SUCCESS" ]]; then
   329	  echo -e "  ${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
   330	  echo -e "  ${GREEN}${BOLD}  🔥 DEPLOYMENT SUCCESSFUL!${NC}"
   331	  echo -e "  ${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
   332	  echo ""
   333	  ok "Son of Anton is live."
   334	  echo ""
   335	  info "Default login: superadmin / (your SUPERADMIN_PASSWORD env var)"
   336	  echo ""
   337	else
   338	  ERRMSG="${DEPLOY_STATUS#FAIL:}"
   339	  echo -e "  ${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   340	  echo -e "  ${RED}${BOLD}  ✘ DEPLOYMENT FAILED${NC}"
   341	  echo -e "  ${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   342	  echo ""
   343	  echo -e "  ${RED}Error: ${ERRMSG}${NC}"
   344	  echo ""
   345	
   346	  step "Fetching build logs..."
   347	  sleep 3
   348	  BUILD_LOG=$(curl -s -X POST "${CAPROVER_URL}/api/v2/user/apps/appData/${APP_NAME}/buildLogs" \
   349	    -H "x-namespace: captain" \
   350	    -H "x-captain-auth: ${TOKEN}" \
   351	    -H "Content-Type: application/json" \
   352	    -d "{\"appName\": \"${APP_NAME}\"}" \
   353	    --connect-timeout 10 --max-time 15 2>&1)
   354	
   355	  LOGS=$(echo "$BUILD_LOG" | python3 -c "
   356	import sys, json
   357	try:
   358	    d = json.load(sys.stdin)
   359	    logs = d.get('data', {}).get('logs', '') or ''
   360	    if not logs: logs = json.dumps(d, indent=2)
   361	    for line in logs.strip().split('\n')[-60:]:
   362	        print(line)
   363	except: print('Could not fetch logs')
   364	" 2>/dev/null)
   365	
   366	  if [[ -n "$LOGS" ]]; then
   367	    echo -e "  ${YELLOW}── Build Logs (last 60 lines) ──${NC}"
   368	    echo ""
   369	    echo "$LOGS" | while IFS= read -r line; do
   370	      if echo "$line" | grep -qiE "error|ERR!|failed|FATAL|not exported"; then
   371	        echo -e "    ${RED}${line}${NC}"
   372	      elif echo "$line" | grep -qiE "Step "; then
   373	        echo -e "    ${CYAN}${line}${NC}"
   374	      else
   375	        echo -e "    ${DIM}${line}${NC}"
   376	      fi
   377	    done
   378	    echo ""
   379	    echo -e "  ${YELLOW}── End ──${NC}"
   380	  fi
   381	  exit 1
   382	fi│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [034]: fix-and-deploy.sh


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [035/60]: frontend/index.html
│ LANGUAGE: html | LINES: 34 | SIZE: 1393 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	<!DOCTYPE html>
     2	<html lang="en" class="dark">
     3	
     4	<head>
     5	  <meta charset="UTF-8" />
     6	  <meta name="viewport"
     7	    content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
     8	  <title>Son of Anton</title>
     9	
    10	  <!-- KILL BROWSER CACHE FOR THIS HTML -->
    11	  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0" />
    12	  <meta http-equiv="Pragma" content="no-cache" />
    13	  <meta http-equiv="Expires" content="0" />
    14	
    15	  <!-- PWA / Mobile -->
    16	  <meta name="apple-mobile-web-app-capable" content="yes" />
    17	  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    18	  <meta name="theme-color" content="#09090f" />
    19	  <meta name="mobile-web-app-capable" content="yes" />
    20	
    21	  <link rel="preconnect" href="https://fonts.googleapis.com" />
    22	  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    23	  <link
    24	    href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
    25	    rel="stylesheet" />
    26	  <link rel="icon"
    27	    href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>" />
    28	</head>
    29	
    30	<body class="bg-anton-bg text-anton-text font-sans overscroll-none">
    31	  <div id="root"></div>
    32	  <script type="module" src="/src/main.jsx"></script>
    33	</body>
    34	
    35	</html>│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [035]: frontend/index.html


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [036/60]: frontend/package-lock.json
│ LANGUAGE: json | LINES: 4542 | SIZE: 162005 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	{
     2	  "name": "son-of-anton-frontend",
     3	  "version": "1.0.0",
     4	  "lockfileVersion": 3,
     5	  "requires": true,
     6	  "packages": {
     7	    "": {
     8	      "name": "son-of-anton-frontend",
     9	      "version": "1.0.0",
    10	      "dependencies": {
    11	        "lucide-react": "^0.469.0",
    12	        "react": "^18.3.1",
    13	        "react-dom": "^18.3.1",
    14	        "react-markdown": "^9.0.1",
    15	        "react-router-dom": "^7.1.1",
    16	        "react-syntax-highlighter": "^15.6.1",
    17	        "remark-gfm": "^4.0.0"
    18	      },
    19	      "devDependencies": {
    20	        "@types/react": "^18.3.18",
    21	        "@vitejs/plugin-react": "^4.3.4",
    22	        "autoprefixer": "^10.4.20",
    23	        "postcss": "^8.4.49",
    24	        "tailwindcss": "^3.4.17",
    25	        "vite": "^6.0.7"
    26	      }
    27	    },
    28	    "node_modules/@alloc/quick-lru": {
    29	      "version": "5.2.0",
    30	      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
    31	      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
    32	      "dev": true,
    33	      "license": "MIT",
    34	      "engines": {
    35	        "node": ">=10"
    36	      },
    37	      "funding": {
    38	        "url": "https://github.com/sponsors/sindresorhus"
    39	      }
    40	    },
    41	    "node_modules/@babel/code-frame": {
    42	      "version": "7.29.0",
    43	      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
    44	      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
    45	      "dev": true,
    46	      "license": "MIT",
    47	      "dependencies": {
    48	        "@babel/helper-validator-identifier": "^7.28.5",
    49	        "js-tokens": "^4.0.0",
    50	        "picocolors": "^1.1.1"
    51	      },
    52	      "engines": {
    53	        "node": ">=6.9.0"
    54	      }
    55	    },
    56	    "node_modules/@babel/compat-data": {
    57	      "version": "7.29.0",
    58	      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
    59	      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
    60	      "dev": true,
    61	      "license": "MIT",
    62	      "engines": {
    63	        "node": ">=6.9.0"
    64	      }
    65	    },
    66	    "node_modules/@babel/core": {
    67	      "version": "7.29.0",
    68	      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
    69	      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
    70	      "dev": true,
    71	      "license": "MIT",
    72	      "dependencies": {
    73	        "@babel/code-frame": "^7.29.0",
    74	        "@babel/generator": "^7.29.0",
    75	        "@babel/helper-compilation-targets": "^7.28.6",
    76	        "@babel/helper-module-transforms": "^7.28.6",
    77	        "@babel/helpers": "^7.28.6",
    78	        "@babel/parser": "^7.29.0",
    79	        "@babel/template": "^7.28.6",
    80	        "@babel/traverse": "^7.29.0",
    81	        "@babel/types": "^7.29.0",
    82	        "@jridgewell/remapping": "^2.3.5",
    83	        "convert-source-map": "^2.0.0",
    84	        "debug": "^4.1.0",
    85	        "gensync": "^1.0.0-beta.2",
    86	        "json5": "^2.2.3",
    87	        "semver": "^6.3.1"
    88	      },
    89	      "engines": {
    90	        "node": ">=6.9.0"
    91	      },
    92	      "funding": {
    93	        "type": "opencollective",
    94	        "url": "https://opencollective.com/babel"
    95	      }
    96	    },
    97	    "node_modules/@babel/generator": {
    98	      "version": "7.29.1",
    99	      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
   100	      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
   101	      "dev": true,
   102	      "license": "MIT",
   103	      "dependencies": {
   104	        "@babel/parser": "^7.29.0",
   105	        "@babel/types": "^7.29.0",
   106	        "@jridgewell/gen-mapping": "^0.3.12",
   107	        "@jridgewell/trace-mapping": "^0.3.28",
   108	        "jsesc": "^3.0.2"
   109	      },
   110	      "engines": {
   111	        "node": ">=6.9.0"
   112	      }
   113	    },
   114	    "node_modules/@babel/helper-compilation-targets": {
   115	      "version": "7.28.6",
   116	      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
   117	      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
   118	      "dev": true,
   119	      "license": "MIT",
   120	      "dependencies": {
   121	        "@babel/compat-data": "^7.28.6",
   122	        "@babel/helper-validator-option": "^7.27.1",
   123	        "browserslist": "^4.24.0",
   124	        "lru-cache": "^5.1.1",
   125	        "semver": "^6.3.1"
   126	      },
   127	      "engines": {
   128	        "node": ">=6.9.0"
   129	      }
   130	    },
   131	    "node_modules/@babel/helper-globals": {
   132	      "version": "7.28.0",
   133	      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
   134	      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
   135	      "dev": true,
   136	      "license": "MIT",
   137	      "engines": {
   138	        "node": ">=6.9.0"
   139	      }
   140	    },
   141	    "node_modules/@babel/helper-module-imports": {
   142	      "version": "7.28.6",
   143	      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
   144	      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
   145	      "dev": true,
   146	      "license": "MIT",
   147	      "dependencies": {
   148	        "@babel/traverse": "^7.28.6",
   149	        "@babel/types": "^7.28.6"
   150	      },
   151	      "engines": {
   152	        "node": ">=6.9.0"
   153	      }
   154	    },
   155	    "node_modules/@babel/helper-module-transforms": {
   156	      "version": "7.28.6",
   157	      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
   158	      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
   159	      "dev": true,
   160	      "license": "MIT",
   161	      "dependencies": {
   162	        "@babel/helper-module-imports": "^7.28.6",
   163	        "@babel/helper-validator-identifier": "^7.28.5",
   164	        "@babel/traverse": "^7.28.6"
   165	      },
   166	      "engines": {
   167	        "node": ">=6.9.0"
   168	      },
   169	      "peerDependencies": {
   170	        "@babel/core": "^7.0.0"
   171	      }
   172	    },
   173	    "node_modules/@babel/helper-plugin-utils": {
   174	      "version": "7.28.6",
   175	      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
   176	      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
   177	      "dev": true,
   178	      "license": "MIT",
   179	      "engines": {
   180	        "node": ">=6.9.0"
   181	      }
   182	    },
   183	    "node_modules/@babel/helper-string-parser": {
   184	      "version": "7.27.1",
   185	      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
   186	      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
   187	      "dev": true,
   188	      "license": "MIT",
   189	      "engines": {
   190	        "node": ">=6.9.0"
   191	      }
   192	    },
   193	    "node_modules/@babel/helper-validator-identifier": {
   194	      "version": "7.28.5",
   195	      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
   196	      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
   197	      "dev": true,
   198	      "license": "MIT",
   199	      "engines": {
   200	        "node": ">=6.9.0"
   201	      }
   202	    },
   203	    "node_modules/@babel/helper-validator-option": {
   204	      "version": "7.27.1",
   205	      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
   206	      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
   207	      "dev": true,
   208	      "license": "MIT",
   209	      "engines": {
   210	        "node": ">=6.9.0"
   211	      }
   212	    },
   213	    "node_modules/@babel/helpers": {
   214	      "version": "7.29.2",
   215	      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
   216	      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
   217	      "dev": true,
   218	      "license": "MIT",
   219	      "dependencies": {
   220	        "@babel/template": "^7.28.6",
   221	        "@babel/types": "^7.29.0"
   222	      },
   223	      "engines": {
   224	        "node": ">=6.9.0"
   225	      }
   226	    },
   227	    "node_modules/@babel/parser": {
   228	      "version": "7.29.2",
   229	      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
   230	      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
   231	      "dev": true,
   232	      "license": "MIT",
   233	      "dependencies": {
   234	        "@babel/types": "^7.29.0"
   235	      },
   236	      "bin": {
   237	        "parser": "bin/babel-parser.js"
   238	      },
   239	      "engines": {
   240	        "node": ">=6.0.0"
   241	      }
   242	    },
   243	    "node_modules/@babel/plugin-transform-react-jsx-self": {
   244	      "version": "7.27.1",
   245	      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
   246	      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
   247	      "dev": true,
   248	      "license": "MIT",
   249	      "dependencies": {
   250	        "@babel/helper-plugin-utils": "^7.27.1"
   251	      },
   252	      "engines": {
   253	        "node": ">=6.9.0"
   254	      },
   255	      "peerDependencies": {
   256	        "@babel/core": "^7.0.0-0"
   257	      }
   258	    },
   259	    "node_modules/@babel/plugin-transform-react-jsx-source": {
   260	      "version": "7.27.1",
   261	      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
   262	      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
   263	      "dev": true,
   264	      "license": "MIT",
   265	      "dependencies": {
   266	        "@babel/helper-plugin-utils": "^7.27.1"
   267	      },
   268	      "engines": {
   269	        "node": ">=6.9.0"
   270	      },
   271	      "peerDependencies": {
   272	        "@babel/core": "^7.0.0-0"
   273	      }
   274	    },
   275	    "node_modules/@babel/runtime": {
   276	      "version": "7.29.2",
   277	      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
   278	      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
   279	      "license": "MIT",
   280	      "engines": {
   281	        "node": ">=6.9.0"
   282	      }
   283	    },
   284	    "node_modules/@babel/template": {
   285	      "version": "7.28.6",
   286	      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
   287	      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
   288	      "dev": true,
   289	      "license": "MIT",
   290	      "dependencies": {
   291	        "@babel/code-frame": "^7.28.6",
   292	        "@babel/parser": "^7.28.6",
   293	        "@babel/types": "^7.28.6"
   294	      },
   295	      "engines": {
   296	        "node": ">=6.9.0"
   297	      }
   298	    },
   299	    "node_modules/@babel/traverse": {
   300	      "version": "7.29.0",
   301	      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
   302	      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
   303	      "dev": true,
   304	      "license": "MIT",
   305	      "dependencies": {
   306	        "@babel/code-frame": "^7.29.0",
   307	        "@babel/generator": "^7.29.0",
   308	        "@babel/helper-globals": "^7.28.0",
   309	        "@babel/parser": "^7.29.0",
   310	        "@babel/template": "^7.28.6",
   311	        "@babel/types": "^7.29.0",
   312	        "debug": "^4.3.1"
   313	      },
   314	      "engines": {
   315	        "node": ">=6.9.0"
   316	      }
   317	    },
   318	    "node_modules/@babel/types": {
   319	      "version": "7.29.0",
   320	      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
   321	      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
   322	      "dev": true,
   323	      "license": "MIT",
   324	      "dependencies": {
   325	        "@babel/helper-string-parser": "^7.27.1",
   326	        "@babel/helper-validator-identifier": "^7.28.5"
   327	      },
   328	      "engines": {
   329	        "node": ">=6.9.0"
   330	      }
   331	    },
   332	    "node_modules/@esbuild/aix-ppc64": {
   333	      "version": "0.25.12",
   334	      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
   335	      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
   336	      "cpu": [
   337	        "ppc64"
   338	      ],
   339	      "dev": true,
   340	      "license": "MIT",
   341	      "optional": true,
   342	      "os": [
   343	        "aix"
   344	      ],
   345	      "engines": {
   346	        "node": ">=18"
   347	      }
   348	    },
   349	    "node_modules/@esbuild/android-arm": {
   350	      "version": "0.25.12",
   351	      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
   352	      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
   353	      "cpu": [
   354	        "arm"
   355	      ],
   356	      "dev": true,
   357	      "license": "MIT",
   358	      "optional": true,
   359	      "os": [
   360	        "android"
   361	      ],
   362	      "engines": {
   363	        "node": ">=18"
   364	      }
   365	    },
   366	    "node_modules/@esbuild/android-arm64": {
   367	      "version": "0.25.12",
   368	      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
   369	      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
   370	      "cpu": [
   371	        "arm64"
   372	      ],
   373	      "dev": true,
   374	      "license": "MIT",
   375	      "optional": true,
   376	      "os": [
   377	        "android"
   378	      ],
   379	      "engines": {
   380	        "node": ">=18"
   381	      }
   382	    },
   383	    "node_modules/@esbuild/android-x64": {
   384	      "version": "0.25.12",
   385	      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
   386	      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
   387	      "cpu": [
   388	        "x64"
   389	      ],
   390	      "dev": true,
   391	      "license": "MIT",
   392	      "optional": true,
   393	      "os": [
   394	        "android"
   395	      ],
   396	      "engines": {
   397	        "node": ">=18"
   398	      }
   399	    },
   400	    "node_modules/@esbuild/darwin-arm64": {
   401	      "version": "0.25.12",
   402	      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
   403	      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
   404	      "cpu": [
   405	        "arm64"
   406	      ],
   407	      "dev": true,
   408	      "license": "MIT",
   409	      "optional": true,
   410	      "os": [
   411	        "darwin"
   412	      ],
   413	      "engines": {
   414	        "node": ">=18"
   415	      }
   416	    },
   417	    "node_modules/@esbuild/darwin-x64": {
   418	      "version": "0.25.12",
   419	      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
   420	      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
   421	      "cpu": [
   422	        "x64"
   423	      ],
   424	      "dev": true,
   425	      "license": "MIT",
   426	      "optional": true,
   427	      "os": [
   428	        "darwin"
   429	      ],
   430	      "engines": {
   431	        "node": ">=18"
   432	      }
   433	    },
   434	    "node_modules/@esbuild/freebsd-arm64": {
   435	      "version": "0.25.12",
   436	      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
   437	      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
   438	      "cpu": [
   439	        "arm64"
   440	      ],
   441	      "dev": true,
   442	      "license": "MIT",
   443	      "optional": true,
   444	      "os": [
   445	        "freebsd"
   446	      ],
   447	      "engines": {
   448	        "node": ">=18"
   449	      }
   450	    },
   451	    "node_modules/@esbuild/freebsd-x64": {
   452	      "version": "0.25.12",
   453	      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
   454	      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
   455	      "cpu": [
   456	        "x64"
   457	      ],
   458	      "dev": true,
   459	      "license": "MIT",
   460	      "optional": true,
   461	      "os": [
   462	        "freebsd"
   463	      ],
   464	      "engines": {
   465	        "node": ">=18"
   466	      }
   467	    },
   468	    "node_modules/@esbuild/linux-arm": {
   469	      "version": "0.25.12",
   470	      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
   471	      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
   472	      "cpu": [
   473	        "arm"
   474	      ],
   475	      "dev": true,
   476	      "license": "MIT",
   477	      "optional": true,
   478	      "os": [
   479	        "linux"
   480	      ],
   481	      "engines": {
   482	        "node": ">=18"
   483	      }
   484	    },
   485	    "node_modules/@esbuild/linux-arm64": {
   486	      "version": "0.25.12",
   487	      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
   488	      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
   489	      "cpu": [
   490	        "arm64"
   491	      ],
   492	      "dev": true,
   493	      "license": "MIT",
   494	      "optional": true,
   495	      "os": [
   496	        "linux"
   497	      ],
   498	      "engines": {
   499	        "node": ">=18"
   500	      }
   501	    },
   502	    "node_modules/@esbuild/linux-ia32": {
   503	      "version": "0.25.12",
   504	      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
   505	      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
   506	      "cpu": [
   507	        "ia32"
   508	      ],
   509	      "dev": true,
   510	      "license": "MIT",
   511	      "optional": true,
   512	      "os": [
   513	        "linux"
   514	      ],
   515	      "engines": {
   516	        "node": ">=18"
   517	      }
   518	    },
   519	    "node_modules/@esbuild/linux-loong64": {
   520	      "version": "0.25.12",
   521	      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
   522	      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
   523	      "cpu": [
   524	        "loong64"
   525	      ],
   526	      "dev": true,
   527	      "license": "MIT",
   528	      "optional": true,
   529	      "os": [
   530	        "linux"
   531	      ],
   532	      "engines": {
   533	        "node": ">=18"
   534	      }
   535	    },
   536	    "node_modules/@esbuild/linux-mips64el": {
   537	      "version": "0.25.12",
   538	      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
   539	      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
   540	      "cpu": [
   541	        "mips64el"
   542	      ],
   543	      "dev": true,
   544	      "license": "MIT",
   545	      "optional": true,
   546	      "os": [
   547	        "linux"
   548	      ],
   549	      "engines": {
   550	        "node": ">=18"
   551	      }
   552	    },
   553	    "node_modules/@esbuild/linux-ppc64": {
   554	      "version": "0.25.12",
   555	      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
   556	      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
   557	      "cpu": [
   558	        "ppc64"
   559	      ],
   560	      "dev": true,
   561	      "license": "MIT",
   562	      "optional": true,
   563	      "os": [
   564	        "linux"
   565	      ],
   566	      "engines": {
   567	        "node": ">=18"
   568	      }
   569	    },
   570	    "node_modules/@esbuild/linux-riscv64": {
   571	      "version": "0.25.12",
   572	      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
   573	      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
   574	      "cpu": [
   575	        "riscv64"
   576	      ],
   577	      "dev": true,
   578	      "license": "MIT",
   579	      "optional": true,
   580	      "os": [
   581	        "linux"
   582	      ],
   583	      "engines": {
   584	        "node": ">=18"
   585	      }
   586	    },
   587	    "node_modules/@esbuild/linux-s390x": {
   588	      "version": "0.25.12",
   589	      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
   590	      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
   591	      "cpu": [
   592	        "s390x"
   593	      ],
   594	      "dev": true,
   595	      "license": "MIT",
   596	      "optional": true,
   597	      "os": [
   598	        "linux"
   599	      ],
   600	      "engines": {
   601	        "node": ">=18"
   602	      }
   603	    },
   604	    "node_modules/@esbuild/linux-x64": {
   605	      "version": "0.25.12",
   606	      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
   607	      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
   608	      "cpu": [
   609	        "x64"
   610	      ],
   611	      "dev": true,
   612	      "license": "MIT",
   613	      "optional": true,
   614	      "os": [
   615	        "linux"
   616	      ],
   617	      "engines": {
   618	        "node": ">=18"
   619	      }
   620	    },
   621	    "node_modules/@esbuild/netbsd-arm64": {
   622	      "version": "0.25.12",
   623	      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
   624	      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
   625	      "cpu": [
   626	        "arm64"
   627	      ],
   628	      "dev": true,
   629	      "license": "MIT",
   630	      "optional": true,
   631	      "os": [
   632	        "netbsd"
   633	      ],
   634	      "engines": {
   635	        "node": ">=18"
   636	      }
   637	    },
   638	    "node_modules/@esbuild/netbsd-x64": {
   639	      "version": "0.25.12",
   640	      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
   641	      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
   642	      "cpu": [
   643	        "x64"
   644	      ],
   645	      "dev": true,
   646	      "license": "MIT",
   647	      "optional": true,
   648	      "os": [
   649	        "netbsd"
   650	      ],
   651	      "engines": {
   652	        "node": ">=18"
   653	      }
   654	    },
   655	    "node_modules/@esbuild/openbsd-arm64": {
   656	      "version": "0.25.12",
   657	      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
   658	      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
   659	      "cpu": [
   660	        "arm64"
   661	      ],
   662	      "dev": true,
   663	      "license": "MIT",
   664	      "optional": true,
   665	      "os": [
   666	        "openbsd"
   667	      ],
   668	      "engines": {
   669	        "node": ">=18"
   670	      }
   671	    },
   672	    "node_modules/@esbuild/openbsd-x64": {
   673	      "version": "0.25.12",
   674	      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
   675	      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
   676	      "cpu": [
   677	        "x64"
   678	      ],
   679	      "dev": true,
   680	      "license": "MIT",
   681	      "optional": true,
   682	      "os": [
   683	        "openbsd"
   684	      ],
   685	      "engines": {
   686	        "node": ">=18"
   687	      }
   688	    },
   689	    "node_modules/@esbuild/openharmony-arm64": {
   690	      "version": "0.25.12",
   691	      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
   692	      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
   693	      "cpu": [
   694	        "arm64"
   695	      ],
   696	      "dev": true,
   697	      "license": "MIT",
   698	      "optional": true,
   699	      "os": [
   700	        "openharmony"
   701	      ],
   702	      "engines": {
   703	        "node": ">=18"
   704	      }
   705	    },
   706	    "node_modules/@esbuild/sunos-x64": {
   707	      "version": "0.25.12",
   708	      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
   709	      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
   710	      "cpu": [
   711	        "x64"
   712	      ],
   713	      "dev": true,
   714	      "license": "MIT",
   715	      "optional": true,
   716	      "os": [
   717	        "sunos"
   718	      ],
   719	      "engines": {
   720	        "node": ">=18"
   721	      }
   722	    },
   723	    "node_modules/@esbuild/win32-arm64": {
   724	      "version": "0.25.12",
   725	      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
   726	      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
   727	      "cpu": [
   728	        "arm64"
   729	      ],
   730	      "dev": true,
   731	      "license": "MIT",
   732	      "optional": true,
   733	      "os": [
   734	        "win32"
   735	      ],
   736	      "engines": {
   737	        "node": ">=18"
   738	      }
   739	    },
   740	    "node_modules/@esbuild/win32-ia32": {
   741	      "version": "0.25.12",
   742	      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
   743	      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
   744	      "cpu": [
   745	        "ia32"
   746	      ],
   747	      "dev": true,
   748	      "license": "MIT",
   749	      "optional": true,
   750	      "os": [
   751	        "win32"
   752	      ],
   753	      "engines": {
   754	        "node": ">=18"
   755	      }
   756	    },
   757	    "node_modules/@esbuild/win32-x64": {
   758	      "version": "0.25.12",
   759	      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
   760	      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
   761	      "cpu": [
   762	        "x64"
   763	      ],
   764	      "dev": true,
   765	      "license": "MIT",
   766	      "optional": true,
   767	      "os": [
   768	        "win32"
   769	      ],
   770	      "engines": {
   771	        "node": ">=18"
   772	      }
   773	    },
   774	    "node_modules/@jridgewell/gen-mapping": {
   775	      "version": "0.3.13",
   776	      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
   777	      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
   778	      "dev": true,
   779	      "license": "MIT",
   780	      "dependencies": {
   781	        "@jridgewell/sourcemap-codec": "^1.5.0",
   782	        "@jridgewell/trace-mapping": "^0.3.24"
   783	      }
   784	    },
   785	    "node_modules/@jridgewell/remapping": {
   786	      "version": "2.3.5",
   787	      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
   788	      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
   789	      "dev": true,
   790	      "license": "MIT",
   791	      "dependencies": {
   792	        "@jridgewell/gen-mapping": "^0.3.5",
   793	        "@jridgewell/trace-mapping": "^0.3.24"
   794	      }
   795	    },
   796	    "node_modules/@jridgewell/resolve-uri": {
   797	      "version": "3.1.2",
   798	      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
   799	      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
   800	      "dev": true,
   801	      "license": "MIT",
   802	      "engines": {
   803	        "node": ">=6.0.0"
   804	      }
   805	    },
   806	    "node_modules/@jridgewell/sourcemap-codec": {
   807	      "version": "1.5.5",
   808	      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
   809	      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
   810	      "dev": true,
   811	      "license": "MIT"
   812	    },
   813	    "node_modules/@jridgewell/trace-mapping": {
   814	      "version": "0.3.31",
   815	      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
   816	      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
   817	      "dev": true,
   818	      "license": "MIT",
   819	      "dependencies": {
   820	        "@jridgewell/resolve-uri": "^3.1.0",
   821	        "@jridgewell/sourcemap-codec": "^1.4.14"
   822	      }
   823	    },
   824	    "node_modules/@nodelib/fs.scandir": {
   825	      "version": "2.1.5",
   826	      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
   827	      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
   828	      "dev": true,
   829	      "license": "MIT",
   830	      "dependencies": {
   831	        "@nodelib/fs.stat": "2.0.5",
   832	        "run-parallel": "^1.1.9"
   833	      },
   834	      "engines": {
   835	        "node": ">= 8"
   836	      }
   837	    },
   838	    "node_modules/@nodelib/fs.stat": {
   839	      "version": "2.0.5",
   840	      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
   841	      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
   842	      "dev": true,
   843	      "license": "MIT",
   844	      "engines": {
   845	        "node": ">= 8"
   846	      }
   847	    },
   848	    "node_modules/@nodelib/fs.walk": {
   849	      "version": "1.2.8",
   850	      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
   851	      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
   852	      "dev": true,
   853	      "license": "MIT",
   854	      "dependencies": {
   855	        "@nodelib/fs.scandir": "2.1.5",
   856	        "fastq": "^1.6.0"
   857	      },
   858	      "engines": {
   859	        "node": ">= 8"
   860	      }
   861	    },
   862	    "node_modules/@rolldown/pluginutils": {
   863	      "version": "1.0.0-beta.27",
   864	      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
   865	      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
   866	      "dev": true,
   867	      "license": "MIT"
   868	    },
   869	    "node_modules/@rollup/rollup-android-arm-eabi": {
   870	      "version": "4.59.0",
   871	      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
   872	      "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
   873	      "cpu": [
   874	        "arm"
   875	      ],
   876	      "dev": true,
   877	      "license": "MIT",
   878	      "optional": true,
   879	      "os": [
   880	        "android"
   881	      ]
   882	    },
   883	    "node_modules/@rollup/rollup-android-arm64": {
   884	      "version": "4.59.0",
   885	      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
   886	      "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
   887	      "cpu": [
   888	        "arm64"
   889	      ],
   890	      "dev": true,
   891	      "license": "MIT",
   892	      "optional": true,
   893	      "os": [
   894	        "android"
   895	      ]
   896	    },
   897	    "node_modules/@rollup/rollup-darwin-arm64": {
   898	      "version": "4.59.0",
   899	      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
   900	      "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
   901	      "cpu": [
   902	        "arm64"
   903	      ],
   904	      "dev": true,
   905	      "license": "MIT",
   906	      "optional": true,
   907	      "os": [
   908	        "darwin"
   909	      ]
   910	    },
   911	    "node_modules/@rollup/rollup-darwin-x64": {
   912	      "version": "4.59.0",
   913	      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
   914	      "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
   915	      "cpu": [
   916	        "x64"
   917	      ],
   918	      "dev": true,
   919	      "license": "MIT",
   920	      "optional": true,
   921	      "os": [
   922	        "darwin"
   923	      ]
   924	    },
   925	    "node_modules/@rollup/rollup-freebsd-arm64": {
   926	      "version": "4.59.0",
   927	      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
   928	      "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
   929	      "cpu": [
   930	        "arm64"
   931	      ],
   932	      "dev": true,
   933	      "license": "MIT",
   934	      "optional": true,
   935	      "os": [
   936	        "freebsd"
   937	      ]
   938	    },
   939	    "node_modules/@rollup/rollup-freebsd-x64": {
   940	      "version": "4.59.0",
   941	      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
   942	      "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
   943	      "cpu": [
   944	        "x64"
   945	      ],
   946	      "dev": true,
   947	      "license": "MIT",
   948	      "optional": true,
   949	      "os": [
   950	        "freebsd"
   951	      ]
   952	    },
   953	    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
   954	      "version": "4.59.0",
   955	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
   956	      "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
   957	      "cpu": [
   958	        "arm"
   959	      ],
   960	      "dev": true,
   961	      "license": "MIT",
   962	      "optional": true,
   963	      "os": [
   964	        "linux"
   965	      ]
   966	    },
   967	    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
   968	      "version": "4.59.0",
   969	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
   970	      "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
   971	      "cpu": [
   972	        "arm"
   973	      ],
   974	      "dev": true,
   975	      "license": "MIT",
   976	      "optional": true,
   977	      "os": [
   978	        "linux"
   979	      ]
   980	    },
   981	    "node_modules/@rollup/rollup-linux-arm64-gnu": {
   982	      "version": "4.59.0",
   983	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
   984	      "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
   985	      "cpu": [
   986	        "arm64"
   987	      ],
   988	      "dev": true,
   989	      "license": "MIT",
   990	      "optional": true,
   991	      "os": [
   992	        "linux"
   993	      ]
   994	    },
   995	    "node_modules/@rollup/rollup-linux-arm64-musl": {
   996	      "version": "4.59.0",
   997	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
   998	      "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
   999	      "cpu": [
  1000	        "arm64"
  1001	      ],
  1002	      "dev": true,
  1003	      "license": "MIT",
  1004	      "optional": true,
  1005	      "os": [
  1006	        "linux"
  1007	      ]
  1008	    },
  1009	    "node_modules/@rollup/rollup-linux-loong64-gnu": {
  1010	      "version": "4.59.0",
  1011	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
  1012	      "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
  1013	      "cpu": [
  1014	        "loong64"
  1015	      ],
  1016	      "dev": true,
  1017	      "license": "MIT",
  1018	      "optional": true,
  1019	      "os": [
  1020	        "linux"
  1021	      ]
  1022	    },
  1023	    "node_modules/@rollup/rollup-linux-loong64-musl": {
  1024	      "version": "4.59.0",
  1025	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
  1026	      "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
  1027	      "cpu": [
  1028	        "loong64"
  1029	      ],
  1030	      "dev": true,
  1031	      "license": "MIT",
  1032	      "optional": true,
  1033	      "os": [
  1034	        "linux"
  1035	      ]
  1036	    },
  1037	    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
  1038	      "version": "4.59.0",
  1039	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
  1040	      "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
  1041	      "cpu": [
  1042	        "ppc64"
  1043	      ],
  1044	      "dev": true,
  1045	      "license": "MIT",
  1046	      "optional": true,
  1047	      "os": [
  1048	        "linux"
  1049	      ]
  1050	    },
  1051	    "node_modules/@rollup/rollup-linux-ppc64-musl": {
  1052	      "version": "4.59.0",
  1053	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
  1054	      "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
  1055	      "cpu": [
  1056	        "ppc64"
  1057	      ],
  1058	      "dev": true,
  1059	      "license": "MIT",
  1060	      "optional": true,
  1061	      "os": [
  1062	        "linux"
  1063	      ]
  1064	    },
  1065	    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
  1066	      "version": "4.59.0",
  1067	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
  1068	      "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
  1069	      "cpu": [
  1070	        "riscv64"
  1071	      ],
  1072	      "dev": true,
  1073	      "license": "MIT",
  1074	      "optional": true,
  1075	      "os": [
  1076	        "linux"
  1077	      ]
  1078	    },
  1079	    "node_modules/@rollup/rollup-linux-riscv64-musl": {
  1080	      "version": "4.59.0",
  1081	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
  1082	      "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
  1083	      "cpu": [
  1084	        "riscv64"
  1085	      ],
  1086	      "dev": true,
  1087	      "license": "MIT",
  1088	      "optional": true,
  1089	      "os": [
  1090	        "linux"
  1091	      ]
  1092	    },
  1093	    "node_modules/@rollup/rollup-linux-s390x-gnu": {
  1094	      "version": "4.59.0",
  1095	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
  1096	      "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
  1097	      "cpu": [
  1098	        "s390x"
  1099	      ],
  1100	      "dev": true,
  1101	      "license": "MIT",
  1102	      "optional": true,
  1103	      "os": [
  1104	        "linux"
  1105	      ]
  1106	    },
  1107	    "node_modules/@rollup/rollup-linux-x64-gnu": {
  1108	      "version": "4.59.0",
  1109	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
  1110	      "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
  1111	      "cpu": [
  1112	        "x64"
  1113	      ],
  1114	      "dev": true,
  1115	      "license": "MIT",
  1116	      "optional": true,
  1117	      "os": [
  1118	        "linux"
  1119	      ]
  1120	    },
  1121	    "node_modules/@rollup/rollup-linux-x64-musl": {
  1122	      "version": "4.59.0",
  1123	      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
  1124	      "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
  1125	      "cpu": [
  1126	        "x64"
  1127	      ],
  1128	      "dev": true,
  1129	      "license": "MIT",
  1130	      "optional": true,
  1131	      "os": [
  1132	        "linux"
  1133	      ]
  1134	    },
  1135	    "node_modules/@rollup/rollup-openbsd-x64": {
  1136	      "version": "4.59.0",
  1137	      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
  1138	      "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
  1139	      "cpu": [
  1140	        "x64"
  1141	      ],
  1142	      "dev": true,
  1143	      "license": "MIT",
  1144	      "optional": true,
  1145	      "os": [
  1146	        "openbsd"
  1147	      ]
  1148	    },
  1149	    "node_modules/@rollup/rollup-openharmony-arm64": {
  1150	      "version": "4.59.0",
  1151	      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
  1152	      "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
  1153	      "cpu": [
  1154	        "arm64"
  1155	      ],
  1156	      "dev": true,
  1157	      "license": "MIT",
  1158	      "optional": true,
  1159	      "os": [
  1160	        "openharmony"
  1161	      ]
  1162	    },
  1163	    "node_modules/@rollup/rollup-win32-arm64-msvc": {
  1164	      "version": "4.59.0",
  1165	      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
  1166	      "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
  1167	      "cpu": [
  1168	        "arm64"
  1169	      ],
  1170	      "dev": true,
  1171	      "license": "MIT",
  1172	      "optional": true,
  1173	      "os": [
  1174	        "win32"
  1175	      ]
  1176	    },
  1177	    "node_modules/@rollup/rollup-win32-ia32-msvc": {
  1178	      "version": "4.59.0",
  1179	      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
  1180	      "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
  1181	      "cpu": [
  1182	        "ia32"
  1183	      ],
  1184	      "dev": true,
  1185	      "license": "MIT",
  1186	      "optional": true,
  1187	      "os": [
  1188	        "win32"
  1189	      ]
  1190	    },
  1191	    "node_modules/@rollup/rollup-win32-x64-gnu": {
  1192	      "version": "4.59.0",
  1193	      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
  1194	      "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
  1195	      "cpu": [
  1196	        "x64"
  1197	      ],
  1198	      "dev": true,
  1199	      "license": "MIT",
  1200	      "optional": true,
  1201	      "os": [
  1202	        "win32"
  1203	      ]
  1204	    },
  1205	    "node_modules/@rollup/rollup-win32-x64-msvc": {
  1206	      "version": "4.59.0",
  1207	      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
  1208	      "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
  1209	      "cpu": [
  1210	        "x64"
  1211	      ],
  1212	      "dev": true,
  1213	      "license": "MIT",
  1214	      "optional": true,
  1215	      "os": [
  1216	        "win32"
  1217	      ]
  1218	    },
  1219	    "node_modules/@types/babel__core": {
  1220	      "version": "7.20.5",
  1221	      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
  1222	      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
  1223	      "dev": true,
  1224	      "license": "MIT",
  1225	      "dependencies": {
  1226	        "@babel/parser": "^7.20.7",
  1227	        "@babel/types": "^7.20.7",
  1228	        "@types/babel__generator": "*",
  1229	        "@types/babel__template": "*",
  1230	        "@types/babel__traverse": "*"
  1231	      }
  1232	    },
  1233	    "node_modules/@types/babel__generator": {
  1234	      "version": "7.27.0",
  1235	      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
  1236	      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
  1237	      "dev": true,
  1238	      "license": "MIT",
  1239	      "dependencies": {
  1240	        "@babel/types": "^7.0.0"
  1241	      }
  1242	    },
  1243	    "node_modules/@types/babel__template": {
  1244	      "version": "7.4.4",
  1245	      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
  1246	      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
  1247	      "dev": true,
  1248	      "license": "MIT",
  1249	      "dependencies": {
  1250	        "@babel/parser": "^7.1.0",
  1251	        "@babel/types": "^7.0.0"
  1252	      }
  1253	    },
  1254	    "node_modules/@types/babel__traverse": {
  1255	      "version": "7.28.0",
  1256	      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
  1257	      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
  1258	      "dev": true,
  1259	      "license": "MIT",
  1260	      "dependencies": {
  1261	        "@babel/types": "^7.28.2"
  1262	      }
  1263	    },
  1264	    "node_modules/@types/debug": {
  1265	      "version": "4.1.13",
  1266	      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
  1267	      "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
  1268	      "license": "MIT",
  1269	      "dependencies": {
  1270	        "@types/ms": "*"
  1271	      }
  1272	    },
  1273	    "node_modules/@types/estree": {
  1274	      "version": "1.0.8",
  1275	      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
  1276	      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
  1277	      "license": "MIT"
  1278	    },
  1279	    "node_modules/@types/estree-jsx": {
  1280	      "version": "1.0.5",
  1281	      "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
  1282	      "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
  1283	      "license": "MIT",
  1284	      "dependencies": {
  1285	        "@types/estree": "*"
  1286	      }
  1287	    },
  1288	    "node_modules/@types/hast": {
  1289	      "version": "3.0.4",
  1290	      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
  1291	      "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
  1292	      "license": "MIT",
  1293	      "dependencies": {
  1294	        "@types/unist": "*"
  1295	      }
  1296	    },
  1297	    "node_modules/@types/mdast": {
  1298	      "version": "4.0.4",
  1299	      "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
  1300	      "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
  1301	      "license": "MIT",
  1302	      "dependencies": {
  1303	        "@types/unist": "*"
  1304	      }
  1305	    },
  1306	    "node_modules/@types/ms": {
  1307	      "version": "2.1.0",
  1308	      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
  1309	      "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
  1310	      "license": "MIT"
  1311	    },
  1312	    "node_modules/@types/prop-types": {
  1313	      "version": "15.7.15",
  1314	      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
  1315	      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
  1316	      "dev": true,
  1317	      "license": "MIT"
  1318	    },
  1319	    "node_modules/@types/react": {
  1320	      "version": "18.3.28",
  1321	      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
  1322	      "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
  1323	      "dev": true,
  1324	      "license": "MIT",
  1325	      "dependencies": {
  1326	        "@types/prop-types": "*",
  1327	        "csstype": "^3.2.2"
  1328	      }
  1329	    },
  1330	    "node_modules/@types/unist": {
  1331	      "version": "3.0.3",
  1332	      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
  1333	      "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
  1334	      "license": "MIT"
  1335	    },
  1336	    "node_modules/@ungap/structured-clone": {
  1337	      "version": "1.3.0",
  1338	      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
  1339	      "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
  1340	      "license": "ISC"
  1341	    },
  1342	    "node_modules/@vitejs/plugin-react": {
  1343	      "version": "4.7.0",
  1344	      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
  1345	      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
  1346	      "dev": true,
  1347	      "license": "MIT",
  1348	      "dependencies": {
  1349	        "@babel/core": "^7.28.0",
  1350	        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
  1351	        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
  1352	        "@rolldown/pluginutils": "1.0.0-beta.27",
  1353	        "@types/babel__core": "^7.20.5",
  1354	        "react-refresh": "^0.17.0"
  1355	      },
  1356	      "engines": {
  1357	        "node": "^14.18.0 || >=16.0.0"
  1358	      },
  1359	      "peerDependencies": {
  1360	        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
  1361	      }
  1362	    },
  1363	    "node_modules/any-promise": {
  1364	      "version": "1.3.0",
  1365	      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
  1366	      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
  1367	      "dev": true,
  1368	      "license": "MIT"
  1369	    },
  1370	    "node_modules/anymatch": {
  1371	      "version": "3.1.3",
  1372	      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
  1373	      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
  1374	      "dev": true,
  1375	      "license": "ISC",
  1376	      "dependencies": {
  1377	        "normalize-path": "^3.0.0",
  1378	        "picomatch": "^2.0.4"
  1379	      },
  1380	      "engines": {
  1381	        "node": ">= 8"
  1382	      }
  1383	    },
  1384	    "node_modules/arg": {
  1385	      "version": "5.0.2",
  1386	      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
  1387	      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
  1388	      "dev": true,
  1389	      "license": "MIT"
  1390	    },
  1391	    "node_modules/autoprefixer": {
  1392	      "version": "10.4.27",
  1393	      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
  1394	      "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
  1395	      "dev": true,
  1396	      "funding": [
  1397	        {
  1398	          "type": "opencollective",
  1399	          "url": "https://opencollective.com/postcss/"
  1400	        },
  1401	        {
  1402	          "type": "tidelift",
  1403	          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
  1404	        },
  1405	        {
  1406	          "type": "github",
  1407	          "url": "https://github.com/sponsors/ai"
  1408	        }
  1409	      ],
  1410	      "license": "MIT",
  1411	      "dependencies": {
  1412	        "browserslist": "^4.28.1",
  1413	        "caniuse-lite": "^1.0.30001774",
  1414	        "fraction.js": "^5.3.4",
  1415	        "picocolors": "^1.1.1",
  1416	        "postcss-value-parser": "^4.2.0"
  1417	      },
  1418	      "bin": {
  1419	        "autoprefixer": "bin/autoprefixer"
  1420	      },
  1421	      "engines": {
  1422	        "node": "^10 || ^12 || >=14"
  1423	      },
  1424	      "peerDependencies": {
  1425	        "postcss": "^8.1.0"
  1426	      }
  1427	    },
  1428	    "node_modules/bail": {
  1429	      "version": "2.0.2",
  1430	      "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
  1431	      "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
  1432	      "license": "MIT",
  1433	      "funding": {
  1434	        "type": "github",
  1435	        "url": "https://github.com/sponsors/wooorm"
  1436	      }
  1437	    },
  1438	    "node_modules/baseline-browser-mapping": {
  1439	      "version": "2.10.8",
  1440	      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
  1441	      "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
  1442	      "dev": true,
  1443	      "license": "Apache-2.0",
  1444	      "bin": {
  1445	        "baseline-browser-mapping": "dist/cli.cjs"
  1446	      },
  1447	      "engines": {
  1448	        "node": ">=6.0.0"
  1449	      }
  1450	    },
  1451	    "node_modules/binary-extensions": {
  1452	      "version": "2.3.0",
  1453	      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
  1454	      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
  1455	      "dev": true,
  1456	      "license": "MIT",
  1457	      "engines": {
  1458	        "node": ">=8"
  1459	      },
  1460	      "funding": {
  1461	        "url": "https://github.com/sponsors/sindresorhus"
  1462	      }
  1463	    },
  1464	    "node_modules/braces": {
  1465	      "version": "3.0.3",
  1466	      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
  1467	      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
  1468	      "dev": true,
  1469	      "license": "MIT",
  1470	      "dependencies": {
  1471	        "fill-range": "^7.1.1"
  1472	      },
  1473	      "engines": {
  1474	        "node": ">=8"
  1475	      }
  1476	    },
  1477	    "node_modules/browserslist": {
  1478	      "version": "4.28.1",
  1479	      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
  1480	      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
  1481	      "dev": true,
  1482	      "funding": [
  1483	        {
  1484	          "type": "opencollective",
  1485	          "url": "https://opencollective.com/browserslist"
  1486	        },
  1487	        {
  1488	          "type": "tidelift",
  1489	          "url": "https://tidelift.com/funding/github/npm/browserslist"
  1490	        },
  1491	        {
  1492	          "type": "github",
  1493	          "url": "https://github.com/sponsors/ai"
  1494	        }
  1495	      ],
  1496	      "license": "MIT",
  1497	      "dependencies": {
  1498	        "baseline-browser-mapping": "^2.9.0",
  1499	        "caniuse-lite": "^1.0.30001759",
  1500	        "electron-to-chromium": "^1.5.263",
  1501	        "node-releases": "^2.0.27",
  1502	        "update-browserslist-db": "^1.2.0"
  1503	      },
  1504	      "bin": {
  1505	        "browserslist": "cli.js"
  1506	      },
  1507	      "engines": {
  1508	        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
  1509	      }
  1510	    },
  1511	    "node_modules/camelcase-css": {
  1512	      "version": "2.0.1",
  1513	      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
  1514	      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
  1515	      "dev": true,
  1516	      "license": "MIT",
  1517	      "engines": {
  1518	        "node": ">= 6"
  1519	      }
  1520	    },
  1521	    "node_modules/caniuse-lite": {
  1522	      "version": "1.0.30001780",
  1523	      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
  1524	      "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
  1525	      "dev": true,
  1526	      "funding": [
  1527	        {
  1528	          "type": "opencollective",
  1529	          "url": "https://opencollective.com/browserslist"
  1530	        },
  1531	        {
  1532	          "type": "tidelift",
  1533	          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
  1534	        },
  1535	        {
  1536	          "type": "github",
  1537	          "url": "https://github.com/sponsors/ai"
  1538	        }
  1539	      ],
  1540	      "license": "CC-BY-4.0"
  1541	    },
  1542	    "node_modules/ccount": {
  1543	      "version": "2.0.1",
  1544	      "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
  1545	      "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
  1546	      "license": "MIT",
  1547	      "funding": {
  1548	        "type": "github",
  1549	        "url": "https://github.com/sponsors/wooorm"
  1550	      }
  1551	    },
  1552	    "node_modules/character-entities": {
  1553	      "version": "2.0.2",
  1554	      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
  1555	      "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
  1556	      "license": "MIT",
  1557	      "funding": {
  1558	        "type": "github",
  1559	        "url": "https://github.com/sponsors/wooorm"
  1560	      }
  1561	    },
  1562	    "node_modules/character-entities-html4": {
  1563	      "version": "2.1.0",
  1564	      "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
  1565	      "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
  1566	      "license": "MIT",
  1567	      "funding": {
  1568	        "type": "github",
  1569	        "url": "https://github.com/sponsors/wooorm"
  1570	      }
  1571	    },
  1572	    "node_modules/character-entities-legacy": {
  1573	      "version": "3.0.0",
  1574	      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
  1575	      "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
  1576	      "license": "MIT",
  1577	      "funding": {
  1578	        "type": "github",
  1579	        "url": "https://github.com/sponsors/wooorm"
  1580	      }
  1581	    },
  1582	    "node_modules/character-reference-invalid": {
  1583	      "version": "2.0.1",
  1584	      "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
  1585	      "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
  1586	      "license": "MIT",
  1587	      "funding": {
  1588	        "type": "github",
  1589	        "url": "https://github.com/sponsors/wooorm"
  1590	      }
  1591	    },
  1592	    "node_modules/chokidar": {
  1593	      "version": "3.6.0",
  1594	      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
  1595	      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
  1596	      "dev": true,
  1597	      "license": "MIT",
  1598	      "dependencies": {
  1599	        "anymatch": "~3.1.2",
  1600	        "braces": "~3.0.2",
  1601	        "glob-parent": "~5.1.2",
  1602	        "is-binary-path": "~2.1.0",
  1603	        "is-glob": "~4.0.1",
  1604	        "normalize-path": "~3.0.0",
  1605	        "readdirp": "~3.6.0"
  1606	      },
  1607	      "engines": {
  1608	        "node": ">= 8.10.0"
  1609	      },
  1610	      "funding": {
  1611	        "url": "https://paulmillr.com/funding/"
  1612	      },
  1613	      "optionalDependencies": {
  1614	        "fsevents": "~2.3.2"
  1615	      }
  1616	    },
  1617	    "node_modules/chokidar/node_modules/glob-parent": {
  1618	      "version": "5.1.2",
  1619	      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
  1620	      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
  1621	      "dev": true,
  1622	      "license": "ISC",
  1623	      "dependencies": {
  1624	        "is-glob": "^4.0.1"
  1625	      },
  1626	      "engines": {
  1627	        "node": ">= 6"
  1628	      }
  1629	    },
  1630	    "node_modules/comma-separated-tokens": {
  1631	      "version": "2.0.3",
  1632	      "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
  1633	      "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
  1634	      "license": "MIT",
  1635	      "funding": {
  1636	        "type": "github",
  1637	        "url": "https://github.com/sponsors/wooorm"
  1638	      }
  1639	    },
  1640	    "node_modules/commander": {
  1641	      "version": "4.1.1",
  1642	      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
  1643	      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
  1644	      "dev": true,
  1645	      "license": "MIT",
  1646	      "engines": {
  1647	        "node": ">= 6"
  1648	      }
  1649	    },
  1650	    "node_modules/convert-source-map": {
  1651	      "version": "2.0.0",
  1652	      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
  1653	      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
  1654	      "dev": true,
  1655	      "license": "MIT"
  1656	    },
  1657	    "node_modules/cookie": {
  1658	      "version": "1.1.1",
  1659	      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
  1660	      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
  1661	      "license": "MIT",
  1662	      "engines": {
  1663	        "node": ">=18"
  1664	      },
  1665	      "funding": {
  1666	        "type": "opencollective",
  1667	        "url": "https://opencollective.com/express"
  1668	      }
  1669	    },
  1670	    "node_modules/cssesc": {
  1671	      "version": "3.0.0",
  1672	      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
  1673	      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
  1674	      "dev": true,
  1675	      "license": "MIT",
  1676	      "bin": {
  1677	        "cssesc": "bin/cssesc"
  1678	      },
  1679	      "engines": {
  1680	        "node": ">=4"
  1681	      }
  1682	    },
  1683	    "node_modules/csstype": {
  1684	      "version": "3.2.3",
  1685	      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
  1686	      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
  1687	      "dev": true,
  1688	      "license": "MIT"
  1689	    },
  1690	    "node_modules/debug": {
  1691	      "version": "4.4.3",
  1692	      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
  1693	      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
  1694	      "license": "MIT",
  1695	      "dependencies": {
  1696	        "ms": "^2.1.3"
  1697	      },
  1698	      "engines": {
  1699	        "node": ">=6.0"
  1700	      },
  1701	      "peerDependenciesMeta": {
  1702	        "supports-color": {
  1703	          "optional": true
  1704	        }
  1705	      }
  1706	    },
  1707	    "node_modules/decode-named-character-reference": {
  1708	      "version": "1.3.0",
  1709	      "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
  1710	      "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
  1711	      "license": "MIT",
  1712	      "dependencies": {
  1713	        "character-entities": "^2.0.0"
  1714	      },
  1715	      "funding": {
  1716	        "type": "github",
  1717	        "url": "https://github.com/sponsors/wooorm"
  1718	      }
  1719	    },
  1720	    "node_modules/dequal": {
  1721	      "version": "2.0.3",
  1722	      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
  1723	      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
  1724	      "license": "MIT",
  1725	      "engines": {
  1726	        "node": ">=6"
  1727	      }
  1728	    },
  1729	    "node_modules/devlop": {
  1730	      "version": "1.1.0",
  1731	      "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
  1732	      "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
  1733	      "license": "MIT",
  1734	      "dependencies": {
  1735	        "dequal": "^2.0.0"
  1736	      },
  1737	      "funding": {
  1738	        "type": "github",
  1739	        "url": "https://github.com/sponsors/wooorm"
  1740	      }
  1741	    },
  1742	    "node_modules/didyoumean": {
  1743	      "version": "1.2.2",
  1744	      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
  1745	      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
  1746	      "dev": true,
  1747	      "license": "Apache-2.0"
  1748	    },
  1749	    "node_modules/dlv": {
  1750	      "version": "1.1.3",
  1751	      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
  1752	      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
  1753	      "dev": true,
  1754	      "license": "MIT"
  1755	    },
  1756	    "node_modules/electron-to-chromium": {
  1757	      "version": "1.5.321",
  1758	      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
  1759	      "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
  1760	      "dev": true,
  1761	      "license": "ISC"
  1762	    },
  1763	    "node_modules/esbuild": {
  1764	      "version": "0.25.12",
  1765	      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
  1766	      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
  1767	      "dev": true,
  1768	      "hasInstallScript": true,
  1769	      "license": "MIT",
  1770	      "bin": {
  1771	        "esbuild": "bin/esbuild"
  1772	      },
  1773	      "engines": {
  1774	        "node": ">=18"
  1775	      },
  1776	      "optionalDependencies": {
  1777	        "@esbuild/aix-ppc64": "0.25.12",
  1778	        "@esbuild/android-arm": "0.25.12",
  1779	        "@esbuild/android-arm64": "0.25.12",
  1780	        "@esbuild/android-x64": "0.25.12",
  1781	        "@esbuild/darwin-arm64": "0.25.12",
  1782	        "@esbuild/darwin-x64": "0.25.12",
  1783	        "@esbuild/freebsd-arm64": "0.25.12",
  1784	        "@esbuild/freebsd-x64": "0.25.12",
  1785	        "@esbuild/linux-arm": "0.25.12",
  1786	        "@esbuild/linux-arm64": "0.25.12",
  1787	        "@esbuild/linux-ia32": "0.25.12",
  1788	        "@esbuild/linux-loong64": "0.25.12",
  1789	        "@esbuild/linux-mips64el": "0.25.12",
  1790	        "@esbuild/linux-ppc64": "0.25.12",
  1791	        "@esbuild/linux-riscv64": "0.25.12",
  1792	        "@esbuild/linux-s390x": "0.25.12",
  1793	        "@esbuild/linux-x64": "0.25.12",
  1794	        "@esbuild/netbsd-arm64": "0.25.12",
  1795	        "@esbuild/netbsd-x64": "0.25.12",
  1796	        "@esbuild/openbsd-arm64": "0.25.12",
  1797	        "@esbuild/openbsd-x64": "0.25.12",
  1798	        "@esbuild/openharmony-arm64": "0.25.12",
  1799	        "@esbuild/sunos-x64": "0.25.12",
  1800	        "@esbuild/win32-arm64": "0.25.12",
  1801	        "@esbuild/win32-ia32": "0.25.12",
  1802	        "@esbuild/win32-x64": "0.25.12"
  1803	      }
  1804	    },
  1805	    "node_modules/escalade": {
  1806	      "version": "3.2.0",
  1807	      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
  1808	      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
  1809	      "dev": true,
  1810	      "license": "MIT",
  1811	      "engines": {
  1812	        "node": ">=6"
  1813	      }
  1814	    },
  1815	    "node_modules/escape-string-regexp": {
  1816	      "version": "5.0.0",
  1817	      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
  1818	      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
  1819	      "license": "MIT",
  1820	      "engines": {
  1821	        "node": ">=12"
  1822	      },
  1823	      "funding": {
  1824	        "url": "https://github.com/sponsors/sindresorhus"
  1825	      }
  1826	    },
  1827	    "node_modules/estree-util-is-identifier-name": {
  1828	      "version": "3.0.0",
  1829	      "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
  1830	      "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
  1831	      "license": "MIT",
  1832	      "funding": {
  1833	        "type": "opencollective",
  1834	        "url": "https://opencollective.com/unified"
  1835	      }
  1836	    },
  1837	    "node_modules/extend": {
  1838	      "version": "3.0.2",
  1839	      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
  1840	      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
  1841	      "license": "MIT"
  1842	    },
  1843	    "node_modules/fast-glob": {
  1844	      "version": "3.3.3",
  1845	      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
  1846	      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
  1847	      "dev": true,
  1848	      "license": "MIT",
  1849	      "dependencies": {
  1850	        "@nodelib/fs.stat": "^2.0.2",
  1851	        "@nodelib/fs.walk": "^1.2.3",
  1852	        "glob-parent": "^5.1.2",
  1853	        "merge2": "^1.3.0",
  1854	        "micromatch": "^4.0.8"
  1855	      },
  1856	      "engines": {
  1857	        "node": ">=8.6.0"
  1858	      }
  1859	    },
  1860	    "node_modules/fast-glob/node_modules/glob-parent": {
  1861	      "version": "5.1.2",
  1862	      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
  1863	      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
  1864	      "dev": true,
  1865	      "license": "ISC",
  1866	      "dependencies": {
  1867	        "is-glob": "^4.0.1"
  1868	      },
  1869	      "engines": {
  1870	        "node": ">= 6"
  1871	      }
  1872	    },
  1873	    "node_modules/fastq": {
  1874	      "version": "1.20.1",
  1875	      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
  1876	      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
  1877	      "dev": true,
  1878	      "license": "ISC",
  1879	      "dependencies": {
  1880	        "reusify": "^1.0.4"
  1881	      }
  1882	    },
  1883	    "node_modules/fault": {
  1884	      "version": "1.0.4",
  1885	      "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
  1886	      "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
  1887	      "license": "MIT",
  1888	      "dependencies": {
  1889	        "format": "^0.2.0"
  1890	      },
  1891	      "funding": {
  1892	        "type": "github",
  1893	        "url": "https://github.com/sponsors/wooorm"
  1894	      }
  1895	    },
  1896	    "node_modules/fdir": {
  1897	      "version": "6.5.0",
  1898	      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
  1899	      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
  1900	      "dev": true,
  1901	      "license": "MIT",
  1902	      "engines": {
  1903	        "node": ">=12.0.0"
  1904	      },
  1905	      "peerDependencies": {
  1906	        "picomatch": "^3 || ^4"
  1907	      },
  1908	      "peerDependenciesMeta": {
  1909	        "picomatch": {
  1910	          "optional": true
  1911	        }
  1912	      }
  1913	    },
  1914	    "node_modules/fill-range": {
  1915	      "version": "7.1.1",
  1916	      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
  1917	      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
  1918	      "dev": true,
  1919	      "license": "MIT",
  1920	      "dependencies": {
  1921	        "to-regex-range": "^5.0.1"
  1922	      },
  1923	      "engines": {
  1924	        "node": ">=8"
  1925	      }
  1926	    },
  1927	    "node_modules/format": {
  1928	      "version": "0.2.2",
  1929	      "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
  1930	      "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
  1931	      "engines": {
  1932	        "node": ">=0.4.x"
  1933	      }
  1934	    },
  1935	    "node_modules/fraction.js": {
  1936	      "version": "5.3.4",
  1937	      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
  1938	      "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
  1939	      "dev": true,
  1940	      "license": "MIT",
  1941	      "engines": {
  1942	        "node": "*"
  1943	      },
  1944	      "funding": {
  1945	        "type": "github",
  1946	        "url": "https://github.com/sponsors/rawify"
  1947	      }
  1948	    },
  1949	    "node_modules/fsevents": {
  1950	      "version": "2.3.3",
  1951	      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
  1952	      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
  1953	      "dev": true,
  1954	      "hasInstallScript": true,
  1955	      "license": "MIT",
  1956	      "optional": true,
  1957	      "os": [
  1958	        "darwin"
  1959	      ],
  1960	      "engines": {
  1961	        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
  1962	      }
  1963	    },
  1964	    "node_modules/function-bind": {
  1965	      "version": "1.1.2",
  1966	      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
  1967	      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
  1968	      "dev": true,
  1969	      "license": "MIT",
  1970	      "funding": {
  1971	        "url": "https://github.com/sponsors/ljharb"
  1972	      }
  1973	    },
  1974	    "node_modules/gensync": {
  1975	      "version": "1.0.0-beta.2",
  1976	      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
  1977	      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
  1978	      "dev": true,
  1979	      "license": "MIT",
  1980	      "engines": {
  1981	        "node": ">=6.9.0"
  1982	      }
  1983	    },
  1984	    "node_modules/glob-parent": {
  1985	      "version": "6.0.2",
  1986	      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
  1987	      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
  1988	      "dev": true,
  1989	      "license": "ISC",
  1990	      "dependencies": {
  1991	        "is-glob": "^4.0.3"
  1992	      },
  1993	      "engines": {
  1994	        "node": ">=10.13.0"
  1995	      }
  1996	    },
  1997	    "node_modules/hasown": {
  1998	      "version": "2.0.2",
  1999	      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
  2000	      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
  2001	      "dev": true,
  2002	      "license": "MIT",
  2003	      "dependencies": {
  2004	        "function-bind": "^1.1.2"
  2005	      },
  2006	      "engines": {
  2007	        "node": ">= 0.4"
  2008	      }
  2009	    },
  2010	    "node_modules/hast-util-parse-selector": {
  2011	      "version": "2.2.5",
  2012	      "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
  2013	      "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
  2014	      "license": "MIT",
  2015	      "funding": {
  2016	        "type": "opencollective",
  2017	        "url": "https://opencollective.com/unified"
  2018	      }
  2019	    },
  2020	    "node_modules/hast-util-to-jsx-runtime": {
  2021	      "version": "2.3.6",
  2022	      "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
  2023	      "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
  2024	      "license": "MIT",
  2025	      "dependencies": {
  2026	        "@types/estree": "^1.0.0",
  2027	        "@types/hast": "^3.0.0",
  2028	        "@types/unist": "^3.0.0",
  2029	        "comma-separated-tokens": "^2.0.0",
  2030	        "devlop": "^1.0.0",
  2031	        "estree-util-is-identifier-name": "^3.0.0",
  2032	        "hast-util-whitespace": "^3.0.0",
  2033	        "mdast-util-mdx-expression": "^2.0.0",
  2034	        "mdast-util-mdx-jsx": "^3.0.0",
  2035	        "mdast-util-mdxjs-esm": "^2.0.0",
  2036	        "property-information": "^7.0.0",
  2037	        "space-separated-tokens": "^2.0.0",
  2038	        "style-to-js": "^1.0.0",
  2039	        "unist-util-position": "^5.0.0",
  2040	        "vfile-message": "^4.0.0"
  2041	      },
  2042	      "funding": {
  2043	        "type": "opencollective",
  2044	        "url": "https://opencollective.com/unified"
  2045	      }
  2046	    },
  2047	    "node_modules/hast-util-whitespace": {
  2048	      "version": "3.0.0",
  2049	      "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
  2050	      "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
  2051	      "license": "MIT",
  2052	      "dependencies": {
  2053	        "@types/hast": "^3.0.0"
  2054	      },
  2055	      "funding": {
  2056	        "type": "opencollective",
  2057	        "url": "https://opencollective.com/unified"
  2058	      }
  2059	    },
  2060	    "node_modules/hastscript": {
  2061	      "version": "6.0.0",
  2062	      "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
  2063	      "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
  2064	      "license": "MIT",
  2065	      "dependencies": {
  2066	        "@types/hast": "^2.0.0",
  2067	        "comma-separated-tokens": "^1.0.0",
  2068	        "hast-util-parse-selector": "^2.0.0",
  2069	        "property-information": "^5.0.0",
  2070	        "space-separated-tokens": "^1.0.0"
  2071	      },
  2072	      "funding": {
  2073	        "type": "opencollective",
  2074	        "url": "https://opencollective.com/unified"
  2075	      }
  2076	    },
  2077	    "node_modules/hastscript/node_modules/@types/hast": {
  2078	      "version": "2.3.10",
  2079	      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
  2080	      "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
  2081	      "license": "MIT",
  2082	      "dependencies": {
  2083	        "@types/unist": "^2"
  2084	      }
  2085	    },
  2086	    "node_modules/hastscript/node_modules/@types/unist": {
  2087	      "version": "2.0.11",
  2088	      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
  2089	      "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
  2090	      "license": "MIT"
  2091	    },
  2092	    "node_modules/hastscript/node_modules/comma-separated-tokens": {
  2093	      "version": "1.0.8",
  2094	      "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
  2095	      "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
  2096	      "license": "MIT",
  2097	      "funding": {
  2098	        "type": "github",
  2099	        "url": "https://github.com/sponsors/wooorm"
  2100	      }
  2101	    },
  2102	    "node_modules/hastscript/node_modules/property-information": {
  2103	      "version": "5.6.0",
  2104	      "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
  2105	      "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
  2106	      "license": "MIT",
  2107	      "dependencies": {
  2108	        "xtend": "^4.0.0"
  2109	      },
  2110	      "funding": {
  2111	        "type": "github",
  2112	        "url": "https://github.com/sponsors/wooorm"
  2113	      }
  2114	    },
  2115	    "node_modules/hastscript/node_modules/space-separated-tokens": {
  2116	      "version": "1.1.5",
  2117	      "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
  2118	      "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
  2119	      "license": "MIT",
  2120	      "funding": {
  2121	        "type": "github",
  2122	        "url": "https://github.com/sponsors/wooorm"
  2123	      }
  2124	    },
  2125	    "node_modules/highlight.js": {
  2126	      "version": "10.7.3",
  2127	      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
  2128	      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
  2129	      "license": "BSD-3-Clause",
  2130	      "engines": {
  2131	        "node": "*"
  2132	      }
  2133	    },
  2134	    "node_modules/highlightjs-vue": {
  2135	      "version": "1.0.0",
  2136	      "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
  2137	      "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
  2138	      "license": "CC0-1.0"
  2139	    },
  2140	    "node_modules/html-url-attributes": {
  2141	      "version": "3.0.1",
  2142	      "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
  2143	      "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
  2144	      "license": "MIT",
  2145	      "funding": {
  2146	        "type": "opencollective",
  2147	        "url": "https://opencollective.com/unified"
  2148	      }
  2149	    },
  2150	    "node_modules/inline-style-parser": {
  2151	      "version": "0.2.7",
  2152	      "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
  2153	      "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
  2154	      "license": "MIT"
  2155	    },
  2156	    "node_modules/is-alphabetical": {
  2157	      "version": "2.0.1",
  2158	      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
  2159	      "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
  2160	      "license": "MIT",
  2161	      "funding": {
  2162	        "type": "github",
  2163	        "url": "https://github.com/sponsors/wooorm"
  2164	      }
  2165	    },
  2166	    "node_modules/is-alphanumerical": {
  2167	      "version": "2.0.1",
  2168	      "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
  2169	      "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
  2170	      "license": "MIT",
  2171	      "dependencies": {
  2172	        "is-alphabetical": "^2.0.0",
  2173	        "is-decimal": "^2.0.0"
  2174	      },
  2175	      "funding": {
  2176	        "type": "github",
  2177	        "url": "https://github.com/sponsors/wooorm"
  2178	      }
  2179	    },
  2180	    "node_modules/is-binary-path": {
  2181	      "version": "2.1.0",
  2182	      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
  2183	      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
  2184	      "dev": true,
  2185	      "license": "MIT",
  2186	      "dependencies": {
  2187	        "binary-extensions": "^2.0.0"
  2188	      },
  2189	      "engines": {
  2190	        "node": ">=8"
  2191	      }
  2192	    },
  2193	    "node_modules/is-core-module": {
  2194	      "version": "2.16.1",
  2195	      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
  2196	      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
  2197	      "dev": true,
  2198	      "license": "MIT",
  2199	      "dependencies": {
  2200	        "hasown": "^2.0.2"
  2201	      },
  2202	      "engines": {
  2203	        "node": ">= 0.4"
  2204	      },
  2205	      "funding": {
  2206	        "url": "https://github.com/sponsors/ljharb"
  2207	      }
  2208	    },
  2209	    "node_modules/is-decimal": {
  2210	      "version": "2.0.1",
  2211	      "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
  2212	      "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
  2213	      "license": "MIT",
  2214	      "funding": {
  2215	        "type": "github",
  2216	        "url": "https://github.com/sponsors/wooorm"
  2217	      }
  2218	    },
  2219	    "node_modules/is-extglob": {
  2220	      "version": "2.1.1",
  2221	      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
  2222	      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
  2223	      "dev": true,
  2224	      "license": "MIT",
  2225	      "engines": {
  2226	        "node": ">=0.10.0"
  2227	      }
  2228	    },
  2229	    "node_modules/is-glob": {
  2230	      "version": "4.0.3",
  2231	      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
  2232	      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
  2233	      "dev": true,
  2234	      "license": "MIT",
  2235	      "dependencies": {
  2236	        "is-extglob": "^2.1.1"
  2237	      },
  2238	      "engines": {
  2239	        "node": ">=0.10.0"
  2240	      }
  2241	    },
  2242	    "node_modules/is-hexadecimal": {
  2243	      "version": "2.0.1",
  2244	      "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
  2245	      "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
  2246	      "license": "MIT",
  2247	      "funding": {
  2248	        "type": "github",
  2249	        "url": "https://github.com/sponsors/wooorm"
  2250	      }
  2251	    },
  2252	    "node_modules/is-number": {
  2253	      "version": "7.0.0",
  2254	      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
  2255	      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
  2256	      "dev": true,
  2257	      "license": "MIT",
  2258	      "engines": {
  2259	        "node": ">=0.12.0"
  2260	      }
  2261	    },
  2262	    "node_modules/is-plain-obj": {
  2263	      "version": "4.1.0",
  2264	      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
  2265	      "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
  2266	      "license": "MIT",
  2267	      "engines": {
  2268	        "node": ">=12"
  2269	      },
  2270	      "funding": {
  2271	        "url": "https://github.com/sponsors/sindresorhus"
  2272	      }
  2273	    },
  2274	    "node_modules/jiti": {
  2275	      "version": "1.21.7",
  2276	      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
  2277	      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
  2278	      "dev": true,
  2279	      "license": "MIT",
  2280	      "bin": {
  2281	        "jiti": "bin/jiti.js"
  2282	      }
  2283	    },
  2284	    "node_modules/js-tokens": {
  2285	      "version": "4.0.0",
  2286	      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
  2287	      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
  2288	      "license": "MIT"
  2289	    },
  2290	    "node_modules/jsesc": {
  2291	      "version": "3.1.0",
  2292	      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
  2293	      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
  2294	      "dev": true,
  2295	      "license": "MIT",
  2296	      "bin": {
  2297	        "jsesc": "bin/jsesc"
  2298	      },
  2299	      "engines": {
  2300	        "node": ">=6"
  2301	      }
  2302	    },
  2303	    "node_modules/json5": {
  2304	      "version": "2.2.3",
  2305	      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
  2306	      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
  2307	      "dev": true,
  2308	      "license": "MIT",
  2309	      "bin": {
  2310	        "json5": "lib/cli.js"
  2311	      },
  2312	      "engines": {
  2313	        "node": ">=6"
  2314	      }
  2315	    },
  2316	    "node_modules/lilconfig": {
  2317	      "version": "3.1.3",
  2318	      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
  2319	      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
  2320	      "dev": true,
  2321	      "license": "MIT",
  2322	      "engines": {
  2323	        "node": ">=14"
  2324	      },
  2325	      "funding": {
  2326	        "url": "https://github.com/sponsors/antonk52"
  2327	      }
  2328	    },
  2329	    "node_modules/lines-and-columns": {
  2330	      "version": "1.2.4",
  2331	      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
  2332	      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
  2333	      "dev": true,
  2334	      "license": "MIT"
  2335	    },
  2336	    "node_modules/longest-streak": {
  2337	      "version": "3.1.0",
  2338	      "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
  2339	      "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
  2340	      "license": "MIT",
  2341	      "funding": {
  2342	        "type": "github",
  2343	        "url": "https://github.com/sponsors/wooorm"
  2344	      }
  2345	    },
  2346	    "node_modules/loose-envify": {
  2347	      "version": "1.4.0",
  2348	      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
  2349	      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
  2350	      "license": "MIT",
  2351	      "dependencies": {
  2352	        "js-tokens": "^3.0.0 || ^4.0.0"
  2353	      },
  2354	      "bin": {
  2355	        "loose-envify": "cli.js"
  2356	      }
  2357	    },
  2358	    "node_modules/lowlight": {
  2359	      "version": "1.20.0",
  2360	      "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
  2361	      "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
  2362	      "license": "MIT",
  2363	      "dependencies": {
  2364	        "fault": "^1.0.0",
  2365	        "highlight.js": "~10.7.0"
  2366	      },
  2367	      "funding": {
  2368	        "type": "github",
  2369	        "url": "https://github.com/sponsors/wooorm"
  2370	      }
  2371	    },
  2372	    "node_modules/lru-cache": {
  2373	      "version": "5.1.1",
  2374	      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
  2375	      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
  2376	      "dev": true,
  2377	      "license": "ISC",
  2378	      "dependencies": {
  2379	        "yallist": "^3.0.2"
  2380	      }
  2381	    },
  2382	    "node_modules/lucide-react": {
  2383	      "version": "0.469.0",
  2384	      "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz",
  2385	      "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==",
  2386	      "license": "ISC",
  2387	      "peerDependencies": {
  2388	        "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  2389	      }
  2390	    },
  2391	    "node_modules/markdown-table": {
  2392	      "version": "3.0.4",
  2393	      "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
  2394	      "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
  2395	      "license": "MIT",
  2396	      "funding": {
  2397	        "type": "github",
  2398	        "url": "https://github.com/sponsors/wooorm"
  2399	      }
  2400	    },
  2401	    "node_modules/mdast-util-find-and-replace": {
  2402	      "version": "3.0.2",
  2403	      "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
  2404	      "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
  2405	      "license": "MIT",
  2406	      "dependencies": {
  2407	        "@types/mdast": "^4.0.0",
  2408	        "escape-string-regexp": "^5.0.0",
  2409	        "unist-util-is": "^6.0.0",
  2410	        "unist-util-visit-parents": "^6.0.0"
  2411	      },
  2412	      "funding": {
  2413	        "type": "opencollective",
  2414	        "url": "https://opencollective.com/unified"
  2415	      }
  2416	    },
  2417	    "node_modules/mdast-util-from-markdown": {
  2418	      "version": "2.0.3",
  2419	      "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
  2420	      "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
  2421	      "license": "MIT",
  2422	      "dependencies": {
  2423	        "@types/mdast": "^4.0.0",
  2424	        "@types/unist": "^3.0.0",
  2425	        "decode-named-character-reference": "^1.0.0",
  2426	        "devlop": "^1.0.0",
  2427	        "mdast-util-to-string": "^4.0.0",
  2428	        "micromark": "^4.0.0",
  2429	        "micromark-util-decode-numeric-character-reference": "^2.0.0",
  2430	        "micromark-util-decode-string": "^2.0.0",
  2431	        "micromark-util-normalize-identifier": "^2.0.0",
  2432	        "micromark-util-symbol": "^2.0.0",
  2433	        "micromark-util-types": "^2.0.0",
  2434	        "unist-util-stringify-position": "^4.0.0"
  2435	      },
  2436	      "funding": {
  2437	        "type": "opencollective",
  2438	        "url": "https://opencollective.com/unified"
  2439	      }
  2440	    },
  2441	    "node_modules/mdast-util-gfm": {
  2442	      "version": "3.1.0",
  2443	      "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
  2444	      "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
  2445	      "license": "MIT",
  2446	      "dependencies": {
  2447	        "mdast-util-from-markdown": "^2.0.0",
  2448	        "mdast-util-gfm-autolink-literal": "^2.0.0",
  2449	        "mdast-util-gfm-footnote": "^2.0.0",
  2450	        "mdast-util-gfm-strikethrough": "^2.0.0",
  2451	        "mdast-util-gfm-table": "^2.0.0",
  2452	        "mdast-util-gfm-task-list-item": "^2.0.0",
  2453	        "mdast-util-to-markdown": "^2.0.0"
  2454	      },
  2455	      "funding": {
  2456	        "type": "opencollective",
  2457	        "url": "https://opencollective.com/unified"
  2458	      }
  2459	    },
  2460	    "node_modules/mdast-util-gfm-autolink-literal": {
  2461	      "version": "2.0.1",
  2462	      "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
  2463	      "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
  2464	      "license": "MIT",
  2465	      "dependencies": {
  2466	        "@types/mdast": "^4.0.0",
  2467	        "ccount": "^2.0.0",
  2468	        "devlop": "^1.0.0",
  2469	        "mdast-util-find-and-replace": "^3.0.0",
  2470	        "micromark-util-character": "^2.0.0"
  2471	      },
  2472	      "funding": {
  2473	        "type": "opencollective",
  2474	        "url": "https://opencollective.com/unified"
  2475	      }
  2476	    },
  2477	    "node_modules/mdast-util-gfm-footnote": {
  2478	      "version": "2.1.0",
  2479	      "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
  2480	      "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
  2481	      "license": "MIT",
  2482	      "dependencies": {
  2483	        "@types/mdast": "^4.0.0",
  2484	        "devlop": "^1.1.0",
  2485	        "mdast-util-from-markdown": "^2.0.0",
  2486	        "mdast-util-to-markdown": "^2.0.0",
  2487	        "micromark-util-normalize-identifier": "^2.0.0"
  2488	      },
  2489	      "funding": {
  2490	        "type": "opencollective",
  2491	        "url": "https://opencollective.com/unified"
  2492	      }
  2493	    },
  2494	    "node_modules/mdast-util-gfm-strikethrough": {
  2495	      "version": "2.0.0",
  2496	      "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
  2497	      "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
  2498	      "license": "MIT",
  2499	      "dependencies": {
  2500	        "@types/mdast": "^4.0.0",
  2501	        "mdast-util-from-markdown": "^2.0.0",
  2502	        "mdast-util-to-markdown": "^2.0.0"
  2503	      },
  2504	      "funding": {
  2505	        "type": "opencollective",
  2506	        "url": "https://opencollective.com/unified"
  2507	      }
  2508	    },
  2509	    "node_modules/mdast-util-gfm-table": {
  2510	      "version": "2.0.0",
  2511	      "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
  2512	      "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
  2513	      "license": "MIT",
  2514	      "dependencies": {
  2515	        "@types/mdast": "^4.0.0",
  2516	        "devlop": "^1.0.0",
  2517	        "markdown-table": "^3.0.0",
  2518	        "mdast-util-from-markdown": "^2.0.0",
  2519	        "mdast-util-to-markdown": "^2.0.0"
  2520	      },
  2521	      "funding": {
  2522	        "type": "opencollective",
  2523	        "url": "https://opencollective.com/unified"
  2524	      }
  2525	    },
  2526	    "node_modules/mdast-util-gfm-task-list-item": {
  2527	      "version": "2.0.0",
  2528	      "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
  2529	      "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
  2530	      "license": "MIT",
  2531	      "dependencies": {
  2532	        "@types/mdast": "^4.0.0",
  2533	        "devlop": "^1.0.0",
  2534	        "mdast-util-from-markdown": "^2.0.0",
  2535	        "mdast-util-to-markdown": "^2.0.0"
  2536	      },
  2537	      "funding": {
  2538	        "type": "opencollective",
  2539	        "url": "https://opencollective.com/unified"
  2540	      }
  2541	    },
  2542	    "node_modules/mdast-util-mdx-expression": {
  2543	      "version": "2.0.1",
  2544	      "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
  2545	      "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
  2546	      "license": "MIT",
  2547	      "dependencies": {
  2548	        "@types/estree-jsx": "^1.0.0",
  2549	        "@types/hast": "^3.0.0",
  2550	        "@types/mdast": "^4.0.0",
  2551	        "devlop": "^1.0.0",
  2552	        "mdast-util-from-markdown": "^2.0.0",
  2553	        "mdast-util-to-markdown": "^2.0.0"
  2554	      },
  2555	      "funding": {
  2556	        "type": "opencollective",
  2557	        "url": "https://opencollective.com/unified"
  2558	      }
  2559	    },
  2560	    "node_modules/mdast-util-mdx-jsx": {
  2561	      "version": "3.2.0",
  2562	      "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
  2563	      "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
  2564	      "license": "MIT",
  2565	      "dependencies": {
  2566	        "@types/estree-jsx": "^1.0.0",
  2567	        "@types/hast": "^3.0.0",
  2568	        "@types/mdast": "^4.0.0",
  2569	        "@types/unist": "^3.0.0",
  2570	        "ccount": "^2.0.0",
  2571	        "devlop": "^1.1.0",
  2572	        "mdast-util-from-markdown": "^2.0.0",
  2573	        "mdast-util-to-markdown": "^2.0.0",
  2574	        "parse-entities": "^4.0.0",
  2575	        "stringify-entities": "^4.0.0",
  2576	        "unist-util-stringify-position": "^4.0.0",
  2577	        "vfile-message": "^4.0.0"
  2578	      },
  2579	      "funding": {
  2580	        "type": "opencollective",
  2581	        "url": "https://opencollective.com/unified"
  2582	      }
  2583	    },
  2584	    "node_modules/mdast-util-mdxjs-esm": {
  2585	      "version": "2.0.1",
  2586	      "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
  2587	      "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
  2588	      "license": "MIT",
  2589	      "dependencies": {
  2590	        "@types/estree-jsx": "^1.0.0",
  2591	        "@types/hast": "^3.0.0",
  2592	        "@types/mdast": "^4.0.0",
  2593	        "devlop": "^1.0.0",
  2594	        "mdast-util-from-markdown": "^2.0.0",
  2595	        "mdast-util-to-markdown": "^2.0.0"
  2596	      },
  2597	      "funding": {
  2598	        "type": "opencollective",
  2599	        "url": "https://opencollective.com/unified"
  2600	      }
  2601	    },
  2602	    "node_modules/mdast-util-phrasing": {
  2603	      "version": "4.1.0",
  2604	      "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
  2605	      "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
  2606	      "license": "MIT",
  2607	      "dependencies": {
  2608	        "@types/mdast": "^4.0.0",
  2609	        "unist-util-is": "^6.0.0"
  2610	      },
  2611	      "funding": {
  2612	        "type": "opencollective",
  2613	        "url": "https://opencollective.com/unified"
  2614	      }
  2615	    },
  2616	    "node_modules/mdast-util-to-hast": {
  2617	      "version": "13.2.1",
  2618	      "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
  2619	      "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
  2620	      "license": "MIT",
  2621	      "dependencies": {
  2622	        "@types/hast": "^3.0.0",
  2623	        "@types/mdast": "^4.0.0",
  2624	        "@ungap/structured-clone": "^1.0.0",
  2625	        "devlop": "^1.0.0",
  2626	        "micromark-util-sanitize-uri": "^2.0.0",
  2627	        "trim-lines": "^3.0.0",
  2628	        "unist-util-position": "^5.0.0",
  2629	        "unist-util-visit": "^5.0.0",
  2630	        "vfile": "^6.0.0"
  2631	      },
  2632	      "funding": {
  2633	        "type": "opencollective",
  2634	        "url": "https://opencollective.com/unified"
  2635	      }
  2636	    },
  2637	    "node_modules/mdast-util-to-markdown": {
  2638	      "version": "2.1.2",
  2639	      "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
  2640	      "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
  2641	      "license": "MIT",
  2642	      "dependencies": {
  2643	        "@types/mdast": "^4.0.0",
  2644	        "@types/unist": "^3.0.0",
  2645	        "longest-streak": "^3.0.0",
  2646	        "mdast-util-phrasing": "^4.0.0",
  2647	        "mdast-util-to-string": "^4.0.0",
  2648	        "micromark-util-classify-character": "^2.0.0",
  2649	        "micromark-util-decode-string": "^2.0.0",
  2650	        "unist-util-visit": "^5.0.0",
  2651	        "zwitch": "^2.0.0"
  2652	      },
  2653	      "funding": {
  2654	        "type": "opencollective",
  2655	        "url": "https://opencollective.com/unified"
  2656	      }
  2657	    },
  2658	    "node_modules/mdast-util-to-string": {
  2659	      "version": "4.0.0",
  2660	      "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
  2661	      "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
  2662	      "license": "MIT",
  2663	      "dependencies": {
  2664	        "@types/mdast": "^4.0.0"
  2665	      },
  2666	      "funding": {
  2667	        "type": "opencollective",
  2668	        "url": "https://opencollective.com/unified"
  2669	      }
  2670	    },
  2671	    "node_modules/merge2": {
  2672	      "version": "1.4.1",
  2673	      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
  2674	      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
  2675	      "dev": true,
  2676	      "license": "MIT",
  2677	      "engines": {
  2678	        "node": ">= 8"
  2679	      }
  2680	    },
  2681	    "node_modules/micromark": {
  2682	      "version": "4.0.2",
  2683	      "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
  2684	      "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
  2685	      "funding": [
  2686	        {
  2687	          "type": "GitHub Sponsors",
  2688	          "url": "https://github.com/sponsors/unifiedjs"
  2689	        },
  2690	        {
  2691	          "type": "OpenCollective",
  2692	          "url": "https://opencollective.com/unified"
  2693	        }
  2694	      ],
  2695	      "license": "MIT",
  2696	      "dependencies": {
  2697	        "@types/debug": "^4.0.0",
  2698	        "debug": "^4.0.0",
  2699	        "decode-named-character-reference": "^1.0.0",
  2700	        "devlop": "^1.0.0",
  2701	        "micromark-core-commonmark": "^2.0.0",
  2702	        "micromark-factory-space": "^2.0.0",
  2703	        "micromark-util-character": "^2.0.0",
  2704	        "micromark-util-chunked": "^2.0.0",
  2705	        "micromark-util-combine-extensions": "^2.0.0",
  2706	        "micromark-util-decode-numeric-character-reference": "^2.0.0",
  2707	        "micromark-util-encode": "^2.0.0",
  2708	        "micromark-util-normalize-identifier": "^2.0.0",
  2709	        "micromark-util-resolve-all": "^2.0.0",
  2710	        "micromark-util-sanitize-uri": "^2.0.0",
  2711	        "micromark-util-subtokenize": "^2.0.0",
  2712	        "micromark-util-symbol": "^2.0.0",
  2713	        "micromark-util-types": "^2.0.0"
  2714	      }
  2715	    },
  2716	    "node_modules/micromark-core-commonmark": {
  2717	      "version": "2.0.3",
  2718	      "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
  2719	      "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
  2720	      "funding": [
  2721	        {
  2722	          "type": "GitHub Sponsors",
  2723	          "url": "https://github.com/sponsors/unifiedjs"
  2724	        },
  2725	        {
  2726	          "type": "OpenCollective",
  2727	          "url": "https://opencollective.com/unified"
  2728	        }
  2729	      ],
  2730	      "license": "MIT",
  2731	      "dependencies": {
  2732	        "decode-named-character-reference": "^1.0.0",
  2733	        "devlop": "^1.0.0",
  2734	        "micromark-factory-destination": "^2.0.0",
  2735	        "micromark-factory-label": "^2.0.0",
  2736	        "micromark-factory-space": "^2.0.0",
  2737	        "micromark-factory-title": "^2.0.0",
  2738	        "micromark-factory-whitespace": "^2.0.0",
  2739	        "micromark-util-character": "^2.0.0",
  2740	        "micromark-util-chunked": "^2.0.0",
  2741	        "micromark-util-classify-character": "^2.0.0",
  2742	        "micromark-util-html-tag-name": "^2.0.0",
  2743	        "micromark-util-normalize-identifier": "^2.0.0",
  2744	        "micromark-util-resolve-all": "^2.0.0",
  2745	        "micromark-util-subtokenize": "^2.0.0",
  2746	        "micromark-util-symbol": "^2.0.0",
  2747	        "micromark-util-types": "^2.0.0"
  2748	      }
  2749	    },
  2750	    "node_modules/micromark-extension-gfm": {
  2751	      "version": "3.0.0",
  2752	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
  2753	      "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
  2754	      "license": "MIT",
  2755	      "dependencies": {
  2756	        "micromark-extension-gfm-autolink-literal": "^2.0.0",
  2757	        "micromark-extension-gfm-footnote": "^2.0.0",
  2758	        "micromark-extension-gfm-strikethrough": "^2.0.0",
  2759	        "micromark-extension-gfm-table": "^2.0.0",
  2760	        "micromark-extension-gfm-tagfilter": "^2.0.0",
  2761	        "micromark-extension-gfm-task-list-item": "^2.0.0",
  2762	        "micromark-util-combine-extensions": "^2.0.0",
  2763	        "micromark-util-types": "^2.0.0"
  2764	      },
  2765	      "funding": {
  2766	        "type": "opencollective",
  2767	        "url": "https://opencollective.com/unified"
  2768	      }
  2769	    },
  2770	    "node_modules/micromark-extension-gfm-autolink-literal": {
  2771	      "version": "2.1.0",
  2772	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
  2773	      "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
  2774	      "license": "MIT",
  2775	      "dependencies": {
  2776	        "micromark-util-character": "^2.0.0",
  2777	        "micromark-util-sanitize-uri": "^2.0.0",
  2778	        "micromark-util-symbol": "^2.0.0",
  2779	        "micromark-util-types": "^2.0.0"
  2780	      },
  2781	      "funding": {
  2782	        "type": "opencollective",
  2783	        "url": "https://opencollective.com/unified"
  2784	      }
  2785	    },
  2786	    "node_modules/micromark-extension-gfm-footnote": {
  2787	      "version": "2.1.0",
  2788	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
  2789	      "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
  2790	      "license": "MIT",
  2791	      "dependencies": {
  2792	        "devlop": "^1.0.0",
  2793	        "micromark-core-commonmark": "^2.0.0",
  2794	        "micromark-factory-space": "^2.0.0",
  2795	        "micromark-util-character": "^2.0.0",
  2796	        "micromark-util-normalize-identifier": "^2.0.0",
  2797	        "micromark-util-sanitize-uri": "^2.0.0",
  2798	        "micromark-util-symbol": "^2.0.0",
  2799	        "micromark-util-types": "^2.0.0"
  2800	      },
  2801	      "funding": {
  2802	        "type": "opencollective",
  2803	        "url": "https://opencollective.com/unified"
  2804	      }
  2805	    },
  2806	    "node_modules/micromark-extension-gfm-strikethrough": {
  2807	      "version": "2.1.0",
  2808	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
  2809	      "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
  2810	      "license": "MIT",
  2811	      "dependencies": {
  2812	        "devlop": "^1.0.0",
  2813	        "micromark-util-chunked": "^2.0.0",
  2814	        "micromark-util-classify-character": "^2.0.0",
  2815	        "micromark-util-resolve-all": "^2.0.0",
  2816	        "micromark-util-symbol": "^2.0.0",
  2817	        "micromark-util-types": "^2.0.0"
  2818	      },
  2819	      "funding": {
  2820	        "type": "opencollective",
  2821	        "url": "https://opencollective.com/unified"
  2822	      }
  2823	    },
  2824	    "node_modules/micromark-extension-gfm-table": {
  2825	      "version": "2.1.1",
  2826	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
  2827	      "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
  2828	      "license": "MIT",
  2829	      "dependencies": {
  2830	        "devlop": "^1.0.0",
  2831	        "micromark-factory-space": "^2.0.0",
  2832	        "micromark-util-character": "^2.0.0",
  2833	        "micromark-util-symbol": "^2.0.0",
  2834	        "micromark-util-types": "^2.0.0"
  2835	      },
  2836	      "funding": {
  2837	        "type": "opencollective",
  2838	        "url": "https://opencollective.com/unified"
  2839	      }
  2840	    },
  2841	    "node_modules/micromark-extension-gfm-tagfilter": {
  2842	      "version": "2.0.0",
  2843	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
  2844	      "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
  2845	      "license": "MIT",
  2846	      "dependencies": {
  2847	        "micromark-util-types": "^2.0.0"
  2848	      },
  2849	      "funding": {
  2850	        "type": "opencollective",
  2851	        "url": "https://opencollective.com/unified"
  2852	      }
  2853	    },
  2854	    "node_modules/micromark-extension-gfm-task-list-item": {
  2855	      "version": "2.1.0",
  2856	      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
  2857	      "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
  2858	      "license": "MIT",
  2859	      "dependencies": {
  2860	        "devlop": "^1.0.0",
  2861	        "micromark-factory-space": "^2.0.0",
  2862	        "micromark-util-character": "^2.0.0",
  2863	        "micromark-util-symbol": "^2.0.0",
  2864	        "micromark-util-types": "^2.0.0"
  2865	      },
  2866	      "funding": {
  2867	        "type": "opencollective",
  2868	        "url": "https://opencollective.com/unified"
  2869	      }
  2870	    },
  2871	    "node_modules/micromark-factory-destination": {
  2872	      "version": "2.0.1",
  2873	      "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
  2874	      "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
  2875	      "funding": [
  2876	        {
  2877	          "type": "GitHub Sponsors",
  2878	          "url": "https://github.com/sponsors/unifiedjs"
  2879	        },
  2880	        {
  2881	          "type": "OpenCollective",
  2882	          "url": "https://opencollective.com/unified"
  2883	        }
  2884	      ],
  2885	      "license": "MIT",
  2886	      "dependencies": {
  2887	        "micromark-util-character": "^2.0.0",
  2888	        "micromark-util-symbol": "^2.0.0",
  2889	        "micromark-util-types": "^2.0.0"
  2890	      }
  2891	    },
  2892	    "node_modules/micromark-factory-label": {
  2893	      "version": "2.0.1",
  2894	      "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
  2895	      "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
  2896	      "funding": [
  2897	        {
  2898	          "type": "GitHub Sponsors",
  2899	          "url": "https://github.com/sponsors/unifiedjs"
  2900	        },
  2901	        {
  2902	          "type": "OpenCollective",
  2903	          "url": "https://opencollective.com/unified"
  2904	        }
  2905	      ],
  2906	      "license": "MIT",
  2907	      "dependencies": {
  2908	        "devlop": "^1.0.0",
  2909	        "micromark-util-character": "^2.0.0",
  2910	        "micromark-util-symbol": "^2.0.0",
  2911	        "micromark-util-types": "^2.0.0"
  2912	      }
  2913	    },
  2914	    "node_modules/micromark-factory-space": {
  2915	      "version": "2.0.1",
  2916	      "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
  2917	      "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
  2918	      "funding": [
  2919	        {
  2920	          "type": "GitHub Sponsors",
  2921	          "url": "https://github.com/sponsors/unifiedjs"
  2922	        },
  2923	        {
  2924	          "type": "OpenCollective",
  2925	          "url": "https://opencollective.com/unified"
  2926	        }
  2927	      ],
  2928	      "license": "MIT",
  2929	      "dependencies": {
  2930	        "micromark-util-character": "^2.0.0",
  2931	        "micromark-util-types": "^2.0.0"
  2932	      }
  2933	    },
  2934	    "node_modules/micromark-factory-title": {
  2935	      "version": "2.0.1",
  2936	      "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
  2937	      "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
  2938	      "funding": [
  2939	        {
  2940	          "type": "GitHub Sponsors",
  2941	          "url": "https://github.com/sponsors/unifiedjs"
  2942	        },
  2943	        {
  2944	          "type": "OpenCollective",
  2945	          "url": "https://opencollective.com/unified"
  2946	        }
  2947	      ],
  2948	      "license": "MIT",
  2949	      "dependencies": {
  2950	        "micromark-factory-space": "^2.0.0",
  2951	        "micromark-util-character": "^2.0.0",
  2952	        "micromark-util-symbol": "^2.0.0",
  2953	        "micromark-util-types": "^2.0.0"
  2954	      }
  2955	    },
  2956	    "node_modules/micromark-factory-whitespace": {
  2957	      "version": "2.0.1",
  2958	      "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
  2959	      "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
  2960	      "funding": [
  2961	        {
  2962	          "type": "GitHub Sponsors",
  2963	          "url": "https://github.com/sponsors/unifiedjs"
  2964	        },
  2965	        {
  2966	          "type": "OpenCollective",
  2967	          "url": "https://opencollective.com/unified"
  2968	        }
  2969	      ],
  2970	      "license": "MIT",
  2971	      "dependencies": {
  2972	        "micromark-factory-space": "^2.0.0",
  2973	        "micromark-util-character": "^2.0.0",
  2974	        "micromark-util-symbol": "^2.0.0",
  2975	        "micromark-util-types": "^2.0.0"
  2976	      }
  2977	    },
  2978	    "node_modules/micromark-util-character": {
  2979	      "version": "2.1.1",
  2980	      "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
  2981	      "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
  2982	      "funding": [
  2983	        {
  2984	          "type": "GitHub Sponsors",
  2985	          "url": "https://github.com/sponsors/unifiedjs"
  2986	        },
  2987	        {
  2988	          "type": "OpenCollective",
  2989	          "url": "https://opencollective.com/unified"
  2990	        }
  2991	      ],
  2992	      "license": "MIT",
  2993	      "dependencies": {
  2994	        "micromark-util-symbol": "^2.0.0",
  2995	        "micromark-util-types": "^2.0.0"
  2996	      }
  2997	    },
  2998	    "node_modules/micromark-util-chunked": {
  2999	      "version": "2.0.1",
  3000	      "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
  3001	      "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
  3002	      "funding": [
  3003	        {
  3004	          "type": "GitHub Sponsors",
  3005	          "url": "https://github.com/sponsors/unifiedjs"
  3006	        },
  3007	        {
  3008	          "type": "OpenCollective",
  3009	          "url": "https://opencollective.com/unified"
  3010	        }
  3011	      ],
  3012	      "license": "MIT",
  3013	      "dependencies": {
  3014	        "micromark-util-symbol": "^2.0.0"
  3015	      }
  3016	    },
  3017	    "node_modules/micromark-util-classify-character": {
  3018	      "version": "2.0.1",
  3019	      "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
  3020	      "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
  3021	      "funding": [
  3022	        {
  3023	          "type": "GitHub Sponsors",
  3024	          "url": "https://github.com/sponsors/unifiedjs"
  3025	        },
  3026	        {
  3027	          "type": "OpenCollective",
  3028	          "url": "https://opencollective.com/unified"
  3029	        }
  3030	      ],
  3031	      "license": "MIT",
  3032	      "dependencies": {
  3033	        "micromark-util-character": "^2.0.0",
  3034	        "micromark-util-symbol": "^2.0.0",
  3035	        "micromark-util-types": "^2.0.0"
  3036	      }
  3037	    },
  3038	    "node_modules/micromark-util-combine-extensions": {
  3039	      "version": "2.0.1",
  3040	      "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
  3041	      "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
  3042	      "funding": [
  3043	        {
  3044	          "type": "GitHub Sponsors",
  3045	          "url": "https://github.com/sponsors/unifiedjs"
  3046	        },
  3047	        {
  3048	          "type": "OpenCollective",
  3049	          "url": "https://opencollective.com/unified"
  3050	        }
  3051	      ],
  3052	      "license": "MIT",
  3053	      "dependencies": {
  3054	        "micromark-util-chunked": "^2.0.0",
  3055	        "micromark-util-types": "^2.0.0"
  3056	      }
  3057	    },
  3058	    "node_modules/micromark-util-decode-numeric-character-reference": {
  3059	      "version": "2.0.2",
  3060	      "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
  3061	      "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
  3062	      "funding": [
  3063	        {
  3064	          "type": "GitHub Sponsors",
  3065	          "url": "https://github.com/sponsors/unifiedjs"
  3066	        },
  3067	        {
  3068	          "type": "OpenCollective",
  3069	          "url": "https://opencollective.com/unified"
  3070	        }
  3071	      ],
  3072	      "license": "MIT",
  3073	      "dependencies": {
  3074	        "micromark-util-symbol": "^2.0.0"
  3075	      }
  3076	    },
  3077	    "node_modules/micromark-util-decode-string": {
  3078	      "version": "2.0.1",
  3079	      "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
  3080	      "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
  3081	      "funding": [
  3082	        {
  3083	          "type": "GitHub Sponsors",
  3084	          "url": "https://github.com/sponsors/unifiedjs"
  3085	        },
  3086	        {
  3087	          "type": "OpenCollective",
  3088	          "url": "https://opencollective.com/unified"
  3089	        }
  3090	      ],
  3091	      "license": "MIT",
  3092	      "dependencies": {
  3093	        "decode-named-character-reference": "^1.0.0",
  3094	        "micromark-util-character": "^2.0.0",
  3095	        "micromark-util-decode-numeric-character-reference": "^2.0.0",
  3096	        "micromark-util-symbol": "^2.0.0"
  3097	      }
  3098	    },
  3099	    "node_modules/micromark-util-encode": {
  3100	      "version": "2.0.1",
  3101	      "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
  3102	      "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
  3103	      "funding": [
  3104	        {
  3105	          "type": "GitHub Sponsors",
  3106	          "url": "https://github.com/sponsors/unifiedjs"
  3107	        },
  3108	        {
  3109	          "type": "OpenCollective",
  3110	          "url": "https://opencollective.com/unified"
  3111	        }
  3112	      ],
  3113	      "license": "MIT"
  3114	    },
  3115	    "node_modules/micromark-util-html-tag-name": {
  3116	      "version": "2.0.1",
  3117	      "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
  3118	      "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
  3119	      "funding": [
  3120	        {
  3121	          "type": "GitHub Sponsors",
  3122	          "url": "https://github.com/sponsors/unifiedjs"
  3123	        },
  3124	        {
  3125	          "type": "OpenCollective",
  3126	          "url": "https://opencollective.com/unified"
  3127	        }
  3128	      ],
  3129	      "license": "MIT"
  3130	    },
  3131	    "node_modules/micromark-util-normalize-identifier": {
  3132	      "version": "2.0.1",
  3133	      "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
  3134	      "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
  3135	      "funding": [
  3136	        {
  3137	          "type": "GitHub Sponsors",
  3138	          "url": "https://github.com/sponsors/unifiedjs"
  3139	        },
  3140	        {
  3141	          "type": "OpenCollective",
  3142	          "url": "https://opencollective.com/unified"
  3143	        }
  3144	      ],
  3145	      "license": "MIT",
  3146	      "dependencies": {
  3147	        "micromark-util-symbol": "^2.0.0"
  3148	      }
  3149	    },
  3150	    "node_modules/micromark-util-resolve-all": {
  3151	      "version": "2.0.1",
  3152	      "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
  3153	      "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
  3154	      "funding": [
  3155	        {
  3156	          "type": "GitHub Sponsors",
  3157	          "url": "https://github.com/sponsors/unifiedjs"
  3158	        },
  3159	        {
  3160	          "type": "OpenCollective",
  3161	          "url": "https://opencollective.com/unified"
  3162	        }
  3163	      ],
  3164	      "license": "MIT",
  3165	      "dependencies": {
  3166	        "micromark-util-types": "^2.0.0"
  3167	      }
  3168	    },
  3169	    "node_modules/micromark-util-sanitize-uri": {
  3170	      "version": "2.0.1",
  3171	      "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
  3172	      "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
  3173	      "funding": [
  3174	        {
  3175	          "type": "GitHub Sponsors",
  3176	          "url": "https://github.com/sponsors/unifiedjs"
  3177	        },
  3178	        {
  3179	          "type": "OpenCollective",
  3180	          "url": "https://opencollective.com/unified"
  3181	        }
  3182	      ],
  3183	      "license": "MIT",
  3184	      "dependencies": {
  3185	        "micromark-util-character": "^2.0.0",
  3186	        "micromark-util-encode": "^2.0.0",
  3187	        "micromark-util-symbol": "^2.0.0"
  3188	      }
  3189	    },
  3190	    "node_modules/micromark-util-subtokenize": {
  3191	      "version": "2.1.0",
  3192	      "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
  3193	      "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
  3194	      "funding": [
  3195	        {
  3196	          "type": "GitHub Sponsors",
  3197	          "url": "https://github.com/sponsors/unifiedjs"
  3198	        },
  3199	        {
  3200	          "type": "OpenCollective",
  3201	          "url": "https://opencollective.com/unified"
  3202	        }
  3203	      ],
  3204	      "license": "MIT",
  3205	      "dependencies": {
  3206	        "devlop": "^1.0.0",
  3207	        "micromark-util-chunked": "^2.0.0",
  3208	        "micromark-util-symbol": "^2.0.0",
  3209	        "micromark-util-types": "^2.0.0"
  3210	      }
  3211	    },
  3212	    "node_modules/micromark-util-symbol": {
  3213	      "version": "2.0.1",
  3214	      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
  3215	      "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
  3216	      "funding": [
  3217	        {
  3218	          "type": "GitHub Sponsors",
  3219	          "url": "https://github.com/sponsors/unifiedjs"
  3220	        },
  3221	        {
  3222	          "type": "OpenCollective",
  3223	          "url": "https://opencollective.com/unified"
  3224	        }
  3225	      ],
  3226	      "license": "MIT"
  3227	    },
  3228	    "node_modules/micromark-util-types": {
  3229	      "version": "2.0.2",
  3230	      "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
  3231	      "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
  3232	      "funding": [
  3233	        {
  3234	          "type": "GitHub Sponsors",
  3235	          "url": "https://github.com/sponsors/unifiedjs"
  3236	        },
  3237	        {
  3238	          "type": "OpenCollective",
  3239	          "url": "https://opencollective.com/unified"
  3240	        }
  3241	      ],
  3242	      "license": "MIT"
  3243	    },
  3244	    "node_modules/micromatch": {
  3245	      "version": "4.0.8",
  3246	      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
  3247	      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
  3248	      "dev": true,
  3249	      "license": "MIT",
  3250	      "dependencies": {
  3251	        "braces": "^3.0.3",
  3252	        "picomatch": "^2.3.1"
  3253	      },
  3254	      "engines": {
  3255	        "node": ">=8.6"
  3256	      }
  3257	    },
  3258	    "node_modules/ms": {
  3259	      "version": "2.1.3",
  3260	      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
  3261	      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
  3262	      "license": "MIT"
  3263	    },
  3264	    "node_modules/mz": {
  3265	      "version": "2.7.0",
  3266	      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
  3267	      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
  3268	      "dev": true,
  3269	      "license": "MIT",
  3270	      "dependencies": {
  3271	        "any-promise": "^1.0.0",
  3272	        "object-assign": "^4.0.1",
  3273	        "thenify-all": "^1.0.0"
  3274	      }
  3275	    },
  3276	    "node_modules/nanoid": {
  3277	      "version": "3.3.11",
  3278	      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
  3279	      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
  3280	      "dev": true,
  3281	      "funding": [
  3282	        {
  3283	          "type": "github",
  3284	          "url": "https://github.com/sponsors/ai"
  3285	        }
  3286	      ],
  3287	      "license": "MIT",
  3288	      "bin": {
  3289	        "nanoid": "bin/nanoid.cjs"
  3290	      },
  3291	      "engines": {
  3292	        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
  3293	      }
  3294	    },
  3295	    "node_modules/node-releases": {
  3296	      "version": "2.0.36",
  3297	      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
  3298	      "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
  3299	      "dev": true,
  3300	      "license": "MIT"
  3301	    },
  3302	    "node_modules/normalize-path": {
  3303	      "version": "3.0.0",
  3304	      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
  3305	      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
  3306	      "dev": true,
  3307	      "license": "MIT",
  3308	      "engines": {
  3309	        "node": ">=0.10.0"
  3310	      }
  3311	    },
  3312	    "node_modules/object-assign": {
  3313	      "version": "4.1.1",
  3314	      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
  3315	      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
  3316	      "dev": true,
  3317	      "license": "MIT",
  3318	      "engines": {
  3319	        "node": ">=0.10.0"
  3320	      }
  3321	    },
  3322	    "node_modules/object-hash": {
  3323	      "version": "3.0.0",
  3324	      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
  3325	      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
  3326	      "dev": true,
  3327	      "license": "MIT",
  3328	      "engines": {
  3329	        "node": ">= 6"
  3330	      }
  3331	    },
  3332	    "node_modules/parse-entities": {
  3333	      "version": "4.0.2",
  3334	      "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
  3335	      "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
  3336	      "license": "MIT",
  3337	      "dependencies": {
  3338	        "@types/unist": "^2.0.0",
  3339	        "character-entities-legacy": "^3.0.0",
  3340	        "character-reference-invalid": "^2.0.0",
  3341	        "decode-named-character-reference": "^1.0.0",
  3342	        "is-alphanumerical": "^2.0.0",
  3343	        "is-decimal": "^2.0.0",
  3344	        "is-hexadecimal": "^2.0.0"
  3345	      },
  3346	      "funding": {
  3347	        "type": "github",
  3348	        "url": "https://github.com/sponsors/wooorm"
  3349	      }
  3350	    },
  3351	    "node_modules/parse-entities/node_modules/@types/unist": {
  3352	      "version": "2.0.11",
  3353	      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
  3354	      "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
  3355	      "license": "MIT"
  3356	    },
  3357	    "node_modules/path-parse": {
  3358	      "version": "1.0.7",
  3359	      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
  3360	      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
  3361	      "dev": true,
  3362	      "license": "MIT"
  3363	    },
  3364	    "node_modules/picocolors": {
  3365	      "version": "1.1.1",
  3366	      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
  3367	      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
  3368	      "dev": true,
  3369	      "license": "ISC"
  3370	    },
  3371	    "node_modules/picomatch": {
  3372	      "version": "2.3.1",
  3373	      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
  3374	      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
  3375	      "dev": true,
  3376	      "license": "MIT",
  3377	      "engines": {
  3378	        "node": ">=8.6"
  3379	      },
  3380	      "funding": {
  3381	        "url": "https://github.com/sponsors/jonschlinkert"
  3382	      }
  3383	    },
  3384	    "node_modules/pify": {
  3385	      "version": "2.3.0",
  3386	      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
  3387	      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
  3388	      "dev": true,
  3389	      "license": "MIT",
  3390	      "engines": {
  3391	        "node": ">=0.10.0"
  3392	      }
  3393	    },
  3394	    "node_modules/pirates": {
  3395	      "version": "4.0.7",
  3396	      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
  3397	      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
  3398	      "dev": true,
  3399	      "license": "MIT",
  3400	      "engines": {
  3401	        "node": ">= 6"
  3402	      }
  3403	    },
  3404	    "node_modules/postcss": {
  3405	      "version": "8.5.8",
  3406	      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
  3407	      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
  3408	      "dev": true,
  3409	      "funding": [
  3410	        {
  3411	          "type": "opencollective",
  3412	          "url": "https://opencollective.com/postcss/"
  3413	        },
  3414	        {
  3415	          "type": "tidelift",
  3416	          "url": "https://tidelift.com/funding/github/npm/postcss"
  3417	        },
  3418	        {
  3419	          "type": "github",
  3420	          "url": "https://github.com/sponsors/ai"
  3421	        }
  3422	      ],
  3423	      "license": "MIT",
  3424	      "dependencies": {
  3425	        "nanoid": "^3.3.11",
  3426	        "picocolors": "^1.1.1",
  3427	        "source-map-js": "^1.2.1"
  3428	      },
  3429	      "engines": {
  3430	        "node": "^10 || ^12 || >=14"
  3431	      }
  3432	    },
  3433	    "node_modules/postcss-import": {
  3434	      "version": "15.1.0",
  3435	      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
  3436	      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
  3437	      "dev": true,
  3438	      "license": "MIT",
  3439	      "dependencies": {
  3440	        "postcss-value-parser": "^4.0.0",
  3441	        "read-cache": "^1.0.0",
  3442	        "resolve": "^1.1.7"
  3443	      },
  3444	      "engines": {
  3445	        "node": ">=14.0.0"
  3446	      },
  3447	      "peerDependencies": {
  3448	        "postcss": "^8.0.0"
  3449	      }
  3450	    },
  3451	    "node_modules/postcss-js": {
  3452	      "version": "4.1.0",
  3453	      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
  3454	      "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
  3455	      "dev": true,
  3456	      "funding": [
  3457	        {
  3458	          "type": "opencollective",
  3459	          "url": "https://opencollective.com/postcss/"
  3460	        },
  3461	        {
  3462	          "type": "github",
  3463	          "url": "https://github.com/sponsors/ai"
  3464	        }
  3465	      ],
  3466	      "license": "MIT",
  3467	      "dependencies": {
  3468	        "camelcase-css": "^2.0.1"
  3469	      },
  3470	      "engines": {
  3471	        "node": "^12 || ^14 || >= 16"
  3472	      },
  3473	      "peerDependencies": {
  3474	        "postcss": "^8.4.21"
  3475	      }
  3476	    },
  3477	    "node_modules/postcss-load-config": {
  3478	      "version": "6.0.1",
  3479	      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
  3480	      "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
  3481	      "dev": true,
  3482	      "funding": [
  3483	        {
  3484	          "type": "opencollective",
  3485	          "url": "https://opencollective.com/postcss/"
  3486	        },
  3487	        {
  3488	          "type": "github",
  3489	          "url": "https://github.com/sponsors/ai"
  3490	        }
  3491	      ],
  3492	      "license": "MIT",
  3493	      "dependencies": {
  3494	        "lilconfig": "^3.1.1"
  3495	      },
  3496	      "engines": {
  3497	        "node": ">= 18"
  3498	      },
  3499	      "peerDependencies": {
  3500	        "jiti": ">=1.21.0",
  3501	        "postcss": ">=8.0.9",
  3502	        "tsx": "^4.8.1",
  3503	        "yaml": "^2.4.2"
  3504	      },
  3505	      "peerDependenciesMeta": {
  3506	        "jiti": {
  3507	          "optional": true
  3508	        },
  3509	        "postcss": {
  3510	          "optional": true
  3511	        },
  3512	        "tsx": {
  3513	          "optional": true
  3514	        },
  3515	        "yaml": {
  3516	          "optional": true
  3517	        }
  3518	      }
  3519	    },
  3520	    "node_modules/postcss-nested": {
  3521	      "version": "6.2.0",
  3522	      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
  3523	      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
  3524	      "dev": true,
  3525	      "funding": [
  3526	        {
  3527	          "type": "opencollective",
  3528	          "url": "https://opencollective.com/postcss/"
  3529	        },
  3530	        {
  3531	          "type": "github",
  3532	          "url": "https://github.com/sponsors/ai"
  3533	        }
  3534	      ],
  3535	      "license": "MIT",
  3536	      "dependencies": {
  3537	        "postcss-selector-parser": "^6.1.1"
  3538	      },
  3539	      "engines": {
  3540	        "node": ">=12.0"
  3541	      },
  3542	      "peerDependencies": {
  3543	        "postcss": "^8.2.14"
  3544	      }
  3545	    },
  3546	    "node_modules/postcss-selector-parser": {
  3547	      "version": "6.1.2",
  3548	      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
  3549	      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
  3550	      "dev": true,
  3551	      "license": "MIT",
  3552	      "dependencies": {
  3553	        "cssesc": "^3.0.0",
  3554	        "util-deprecate": "^1.0.2"
  3555	      },
  3556	      "engines": {
  3557	        "node": ">=4"
  3558	      }
  3559	    },
  3560	    "node_modules/postcss-value-parser": {
  3561	      "version": "4.2.0",
  3562	      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
  3563	      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
  3564	      "dev": true,
  3565	      "license": "MIT"
  3566	    },
  3567	    "node_modules/prismjs": {
  3568	      "version": "1.30.0",
  3569	      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
  3570	      "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
  3571	      "license": "MIT",
  3572	      "engines": {
  3573	        "node": ">=6"
  3574	      }
  3575	    },
  3576	    "node_modules/property-information": {
  3577	      "version": "7.1.0",
  3578	      "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
  3579	      "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
  3580	      "license": "MIT",
  3581	      "funding": {
  3582	        "type": "github",
  3583	        "url": "https://github.com/sponsors/wooorm"
  3584	      }
  3585	    },
  3586	    "node_modules/queue-microtask": {
  3587	      "version": "1.2.3",
  3588	      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
  3589	      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
  3590	      "dev": true,
  3591	      "funding": [
  3592	        {
  3593	          "type": "github",
  3594	          "url": "https://github.com/sponsors/feross"
  3595	        },
  3596	        {
  3597	          "type": "patreon",
  3598	          "url": "https://www.patreon.com/feross"
  3599	        },
  3600	        {
  3601	          "type": "consulting",
  3602	          "url": "https://feross.org/support"
  3603	        }
  3604	      ],
  3605	      "license": "MIT"
  3606	    },
  3607	    "node_modules/react": {
  3608	      "version": "18.3.1",
  3609	      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
  3610	      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
  3611	      "license": "MIT",
  3612	      "dependencies": {
  3613	        "loose-envify": "^1.1.0"
  3614	      },
  3615	      "engines": {
  3616	        "node": ">=0.10.0"
  3617	      }
  3618	    },
  3619	    "node_modules/react-dom": {
  3620	      "version": "18.3.1",
  3621	      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
  3622	      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
  3623	      "license": "MIT",
  3624	      "dependencies": {
  3625	        "loose-envify": "^1.1.0",
  3626	        "scheduler": "^0.23.2"
  3627	      },
  3628	      "peerDependencies": {
  3629	        "react": "^18.3.1"
  3630	      }
  3631	    },
  3632	    "node_modules/react-markdown": {
  3633	      "version": "9.1.0",
  3634	      "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
  3635	      "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
  3636	      "license": "MIT",
  3637	      "dependencies": {
  3638	        "@types/hast": "^3.0.0",
  3639	        "@types/mdast": "^4.0.0",
  3640	        "devlop": "^1.0.0",
  3641	        "hast-util-to-jsx-runtime": "^2.0.0",
  3642	        "html-url-attributes": "^3.0.0",
  3643	        "mdast-util-to-hast": "^13.0.0",
  3644	        "remark-parse": "^11.0.0",
  3645	        "remark-rehype": "^11.0.0",
  3646	        "unified": "^11.0.0",
  3647	        "unist-util-visit": "^5.0.0",
  3648	        "vfile": "^6.0.0"
  3649	      },
  3650	      "funding": {
  3651	        "type": "opencollective",
  3652	        "url": "https://opencollective.com/unified"
  3653	      },
  3654	      "peerDependencies": {
  3655	        "@types/react": ">=18",
  3656	        "react": ">=18"
  3657	      }
  3658	    },
  3659	    "node_modules/react-refresh": {
  3660	      "version": "0.17.0",
  3661	      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
  3662	      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
  3663	      "dev": true,
  3664	      "license": "MIT",
  3665	      "engines": {
  3666	        "node": ">=0.10.0"
  3667	      }
  3668	    },
  3669	    "node_modules/react-router": {
  3670	      "version": "7.13.1",
  3671	      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
  3672	      "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
  3673	      "license": "MIT",
  3674	      "dependencies": {
  3675	        "cookie": "^1.0.1",
  3676	        "set-cookie-parser": "^2.6.0"
  3677	      },
  3678	      "engines": {
  3679	        "node": ">=20.0.0"
  3680	      },
  3681	      "peerDependencies": {
  3682	        "react": ">=18",
  3683	        "react-dom": ">=18"
  3684	      },
  3685	      "peerDependenciesMeta": {
  3686	        "react-dom": {
  3687	          "optional": true
  3688	        }
  3689	      }
  3690	    },
  3691	    "node_modules/react-router-dom": {
  3692	      "version": "7.13.1",
  3693	      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
  3694	      "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
  3695	      "license": "MIT",
  3696	      "dependencies": {
  3697	        "react-router": "7.13.1"
  3698	      },
  3699	      "engines": {
  3700	        "node": ">=20.0.0"
  3701	      },
  3702	      "peerDependencies": {
  3703	        "react": ">=18",
  3704	        "react-dom": ">=18"
  3705	      }
  3706	    },
  3707	    "node_modules/react-syntax-highlighter": {
  3708	      "version": "15.6.6",
  3709	      "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
  3710	      "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
  3711	      "license": "MIT",
  3712	      "dependencies": {
  3713	        "@babel/runtime": "^7.3.1",
  3714	        "highlight.js": "^10.4.1",
  3715	        "highlightjs-vue": "^1.0.0",
  3716	        "lowlight": "^1.17.0",
  3717	        "prismjs": "^1.30.0",
  3718	        "refractor": "^3.6.0"
  3719	      },
  3720	      "peerDependencies": {
  3721	        "react": ">= 0.14.0"
  3722	      }
  3723	    },
  3724	    "node_modules/read-cache": {
  3725	      "version": "1.0.0",
  3726	      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
  3727	      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
  3728	      "dev": true,
  3729	      "license": "MIT",
  3730	      "dependencies": {
  3731	        "pify": "^2.3.0"
  3732	      }
  3733	    },
  3734	    "node_modules/readdirp": {
  3735	      "version": "3.6.0",
  3736	      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
  3737	      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
  3738	      "dev": true,
  3739	      "license": "MIT",
  3740	      "dependencies": {
  3741	        "picomatch": "^2.2.1"
  3742	      },
  3743	      "engines": {
  3744	        "node": ">=8.10.0"
  3745	      }
  3746	    },
  3747	    "node_modules/refractor": {
  3748	      "version": "3.6.0",
  3749	      "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
  3750	      "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
  3751	      "license": "MIT",
  3752	      "dependencies": {
  3753	        "hastscript": "^6.0.0",
  3754	        "parse-entities": "^2.0.0",
  3755	        "prismjs": "~1.27.0"
  3756	      },
  3757	      "funding": {
  3758	        "type": "github",
  3759	        "url": "https://github.com/sponsors/wooorm"
  3760	      }
  3761	    },
  3762	    "node_modules/refractor/node_modules/character-entities": {
  3763	      "version": "1.2.4",
  3764	      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
  3765	      "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
  3766	      "license": "MIT",
  3767	      "funding": {
  3768	        "type": "github",
  3769	        "url": "https://github.com/sponsors/wooorm"
  3770	      }
  3771	    },
  3772	    "node_modules/refractor/node_modules/character-entities-legacy": {
  3773	      "version": "1.1.4",
  3774	      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
  3775	      "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
  3776	      "license": "MIT",
  3777	      "funding": {
  3778	        "type": "github",
  3779	        "url": "https://github.com/sponsors/wooorm"
  3780	      }
  3781	    },
  3782	    "node_modules/refractor/node_modules/character-reference-invalid": {
  3783	      "version": "1.1.4",
  3784	      "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
  3785	      "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
  3786	      "license": "MIT",
  3787	      "funding": {
  3788	        "type": "github",
  3789	        "url": "https://github.com/sponsors/wooorm"
  3790	      }
  3791	    },
  3792	    "node_modules/refractor/node_modules/is-alphabetical": {
  3793	      "version": "1.0.4",
  3794	      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
  3795	      "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
  3796	      "license": "MIT",
  3797	      "funding": {
  3798	        "type": "github",
  3799	        "url": "https://github.com/sponsors/wooorm"
  3800	      }
  3801	    },
  3802	    "node_modules/refractor/node_modules/is-alphanumerical": {
  3803	      "version": "1.0.4",
  3804	      "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
  3805	      "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
  3806	      "license": "MIT",
  3807	      "dependencies": {
  3808	        "is-alphabetical": "^1.0.0",
  3809	        "is-decimal": "^1.0.0"
  3810	      },
  3811	      "funding": {
  3812	        "type": "github",
  3813	        "url": "https://github.com/sponsors/wooorm"
  3814	      }
  3815	    },
  3816	    "node_modules/refractor/node_modules/is-decimal": {
  3817	      "version": "1.0.4",
  3818	      "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
  3819	      "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
  3820	      "license": "MIT",
  3821	      "funding": {
  3822	        "type": "github",
  3823	        "url": "https://github.com/sponsors/wooorm"
  3824	      }
  3825	    },
  3826	    "node_modules/refractor/node_modules/is-hexadecimal": {
  3827	      "version": "1.0.4",
  3828	      "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
  3829	      "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
  3830	      "license": "MIT",
  3831	      "funding": {
  3832	        "type": "github",
  3833	        "url": "https://github.com/sponsors/wooorm"
  3834	      }
  3835	    },
  3836	    "node_modules/refractor/node_modules/parse-entities": {
  3837	      "version": "2.0.0",
  3838	      "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
  3839	      "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
  3840	      "license": "MIT",
  3841	      "dependencies": {
  3842	        "character-entities": "^1.0.0",
  3843	        "character-entities-legacy": "^1.0.0",
  3844	        "character-reference-invalid": "^1.0.0",
  3845	        "is-alphanumerical": "^1.0.0",
  3846	        "is-decimal": "^1.0.0",
  3847	        "is-hexadecimal": "^1.0.0"
  3848	      },
  3849	      "funding": {
  3850	        "type": "github",
  3851	        "url": "https://github.com/sponsors/wooorm"
  3852	      }
  3853	    },
  3854	    "node_modules/refractor/node_modules/prismjs": {
  3855	      "version": "1.27.0",
  3856	      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
  3857	      "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
  3858	      "license": "MIT",
  3859	      "engines": {
  3860	        "node": ">=6"
  3861	      }
  3862	    },
  3863	    "node_modules/remark-gfm": {
  3864	      "version": "4.0.1",
  3865	      "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
  3866	      "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
  3867	      "license": "MIT",
  3868	      "dependencies": {
  3869	        "@types/mdast": "^4.0.0",
  3870	        "mdast-util-gfm": "^3.0.0",
  3871	        "micromark-extension-gfm": "^3.0.0",
  3872	        "remark-parse": "^11.0.0",
  3873	        "remark-stringify": "^11.0.0",
  3874	        "unified": "^11.0.0"
  3875	      },
  3876	      "funding": {
  3877	        "type": "opencollective",
  3878	        "url": "https://opencollective.com/unified"
  3879	      }
  3880	    },
  3881	    "node_modules/remark-parse": {
  3882	      "version": "11.0.0",
  3883	      "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
  3884	      "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
  3885	      "license": "MIT",
  3886	      "dependencies": {
  3887	        "@types/mdast": "^4.0.0",
  3888	        "mdast-util-from-markdown": "^2.0.0",
  3889	        "micromark-util-types": "^2.0.0",
  3890	        "unified": "^11.0.0"
  3891	      },
  3892	      "funding": {
  3893	        "type": "opencollective",
  3894	        "url": "https://opencollective.com/unified"
  3895	      }
  3896	    },
  3897	    "node_modules/remark-rehype": {
  3898	      "version": "11.1.2",
  3899	      "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
  3900	      "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
  3901	      "license": "MIT",
  3902	      "dependencies": {
  3903	        "@types/hast": "^3.0.0",
  3904	        "@types/mdast": "^4.0.0",
  3905	        "mdast-util-to-hast": "^13.0.0",
  3906	        "unified": "^11.0.0",
  3907	        "vfile": "^6.0.0"
  3908	      },
  3909	      "funding": {
  3910	        "type": "opencollective",
  3911	        "url": "https://opencollective.com/unified"
  3912	      }
  3913	    },
  3914	    "node_modules/remark-stringify": {
  3915	      "version": "11.0.0",
  3916	      "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
  3917	      "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
  3918	      "license": "MIT",
  3919	      "dependencies": {
  3920	        "@types/mdast": "^4.0.0",
  3921	        "mdast-util-to-markdown": "^2.0.0",
  3922	        "unified": "^11.0.0"
  3923	      },
  3924	      "funding": {
  3925	        "type": "opencollective",
  3926	        "url": "https://opencollective.com/unified"
  3927	      }
  3928	    },
  3929	    "node_modules/resolve": {
  3930	      "version": "1.22.11",
  3931	      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
  3932	      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
  3933	      "dev": true,
  3934	      "license": "MIT",
  3935	      "dependencies": {
  3936	        "is-core-module": "^2.16.1",
  3937	        "path-parse": "^1.0.7",
  3938	        "supports-preserve-symlinks-flag": "^1.0.0"
  3939	      },
  3940	      "bin": {
  3941	        "resolve": "bin/resolve"
  3942	      },
  3943	      "engines": {
  3944	        "node": ">= 0.4"
  3945	      },
  3946	      "funding": {
  3947	        "url": "https://github.com/sponsors/ljharb"
  3948	      }
  3949	    },
  3950	    "node_modules/reusify": {
  3951	      "version": "1.1.0",
  3952	      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
  3953	      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
  3954	      "dev": true,
  3955	      "license": "MIT",
  3956	      "engines": {
  3957	        "iojs": ">=1.0.0",
  3958	        "node": ">=0.10.0"
  3959	      }
  3960	    },
  3961	    "node_modules/rollup": {
  3962	      "version": "4.59.0",
  3963	      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
  3964	      "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
  3965	      "dev": true,
  3966	      "license": "MIT",
  3967	      "dependencies": {
  3968	        "@types/estree": "1.0.8"
  3969	      },
  3970	      "bin": {
  3971	        "rollup": "dist/bin/rollup"
  3972	      },
  3973	      "engines": {
  3974	        "node": ">=18.0.0",
  3975	        "npm": ">=8.0.0"
  3976	      },
  3977	      "optionalDependencies": {
  3978	        "@rollup/rollup-android-arm-eabi": "4.59.0",
  3979	        "@rollup/rollup-android-arm64": "4.59.0",
  3980	        "@rollup/rollup-darwin-arm64": "4.59.0",
  3981	        "@rollup/rollup-darwin-x64": "4.59.0",
  3982	        "@rollup/rollup-freebsd-arm64": "4.59.0",
  3983	        "@rollup/rollup-freebsd-x64": "4.59.0",
  3984	        "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
  3985	        "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
  3986	        "@rollup/rollup-linux-arm64-gnu": "4.59.0",
  3987	        "@rollup/rollup-linux-arm64-musl": "4.59.0",
  3988	        "@rollup/rollup-linux-loong64-gnu": "4.59.0",
  3989	        "@rollup/rollup-linux-loong64-musl": "4.59.0",
  3990	        "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
  3991	        "@rollup/rollup-linux-ppc64-musl": "4.59.0",
  3992	        "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
  3993	        "@rollup/rollup-linux-riscv64-musl": "4.59.0",
  3994	        "@rollup/rollup-linux-s390x-gnu": "4.59.0",
  3995	        "@rollup/rollup-linux-x64-gnu": "4.59.0",
  3996	        "@rollup/rollup-linux-x64-musl": "4.59.0",
  3997	        "@rollup/rollup-openbsd-x64": "4.59.0",
  3998	        "@rollup/rollup-openharmony-arm64": "4.59.0",
  3999	        "@rollup/rollup-win32-arm64-msvc": "4.59.0",
  4000	        "@rollup/rollup-win32-ia32-msvc": "4.59.0",
  4001	        "@rollup/rollup-win32-x64-gnu": "4.59.0",
  4002	        "@rollup/rollup-win32-x64-msvc": "4.59.0",
  4003	        "fsevents": "~2.3.2"
  4004	      }
  4005	    },
  4006	    "node_modules/run-parallel": {
  4007	      "version": "1.2.0",
  4008	      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
  4009	      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
  4010	      "dev": true,
  4011	      "funding": [
  4012	        {
  4013	          "type": "github",
  4014	          "url": "https://github.com/sponsors/feross"
  4015	        },
  4016	        {
  4017	          "type": "patreon",
  4018	          "url": "https://www.patreon.com/feross"
  4019	        },
  4020	        {
  4021	          "type": "consulting",
  4022	          "url": "https://feross.org/support"
  4023	        }
  4024	      ],
  4025	      "license": "MIT",
  4026	      "dependencies": {
  4027	        "queue-microtask": "^1.2.2"
  4028	      }
  4029	    },
  4030	    "node_modules/scheduler": {
  4031	      "version": "0.23.2",
  4032	      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
  4033	      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
  4034	      "license": "MIT",
  4035	      "dependencies": {
  4036	        "loose-envify": "^1.1.0"
  4037	      }
  4038	    },
  4039	    "node_modules/semver": {
  4040	      "version": "6.3.1",
  4041	      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
  4042	      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
  4043	      "dev": true,
  4044	      "license": "ISC",
  4045	      "bin": {
  4046	        "semver": "bin/semver.js"
  4047	      }
  4048	    },
  4049	    "node_modules/set-cookie-parser": {
  4050	      "version": "2.7.2",
  4051	      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
  4052	      "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
  4053	      "license": "MIT"
  4054	    },
  4055	    "node_modules/source-map-js": {
  4056	      "version": "1.2.1",
  4057	      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
  4058	      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
  4059	      "dev": true,
  4060	      "license": "BSD-3-Clause",
  4061	      "engines": {
  4062	        "node": ">=0.10.0"
  4063	      }
  4064	    },
  4065	    "node_modules/space-separated-tokens": {
  4066	      "version": "2.0.2",
  4067	      "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
  4068	      "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
  4069	      "license": "MIT",
  4070	      "funding": {
  4071	        "type": "github",
  4072	        "url": "https://github.com/sponsors/wooorm"
  4073	      }
  4074	    },
  4075	    "node_modules/stringify-entities": {
  4076	      "version": "4.0.4",
  4077	      "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
  4078	      "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
  4079	      "license": "MIT",
  4080	      "dependencies": {
  4081	        "character-entities-html4": "^2.0.0",
  4082	        "character-entities-legacy": "^3.0.0"
  4083	      },
  4084	      "funding": {
  4085	        "type": "github",
  4086	        "url": "https://github.com/sponsors/wooorm"
  4087	      }
  4088	    },
  4089	    "node_modules/style-to-js": {
  4090	      "version": "1.1.21",
  4091	      "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
  4092	      "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
  4093	      "license": "MIT",
  4094	      "dependencies": {
  4095	        "style-to-object": "1.0.14"
  4096	      }
  4097	    },
  4098	    "node_modules/style-to-object": {
  4099	      "version": "1.0.14",
  4100	      "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
  4101	      "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
  4102	      "license": "MIT",
  4103	      "dependencies": {
  4104	        "inline-style-parser": "0.2.7"
  4105	      }
  4106	    },
  4107	    "node_modules/sucrase": {
  4108	      "version": "3.35.1",
  4109	      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
  4110	      "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
  4111	      "dev": true,
  4112	      "license": "MIT",
  4113	      "dependencies": {
  4114	        "@jridgewell/gen-mapping": "^0.3.2",
  4115	        "commander": "^4.0.0",
  4116	        "lines-and-columns": "^1.1.6",
  4117	        "mz": "^2.7.0",
  4118	        "pirates": "^4.0.1",
  4119	        "tinyglobby": "^0.2.11",
  4120	        "ts-interface-checker": "^0.1.9"
  4121	      },
  4122	      "bin": {
  4123	        "sucrase": "bin/sucrase",
  4124	        "sucrase-node": "bin/sucrase-node"
  4125	      },
  4126	      "engines": {
  4127	        "node": ">=16 || 14 >=14.17"
  4128	      }
  4129	    },
  4130	    "node_modules/supports-preserve-symlinks-flag": {
  4131	      "version": "1.0.0",
  4132	      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
  4133	      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
  4134	      "dev": true,
  4135	      "license": "MIT",
  4136	      "engines": {
  4137	        "node": ">= 0.4"
  4138	      },
  4139	      "funding": {
  4140	        "url": "https://github.com/sponsors/ljharb"
  4141	      }
  4142	    },
  4143	    "node_modules/tailwindcss": {
  4144	      "version": "3.4.19",
  4145	      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
  4146	      "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
  4147	      "dev": true,
  4148	      "license": "MIT",
  4149	      "dependencies": {
  4150	        "@alloc/quick-lru": "^5.2.0",
  4151	        "arg": "^5.0.2",
  4152	        "chokidar": "^3.6.0",
  4153	        "didyoumean": "^1.2.2",
  4154	        "dlv": "^1.1.3",
  4155	        "fast-glob": "^3.3.2",
  4156	        "glob-parent": "^6.0.2",
  4157	        "is-glob": "^4.0.3",
  4158	        "jiti": "^1.21.7",
  4159	        "lilconfig": "^3.1.3",
  4160	        "micromatch": "^4.0.8",
  4161	        "normalize-path": "^3.0.0",
  4162	        "object-hash": "^3.0.0",
  4163	        "picocolors": "^1.1.1",
  4164	        "postcss": "^8.4.47",
  4165	        "postcss-import": "^15.1.0",
  4166	        "postcss-js": "^4.0.1",
  4167	        "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
  4168	        "postcss-nested": "^6.2.0",
  4169	        "postcss-selector-parser": "^6.1.2",
  4170	        "resolve": "^1.22.8",
  4171	        "sucrase": "^3.35.0"
  4172	      },
  4173	      "bin": {
  4174	        "tailwind": "lib/cli.js",
  4175	        "tailwindcss": "lib/cli.js"
  4176	      },
  4177	      "engines": {
  4178	        "node": ">=14.0.0"
  4179	      }
  4180	    },
  4181	    "node_modules/thenify": {
  4182	      "version": "3.3.1",
  4183	      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
  4184	      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
  4185	      "dev": true,
  4186	      "license": "MIT",
  4187	      "dependencies": {
  4188	        "any-promise": "^1.0.0"
  4189	      }
  4190	    },
  4191	    "node_modules/thenify-all": {
  4192	      "version": "1.6.0",
  4193	      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
  4194	      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
  4195	      "dev": true,
  4196	      "license": "MIT",
  4197	      "dependencies": {
  4198	        "thenify": ">= 3.1.0 < 4"
  4199	      },
  4200	      "engines": {
  4201	        "node": ">=0.8"
  4202	      }
  4203	    },
  4204	    "node_modules/tinyglobby": {
  4205	      "version": "0.2.15",
  4206	      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
  4207	      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
  4208	      "dev": true,
  4209	      "license": "MIT",
  4210	      "dependencies": {
  4211	        "fdir": "^6.5.0",
  4212	        "picomatch": "^4.0.3"
  4213	      },
  4214	      "engines": {
  4215	        "node": ">=12.0.0"
  4216	      },
  4217	      "funding": {
  4218	        "url": "https://github.com/sponsors/SuperchupuDev"
  4219	      }
  4220	    },
  4221	    "node_modules/tinyglobby/node_modules/picomatch": {
  4222	      "version": "4.0.3",
  4223	      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
  4224	      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
  4225	      "dev": true,
  4226	      "license": "MIT",
  4227	      "engines": {
  4228	        "node": ">=12"
  4229	      },
  4230	      "funding": {
  4231	        "url": "https://github.com/sponsors/jonschlinkert"
  4232	      }
  4233	    },
  4234	    "node_modules/to-regex-range": {
  4235	      "version": "5.0.1",
  4236	      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
  4237	      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
  4238	      "dev": true,
  4239	      "license": "MIT",
  4240	      "dependencies": {
  4241	        "is-number": "^7.0.0"
  4242	      },
  4243	      "engines": {
  4244	        "node": ">=8.0"
  4245	      }
  4246	    },
  4247	    "node_modules/trim-lines": {
  4248	      "version": "3.0.1",
  4249	      "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
  4250	      "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
  4251	      "license": "MIT",
  4252	      "funding": {
  4253	        "type": "github",
  4254	        "url": "https://github.com/sponsors/wooorm"
  4255	      }
  4256	    },
  4257	    "node_modules/trough": {
  4258	      "version": "2.2.0",
  4259	      "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
  4260	      "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
  4261	      "license": "MIT",
  4262	      "funding": {
  4263	        "type": "github",
  4264	        "url": "https://github.com/sponsors/wooorm"
  4265	      }
  4266	    },
  4267	    "node_modules/ts-interface-checker": {
  4268	      "version": "0.1.13",
  4269	      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
  4270	      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
  4271	      "dev": true,
  4272	      "license": "Apache-2.0"
  4273	    },
  4274	    "node_modules/unified": {
  4275	      "version": "11.0.5",
  4276	      "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
  4277	      "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
  4278	      "license": "MIT",
  4279	      "dependencies": {
  4280	        "@types/unist": "^3.0.0",
  4281	        "bail": "^2.0.0",
  4282	        "devlop": "^1.0.0",
  4283	        "extend": "^3.0.0",
  4284	        "is-plain-obj": "^4.0.0",
  4285	        "trough": "^2.0.0",
  4286	        "vfile": "^6.0.0"
  4287	      },
  4288	      "funding": {
  4289	        "type": "opencollective",
  4290	        "url": "https://opencollective.com/unified"
  4291	      }
  4292	    },
  4293	    "node_modules/unist-util-is": {
  4294	      "version": "6.0.1",
  4295	      "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
  4296	      "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
  4297	      "license": "MIT",
  4298	      "dependencies": {
  4299	        "@types/unist": "^3.0.0"
  4300	      },
  4301	      "funding": {
  4302	        "type": "opencollective",
  4303	        "url": "https://opencollective.com/unified"
  4304	      }
  4305	    },
  4306	    "node_modules/unist-util-position": {
  4307	      "version": "5.0.0",
  4308	      "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
  4309	      "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
  4310	      "license": "MIT",
  4311	      "dependencies": {
  4312	        "@types/unist": "^3.0.0"
  4313	      },
  4314	      "funding": {
  4315	        "type": "opencollective",
  4316	        "url": "https://opencollective.com/unified"
  4317	      }
  4318	    },
  4319	    "node_modules/unist-util-stringify-position": {
  4320	      "version": "4.0.0",
  4321	      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
  4322	      "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
  4323	      "license": "MIT",
  4324	      "dependencies": {
  4325	        "@types/unist": "^3.0.0"
  4326	      },
  4327	      "funding": {
  4328	        "type": "opencollective",
  4329	        "url": "https://opencollective.com/unified"
  4330	      }
  4331	    },
  4332	    "node_modules/unist-util-visit": {
  4333	      "version": "5.1.0",
  4334	      "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
  4335	      "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
  4336	      "license": "MIT",
  4337	      "dependencies": {
  4338	        "@types/unist": "^3.0.0",
  4339	        "unist-util-is": "^6.0.0",
  4340	        "unist-util-visit-parents": "^6.0.0"
  4341	      },
  4342	      "funding": {
  4343	        "type": "opencollective",
  4344	        "url": "https://opencollective.com/unified"
  4345	      }
  4346	    },
  4347	    "node_modules/unist-util-visit-parents": {
  4348	      "version": "6.0.2",
  4349	      "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
  4350	      "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
  4351	      "license": "MIT",
  4352	      "dependencies": {
  4353	        "@types/unist": "^3.0.0",
  4354	        "unist-util-is": "^6.0.0"
  4355	      },
  4356	      "funding": {
  4357	        "type": "opencollective",
  4358	        "url": "https://opencollective.com/unified"
  4359	      }
  4360	    },
  4361	    "node_modules/update-browserslist-db": {
  4362	      "version": "1.2.3",
  4363	      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
  4364	      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
  4365	      "dev": true,
  4366	      "funding": [
  4367	        {
  4368	          "type": "opencollective",
  4369	          "url": "https://opencollective.com/browserslist"
  4370	        },
  4371	        {
  4372	          "type": "tidelift",
  4373	          "url": "https://tidelift.com/funding/github/npm/browserslist"
  4374	        },
  4375	        {
  4376	          "type": "github",
  4377	          "url": "https://github.com/sponsors/ai"
  4378	        }
  4379	      ],
  4380	      "license": "MIT",
  4381	      "dependencies": {
  4382	        "escalade": "^3.2.0",
  4383	        "picocolors": "^1.1.1"
  4384	      },
  4385	      "bin": {
  4386	        "update-browserslist-db": "cli.js"
  4387	      },
  4388	      "peerDependencies": {
  4389	        "browserslist": ">= 4.21.0"
  4390	      }
  4391	    },
  4392	    "node_modules/util-deprecate": {
  4393	      "version": "1.0.2",
  4394	      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
  4395	      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
  4396	      "dev": true,
  4397	      "license": "MIT"
  4398	    },
  4399	    "node_modules/vfile": {
  4400	      "version": "6.0.3",
  4401	      "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
  4402	      "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
  4403	      "license": "MIT",
  4404	      "dependencies": {
  4405	        "@types/unist": "^3.0.0",
  4406	        "vfile-message": "^4.0.0"
  4407	      },
  4408	      "funding": {
  4409	        "type": "opencollective",
  4410	        "url": "https://opencollective.com/unified"
  4411	      }
  4412	    },
  4413	    "node_modules/vfile-message": {
  4414	      "version": "4.0.3",
  4415	      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
  4416	      "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
  4417	      "license": "MIT",
  4418	      "dependencies": {
  4419	        "@types/unist": "^3.0.0",
  4420	        "unist-util-stringify-position": "^4.0.0"
  4421	      },
  4422	      "funding": {
  4423	        "type": "opencollective",
  4424	        "url": "https://opencollective.com/unified"
  4425	      }
  4426	    },
  4427	    "node_modules/vite": {
  4428	      "version": "6.4.1",
  4429	      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
  4430	      "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
  4431	      "dev": true,
  4432	      "license": "MIT",
  4433	      "dependencies": {
  4434	        "esbuild": "^0.25.0",
  4435	        "fdir": "^6.4.4",
  4436	        "picomatch": "^4.0.2",
  4437	        "postcss": "^8.5.3",
  4438	        "rollup": "^4.34.9",
  4439	        "tinyglobby": "^0.2.13"
  4440	      },
  4441	      "bin": {
  4442	        "vite": "bin/vite.js"
  4443	      },
  4444	      "engines": {
  4445	        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
  4446	      },
  4447	      "funding": {
  4448	        "url": "https://github.com/vitejs/vite?sponsor=1"
  4449	      },
  4450	      "optionalDependencies": {
  4451	        "fsevents": "~2.3.3"
  4452	      },
  4453	      "peerDependencies": {
  4454	        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
  4455	        "jiti": ">=1.21.0",
  4456	        "less": "*",
  4457	        "lightningcss": "^1.21.0",
  4458	        "sass": "*",
  4459	        "sass-embedded": "*",
  4460	        "stylus": "*",
  4461	        "sugarss": "*",
  4462	        "terser": "^5.16.0",
  4463	        "tsx": "^4.8.1",
  4464	        "yaml": "^2.4.2"
  4465	      },
  4466	      "peerDependenciesMeta": {
  4467	        "@types/node": {
  4468	          "optional": true
  4469	        },
  4470	        "jiti": {
  4471	          "optional": true
  4472	        },
  4473	        "less": {
  4474	          "optional": true
  4475	        },
  4476	        "lightningcss": {
  4477	          "optional": true
  4478	        },
  4479	        "sass": {
  4480	          "optional": true
  4481	        },
  4482	        "sass-embedded": {
  4483	          "optional": true
  4484	        },
  4485	        "stylus": {
  4486	          "optional": true
  4487	        },
  4488	        "sugarss": {
  4489	          "optional": true
  4490	        },
  4491	        "terser": {
  4492	          "optional": true
  4493	        },
  4494	        "tsx": {
  4495	          "optional": true
  4496	        },
  4497	        "yaml": {
  4498	          "optional": true
  4499	        }
  4500	      }
  4501	    },
  4502	    "node_modules/vite/node_modules/picomatch": {
  4503	      "version": "4.0.3",
  4504	      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
  4505	      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
  4506	      "dev": true,
  4507	      "license": "MIT",
  4508	      "engines": {
  4509	        "node": ">=12"
  4510	      },
  4511	      "funding": {
  4512	        "url": "https://github.com/sponsors/jonschlinkert"
  4513	      }
  4514	    },
  4515	    "node_modules/xtend": {
  4516	      "version": "4.0.2",
  4517	      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
  4518	      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
  4519	      "license": "MIT",
  4520	      "engines": {
  4521	        "node": ">=0.4"
  4522	      }
  4523	    },
  4524	    "node_modules/yallist": {
  4525	      "version": "3.1.1",
  4526	      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
  4527	      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
  4528	      "dev": true,
  4529	      "license": "ISC"
  4530	    },
  4531	    "node_modules/zwitch": {
  4532	      "version": "2.0.4",
  4533	      "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
  4534	      "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
  4535	      "license": "MIT",
  4536	      "funding": {
  4537	        "type": "github",
  4538	        "url": "https://github.com/sponsors/wooorm"
  4539	      }
  4540	    }
  4541	  }
  4542	}
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [036]: frontend/package-lock.json


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [037/60]: frontend/package.json
│ LANGUAGE: json | LINES: 27 | SIZE: 646 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	{
     2	  "name": "son-of-anton-frontend",
     3	  "version": "1.0.0",
     4	  "private": true,
     5	  "type": "module",
     6	  "scripts": {
     7	    "dev": "vite",
     8	    "build": "vite build",
     9	    "preview": "vite preview"
    10	  },
    11	  "dependencies": {
    12	    "lucide-react": "^0.469.0",
    13	    "react": "^18.3.1",
    14	    "react-dom": "^18.3.1",
    15	    "react-markdown": "^9.0.1",
    16	    "react-router-dom": "^7.1.1",
    17	    "react-syntax-highlighter": "^15.6.1",
    18	    "remark-gfm": "^4.0.0"
    19	  },
    20	  "devDependencies": {
    21	    "@types/react": "^18.3.18",
    22	    "@vitejs/plugin-react": "^4.3.4",
    23	    "autoprefixer": "^10.4.20",
    24	    "postcss": "^8.4.49",
    25	    "tailwindcss": "^3.4.17",
    26	    "vite": "^6.0.7"
    27	  }
    28	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [037]: frontend/package.json


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [038/60]: frontend/postcss.config.js
│ LANGUAGE: javascript | LINES: 5 | SIZE: 80 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	export default {
     2	  plugins: {
     3	    tailwindcss: {},
     4	    autoprefixer: {},
     5	  },
     6	};│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [038]: frontend/postcss.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [039/60]: frontend/src/App.jsx
│ LANGUAGE: jsx | LINES: 58 | SIZE: 1961 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useEffect, useState } from "react";
     2	import { Routes, Route } from "react-router-dom";
     3	import { useApp } from "./store";
     4	import { getMe } from "./api";
     5	import * as streamManager from "./streamManager";
     6	import LoginPage from "./pages/LoginPage";
     7	import ChatPage from "./pages/ChatPage";
     8	import AdminPage from "./pages/AdminPage";
     9	import KnowledgePage from "./pages/KnowledgePage";
    10	import GitLabPage from "./pages/GitLabPage";
    11	import { Flame } from "lucide-react";
    12	
    13	export default function App() {
    14	  const { state, dispatch } = useApp();
    15	  const [authChecked, setAuthChecked] = useState(!state.token);
    16	
    17	  useEffect(() => {
    18	    streamManager.setDispatch(dispatch);
    19	  }, [dispatch]);
    20	
    21	  useEffect(() => {
    22	    if (!state.token) { setAuthChecked(true); return; }
    23	    if (state.user) { setAuthChecked(true); return; }
    24	    (async () => {
    25	      try {
    26	        const user = await getMe(state.token);
    27	        dispatch({ type: "SET_USER", user });
    28	      } catch {
    29	        dispatch({ type: "LOGOUT" });
    30	      } finally {
    31	        setAuthChecked(true);
    32	      }
    33	    })();
    34	  }, [state.token, state.user, dispatch]);
    35	
    36	  if (!authChecked) {
    37	    return (
    38	      <div className="h-dvh flex items-center justify-center bg-anton-bg">
    39	        <div className="flex flex-col items-center gap-4 animate-fade-in">
    40	          <div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
    41	            <Flame size={32} className="text-white animate-pulse" />
    42	          </div>
    43	          <p className="text-anton-muted text-sm">Loading...</p>
    44	        </div>
    45	      </div>
    46	    );
    47	  }
    48	
    49	  if (!state.token) return <LoginPage />;
    50	
    51	  return (
    52	    <Routes>
    53	      <Route path="/admin" element={<AdminPage />} />
    54	      <Route path="/knowledge" element={<KnowledgePage />} />
    55	      <Route path="/gitlab" element={<GitLabPage />} />
    56	      <Route path="/*" element={<ChatPage />} />
    57	    </Routes>
    58	  );
    59	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [039]: frontend/src/App.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [040/60]: frontend/src/api.js
│ LANGUAGE: javascript | LINES: 194 | SIZE: 11858 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	const BASE = "/api";
     2	
     3	function headers(token) {
     4	  const h = { "Content-Type": "application/json" };
     5	  if (token) h["Authorization"] = `Bearer ${token}`;
     6	  return h;
     7	}
     8	
     9	function authHeader(token) {
    10	  return token ? { Authorization: `Bearer ${token}` } : {};
    11	}
    12	
    13	async function request(method, path, token, body) {
    14	  const opts = { method, headers: headers(token) };
    15	  if (body) opts.body = JSON.stringify(body);
    16	  const res = await fetch(`${BASE}${path}`, opts);
    17	  if (!res.ok) {
    18	    const err = await res.json().catch(() => ({ detail: res.statusText }));
    19	    throw new Error(err.detail || err.message || "Request failed");
    20	  }
    21	  return res.json();
    22	}
    23	
    24	// ═══════════════════════════════════════════════════
    25	//  Auth
    26	// ═══════════════════════════════════════════════════
    27	export const login = (username, password) =>
    28	  request("POST", "/auth/login", null, { username, password });
    29	export const register = (username, email, password) =>
    30	  request("POST", "/auth/register", null, { username, email, password });
    31	export const getMe = (token) => request("GET", "/auth/me", token);
    32	
    33	// ═══════════════════════════════════════════════════
    34	//  Chats
    35	// ═══════════════════════════════════════════════════
    36	export const listChats = (token) => request("GET", "/chats", token);
    37	export const createChat = (token, data = {}) => request("POST", "/chats", token, data);
    38	export const updateChat = (token, chatId, data) => request("PUT", `/chats/${chatId}`, token, data);
    39	export const renameChat = (token, chatId, title) => updateChat(token, chatId, { title });
    40	export const deleteChat = (token, chatId) => request("DELETE", `/chats/${chatId}`, token);
    41	export const getMessages = (token, chatId) => request("GET", `/chats/${chatId}/messages`, token);
    42	export const checkGenerating = (token, chatId) => request("GET", `/chats/${chatId}/generating`, token);
    43	
    44	// ═══════════════════════════════════════════════════
    45	//  Streaming
    46	// ═══════════════════════════════════════════════════
    47	export async function* streamMessage(token, chatId, body, signal) {
    48	  const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
    49	    method: "POST", headers: headers(token), body: JSON.stringify(body), signal,
    50	  });
    51	  if (!res.ok) {
    52	    const err = await res.json().catch(() => ({ detail: res.statusText }));
    53	    throw new Error(err.detail || "Stream failed");
    54	  }
    55	  const reader = res.body.getReader();
    56	  const decoder = new TextDecoder();
    57	  let buffer = "";
    58	  while (true) {
    59	    const { done, value } = await reader.read();
    60	    if (done) break;
    61	    buffer += decoder.decode(value, { stream: true });
    62	    const parts = buffer.split("\n\n");
    63	    buffer = parts.pop() || "";
    64	    for (const part of parts) {
    65	      const line = part.trim();
    66	      if (line.startsWith("data: ")) { try { yield JSON.parse(line.slice(6)); } catch { } }
    67	    }
    68	  }
    69	  if (buffer.trim().startsWith("data: ")) { try { yield JSON.parse(buffer.trim().slice(6)); } catch { } }
    70	}
    71	
    72	// ═══════════════════════════════════════════════════
    73	//  Chat Attachments
    74	// ═══════════════════════════════════════════════════
    75	export async function uploadAttachments(token, chatId, files) {
    76	  const form = new FormData();
    77	  for (const file of files) form.append("files", file);
    78	  const res = await fetch(`${BASE}/chats/${chatId}/attachments`, { method: "POST", headers: authHeader(token), body: form });
    79	  if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Upload failed"); }
    80	  return res.json();
    81	}
    82	export function getAttachmentUrl(attachmentId) { return `${BASE}/attachments/${attachmentId}/file`; }
    83	export const deleteAttachment = (token, attachmentId) => request("DELETE", `/attachments/${attachmentId}`, token);
    84	
    85	// ═══════════════════════════════════════════════════
    86	//  Knowledge Bases
    87	// ═══════════════════════════════════════════════════
    88	export const listKnowledgeBases = (token) => request("GET", "/knowledge", token);
    89	export const createKnowledgeBase = (token, name, description = "") => request("POST", "/knowledge", token, { name, description });
    90	export const getKnowledgeBase = (token, kbId) => request("GET", `/knowledge/${kbId}`, token);
    91	export const updateKnowledgeBase = (token, kbId, data) => request("PUT", `/knowledge/${kbId}`, token, data);
    92	export const deleteKnowledgeBase = (token, kbId) => request("DELETE", `/knowledge/${kbId}`, token);
    93	export const listKnowledgeDocuments = (token, kbId) => request("GET", `/knowledge/${kbId}/documents`, token);
    94	export const deleteKnowledgeDocument = (token, kbId, docId) => request("DELETE", `/knowledge/${kbId}/documents/${docId}`, token);
    95	export async function uploadDocuments(token, kbId, files) {
    96	  const form = new FormData();
    97	  for (const file of files) form.append("files", file);
    98	  const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, { method: "POST", headers: authHeader(token), body: form });
    99	  if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Upload failed"); }
   100	  return res.json();
   101	}
   102	export const uploadDocument = (token, kbId, file) => uploadDocuments(token, kbId, [file]);
   103	
   104	// ═══════════════════════════════════════════════════
   105	//  Admin
   106	// ═══════════════════════════════════════════════════
   107	export const adminStats = (token) => request("GET", "/admin/stats", token);
   108	export const adminListUsers = (token) => request("GET", "/admin/users", token);
   109	export const adminCreateUser = (token, data) => request("POST", "/admin/users", token, data);
   110	export const adminUpdateUser = (token, userId, data) => request("PUT", `/admin/users/${userId}`, token, data);
   111	export const adminDeleteUser = (token, userId) => request("DELETE", `/admin/users/${userId}`, token);
   112	export const adminListChats = (token) => request("GET", "/admin/chats", token);
   113	
   114	// ═══════════════════════════════════════════════════
   115	//  Code Download
   116	// ═══════════════════════════════════════════════════
   117	export async function downloadZip(token, markdown, chatTitle) {
   118	  const res = await fetch(`${BASE}/files/download-zip`, {
   119	    method: "POST", headers: headers(token), body: JSON.stringify({ markdown, title: chatTitle || null }),
   120	  });
   121	  if (!res.ok) throw new Error("Download failed");
   122	  const ct = res.headers.get("content-type") || "";
   123	  if (ct.includes("application/zip")) {
   124	    const blob = await res.blob();
   125	    const url = URL.createObjectURL(blob);
   126	    const a = document.createElement("a");
   127	    a.href = url;
   128	    const raw = (chatTitle || "").trim();
   129	    const safeName = raw && raw !== "New Chat" ? raw.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "code" : "code";
   130	    a.download = `${safeName}.zip`;
   131	    a.click();
   132	    URL.revokeObjectURL(url);
   133	  } else {
   134	    const data = await res.json();
   135	    if (data.error) throw new Error(data.error);
   136	  }
   137	}
   138	
   139	// ═══════════════════════════════════════════════════
   140	//  🔥 GitLab CE Integration (Superadmin Only)
   141	// ═══════════════════════════════════════════════════
   142	
   143	// Config
   144	export const gitlabGetConfig = (token) => request("GET", "/gitlab/config", token);
   145	export const gitlabSaveConfig = (token, data) => request("POST", "/gitlab/config", token, data);
   146	export const gitlabTestConnection = (token, data) => request("POST", "/gitlab/test-connection", token, data);
   147	
   148	// Projects
   149	export const gitlabListProjects = (token, search = "", page = 1) =>
   150	  request("GET", `/gitlab/projects?search=${encodeURIComponent(search)}&page=${page}`, token);
   151	export const gitlabGetProject = (token, projectId) => request("GET", `/gitlab/projects/${projectId}`, token);
   152	export const gitlabCreateProject = (token, data) => request("POST", "/gitlab/projects", token, data);
   153	export const gitlabDeleteProject = (token, projectId) => request("DELETE", `/gitlab/projects/${projectId}`, token);
   154	
   155	// Branches
   156	export const gitlabListBranches = (token, projectId) => request("GET", `/gitlab/projects/${projectId}/branches`, token);
   157	export const gitlabCreateBranch = (token, projectId, data) => request("POST", `/gitlab/projects/${projectId}/branches`, token, data);
   158	
   159	// Files
   160	export const gitlabGetTree = (token, projectId, path = "", ref = "main", recursive = false) =>
   161	  request("GET", `/gitlab/projects/${projectId}/tree?path=${encodeURIComponent(path)}&ref=${ref}&recursive=${recursive}`, token);
   162	export const gitlabGetFile = (token, projectId, filePath, ref = "main") =>
   163	  request("GET", `/gitlab/projects/${projectId}/files?file_path=${encodeURIComponent(filePath)}&ref=${ref}`, token);
   164	
   165	// Merge Requests
   166	export const gitlabListMRs = (token, projectId, state = "opened") =>
   167	  request("GET", `/gitlab/projects/${projectId}/merge-requests?state=${state}`, token);
   168	export const gitlabCreateMR = (token, projectId, data) =>
   169	  request("POST", `/gitlab/projects/${projectId}/merge-requests`, token, data);
   170	
   171	// Pipelines
   172	export const gitlabListPipelines = (token, projectId) =>
   173	  request("GET", `/gitlab/projects/${projectId}/pipelines`, token);
   174	export const gitlabGetPipeline = (token, projectId, pipelineId) =>
   175	  request("GET", `/gitlab/projects/${projectId}/pipelines/${pipelineId}`, token);
   176	export const gitlabTriggerPipeline = (token, projectId, ref = "main") =>
   177	  request("POST", `/gitlab/projects/${projectId}/pipelines/trigger?ref=${ref}`, token);
   178	
   179	// Operations Queue
   180	export const gitlabQueueOperation = (token, data) => request("POST", "/gitlab/operations", token, data);
   181	export const gitlabListOperations = (token, status = "pending") =>
   182	  request("GET", `/gitlab/operations?status=${status}`, token);
   183	export const gitlabApproveOperation = (token, opId) => request("POST", `/gitlab/operations/${opId}/approve`, token);
   184	export const gitlabRejectOperation = (token, opId) => request("POST", `/gitlab/operations/${opId}/reject`, token);
   185	export const gitlabDeleteOperation = (token, opId) => request("DELETE", `/gitlab/operations/${opId}`, token);
   186	
   187	// Direct Execute (bypass queue)
   188	export const gitlabDirectCommit = (token, data) => request("POST", "/gitlab/execute/commit", token, data);
   189	export const gitlabDirectFileOp = (token, data) => request("POST", "/gitlab/execute/file", token, data);
   190	
   191	// Audit
   192	export const gitlabAuditLog = (token, page = 1) => request("GET", `/gitlab/audit-log?page=${page}`, token);
   193	
   194	// Namespaces
   195	export const gitlabListNamespaces = (token) => request("GET", "/gitlab/namespaces", token);│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [040]: frontend/src/api.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [041/60]: frontend/src/components/AttachmentPreview.jsx
│ LANGUAGE: jsx | LINES: 158 | SIZE: 4375 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useMemo } from "react";
     2	import {
     3	  X,
     4	  FileText,
     5	  Image as ImageIcon,
     6	  Film,
     7	  File,
     8	  FileSpreadsheet,
     9	} from "lucide-react";
    10	
    11	/**
    12	 * Renders a preview chip for an attached file.
    13	 * Used both in the pending-files area (before send) and in message bubbles (after send).
    14	 *
    15	 * Props:
    16	 *   file?: File object (for pending uploads — generates preview from File API)
    17	 *   attachment?: { filename, mime_type, file_size, media_type, preview_url } (for sent messages)
    18	 *   onRemove?: () => void (if removable)
    19	 *   isPending?: boolean (styling difference)
    20	 */
    21	export default function AttachmentPreview({
    22	  file,
    23	  attachment,
    24	  onRemove,
    25	  isPending,
    26	}) {
    27	  const info = useMemo(() => {
    28	    if (file) {
    29	      return {
    30	        name: file.name,
    31	        size: file.size,
    32	        type: file.type,
    33	        mediaType: classifyMime(file.type),
    34	        previewUrl: file.type.startsWith("image/")
    35	          ? URL.createObjectURL(file)
    36	          : null,
    37	      };
    38	    }
    39	    if (attachment) {
    40	      return {
    41	        name: attachment.filename || attachment.original_filename || "file",
    42	        size: attachment.file_size,
    43	        type: attachment.mime_type,
    44	        mediaType: attachment.media_type,
    45	        previewUrl: attachment.preview_url || null,
    46	      };
    47	    }
    48	    return { name: "unknown", size: 0, type: "", mediaType: "unknown", previewUrl: null };
    49	  }, [file, attachment]);
    50	
    51	  const Icon = getIcon(info.mediaType);
    52	  const sizeStr = formatSize(info.size);
    53	
    54	  return (
    55	    <div
    56	      className={`
    57	      group relative flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs
    58	      ${
    59	        isPending
    60	          ? "bg-anton-card border-anton-accent/30 text-anton-text"
    61	          : "bg-anton-surface border-anton-border text-anton-muted"
    62	      }
    63	      transition hover:border-anton-accent/50
    64	    `}
    65	    >
    66	      {/* Image thumbnail */}
    67	      {info.previewUrl && info.mediaType === "image" ? (
    68	        <img
    69	          src={info.previewUrl}
    70	          alt={info.name}
    71	          className="w-8 h-8 rounded object-cover flex-shrink-0"
    72	          onLoad={() => {
    73	            // Revoke blob URL after load to free memory (only for pending files)
    74	            if (file && info.previewUrl) {
    75	              // Don't revoke immediately — component might re-render
    76	            }
    77	          }}
    78	        />
    79	      ) : (
    80	        <Icon
    81	          size={16}
    82	          className={`flex-shrink-0 ${
    83	            isPending ? "text-anton-accent" : "text-anton-muted"
    84	          }`}
    85	        />
    86	      )}
    87	
    88	      <div className="flex flex-col min-w-0">
    89	        <span className="truncate max-w-[140px] font-medium">{info.name}</span>
    90	        <span className="text-[10px] text-anton-muted">{sizeStr}</span>
    91	      </div>
    92	
    93	      {/* Remove button */}
    94	      {onRemove && (
    95	        <button
    96	          onClick={(e) => {
    97	            e.stopPropagation();
    98	            onRemove();
    99	          }}
   100	          className="ml-1 p-0.5 rounded-full text-anton-muted hover:text-anton-danger hover:bg-anton-danger/10 transition opacity-0 group-hover:opacity-100"
   101	          title="Remove"
   102	        >
   103	          <X size={12} />
   104	        </button>
   105	      )}
   106	
   107	      {/* Video badge */}
   108	      {info.mediaType === "video" && (
   109	        <span className="absolute -top-1 -right-1 bg-anton-accent text-white text-[9px] font-bold px-1 rounded">
   110	          VID
   111	        </span>
   112	      )}
   113	    </div>
   114	  );
   115	}
   116	
   117	function classifyMime(mime) {
   118	  if (!mime) return "unknown";
   119	  if (mime.startsWith("image/")) return "image";
   120	  if (mime.startsWith("video/")) return "video";
   121	  if (
   122	    mime === "application/pdf" ||
   123	    mime.includes("word") ||
   124	    mime.includes("document")
   125	  )
   126	    return "document";
   127	  if (
   128	    mime.includes("excel") ||
   129	    mime.includes("spreadsheet") ||
   130	    mime === "text/csv"
   131	  )
   132	    return "spreadsheet";
   133	  if (mime.startsWith("text/")) return "text";
   134	  return "unknown";
   135	}
   136	
   137	function getIcon(mediaType) {
   138	  switch (mediaType) {
   139	    case "image":
   140	      return ImageIcon;
   141	    case "video":
   142	      return Film;
   143	    case "document":
   144	      return FileText;
   145	    case "spreadsheet":
   146	      return FileSpreadsheet;
   147	    case "text":
   148	      return FileText;
   149	    default:
   150	      return File;
   151	  }
   152	}
   153	
   154	function formatSize(bytes) {
   155	  if (!bytes || bytes === 0) return "0 B";
   156	  if (bytes < 1024) return `${bytes} B`;
   157	  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
   158	  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
   159	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [041]: frontend/src/components/AttachmentPreview.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [042/60]: frontend/src/components/ChatView.jsx
│ LANGUAGE: jsx | LINES: 418 | SIZE: 17897 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useRef, useCallback } from "react";
     2	import { useApp } from "../store";
     3	import { getMessages, downloadZip, listKnowledgeBases, updateChat, uploadAttachments } from "../api";
     4	import * as streamManager from "../streamManager";
     5	import MessageBubble from "./MessageBubble";
     6	import {
     7	  Send, Square, Settings2, X, Brain, BookOpen, Paperclip,
     8	  FileText, Loader2, Upload, Film, Image as ImageIcon, FileCode,
     9	} from "lucide-react";
    10	
    11	const MODELS = [
    12	  { id: "eu.anthropic.claude-opus-4-6-v1", label: "Opus 4.6" },
    13	  { id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Haiku 4.5" },
    14	];
    15	
    16	const TYPE_ICONS = { image: ImageIcon, video: Film, document: FileText, text: FileCode };
    17	const TYPE_COLORS = { image: "border-blue-500/40 bg-blue-500/10", video: "border-purple-500/40 bg-purple-500/10", document: "border-amber-500/40 bg-amber-500/10", text: "border-green-500/40 bg-green-500/10" };
    18	const TYPE_ICON_COLORS = { image: "text-blue-400", video: "text-purple-400", document: "text-amber-400", text: "text-green-400" };
    19	
    20	function classifyFile(f) {
    21	  const ext = (f.name || "").split(".").pop().toLowerCase();
    22	  const mime = f.type || "";
    23	  if (mime.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(ext)) return "image";
    24	  if (mime.startsWith("video/") || ["mp4", "mov", "avi", "mkv", "webm"].includes(ext)) return "video";
    25	  if (mime === "application/pdf" || ext === "pdf") return "document";
    26	  return "text";
    27	}
    28	
    29	function fmtSize(b) {
    30	  if (!b) return "0B";
    31	  if (b < 1024) return b + "B";
    32	  if (b < 1048576) return (b / 1024).toFixed(0) + "KB";
    33	  return (b / 1048576).toFixed(1) + "MB";
    34	}
    35	
    36	export default function ChatView({ chatId }) {
    37	  const { state, dispatch } = useApp();
    38	  const currentChat = state.chats.find((c) => c.id === chatId);
    39	  const messages = state.chatMessages[chatId] || [];
    40	
    41	  const [input, setInput] = useState("");
    42	  const [showSettings, setShowSettings] = useState(false);
    43	  const [model, setModel] = useState(currentChat?.model || MODELS[0].id);
    44	  const [maxTokens, setMaxTokens] = useState(currentChat?.max_tokens || 4096);
    45	  const [reasoningBudget, setReasoningBudget] = useState(currentChat?.reasoning_budget ?? 0);
    46	  const [selectedKbId, setSelectedKbId] = useState(currentChat?.knowledge_base_id || null);
    47	  const [kbs, setKbs] = useState([]);
    48	  const [pendingFiles, setPendingFiles] = useState([]);
    49	  const [uploading, setUploading] = useState(false);
    50	  const [dragOver, setDragOver] = useState(false);
    51	  const [streamData, setStreamData] = useState(streamManager.getStreamData(chatId));
    52	
    53	  const scrollRef = useRef(null);
    54	  const inputRef = useRef(null);
    55	  const fileRef = useRef(null);
    56	  const autoScroll = useRef(true);
    57	  const rafRef = useRef(null);
    58	
    59	  useEffect(() => {
    60	    setStreamData(streamManager.getStreamData(chatId));
    61	    return streamManager.subscribe(chatId, () => setStreamData(streamManager.getStreamData(chatId)));
    62	  }, [chatId]);
    63	
    64	  const scrollBottom = useCallback(() => {
    65	    if (!autoScroll.current || rafRef.current) return;
    66	    rafRef.current = requestAnimationFrame(() => {
    67	      scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
    68	      rafRef.current = null;
    69	    });
    70	  }, []);
    71	
    72	  useEffect(() => {
    73	    (async () => {
    74	      try {
    75	        const [msgs, kbData] = await Promise.all([
    76	          getMessages(state.token, chatId),
    77	          listKnowledgeBases(state.token),
    78	        ]);
    79	        dispatch({ type: "SET_MESSAGES", chatId, messages: msgs });
    80	        setKbs(kbData);
    81	      } catch { /* ignore */ }
    82	    })();
    83	  }, [chatId, state.token, dispatch]);
    84	
    85	  useEffect(scrollBottom, [messages, streamData.text, streamData.thinking, scrollBottom]);
    86	  useEffect(() => { inputRef.current?.focus(); }, [chatId]);
    87	
    88	  useEffect(() => {
    89	    if (currentChat) {
    90	      setModel(currentChat.model || MODELS[0].id);
    91	      setMaxTokens(currentChat.max_tokens || 4096);
    92	      setReasoningBudget(currentChat.reasoning_budget ?? 0);
    93	      setSelectedKbId(currentChat.knowledge_base_id || null);
    94	    }
    95	  }, [chatId]);
    96	
    97	  function onScroll() {
    98	    const el = scrollRef.current;
    99	    if (!el) return;
   100	    autoScroll.current = el.scrollHeight - el.scrollTop - el.clientHeight < 200;
   101	  }
   102	
   103	  async function saveSettings() {
   104	    try {
   105	      await updateChat(state.token, chatId, { model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId || "" });
   106	      dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, model, max_tokens: maxTokens, reasoning_budget: reasoningBudget, knowledge_base_id: selectedKbId } });
   107	    } catch { /* ignore */ }
   108	  }
   109	
   110	  function toggleSettings() {
   111	    if (showSettings) saveSettings();
   112	    setShowSettings(!showSettings);
   113	  }
   114	
   115	  function addFiles(files) {
   116	    setPendingFiles((prev) => [
   117	      ...prev,
   118	      ...files.map((f) => ({
   119	        file: f,
   120	        type: classifyFile(f),
   121	        preview: classifyFile(f) === "image" ? URL.createObjectURL(f) : null,
   122	      })),
   123	    ]);
   124	  }
   125	
   126	  function removePending(i) {
   127	    setPendingFiles((prev) => {
   128	      if (prev[i]?.preview) URL.revokeObjectURL(prev[i].preview);
   129	      return prev.filter((_, j) => j !== i);
   130	    });
   131	  }
   132	
   133	  async function handleSend() {
   134	    const content = input.trim();
   135	    if ((!content && !pendingFiles.length) || streamData.streaming) return;
   136	    const text = content || "Please analyze the attached file(s).";
   137	
   138	    let attIds = [], uploaded = [];
   139	    if (pendingFiles.length) {
   140	      setUploading(true);
   141	      try {
   142	        const res = await uploadAttachments(state.token, chatId, pendingFiles.map((p) => p.file));
   143	        uploaded = (res.attachments || []).filter((a) => !a.error);
   144	        attIds = uploaded.map((a) => a.id);
   145	      } catch { setUploading(false); return; }
   146	      setUploading(false);
   147	    }
   148	
   149	    dispatch({
   150	      type: "ADD_MESSAGE",
   151	      chatId,
   152	      message: {
   153	        id: `tmp-${Date.now()}`,
   154	        role: "user",
   155	        content: text,
   156	        created_at: new Date().toISOString(),
   157	        attachments: uploaded,
   158	      },
   159	    });
   160	
   161	    setInput("");
   162	    pendingFiles.forEach((p) => { if (p.preview) URL.revokeObjectURL(p.preview); });
   163	    setPendingFiles([]);
   164	    autoScroll.current = true;
   165	
   166	    // Reset textarea height
   167	    if (inputRef.current) inputRef.current.style.height = "auto";
   168	
   169	    streamManager.startStream({
   170	      token: state.token,
   171	      chatId,
   172	      body: {
   173	        content: text, model, max_tokens: maxTokens,
   174	        reasoning_budget: reasoningBudget,
   175	        knowledge_base_id: selectedKbId,
   176	        attachment_ids: attIds,
   177	      },
   178	    });
   179	  }
   180	
   181	  function handleKeyDown(e) {
   182	    if (e.key === "Enter" && !e.shiftKey) {
   183	      e.preventDefault();
   184	      handleSend();
   185	    }
   186	  }
   187	
   188	  function handlePaste(e) {
   189	    const items = Array.from(e.clipboardData?.items || []).filter((i) => i.kind === "file");
   190	    if (!items.length) return;
   191	    e.preventDefault();
   192	    addFiles(items.map((i) => i.getAsFile()).filter(Boolean));
   193	  }
   194	
   195	  function handleDrop(e) {
   196	    e.preventDefault();
   197	    setDragOver(false);
   198	    const files = Array.from(e.dataTransfer?.files || []);
   199	    if (files.length) addFiles(files);
   200	  }
   201	
   202	  const streaming = streamData.streaming;
   203	
   204	  return (
   205	    <div
   206	      className="flex-1 flex flex-col min-h-0 relative"
   207	      onDrop={handleDrop}
   208	      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
   209	      onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOver(false); }}
   210	    >
   211	      {/* Drag overlay */}
   212	      {dragOver && (
   213	        <div className="absolute inset-0 z-40 bg-anton-accent/10 backdrop-blur-sm border-2 border-dashed border-anton-accent rounded-lg flex items-center justify-center pointer-events-none">
   214	          <div className="text-center">
   215	            <Upload size={36} className="text-anton-accent mx-auto mb-2 animate-bounce" />
   216	            <p className="text-white font-semibold text-sm">Drop files here</p>
   217	          </div>
   218	        </div>
   219	      )}
   220	
   221	      {/* Messages */}
   222	      <div ref={scrollRef} onScroll={onScroll} className="flex-1 overflow-y-auto overscroll-contain px-3 sm:px-4 py-3 sm:py-4 space-y-3">
   223	        {messages.map((m) => (
   224	          <MessageBubble key={m.id} message={m} token={state.token} />
   225	        ))}
   226	
   227	        {streaming && (streamData.thinking || streamData.text) && (
   228	          <MessageBubble
   229	            message={{
   230	              id: "streaming", role: "assistant", content: streamData.text,
   231	              thinking_content: streamData.thinking || null, attachments: [],
   232	            }}
   233	            isStreaming
   234	            isThinking={streamData.isThinking}
   235	            token={state.token}
   236	          />
   237	        )}
   238	
   239	        {streaming && !streamData.text && !streamData.thinking && (
   240	          <div className="flex items-center gap-2 px-3 py-3 animate-fade-in">
   241	            <div className="flex gap-1">
   242	              {[0, 150, 300].map((d) => (
   243	                <span key={d} className="w-1.5 h-1.5 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: d + "ms" }} />
   244	              ))}
   245	            </div>
   246	            <span className="text-anton-muted text-sm">Thinking…</span>
   247	          </div>
   248	        )}
   249	      </div>
   250	
   251	      {/* Input area */}
   252	      <div className="border-t border-anton-border bg-anton-surface px-3 pt-2 pb-2 sm:px-4 sm:pt-3 sm:pb-3 safe-bottom">
   253	        {/* Settings panel */}
   254	        {showSettings && (
   255	          <div className="mb-2 bg-anton-card border border-anton-border rounded-xl p-3 space-y-3 animate-fade-in max-h-[50vh] overflow-y-auto">
   256	            <div className="flex items-center justify-between">
   257	              <h3 className="text-sm font-semibold text-white flex items-center gap-1.5">
   258	                <Settings2 size={14} className="text-anton-accent" /> Settings
   259	              </h3>
   260	              <button onClick={toggleSettings} className="p-1 text-anton-muted hover:text-white">
   261	                <X size={14} />
   262	              </button>
   263	            </div>
   264	
   265	            <div>
   266	              <label className="text-xs text-anton-muted mb-1 block">Model</label>
   267	              <select value={model} onChange={(e) => setModel(e.target.value)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-anton-accent">
   268	                {MODELS.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
   269	              </select>
   270	            </div>
   271	
   272	            <div>
   273	              <div className="flex justify-between text-xs mb-1.5">
   274	                <span className="text-anton-muted">Max Tokens</span>
   275	                <span className="text-anton-accent font-mono">{maxTokens.toLocaleString()}</span>
   276	              </div>
   277	              <input type="range" min={256} max={65536} step={256} value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))} />
   278	            </div>
   279	
   280	            <div>
   281	              <div className="flex justify-between text-xs mb-1.5">
   282	                <span className="text-anton-muted flex items-center gap-1">
   283	                  <Brain size={12} className="text-purple-400" /> Reasoning
   284	                </span>
   285	                <span className="text-purple-400 font-mono">{reasoningBudget === 0 ? "Off" : reasoningBudget.toLocaleString()}</span>
   286	              </div>
   287	              <input type="range" min={0} max={32000} step={500} value={reasoningBudget} onChange={(e) => setReasoningBudget(Number(e.target.value))} />
   288	            </div>
   289	
   290	            <div>
   291	              <label className="text-xs text-anton-muted mb-1 flex items-center gap-1">
   292	                <BookOpen size={12} /> Knowledge Base
   293	              </label>
   294	              <select value={selectedKbId || ""} onChange={(e) => setSelectedKbId(e.target.value || null)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-anton-accent">
   295	                <option value="">None</option>
   296	                {kbs.map((kb) => <option key={kb.id} value={kb.id}>{kb.name} ({kb.document_count} docs)</option>)}
   297	              </select>
   298	            </div>
   299	          </div>
   300	        )}
   301	
   302	        {/* Pending files */}
   303	        {pendingFiles.length > 0 && (
   304	          <div className="mb-2 flex flex-wrap gap-1.5 animate-fade-in">
   305	            {pendingFiles.map((pf, i) => {
   306	              const Icon = TYPE_ICONS[pf.type] || FileText;
   307	              return (
   308	                <div key={i} className={`relative group rounded-lg overflow-hidden border ${TYPE_COLORS[pf.type] || "border-anton-border bg-anton-card"}`}>
   309	                  {pf.type === "image" && pf.preview ? (
   310	                    <img src={pf.preview} alt="" className="w-14 h-14 sm:w-16 sm:h-16 object-cover" />
   311	                  ) : (
   312	                    <div className="w-14 h-14 sm:w-16 sm:h-16 flex flex-col items-center justify-center px-1">
   313	                      <Icon size={16} className={`${TYPE_ICON_COLORS[pf.type] || "text-anton-muted"} mb-0.5`} />
   314	                      <span className="text-[7px] text-anton-muted text-center truncate w-full">{pf.file.name.slice(0, 8)}</span>
   315	                    </div>
   316	                  )}
   317	                  <button
   318	                    onClick={() => removePending(i)}
   319	                    className="absolute -top-0.5 -right-0.5 w-5 h-5 bg-red-600 rounded-full flex items-center justify-center text-white shadow transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
   320	                  >
   321	                    <X size={10} />
   322	                  </button>
   323	                  <div className="absolute bottom-0 left-0 right-0 bg-black/70 text-[7px] text-white text-center py-px">
   324	                    {fmtSize(pf.file.size)}
   325	                  </div>
   326	                </div>
   327	              );
   328	            })}
   329	          </div>
   330	        )}
   331	
   332	        {/* Input row */}
   333	        <div className="flex items-end gap-1.5">
   334	          <button
   335	            onClick={toggleSettings}
   336	            className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card active:bg-anton-card"
   337	              }`}
   338	          >
   339	            <Settings2 size={18} />
   340	          </button>
   341	
   342	          <button
   343	            onClick={() => fileRef.current?.click()}
   344	            className={`p-2.5 rounded-xl transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center ${pendingFiles.length ? "bg-green-500/20 text-green-400" : "text-anton-muted hover:text-white hover:bg-anton-card active:bg-anton-card"
   345	              }`}
   346	            title="Attach files"
   347	          >
   348	            <Paperclip size={18} />
   349	          </button>
   350	
   351	          <input
   352	            ref={fileRef}
   353	            type="file"
   354	            multiple
   355	            className="hidden"
   356	            accept="image/*,video/*,.pdf,.txt,.md,.py,.js,.ts,.jsx,.tsx,.cs,.java,.cpp,.c,.h,.go,.rs,.rb,.php,.html,.css,.json,.yaml,.yml,.xml,.toml,.csv,.sql,.sh,.swift,.kt,.lua,.gd,.dart,.vue,.svelte,.log"
   357	            onChange={(e) => { addFiles(Array.from(e.target.files || [])); e.target.value = ""; }}
   358	          />
   359	
   360	          <div className="flex-1 min-w-0">
   361	            <textarea
   362	              ref={inputRef}
   363	              value={input}
   364	              onChange={(e) => setInput(e.target.value)}
   365	              onKeyDown={handleKeyDown}
   366	              onPaste={handlePaste}
   367	              placeholder={pendingFiles.length ? "Add a message…" : "Ask anything…"}
   368	              rows={1}
   369	              style={{ maxHeight: "120px" }}
   370	              className="w-full bg-anton-card border border-anton-border rounded-xl px-3 py-2.5 text-white resize-none focus:outline-none focus:border-anton-accent transition leading-snug"
   371	              onInput={(e) => {
   372	                e.target.style.height = "auto";
   373	                e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
   374	              }}
   375	            />
   376	          </div>
   377	
   378	          {streaming ? (
   379	            <button
   380	              onClick={() => streamManager.abortStream(chatId)}
   381	              className="p-2.5 rounded-xl bg-anton-danger text-white hover:opacity-80 transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center active:scale-95"
   382	            >
   383	              <Square size={18} />
   384	            </button>
   385	          ) : (
   386	            <button
   387	              onClick={handleSend}
   388	              disabled={(!input.trim() && !pendingFiles.length) || uploading}
   389	              className="p-2.5 rounded-xl bg-anton-accent text-white hover:opacity-80 transition shrink-0 min-w-[40px] min-h-[40px] flex items-center justify-center disabled:opacity-30 active:scale-95"
   390	            >
   391	              {uploading ? <Loader2 size={18} className="animate-spin" /> : <Send size={18} />}
   392	            </button>
   393	          )}
   394	        </div>
   395	
   396	        {/* Status bar */}
   397	        <div className="flex items-center gap-1.5 mt-1.5 text-[10px] text-anton-muted flex-wrap">
   398	          <span>{MODELS.find((m) => m.id === model)?.label}</span>
   399	          <span>•</span>
   400	          <span>{maxTokens.toLocaleString()} tok</span>
   401	          {reasoningBudget > 0 && <><span>•</span><span className="text-purple-400">🧠 {reasoningBudget.toLocaleString()}</span></>}
   402	          {selectedKbId && <><span>•</span><span className="text-green-400">📚 RAG</span></>}
   403	          {pendingFiles.length > 0 && <><span>•</span><span className="text-blue-400">📎 {pendingFiles.length}</span></>}
   404	          {messages.some((m) => m.role === "assistant") && (
   405	            <button
   406	              onClick={async () => {
   407	                const all = messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n");
   408	                if (all) try { await downloadZip(state.token, all, currentChat?.title); } catch { /* */ }
   409	              }}
   410	              className="ml-auto hover:text-anton-accent transition"
   411	            >
   412	              ⬇ Code
   413	            </button>
   414	          )}
   415	        </div>
   416	      </div>
   417	    </div>
   418	  );
   419	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [042]: frontend/src/components/ChatView.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [043/60]: frontend/src/components/CodeBlock.jsx
│ LANGUAGE: jsx | LINES: 95 | SIZE: 3541 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
     3	import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
     4	import { Copy, Check, Download, FileCode } from "lucide-react";
     5	
     6	const LANG_MAP = {
     7	  cs: "csharp", sh: "bash", shell: "bash", yml: "yaml",
     8	  dockerfile: "docker", jsx: "jsx", tsx: "tsx", py: "python",
     9	  js: "javascript", ts: "typescript", rb: "ruby", rs: "rust",
    10	  kt: "kotlin", gd: "gdscript",
    11	};
    12	
    13	const customStyle = {
    14	  ...oneDark,
    15	  'pre[class*="language-"]': {
    16	    ...oneDark['pre[class*="language-"]'],
    17	    background: "#0d0d14",
    18	    margin: 0,
    19	    borderRadius: 0,
    20	    fontSize: "0.78rem",
    21	    lineHeight: "1.55",
    22	  },
    23	  'code[class*="language-"]': {
    24	    ...oneDark['code[class*="language-"]'],
    25	    background: "none",
    26	    fontSize: "0.78rem",
    27	  },
    28	};
    29	
    30	export default function CodeBlock({ language, filename, code }) {
    31	  const [copied, setCopied] = useState(false);
    32	  const hlLang = LANG_MAP[language] || language || "text";
    33	
    34	  function handleCopy() {
    35	    navigator.clipboard.writeText(code);
    36	    setCopied(true);
    37	    setTimeout(() => setCopied(false), 2000);
    38	  }
    39	
    40	  function handleDownload() {
    41	    const name = filename || `code.${language || "txt"}`;
    42	    const blob = new Blob([code], { type: "text/plain;charset=utf-8" });
    43	    const url = URL.createObjectURL(blob);
    44	    const a = document.createElement("a");
    45	    a.href = url;
    46	    a.download = name;
    47	    a.click();
    48	    URL.revokeObjectURL(url);
    49	  }
    50	
    51	  return (
    52	    <div className="my-2.5 rounded-lg overflow-hidden border border-anton-border bg-[#0d0d14]">
    53	      {/* Header */}
    54	      <div className="flex items-center justify-between px-2.5 sm:px-3 py-1.5 bg-anton-border/30 gap-2">
    55	        <div className="flex items-center gap-1.5 text-xs text-anton-muted min-w-0">
    56	          <FileCode size={11} className="text-anton-accent shrink-0" />
    57	          {filename ? (
    58	            <span className="text-anton-text font-mono truncate text-[11px]">{filename}</span>
    59	          ) : (
    60	            <span className="text-[11px]">{hlLang}</span>
    61	          )}
    62	        </div>
    63	        <div className="flex items-center gap-0.5 shrink-0">
    64	          <button
    65	            onClick={handleCopy}
    66	            className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-white hover:bg-anton-card transition min-h-[28px]"
    67	          >
    68	            {copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
    69	            <span className="hidden sm:inline">{copied ? "Copied" : "Copy"}</span>
    70	          </button>
    71	          <button
    72	            onClick={handleDownload}
    73	            className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition min-h-[28px]"
    74	          >
    75	            <Download size={10} />
    76	            <span className="hidden sm:inline">Download</span>
    77	          </button>
    78	        </div>
    79	      </div>
    80	
    81	      {/* Code with horizontal scroll */}
    82	      <div className="overflow-x-auto overscroll-x-contain -webkit-overflow-scrolling-touch">
    83	        <SyntaxHighlighter
    84	          language={hlLang}
    85	          style={customStyle}
    86	          showLineNumbers={code.split("\n").length > 3}
    87	          lineNumberStyle={{ color: "#333", fontSize: "0.7rem", minWidth: "2em", paddingRight: "0.5em" }}
    88	          customStyle={{ padding: "0.75rem", minWidth: "fit-content" }}
    89	          wrapLongLines={false}
    90	        >
    91	          {code}
    92	        </SyntaxHighlighter>
    93	      </div>
    94	    </div>
    95	  );
    96	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [043]: frontend/src/components/CodeBlock.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [044/60]: frontend/src/components/FileUploadButton.jsx
│ LANGUAGE: jsx | LINES: 81 | SIZE: 1777 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useRef } from "react";
     2	import { Paperclip } from "lucide-react";
     3	
     4	const ACCEPT = [
     5	  // Images
     6	  "image/jpeg",
     7	  "image/png",
     8	  "image/gif",
     9	  "image/webp",
    10	  // Videos
    11	  "video/mp4",
    12	  "video/webm",
    13	  "video/quicktime",
    14	  "video/mpeg",
    15	  "video/x-matroska",
    16	  // Documents
    17	  "application/pdf",
    18	  "text/csv",
    19	  "application/msword",
    20	  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    21	  "application/vnd.ms-excel",
    22	  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    23	  "text/html",
    24	  "text/plain",
    25	  "text/markdown",
    26	  // Also accept by extension for browsers that are bad at MIME
    27	  ".jpg",
    28	  ".jpeg",
    29	  ".png",
    30	  ".gif",
    31	  ".webp",
    32	  ".mp4",
    33	  ".webm",
    34	  ".mov",
    35	  ".mkv",
    36	  ".pdf",
    37	  ".csv",
    38	  ".doc",
    39	  ".docx",
    40	  ".xls",
    41	  ".xlsx",
    42	  ".html",
    43	  ".txt",
    44	  ".md",
    45	].join(",");
    46	
    47	export default function FileUploadButton({ onFilesSelected }) {
    48	  const inputRef = useRef(null);
    49	
    50	  function handleClick() {
    51	    inputRef.current?.click();
    52	  }
    53	
    54	  function handleChange(e) {
    55	    const files = e.target.files;
    56	    if (files && files.length > 0) {
    57	      onFilesSelected(files);
    58	    }
    59	    // Reset so the same file can be re-selected
    60	    e.target.value = "";
    61	  }
    62	
    63	  return (
    64	    <>
    65	      <input
    66	        ref={inputRef}
    67	        type="file"
    68	        multiple
    69	        accept={ACCEPT}
    70	        onChange={handleChange}
    71	        className="hidden"
    72	      />
    73	      <button
    74	        onClick={handleClick}
    75	        className="flex items-center justify-center w-10 h-10 rounded-xl border border-anton-border text-anton-muted hover:text-anton-accent hover:border-anton-accent/40 hover:bg-anton-accent/10 transition"
    76	        title="Attach files (images, videos, documents)"
    77	      >
    78	        <Paperclip size={16} />
    79	      </button>
    80	    </>
    81	  );
    82	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [044]: frontend/src/components/FileUploadButton.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [045/60]: frontend/src/components/MessageBubble.jsx
│ LANGUAGE: jsx | LINES: 188 | SIZE: 8304 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import ReactMarkdown from "react-markdown";
     3	import remarkGfm from "remark-gfm";
     4	import CodeBlock from "./CodeBlock";
     5	import { getAttachmentUrl } from "../api";
     6	import {
     7	  User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check,
     8	  Image, Film, FileText, ExternalLink,
     9	} from "lucide-react";
    10	
    11	const FILE_TYPE_ICONS = {
    12	  image: Image, video: Film, document: FileText, text: FileText,
    13	};
    14	
    15	const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, isThinking, token }) {
    16	  const { role, content, thinking_content, input_tokens, output_tokens, attachments } = message;
    17	  const isUser = role === "user";
    18	  const [showThinking, setShowThinking] = useState(false);
    19	  const [copied, setCopied] = useState(false);
    20	  const [expandedImage, setExpandedImage] = useState(null);
    21	
    22	  function handleCopy() {
    23	    navigator.clipboard.writeText(content || "");
    24	    setCopied(true);
    25	    setTimeout(() => setCopied(false), 2000);
    26	  }
    27	
    28	  const hasAttachments = attachments && attachments.length > 0;
    29	
    30	  return (
    31	    <div className={`flex gap-2 sm:gap-3 animate-fade-in ${isUser ? "justify-end" : ""}`}>
    32	      {!isUser && (
    33	        <div className="shrink-0 mt-1">
    34	          <div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/10">
    35	            <Flame size={14} className="text-white" />
    36	          </div>
    37	        </div>
    38	      )}
    39	
    40	      <div className={`min-w-0 ${isUser ? "max-w-[85%] sm:max-w-[75%]" : "max-w-[90%] sm:max-w-[80%]"}`}>
    41	        {/* Thinking block */}
    42	        {thinking_content && (
    43	          <div className="mb-2">
    44	            <button
    45	              onClick={() => setShowThinking(!showThinking)}
    46	              className="flex items-center gap-1.5 text-xs text-purple-400 hover:text-purple-300 transition mb-1 min-h-[32px]"
    47	            >
    48	              <Brain size={12} />
    49	              {showThinking ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
    50	              {isThinking ? <span className="thinking-pulse">Reasoning…</span> : <span>View reasoning</span>}
    51	            </button>
    52	            {(showThinking || isThinking) && (
    53	              <div className="bg-purple-500/5 border border-purple-500/20 rounded-lg p-2.5 sm:p-3 text-xs text-purple-300/80 font-mono whitespace-pre-wrap max-h-48 sm:max-h-60 overflow-y-auto overscroll-contain break-words">
    54	                {thinking_content}
    55	                {isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
    56	              </div>
    57	            )}
    58	          </div>
    59	        )}
    60	
    61	        {/* Attachments */}
    62	        {hasAttachments && (
    63	          <div className="mb-2 flex flex-wrap gap-1.5">
    64	            {attachments.map((att) => {
    65	              const Icon = FILE_TYPE_ICONS[att.file_type] || FileText;
    66	              const url = getAttachmentUrl(att.id);
    67	
    68	              if (att.file_type === "image") {
    69	                return (
    70	                  <div key={att.id} className="relative">
    71	                    <img
    72	                      src={`${url}?token=${token}`}
    73	                      alt={att.original_filename}
    74	                      className="max-w-[200px] sm:max-w-[240px] max-h-[160px] sm:max-h-[200px] rounded-lg border border-anton-border object-cover cursor-pointer hover:opacity-90 transition"
    75	                      onClick={() => setExpandedImage(expandedImage === att.id ? null : att.id)}
    76	                      onError={(e) => { e.target.style.display = "none"; }}
    77	                    />
    78	                    {expandedImage === att.id && (
    79	                      <div
    80	                        className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 sm:p-8 cursor-pointer"
    81	                        onClick={() => setExpandedImage(null)}
    82	                      >
    83	                        <img
    84	                          src={`${url}?token=${token}`}
    85	                          alt={att.original_filename}
    86	                          className="max-w-full max-h-full object-contain rounded-lg"
    87	                        />
    88	                      </div>
    89	                    )}
    90	                    <div className="absolute bottom-1 left-1 bg-black/60 text-[8px] text-white px-1 py-0.5 rounded">
    91	                      {att.original_filename}
    92	                    </div>
    93	                  </div>
    94	                );
    95	              }
    96	
    97	              return (
    98	                <a
    99	                  key={att.id}
   100	                  href={`${url}?token=${token}`}
   101	                  target="_blank"
   102	                  rel="noopener noreferrer"
   103	                  className="flex items-center gap-2 bg-anton-card border border-anton-border rounded-lg px-2.5 py-2 hover:border-anton-accent transition group min-h-[44px]"
   104	                >
   105	                  <Icon size={14} className="shrink-0 text-blue-400" />
   106	                  <div className="min-w-0">
   107	                    <div className="text-xs text-white truncate max-w-[120px] sm:max-w-[160px]">{att.original_filename}</div>
   108	                    <div className="text-[9px] text-anton-muted">{(att.file_size / 1024).toFixed(0)}KB</div>
   109	                  </div>
   110	                  <ExternalLink size={10} className="text-anton-muted group-hover:text-anton-accent shrink-0" />
   111	                </a>
   112	              );
   113	            })}
   114	          </div>
   115	        )}
   116	
   117	        {/* Message bubble */}
   118	        <div className={`rounded-2xl px-3.5 py-2.5 sm:px-4 sm:py-3 ${
   119	          isUser
   120	            ? "bg-anton-accent text-white rounded-br-md"
   121	            : "bg-anton-card border border-anton-border rounded-bl-md"
   122	        }`}>
   123	          {isUser ? (
   124	            <div className="text-sm whitespace-pre-wrap break-words leading-relaxed">{_stripPrefixes(content)}</div>
   125	          ) : (
   126	            <div className="prose-anton text-sm">
   127	              <ReactMarkdown
   128	                remarkPlugins={[remarkGfm]}
   129	                components={{
   130	                  code({ node, inline, className, children, ...props }) {
   131	                    const match = /language-(\S+)/.exec(className || "");
   132	                    const rawLang = match?.[1] || "";
   133	                    if (inline) return <code className={className} {...props}>{children}</code>;
   134	                    let lang = rawLang, filename = null;
   135	                    if (rawLang.includes(":")) {
   136	                      const idx = rawLang.indexOf(":");
   137	                      lang = rawLang.slice(0, idx);
   138	                      filename = rawLang.slice(idx + 1);
   139	                    }
   140	                    return <CodeBlock language={lang} filename={filename} code={String(children).replace(/\n$/, "")} />;
   141	                  },
   142	                  pre({ children }) { return <>{children}</>; },
   143	                }}
   144	              >
   145	                {content || ""}
   146	              </ReactMarkdown>
   147	              {isStreaming && !isThinking && (
   148	                <span className="inline-block w-1.5 h-4 bg-anton-accent ml-0.5 animate-pulse" />
   149	              )}
   150	            </div>
   151	          )}
   152	        </div>
   153	
   154	        {/* Actions */}
   155	        {!isUser && !isStreaming && content && (
   156	          <div className="flex items-center gap-3 mt-1 px-1">
   157	            <button
   158	              onClick={handleCopy}
   159	              className="flex items-center gap-1 text-[10px] text-anton-muted hover:text-white transition min-h-[28px]"
   160	            >
   161	              {copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
   162	              {copied ? "Copied" : "Copy"}
   163	            </button>
   164	            {(input_tokens > 0 || output_tokens > 0) && (
   165	              <span className="text-[10px] text-anton-muted">
   166	                {input_tokens?.toLocaleString()}↓ {output_tokens?.toLocaleString()}↑
   167	              </span>
   168	            )}
   169	          </div>
   170	        )}
   171	      </div>
   172	
   173	      {isUser && (
   174	        <div className="shrink-0 mt-1">
   175	          <div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center">
   176	            <User size={14} className="text-anton-muted" />
   177	          </div>
   178	        </div>
   179	      )}
   180	    </div>
   181	  );
   182	});
   183	
   184	function _stripPrefixes(text) {
   185	  if (!text) return "";
   186	  return text.replace(/^\[(?:Image|Video|Document|File):\s[^\]]*\]\n?/gm, "").trim();
   187	}
   188	
   189	export default MessageBubble;│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [045]: frontend/src/components/MessageBubble.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [046/60]: frontend/src/components/Sidebar.jsx
│ LANGUAGE: jsx | LINES: 142 | SIZE: 7075 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useRef } from "react";
     2	import { useNavigate } from "react-router-dom";
     3	import { useApp } from "../store";
     4	import { listChats, createChat, deleteChat, renameChat } from "../api";
     5	import {
     6	  Plus, Trash2, MessageSquare, LogOut, Shield, BookOpen,
     7	  MoreHorizontal, Pencil, Check, X, Flame, Menu, FolderGit2,
     8	} from "lucide-react";
     9	
    10	export default function Sidebar({ activeChatId, onSelectChat, isOpen, onToggle }) {
    11	  const { state, dispatch } = useApp();
    12	  const nav = useNavigate();
    13	  const [editingId, setEditingId] = useState(null);
    14	  const [editTitle, setEditTitle] = useState("");
    15	  const [menuId, setMenuId] = useState(null);
    16	  const menuRef = useRef(null);
    17	
    18	  useEffect(() => {
    19	    (async () => {
    20	      try {
    21	        const chats = await listChats(state.token);
    22	        dispatch({ type: "SET_CHATS", chats });
    23	      } catch { /* ignore */ }
    24	    })();
    25	  }, [state.token, dispatch]);
    26	
    27	  useEffect(() => {
    28	    function handleClick(e) { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuId(null); }
    29	    document.addEventListener("mousedown", handleClick);
    30	    return () => document.removeEventListener("mousedown", handleClick);
    31	  }, []);
    32	
    33	  async function handleNew() {
    34	    try {
    35	      const chat = await createChat(state.token);
    36	      dispatch({ type: "ADD_CHAT", chat });
    37	      onSelectChat(chat.id);
    38	    } catch { /* ignore */ }
    39	  }
    40	
    41	  async function handleDelete(id) {
    42	    try {
    43	      await deleteChat(state.token, id);
    44	      dispatch({ type: "DELETE_CHAT", chatId: id });
    45	      if (activeChatId === id) {
    46	        const remaining = state.chats.filter((c) => c.id !== id);
    47	        onSelectChat(remaining.length ? remaining[0].id : null);
    48	      }
    49	    } catch { /* ignore */ }
    50	    setMenuId(null);
    51	  }
    52	
    53	  async function handleRename(id) {
    54	    if (!editTitle.trim()) { setEditingId(null); return; }
    55	    try {
    56	      await renameChat(state.token, id, editTitle);
    57	      dispatch({ type: "UPDATE_CHAT", chat: { id, title: editTitle } });
    58	    } catch { /* ignore */ }
    59	    setEditingId(null);
    60	  }
    61	
    62	  function startRename(chat) {
    63	    setEditingId(chat.id);
    64	    setEditTitle(chat.title);
    65	    setMenuId(null);
    66	  }
    67	
    68	  const isSuperadmin = state.user?.role === "superadmin";
    69	
    70	  return (
    71	    <>
    72	      {isOpen && <div className="fixed inset-0 bg-black/50 z-30 md:hidden" onClick={onToggle} />}
    73	
    74	      <aside className={`fixed md:static inset-y-0 left-0 z-40 w-72 bg-anton-surface border-r border-anton-border flex flex-col transition-transform duration-200 ${isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"}`}>
    75	        {/* Header */}
    76	        <div className="p-3 border-b border-anton-border">
    77	          <div className="flex items-center gap-2 mb-3">
    78	            <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/10">
    79	              <Flame size={16} className="text-white" />
    80	            </div>
    81	            <div>
    82	              <h1 className="text-sm font-bold text-white">Son of Anton</h1>
    83	              <p className="text-[10px] text-anton-muted">v3.0 • {state.user?.username}</p>
    84	            </div>
    85	          </div>
    86	          <button onClick={handleNew} className="w-full flex items-center justify-center gap-1.5 bg-anton-accent text-white rounded-lg py-2 text-sm font-medium hover:opacity-90 transition active:scale-[0.98]">
    87	            <Plus size={16} /> New Chat
    88	          </button>
    89	        </div>
    90	
    91	        {/* Chat list */}
    92	        <div className="flex-1 overflow-y-auto py-1">
    93	          {state.chats.map((chat) => (
    94	            <div key={chat.id} className={`group relative mx-1.5 my-0.5 rounded-lg transition ${activeChatId === chat.id ? "bg-anton-card border border-anton-border" : "hover:bg-anton-card/50"}`}>
    95	              {editingId === chat.id ? (
    96	                <div className="flex items-center gap-1 px-2 py-2">
    97	                  <input value={editTitle} onChange={(e) => setEditTitle(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleRename(chat.id)} className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-1 text-xs text-white focus:outline-none" autoFocus />
    98	                  <button onClick={() => handleRename(chat.id)} className="p-1 text-green-400 hover:bg-green-500/20 rounded"><Check size={12} /></button>
    99	                  <button onClick={() => setEditingId(null)} className="p-1 text-anton-muted hover:bg-anton-card rounded"><X size={12} /></button>
   100	                </div>
   101	              ) : (
   102	                <button onClick={() => { onSelectChat(chat.id); if (window.innerWidth < 768) onToggle?.(); }} className="w-full text-left px-3 py-2.5 flex items-center gap-2">
   103	                  <MessageSquare size={14} className="text-anton-muted shrink-0" />
   104	                  <span className="text-sm truncate flex-1">{chat.title}</span>
   105	                  <button onClick={(e) => { e.stopPropagation(); setMenuId(menuId === chat.id ? null : chat.id); }} className="p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-anton-bg text-anton-muted">
   106	                    <MoreHorizontal size={12} />
   107	                  </button>
   108	                </button>
   109	              )}
   110	
   111	              {menuId === chat.id && (
   112	                <div ref={menuRef} className="absolute right-2 top-9 z-50 bg-anton-card border border-anton-border rounded-lg shadow-xl py-1 w-36 animate-fade-in">
   113	                  <button onClick={() => startRename(chat)} className="w-full text-left px-3 py-1.5 text-xs hover:bg-anton-bg flex items-center gap-2"><Pencil size={10} /> Rename</button>
   114	                  <button onClick={() => handleDelete(chat.id)} className="w-full text-left px-3 py-1.5 text-xs text-red-400 hover:bg-red-500/10 flex items-center gap-2"><Trash2 size={10} /> Delete</button>
   115	                </div>
   116	              )}
   117	            </div>
   118	          ))}
   119	        </div>
   120	
   121	        {/* Bottom nav */}
   122	        <div className="border-t border-anton-border p-2 space-y-0.5">
   123	          {isSuperadmin && (
   124	            <>
   125	              <button onClick={() => nav("/gitlab")} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-orange-400 hover:bg-orange-500/10 transition">
   126	                <FolderGit2 size={14} /> GitLab Command Center
   127	              </button>
   128	              <button onClick={() => nav("/admin")} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card transition">
   129	                <Shield size={14} /> Admin Dashboard
   130	              </button>
   131	            </>
   132	          )}
   133	          <button onClick={() => nav("/knowledge")} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:bg-anton-card transition">
   134	            <BookOpen size={14} /> Knowledge Bases
   135	          </button>
   136	          <button onClick={() => dispatch({ type: "LOGOUT" })} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-red-400 hover:bg-red-500/10 transition">
   137	            <LogOut size={14} /> Logout
   138	          </button>
   139	        </div>
   140	      </aside>
   141	    </>
   142	  );
   143	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [046]: frontend/src/components/Sidebar.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [047/60]: frontend/src/index.css
│ LANGUAGE: css | LINES: 284 | SIZE: 8919 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	@tailwind base;
     2	@tailwind components;
     3	@tailwind utilities;
     4	
     5	/* ═══════════════════════════════════════════════════
     6	   ROOT VARIABLES & BASE
     7	   ═══════════════════════════════════════════════════ */
     8	
     9	:root {
    10	  --sat: env(safe-area-inset-top, 0px);
    11	  --sar: env(safe-area-inset-right, 0px);
    12	  --sab: env(safe-area-inset-bottom, 0px);
    13	  --sal: env(safe-area-inset-left, 0px);
    14	  --header-h: 3.25rem;
    15	  color-scheme: dark;
    16	}
    17	
    18	/* ═══════════════════════════════════════════════════
    19	   GLOBAL RESETS FOR MOBILE
    20	   ═══════════════════════════════════════════════════ */
    21	
    22	*, *::before, *::after {
    23	  -webkit-tap-highlight-color: transparent;
    24	  -webkit-touch-callout: none;
    25	}
    26	
    27	html {
    28	  overflow: hidden;
    29	  height: 100%;
    30	  height: 100dvh;
    31	}
    32	
    33	body {
    34	  overflow: hidden;
    35	  height: 100%;
    36	  height: 100dvh;
    37	  overscroll-behavior: none;
    38	  -webkit-overflow-scrolling: touch;
    39	  font-family: 'Inter', system-ui, -apple-system, sans-serif;
    40	  position: fixed;
    41	  width: 100%;
    42	  top: 0;
    43	  left: 0;
    44	}
    45	
    46	#root {
    47	  height: 100%;
    48	  height: 100dvh;
    49	  overflow: hidden;
    50	  display: flex;
    51	  flex-direction: column;
    52	}
    53	
    54	/* ═══════════════════════════════════════════════════
    55	   SAFE AREA UTILITIES
    56	   ═══════════════════════════════════════════════════ */
    57	
    58	.safe-top { padding-top: var(--sat); }
    59	.safe-bottom { padding-bottom: max(var(--sab), 8px); }
    60	.safe-left { padding-left: var(--sal); }
    61	.safe-right { padding-right: var(--sar); }
    62	
    63	/* ═══════════════════════════════════════════════════
    64	   SCROLLBAR
    65	   ═══════════════════════════════════════════════════ */
    66	
    67	::-webkit-scrollbar { width: 4px; height: 4px; }
    68	::-webkit-scrollbar-track { background: transparent; }
    69	::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
    70	::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
    71	
    72	/* ═══════════════════════════════════════════════════
    73	   ANIMATIONS
    74	   ═══════════════════════════════════════════════════ */
    75	
    76	@keyframes fadeIn {
    77	  from { opacity: 0; transform: translateY(6px); }
    78	  to   { opacity: 1; transform: translateY(0); }
    79	}
    80	
    81	@keyframes slideInLeft {
    82	  from { transform: translateX(-100%); }
    83	  to   { transform: translateX(0); }
    84	}
    85	
    86	@keyframes slideOutLeft {
    87	  from { transform: translateX(0); }
    88	  to   { transform: translateX(-100%); }
    89	}
    90	
    91	@keyframes fadeOverlayIn {
    92	  from { opacity: 0; }
    93	  to   { opacity: 1; }
    94	}
    95	
    96	@keyframes fadeOverlayOut {
    97	  from { opacity: 1; }
    98	  to   { opacity: 0; }
    99	}
   100	
   101	.animate-fade-in { animation: fadeIn 0.2s ease-out both; }
   102	.animate-slide-in { animation: slideInLeft 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; }
   103	.animate-slide-out { animation: slideOutLeft 0.2s ease-in both; }
   104	.animate-overlay-in { animation: fadeOverlayIn 0.2s ease-out both; }
   105	.animate-overlay-out { animation: fadeOverlayOut 0.15s ease-in both; }
   106	
   107	/* ═══════════════════════════════════════════════════
   108	   MOBILE INPUT FIXES
   109	   ═══════════════════════════════════════════════════ */
   110	
   111	textarea, input, select {
   112	  font-size: 16px !important; /* Prevents iOS zoom on focus */
   113	}
   114	
   115	@media (min-width: 640px) {
   116	  textarea, input, select {
   117	    font-size: 14px !important;
   118	  }
   119	}
   120	
   121	textarea {
   122	  -webkit-appearance: none;
   123	  appearance: none;
   124	}
   125	
   126	select {
   127	  -webkit-appearance: none;
   128	  appearance: none;
   129	  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
   130	  background-repeat: no-repeat;
   131	  background-position: right 10px center;
   132	  padding-right: 28px;
   133	}
   134	
   135	/* ═══════════════════════════════════════════════════
   136	   TOUCH-FRIENDLY RANGE SLIDER
   137	   ═══════════════════════════════════════════════════ */
   138	
   139	input[type="range"] {
   140	  -webkit-appearance: none;
   141	  appearance: none;
   142	  width: 100%;
   143	  height: 6px;
   144	  border-radius: 3px;
   145	  background: rgba(255,255,255,0.08);
   146	  outline: none;
   147	  cursor: pointer;
   148	}
   149	
   150	input[type="range"]::-webkit-slider-thumb {
   151	  -webkit-appearance: none;
   152	  appearance: none;
   153	  width: 22px;
   154	  height: 22px;
   155	  border-radius: 50%;
   156	  background: #e53e3e;
   157	  border: 2px solid #1a1a2e;
   158	  cursor: pointer;
   159	  box-shadow: 0 0 8px rgba(229,62,62,0.3);
   160	}
   161	
   162	input[type="range"]::-moz-range-thumb {
   163	  width: 22px;
   164	  height: 22px;
   165	  border-radius: 50%;
   166	  background: #e53e3e;
   167	  border: 2px solid #1a1a2e;
   168	  cursor: pointer;
   169	}
   170	
   171	/* ═══════════════════════════════════════════════════
   172	   MARKDOWN PROSE
   173	   ═══════════════════════════════════════════════════ */
   174	
   175	.prose-anton {
   176	  color: #e2e2ea;
   177	  line-height: 1.65;
   178	  word-break: break-word;
   179	  overflow-wrap: anywhere;
   180	}
   181	
   182	.prose-anton h1, .prose-anton h2, .prose-anton h3,
   183	.prose-anton h4, .prose-anton h5, .prose-anton h6 {
   184	  color: #fff;
   185	  font-weight: 600;
   186	  margin-top: 1.2em;
   187	  margin-bottom: 0.5em;
   188	}
   189	
   190	.prose-anton h1 { font-size: 1.4em; }
   191	.prose-anton h2 { font-size: 1.2em; }
   192	.prose-anton h3 { font-size: 1.05em; }
   193	
   194	.prose-anton p { margin-bottom: 0.75em; }
   195	
   196	.prose-anton ul, .prose-anton ol {
   197	  padding-left: 1.4em;
   198	  margin-bottom: 0.75em;
   199	}
   200	
   201	.prose-anton li { margin-bottom: 0.25em; }
   202	.prose-anton li::marker { color: #555; }
   203	
   204	.prose-anton code:not(pre code) {
   205	  background: rgba(255,255,255,0.06);
   206	  border: 1px solid rgba(255,255,255,0.08);
   207	  border-radius: 4px;
   208	  padding: 0.15em 0.35em;
   209	  font-family: 'JetBrains Mono', monospace;
   210	  font-size: 0.85em;
   211	  color: #ff6b6b;
   212	  word-break: break-all;
   213	}
   214	
   215	.prose-anton a {
   216	  color: #e53e3e;
   217	  text-decoration: underline;
   218	  text-underline-offset: 2px;
   219	}
   220	
   221	.prose-anton blockquote {
   222	  border-left: 3px solid #e53e3e;
   223	  padding: 0.5em 1em;
   224	  margin: 0.75em 0;
   225	  background: rgba(229,62,62,0.04);
   226	  border-radius: 0 6px 6px 0;
   227	  color: #aaa;
   228	}
   229	
   230	.prose-anton hr {
   231	  border: none;
   232	  border-top: 1px solid rgba(255,255,255,0.08);
   233	  margin: 1.5em 0;
   234	}
   235	
   236	.prose-anton table {
   237	  width: 100%;
   238	  border-collapse: collapse;
   239	  font-size: 0.85em;
   240	  margin: 0.75em 0;
   241	  display: block;
   242	  overflow-x: auto;
   243	  -webkit-overflow-scrolling: touch;
   244	}
   245	
   246	.prose-anton th, .prose-anton td {
   247	  border: 1px solid rgba(255,255,255,0.08);
   248	  padding: 0.4em 0.7em;
   249	  text-align: left;
   250	  white-space: nowrap;
   251	}
   252	
   253	.prose-anton th {
   254	  background: rgba(255,255,255,0.04);
   255	  font-weight: 600;
   256	}
   257	
   258	.prose-anton strong { color: #fff; font-weight: 600; }
   259	.prose-anton em { font-style: italic; }
   260	
   261	/* ═══════════════════════════════════════════════════
   262	   THINKING PULSE
   263	   ═══════════════════════════════════════════════════ */
   264	
   265	@keyframes thinkPulse {
   266	  0%, 100% { opacity: 1; }
   267	  50% { opacity: 0.5; }
   268	}
   269	
   270	.thinking-pulse { animation: thinkPulse 1.5s ease-in-out infinite; }
   271	
   272	/* ═══════════════════════════════════════════════════
   273	   MOBILE-SPECIFIC OVERRIDES
   274	   ═══════════════════════════════════════════════════ */
   275	
   276	@media (max-width: 639px) {
   277	  .prose-anton { font-size: 0.9rem; line-height: 1.6; }
   278	  .prose-anton h1 { font-size: 1.25em; }
   279	  .prose-anton h2 { font-size: 1.15em; }
   280	}
   281	
   282	/* Prevent body scroll when modal/drawer is open */
   283	body.drawer-open {
   284	  touch-action: none;
   285	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [047]: frontend/src/index.css


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [048/60]: frontend/src/main.jsx
│ LANGUAGE: jsx | LINES: 15 | SIZE: 409 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React from "react";
     2	import ReactDOM from "react-dom/client";
     3	import { BrowserRouter } from "react-router-dom";
     4	import { AppProvider } from "./store";
     5	import App from "./App";
     6	import "./index.css";
     7	
     8	ReactDOM.createRoot(document.getElementById("root")).render(
     9	  <React.StrictMode>
    10	    <BrowserRouter>
    11	      <AppProvider>
    12	        <App />
    13	      </AppProvider>
    14	    </BrowserRouter>
    15	  </React.StrictMode>
    16	);│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [048]: frontend/src/main.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [049/60]: frontend/src/pages/AdminPage.jsx
│ LANGUAGE: jsx | LINES: 260 | SIZE: 12500 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useCallback } from "react";
     2	import { useNavigate } from "react-router-dom";
     3	import { useApp } from "../store";
     4	import {
     5	  adminStats,
     6	  adminListUsers,
     7	  adminCreateUser,
     8	  adminUpdateUser,
     9	  adminDeleteUser,
    10	} from "../api";
    11	import {
    12	  ArrowLeft, Users, MessageSquare, Database, Zap,
    13	  UserPlus, Trash2, Shield, ShieldOff, Save, X,
    14	} from "lucide-react";
    15	
    16	export default function AdminPage() {
    17	  const { state } = useApp();
    18	  const navigate = useNavigate();
    19	  const [stats, setStats] = useState(null);
    20	  const [users, setUsers] = useState([]);
    21	  const [showCreate, setShowCreate] = useState(false);
    22	  const [editId, setEditId] = useState(null);
    23	  const [editData, setEditData] = useState({});
    24	  const [newUser, setNewUser] = useState({
    25	    username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000,
    26	  });
    27	  const [error, setError] = useState("");
    28	
    29	  const load = useCallback(async () => {
    30	    try {
    31	      const [s, u] = await Promise.all([
    32	        adminStats(state.token),
    33	        adminListUsers(state.token),
    34	      ]);
    35	      setStats(s);
    36	      setUsers(u);
    37	    } catch (err) {
    38	      setError(err.message);
    39	    }
    40	  }, [state.token]);
    41	
    42	  useEffect(() => { load(); }, [load]);
    43	
    44	  async function handleCreate(e) {
    45	    e.preventDefault();
    46	    try {
    47	      await adminCreateUser(state.token, newUser);
    48	      setShowCreate(false);
    49	      setNewUser({ username: "", email: "", password: "", role: "user", quota_tokens_monthly: 2000000 });
    50	      load();
    51	    } catch (err) { setError(err.message); }
    52	  }
    53	
    54	  async function handleSaveEdit(userId) {
    55	    try {
    56	      await adminUpdateUser(state.token, userId, editData);
    57	      setEditId(null);
    58	      load();
    59	    } catch (err) { setError(err.message); }
    60	  }
    61	
    62	  async function handleDelete(userId, username) {
    63	    if (!confirm(`Delete user "${username}"? This is permanent.`)) return;
    64	    try {
    65	      await adminDeleteUser(state.token, userId);
    66	      load();
    67	    } catch (err) { setError(err.message); }
    68	  }
    69	
    70	  function formatNum(n) {
    71	    if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
    72	    if (n >= 1_000) return (n / 1_000).toFixed(0) + "K";
    73	    return String(n);
    74	  }
    75	
    76	  if (state.user?.role !== "superadmin") {
    77	    return (
    78	      <div className="h-full flex items-center justify-center">
    79	        <p className="text-anton-danger text-lg">⛔ Access Denied</p>
    80	      </div>
    81	    );
    82	  }
    83	
    84	  return (
    85	    <div className="h-full overflow-y-auto bg-anton-bg p-6">
    86	      <div className="max-w-6xl mx-auto space-y-6 animate-fade-in">
    87	        {/* Header */}
    88	        <div className="flex items-center gap-4">
    89	          <button onClick={() => navigate("/")} className="p-2 rounded-lg bg-anton-surface border border-anton-border hover:border-anton-accent transition">
    90	            <ArrowLeft size={20} />
    91	          </button>
    92	          <div>
    93	            <h1 className="text-2xl font-bold text-white flex items-center gap-2">
    94	              <Shield size={24} className="text-anton-accent" /> Admin Panel
    95	            </h1>
    96	            <p className="text-anton-muted text-sm">Manage everything</p>
    97	          </div>
    98	        </div>
    99	
   100	        {error && (
   101	          <div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
   102	            {error}
   103	            <button onClick={() => setError("")} className="ml-2 text-red-300 hover:text-white">✕</button>
   104	          </div>
   105	        )}
   106	
   107	        {/* Stats */}
   108	        {stats && (
   109	          <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
   110	            {[
   111	              { label: "Users", value: stats.total_users, icon: Users, color: "text-blue-400" },
   112	              { label: "Chats", value: stats.total_chats, icon: MessageSquare, color: "text-green-400" },
   113	              { label: "Messages", value: formatNum(stats.total_messages), icon: Zap, color: "text-anton-accent" },
   114	              { label: "Tokens Used", value: formatNum(stats.total_tokens_used), icon: Database, color: "text-purple-400" },
   115	            ].map((s) => (
   116	              <div key={s.label} className="bg-anton-surface border border-anton-border rounded-xl p-4">
   117	                <div className="flex items-center gap-2 mb-1">
   118	                  <s.icon size={16} className={s.color} />
   119	                  <span className="text-anton-muted text-sm">{s.label}</span>
   120	                </div>
   121	                <p className="text-2xl font-bold text-white">{s.value}</p>
   122	              </div>
   123	            ))}
   124	          </div>
   125	        )}
   126	
   127	        {/* User Management */}
   128	        <div className="bg-anton-surface border border-anton-border rounded-xl overflow-hidden">
   129	          <div className="px-5 py-4 border-b border-anton-border flex items-center justify-between">
   130	            <h2 className="text-lg font-semibold text-white">Users</h2>
   131	            <button
   132	              onClick={() => setShowCreate(!showCreate)}
   133	              className="flex items-center gap-1.5 px-3 py-1.5 bg-anton-accent text-white rounded-lg text-sm font-medium hover:opacity-90 transition"
   134	            >
   135	              {showCreate ? <X size={14} /> : <UserPlus size={14} />}
   136	              {showCreate ? "Cancel" : "New User"}
   137	            </button>
   138	          </div>
   139	
   140	          {showCreate && (
   141	            <form onSubmit={handleCreate} className="px-5 py-4 border-b border-anton-border bg-anton-card grid grid-cols-1 md:grid-cols-3 gap-3">
   142	              <input placeholder="Username" required value={newUser.username}
   143	                onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
   144	                className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
   145	              />
   146	              <input placeholder="Email" type="email" required value={newUser.email}
   147	                onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
   148	                className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
   149	              />
   150	              <input placeholder="Password" required value={newUser.password}
   151	                onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
   152	                className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
   153	              />
   154	              <select value={newUser.role}
   155	                onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
   156	                className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
   157	              >
   158	                <option value="user">User</option>
   159	                <option value="admin">Admin</option>
   160	                <option value="superadmin">Superadmin</option>
   161	              </select>
   162	              <input placeholder="Monthly quota" type="number" value={newUser.quota_tokens_monthly}
   163	                onChange={(e) => setNewUser({ ...newUser, quota_tokens_monthly: Number(e.target.value) })}
   164	                className="bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent"
   165	              />
   166	              <button type="submit" className="bg-anton-success text-white rounded-lg px-3 py-2 text-sm font-medium hover:opacity-90">
   167	                Create
   168	              </button>
   169	            </form>
   170	          )}
   171	
   172	          <div className="overflow-x-auto">
   173	            <table className="w-full text-sm">
   174	              <thead>
   175	                <tr className="text-left text-anton-muted border-b border-anton-border">
   176	                  <th className="px-5 py-3">User</th>
   177	                  <th className="px-5 py-3">Role</th>
   178	                  <th className="px-5 py-3">Quota</th>
   179	                  <th className="px-5 py-3">Used</th>
   180	                  <th className="px-5 py-3">Chats</th>
   181	                  <th className="px-5 py-3">Status</th>
   182	                  <th className="px-5 py-3">Actions</th>
   183	                </tr>
   184	              </thead>
   185	              <tbody>
   186	                {users.map((u) => (
   187	                  <tr key={u.id} className="border-b border-anton-border/50 hover:bg-anton-card/50 transition">
   188	                    <td className="px-5 py-3">
   189	                      <div className="text-white font-medium">{u.username}</div>
   190	                      <div className="text-anton-muted text-xs">{u.email}</div>
   191	                    </td>
   192	                    <td className="px-5 py-3">
   193	                      {editId === u.id ? (
   194	                        <select value={editData.role ?? u.role}
   195	                          onChange={(e) => setEditData({ ...editData, role: e.target.value })}
   196	                          className="bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs"
   197	                        >
   198	                          <option value="user">user</option>
   199	                          <option value="admin">admin</option>
   200	                          <option value="superadmin">superadmin</option>
   201	                        </select>
   202	                      ) : (
   203	                        <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
   204	                          u.role === "superadmin" ? "bg-anton-accent/20 text-anton-accent"
   205	                          : u.role === "admin" ? "bg-blue-500/20 text-blue-400"
   206	                          : "bg-anton-border text-anton-muted"
   207	                        }`}>
   208	                          {u.role}
   209	                        </span>
   210	                      )}
   211	                    </td>
   212	                    <td className="px-5 py-3 text-anton-muted">
   213	                      {editId === u.id ? (
   214	                        <input type="number" value={editData.quota_tokens_monthly ?? u.quota_tokens_monthly}
   215	                          onChange={(e) => setEditData({ ...editData, quota_tokens_monthly: Number(e.target.value) })}
   216	                          className="bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs w-28"
   217	                        />
   218	                      ) : formatNum(u.quota_tokens_monthly)}
   219	                    </td>
   220	                    <td className="px-5 py-3 text-anton-muted">
   221	                      {formatNum(u.tokens_used_this_month)}
   222	                    </td>
   223	                    <td className="px-5 py-3 text-anton-muted">{u.chat_count}</td>
   224	                    <td className="px-5 py-3">
   225	                      <span className={`w-2 h-2 inline-block rounded-full mr-1 ${u.is_active ? "bg-anton-success" : "bg-anton-danger"}`} />
   226	                      <span className="text-xs text-anton-muted">{u.is_active ? "Active" : "Disabled"}</span>
   227	                    </td>
   228	                    <td className="px-5 py-3">
   229	                      <div className="flex items-center gap-1">
   230	                        {editId === u.id ? (
   231	                          <>
   232	                            <button onClick={() => handleSaveEdit(u.id)} className="p-1 rounded hover:bg-anton-success/20 text-anton-success"><Save size={14} /></button>
   233	                            <button onClick={() => setEditId(null)} className="p-1 rounded hover:bg-anton-border text-anton-muted"><X size={14} /></button>
   234	                          </>
   235	                        ) : (
   236	                          <>
   237	                            <button onClick={() => { setEditId(u.id); setEditData({}); }}
   238	                              className="p-1 rounded hover:bg-anton-accent/20 text-anton-accent text-xs">Edit</button>
   239	                            <button onClick={() => adminUpdateUser(state.token, u.id, { is_active: !u.is_active }).then(load)}
   240	                              className="p-1 rounded hover:bg-anton-border text-anton-muted"
   241	                              title={u.is_active ? "Disable" : "Enable"}>
   242	                              {u.is_active ? <ShieldOff size={14} /> : <Shield size={14} />}
   243	                            </button>
   244	                            {u.role !== "superadmin" && (
   245	                              <button onClick={() => handleDelete(u.id, u.username)}
   246	                                className="p-1 rounded hover:bg-red-500/20 text-anton-danger"><Trash2 size={14} /></button>
   247	                            )}
   248	                          </>
   249	                        )}
   250	                      </div>
   251	                    </td>
   252	                  </tr>
   253	                ))}
   254	              </tbody>
   255	            </table>
   256	          </div>
   257	        </div>
   258	      </div>
   259	    </div>
   260	  );
   261	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [049]: frontend/src/pages/AdminPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [050/60]: frontend/src/pages/ChatPage.jsx
│ LANGUAGE: jsx | LINES: 100 | SIZE: 3969 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useEffect } from "react";
     2	import { useApp } from "../store";
     3	import { listChats, createChat } from "../api";
     4	import Sidebar from "../components/Sidebar";
     5	import ChatView from "../components/ChatView";
     6	import { Flame, Menu, Plus, MessageSquare } from "lucide-react";
     7	
     8	export default function ChatPage() {
     9	  const { state, dispatch } = useApp();
    10	
    11	  useEffect(() => {
    12	    (async () => {
    13	      try {
    14	        const chats = await listChats(state.token);
    15	        dispatch({ type: "SET_CHATS", chats });
    16	        if (!state.activeChatId && chats.length > 0) {
    17	          dispatch({ type: "SET_ACTIVE_CHAT", chatId: chats[0].id });
    18	        }
    19	      } catch { /* ignore */ }
    20	    })();
    21	  }, [state.token]);
    22	
    23	  async function handleNewChat() {
    24	    try {
    25	      const chat = await createChat(state.token);
    26	      dispatch({ type: "ADD_CHAT", chat });
    27	    } catch { /* ignore */ }
    28	  }
    29	
    30	  return (
    31	    <div className="h-full h-dvh flex overflow-hidden bg-anton-bg">
    32	      {/* Desktop sidebar */}
    33	      <div className="hidden sm:flex">
    34	        <Sidebar />
    35	      </div>
    36	
    37	      {/* Mobile sidebar overlay */}
    38	      {state.sidebarOpen && (
    39	        <>
    40	          <div
    41	            className="sm:hidden fixed inset-0 z-40 bg-black/60 animate-overlay-in"
    42	            onClick={() => dispatch({ type: "SET_SIDEBAR_OPEN", open: false })}
    43	          />
    44	          <div className="sm:hidden fixed inset-y-0 left-0 z-50 w-[280px] animate-slide-in safe-top safe-bottom">
    45	            <Sidebar mobile onClose={() => dispatch({ type: "SET_SIDEBAR_OPEN", open: false })} />
    46	          </div>
    47	        </>
    48	      )}
    49	
    50	      {/* Main content */}
    51	      <div className="flex-1 flex flex-col min-w-0">
    52	        {/* Mobile header */}
    53	        <div className="sm:hidden flex items-center gap-2 px-3 py-2.5 border-b border-anton-border bg-anton-surface safe-top">
    54	          <button
    55	            onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })}
    56	            className="p-2 -ml-1 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
    57	          >
    58	            <Menu size={20} />
    59	          </button>
    60	          <div className="flex-1 min-w-0 flex items-center gap-2">
    61	            <div className="w-6 h-6 rounded-md bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shrink-0">
    62	              <Flame size={12} className="text-white" />
    63	            </div>
    64	            <span className="text-sm font-medium text-white truncate">
    65	              {state.chats.find((c) => c.id === state.activeChatId)?.title || "Son of Anton"}
    66	            </span>
    67	          </div>
    68	          <button
    69	            onClick={handleNewChat}
    70	            className="p-2 -mr-1 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
    71	          >
    72	            <Plus size={20} />
    73	          </button>
    74	        </div>
    75	
    76	        {/* Chat or empty state */}
    77	        {state.activeChatId ? (
    78	          <ChatView chatId={state.activeChatId} />
    79	        ) : (
    80	          <div className="flex-1 flex items-center justify-center p-6">
    81	            <div className="text-center max-w-sm">
    82	              <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
    83	                <Flame size={32} className="text-white" />
    84	              </div>
    85	              <h2 className="text-xl font-bold text-white mb-2">Son of Anton</h2>
    86	              <p className="text-anton-muted text-sm mb-6">
    87	                Avatar of All Elements of Code
    88	              </p>
    89	              <button
    90	                onClick={handleNewChat}
    91	                className="inline-flex items-center gap-2 px-5 py-3 bg-anton-accent text-white rounded-xl font-medium hover:opacity-90 transition active:scale-95"
    92	              >
    93	                <MessageSquare size={18} /> Start a conversation
    94	              </button>
    95	            </div>
    96	          </div>
    97	        )}
    98	      </div>
    99	    </div>
   100	  );
   101	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [050]: frontend/src/pages/ChatPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [051/60]: frontend/src/pages/GitLabPage.jsx
│ LANGUAGE: jsx | LINES: 539 | SIZE: 31384 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useCallback } from "react";
     2	import { useNavigate } from "react-router-dom";
     3	import { useApp } from "../store";
     4	import {
     5	    gitlabGetConfig, gitlabSaveConfig, gitlabTestConnection,
     6	    gitlabListProjects, gitlabCreateProject, gitlabDeleteProject,
     7	    gitlabGetTree, gitlabGetFile, gitlabListBranches, gitlabCreateBranch,
     8	    gitlabListOperations, gitlabApproveOperation, gitlabRejectOperation,
     9	    gitlabDeleteOperation, gitlabDirectCommit, gitlabAuditLog,
    10	    gitlabListPipelines, gitlabTriggerPipeline, gitlabListMRs,
    11	} from "../api";
    12	import {
    13	    ArrowLeft, Settings, FolderGit2, ListChecks, ScrollText,
    14	    Plus, Trash2, RefreshCw, Check, X, ExternalLink, GitBranch,
    15	    File, Folder, ChevronRight, ChevronDown, Loader2, Shield,
    16	    Zap, Clock, CheckCircle, XCircle, AlertCircle, Play, Eye,
    17	    Copy, TerminalSquare, GitMerge, Rocket,
    18	} from "lucide-react";
    19	
    20	const TABS = [
    21	    { id: "settings", label: "Settings", icon: Settings },
    22	    { id: "repos", label: "Repositories", icon: FolderGit2 },
    23	    { id: "operations", label: "Operations", icon: ListChecks },
    24	    { id: "audit", label: "Audit Log", icon: ScrollText },
    25	];
    26	
    27	const STATUS_STYLES = {
    28	    pending: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
    29	    executed: "bg-green-500/20 text-green-400 border-green-500/30",
    30	    failed: "bg-red-500/20 text-red-400 border-red-500/30",
    31	    rejected: "bg-gray-500/20 text-gray-400 border-gray-500/30",
    32	};
    33	
    34	export default function GitLabPage() {
    35	    const { state } = useApp();
    36	    const nav = useNavigate();
    37	    const [tab, setTab] = useState("settings");
    38	    const [config, setConfig] = useState(null);
    39	    const [loading, setLoading] = useState(true);
    40	
    41	    useEffect(() => {
    42	        (async () => {
    43	            try {
    44	                const c = await gitlabGetConfig(state.token);
    45	                setConfig(c);
    46	            } catch { /* ignore */ }
    47	            setLoading(false);
    48	        })();
    49	    }, [state.token]);
    50	
    51	    if (state.user?.role !== "superadmin") {
    52	        return (
    53	            <div className="h-dvh flex items-center justify-center bg-anton-bg">
    54	                <div className="text-center">
    55	                    <Shield size={48} className="text-red-500 mx-auto mb-4" />
    56	                    <h1 className="text-xl font-bold text-white mb-2">Access Denied</h1>
    57	                    <p className="text-anton-muted">Superadmin access required.</p>
    58	                    <button onClick={() => nav("/")} className="mt-4 px-4 py-2 bg-anton-accent rounded-lg text-white text-sm">Back to Chat</button>
    59	                </div>
    60	            </div>
    61	        );
    62	    }
    63	
    64	    return (
    65	        <div className="h-dvh flex flex-col bg-anton-bg text-white">
    66	            {/* Header */}
    67	            <div className="border-b border-anton-border bg-anton-surface px-4 py-3 flex items-center gap-3 shrink-0">
    68	                <button onClick={() => nav("/")} className="p-1.5 rounded-lg hover:bg-anton-card text-anton-muted hover:text-white">
    69	                    <ArrowLeft size={18} />
    70	                </button>
    71	                <FolderGit2 size={20} className="text-orange-400" />
    72	                <h1 className="text-lg font-bold">GitLab Command Center</h1>
    73	                <span className="text-xs text-anton-muted ml-auto">Superadmin Only</span>
    74	            </div>
    75	
    76	            {/* Tabs */}
    77	            <div className="border-b border-anton-border bg-anton-surface px-4 flex gap-1 shrink-0 overflow-x-auto">
    78	                {TABS.map((t) => (
    79	                    <button
    80	                        key={t.id}
    81	                        onClick={() => setTab(t.id)}
    82	                        className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition whitespace-nowrap ${tab === t.id ? "border-orange-400 text-orange-400" : "border-transparent text-anton-muted hover:text-white"
    83	                            }`}
    84	                    >
    85	                        <t.icon size={14} />
    86	                        {t.label}
    87	                    </button>
    88	                ))}
    89	            </div>
    90	
    91	            {/* Content */}
    92	            <div className="flex-1 overflow-y-auto p-4">
    93	                {loading ? (
    94	                    <div className="flex items-center justify-center py-20">
    95	                        <Loader2 className="animate-spin text-anton-accent" size={24} />
    96	                    </div>
    97	                ) : (
    98	                    <>
    99	                        {tab === "settings" && <SettingsTab config={config} setConfig={setConfig} token={state.token} />}
   100	                        {tab === "repos" && <ReposTab config={config} token={state.token} />}
   101	                        {tab === "operations" && <OperationsTab token={state.token} />}
   102	                        {tab === "audit" && <AuditTab token={state.token} />}
   103	                    </>
   104	                )}
   105	            </div>
   106	        </div>
   107	    );
   108	}
   109	
   110	// ══════════════════════════════════════════════════════
   111	//  Settings Tab
   112	// ══════════════════════════════════════════════════════
   113	function SettingsTab({ config, setConfig, token }) {
   114	    const [url, setUrl] = useState(config?.gitlab_url || "");
   115	    const [pat, setPat] = useState("");
   116	    const [ns, setNs] = useState(config?.default_namespace || "");
   117	    const [saving, setSaving] = useState(false);
   118	    const [testing, setTesting] = useState(false);
   119	    const [msg, setMsg] = useState(null);
   120	
   121	    async function handleTest() {
   122	        if (!url || !pat) return;
   123	        setTesting(true); setMsg(null);
   124	        try {
   125	            const r = await gitlabTestConnection(token, { gitlab_url: url, access_token: pat });
   126	            setMsg(r.ok ? { type: "success", text: `Connected as ${r.name} (@${r.user})${r.is_admin ? " [Admin]" : ""}` } : { type: "error", text: r.error });
   127	        } catch (e) { setMsg({ type: "error", text: e.message }); }
   128	        setTesting(false);
   129	    }
   130	
   131	    async function handleSave() {
   132	        if (!url || !pat) return;
   133	        setSaving(true); setMsg(null);
   134	        try {
   135	            const r = await gitlabSaveConfig(token, { gitlab_url: url, access_token: pat, default_namespace: ns || null });
   136	            setMsg({ type: "success", text: `Saved! Connected as ${r.name} (@${r.user})` });
   137	            setConfig({ configured: true, gitlab_url: url });
   138	        } catch (e) { setMsg({ type: "error", text: e.message }); }
   139	        setSaving(false);
   140	    }
   141	
   142	    return (
   143	        <div className="max-w-xl mx-auto space-y-6">
   144	            <div className="bg-anton-card border border-anton-border rounded-xl p-5 space-y-4">
   145	                <h2 className="text-lg font-bold flex items-center gap-2"><Settings size={18} className="text-orange-400" /> GitLab Connection</h2>
   146	                {config?.configured && <p className="text-xs text-green-400">✓ Currently connected to {config.gitlab_url} (token: {config.token_masked})</p>}
   147	
   148	                <div>
   149	                    <label className="text-xs text-anton-muted block mb-1">GitLab URL</label>
   150	                    <input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://gitlab.yourdomain.com" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-orange-400" />
   151	                </div>
   152	
   153	                <div>
   154	                    <label className="text-xs text-anton-muted block mb-1">Personal Access Token</label>
   155	                    <input type="password" value={pat} onChange={(e) => setPat(e.target.value)} placeholder="glpat-xxxxxxxxxxxxxxxxxxxx" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-orange-400" />
   156	                    <p className="text-[10px] text-anton-muted mt-1">Needs scopes: api, read_repository, write_repository</p>
   157	                </div>
   158	
   159	                <div>
   160	                    <label className="text-xs text-anton-muted block mb-1">Default Namespace (optional)</label>
   161	                    <input value={ns} onChange={(e) => setNs(e.target.value)} placeholder="your-group" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-orange-400" />
   162	                </div>
   163	
   164	                {msg && (
   165	                    <div className={`text-sm p-3 rounded-lg ${msg.type === "success" ? "bg-green-500/10 text-green-400 border border-green-500/20" : "bg-red-500/10 text-red-400 border border-red-500/20"}`}>
   166	                        {msg.text}
   167	                    </div>
   168	                )}
   169	
   170	                <div className="flex gap-2">
   171	                    <button onClick={handleTest} disabled={testing || !url || !pat} className="px-4 py-2 bg-anton-card border border-anton-border rounded-lg text-sm hover:border-orange-400 disabled:opacity-30 flex items-center gap-1.5">
   172	                        {testing ? <Loader2 size={14} className="animate-spin" /> : <Zap size={14} />} Test
   173	                    </button>
   174	                    <button onClick={handleSave} disabled={saving || !url || !pat} className="px-4 py-2 bg-orange-500 rounded-lg text-sm text-white hover:bg-orange-600 disabled:opacity-30 flex items-center gap-1.5">
   175	                        {saving ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />} Save & Connect
   176	                    </button>
   177	                </div>
   178	            </div>
   179	        </div>
   180	    );
   181	}
   182	
   183	// ══════════════════════════════════════════════════════
   184	//  Repos Tab
   185	// ══════════════════════════════════════════════════════
   186	function ReposTab({ config, token }) {
   187	    const [projects, setProjects] = useState([]);
   188	    const [search, setSearch] = useState("");
   189	    const [loading, setLoading] = useState(false);
   190	    const [showCreate, setShowCreate] = useState(false);
   191	    const [newName, setNewName] = useState("");
   192	    const [newDesc, setNewDesc] = useState("");
   193	    const [creating, setCreating] = useState(false);
   194	    const [selectedProject, setSelectedProject] = useState(null);
   195	
   196	    const loadProjects = useCallback(async () => {
   197	        setLoading(true);
   198	        try {
   199	            const r = await gitlabListProjects(token, search);
   200	            setProjects(r.projects || []);
   201	        } catch { /* ignore */ }
   202	        setLoading(false);
   203	    }, [token, search]);
   204	
   205	    useEffect(() => { if (config?.configured) loadProjects(); }, [config, loadProjects]);
   206	
   207	    async function handleCreate() {
   208	        if (!newName.trim()) return;
   209	        setCreating(true);
   210	        try {
   211	            await gitlabCreateProject(token, { name: newName, description: newDesc });
   212	            setNewName(""); setNewDesc(""); setShowCreate(false);
   213	            loadProjects();
   214	        } catch { /* ignore */ }
   215	        setCreating(false);
   216	    }
   217	
   218	    if (!config?.configured) return <p className="text-anton-muted text-center py-10">Configure GitLab connection in Settings first.</p>;
   219	
   220	    if (selectedProject) {
   221	        return <ProjectDetail project={selectedProject} token={token} onBack={() => { setSelectedProject(null); loadProjects(); }} />;
   222	    }
   223	
   224	    return (
   225	        <div className="space-y-4">
   226	            <div className="flex items-center gap-2 flex-wrap">
   227	                <input value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={(e) => e.key === "Enter" && loadProjects()} placeholder="Search repos…" className="flex-1 min-w-[200px] bg-anton-card border border-anton-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-orange-400" />
   228	                <button onClick={loadProjects} className="p-2 bg-anton-card border border-anton-border rounded-lg hover:border-orange-400"><RefreshCw size={16} /></button>
   229	                <button onClick={() => setShowCreate(!showCreate)} className="px-3 py-2 bg-orange-500 rounded-lg text-sm text-white flex items-center gap-1.5 hover:bg-orange-600"><Plus size={14} /> New Repo</button>
   230	            </div>
   231	
   232	            {showCreate && (
   233	                <div className="bg-anton-card border border-orange-500/30 rounded-xl p-4 space-y-3 animate-fade-in">
   234	                    <input value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="Repository name" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-orange-400" />
   235	                    <input value={newDesc} onChange={(e) => setNewDesc(e.target.value)} placeholder="Description (optional)" className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-orange-400" />
   236	                    <div className="flex gap-2">
   237	                        <button onClick={handleCreate} disabled={creating || !newName.trim()} className="px-4 py-2 bg-orange-500 rounded-lg text-sm text-white disabled:opacity-30 flex items-center gap-1.5">
   238	                            {creating ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />} Create
   239	                        </button>
   240	                        <button onClick={() => setShowCreate(false)} className="px-4 py-2 bg-anton-card border border-anton-border rounded-lg text-sm">Cancel</button>
   241	                    </div>
   242	                </div>
   243	            )}
   244	
   245	            {loading ? (
   246	                <div className="flex justify-center py-10"><Loader2 className="animate-spin text-anton-accent" /></div>
   247	            ) : (
   248	                <div className="grid gap-2">
   249	                    {projects.map((p) => (
   250	                        <button key={p.id} onClick={() => setSelectedProject(p)} className="w-full text-left bg-anton-card border border-anton-border rounded-xl p-4 hover:border-orange-400/50 transition group">
   251	                            <div className="flex items-start justify-between">
   252	                                <div className="min-w-0">
   253	                                    <div className="font-semibold text-sm truncate group-hover:text-orange-400 transition">{p.path_with_namespace || p.name}</div>
   254	                                    {p.description && <p className="text-xs text-anton-muted mt-0.5 truncate">{p.description}</p>}
   255	                                </div>
   256	                                <ChevronRight size={16} className="text-anton-muted shrink-0 mt-0.5" />
   257	                            </div>
   258	                            <div className="flex gap-3 mt-2 text-[10px] text-anton-muted">
   259	                                <span>{p.visibility}</span>
   260	                                <span>{p.default_branch || "main"}</span>
   261	                                {p.last_activity_at && <span>{new Date(p.last_activity_at).toLocaleDateString()}</span>}
   262	                            </div>
   263	                        </button>
   264	                    ))}
   265	                    {!projects.length && <p className="text-anton-muted text-center py-6 text-sm">No repositories found.</p>}
   266	                </div>
   267	            )}
   268	        </div>
   269	    );
   270	}
   271	
   272	// ══════════════════════════════════════════════════════
   273	//  Project Detail (File Browser + Actions)
   274	// ══════════════════════════════════════════════════════
   275	function ProjectDetail({ project, token, onBack }) {
   276	    const [tree, setTree] = useState([]);
   277	    const [path, setPath] = useState("");
   278	    const [branches, setBranches] = useState([]);
   279	    const [branch, setBranch] = useState(project.default_branch || "main");
   280	    const [loading, setLoading] = useState(false);
   281	    const [fileContent, setFileContent] = useState(null);
   282	    const [pipelines, setPipelines] = useState([]);
   283	    const [subTab, setSubTab] = useState("files");
   284	
   285	    const loadTree = useCallback(async () => {
   286	        setLoading(true);
   287	        try {
   288	            const t = await gitlabGetTree(token, project.id, path, branch);
   289	            setTree(t.sort((a, b) => (a.type === "tree" ? -1 : 1) - (b.type === "tree" ? -1 : 1) || a.name.localeCompare(b.name)));
   290	        } catch { setTree([]); }
   291	        setLoading(false);
   292	    }, [token, project.id, path, branch]);
   293	
   294	    useEffect(() => {
   295	        loadTree();
   296	        gitlabListBranches(token, project.id).then(setBranches).catch(() => { });
   297	    }, [loadTree, token, project.id]);
   298	
   299	    async function openFile(filePath) {
   300	        try {
   301	            const f = await gitlabGetFile(token, project.id, filePath, branch);
   302	            setFileContent({ ...f, path: filePath });
   303	        } catch { /* ignore */ }
   304	    }
   305	
   306	    async function loadPipelines() {
   307	        try {
   308	            const p = await gitlabListPipelines(token, project.id);
   309	            setPipelines(p);
   310	        } catch { setPipelines([]); }
   311	    }
   312	
   313	    return (
   314	        <div className="space-y-3">
   315	            <div className="flex items-center gap-2 flex-wrap">
   316	                <button onClick={onBack} className="p-1.5 rounded-lg hover:bg-anton-card text-anton-muted hover:text-white"><ArrowLeft size={16} /></button>
   317	                <h2 className="font-bold text-sm">{project.path_with_namespace}</h2>
   318	                <select value={branch} onChange={(e) => { setBranch(e.target.value); setPath(""); setFileContent(null); }} className="ml-auto bg-anton-card border border-anton-border rounded-lg px-2 py-1 text-xs text-white">
   319	                    {branches.map((b) => <option key={b.name} value={b.name}>{b.name}</option>)}
   320	                </select>
   321	            </div>
   322	
   323	            {/* Sub-tabs */}
   324	            <div className="flex gap-1 border-b border-anton-border pb-1">
   325	                {[{ id: "files", label: "Files", icon: Folder }, { id: "pipelines", label: "Pipelines", icon: Rocket }].map((t) => (
   326	                    <button key={t.id} onClick={() => { setSubTab(t.id); if (t.id === "pipelines") loadPipelines(); }}
   327	                        className={`flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg transition ${subTab === t.id ? "bg-orange-500/20 text-orange-400" : "text-anton-muted hover:text-white"}`}>
   328	                        <t.icon size={12} />{t.label}
   329	                    </button>
   330	                ))}
   331	            </div>
   332	
   333	            {subTab === "files" && (
   334	                <>
   335	                    {/* Breadcrumb */}
   336	                    {path && (
   337	                        <div className="flex items-center gap-1 text-xs text-anton-muted flex-wrap">
   338	                            <button onClick={() => { setPath(""); setFileContent(null); }} className="hover:text-orange-400">{project.name}</button>
   339	                            {path.split("/").map((seg, i, arr) => (
   340	                                <React.Fragment key={i}>
   341	                                    <ChevronRight size={10} />
   342	                                    <button onClick={() => { setPath(arr.slice(0, i + 1).join("/")); setFileContent(null); }} className="hover:text-orange-400">{seg}</button>
   343	                                </React.Fragment>
   344	                            ))}
   345	                        </div>
   346	                    )}
   347	
   348	                    {fileContent ? (
   349	                        <div className="bg-anton-card border border-anton-border rounded-xl overflow-hidden">
   350	                            <div className="flex items-center justify-between px-3 py-2 border-b border-anton-border bg-anton-surface">
   351	                                <span className="text-xs font-mono text-anton-muted">{fileContent.path}</span>
   352	                                <div className="flex gap-1.5">
   353	                                    <button onClick={() => { navigator.clipboard.writeText(fileContent.decoded_content || ""); }} className="text-[10px] text-anton-muted hover:text-white flex items-center gap-1"><Copy size={10} /> Copy</button>
   354	                                    <button onClick={() => setFileContent(null)} className="text-[10px] text-anton-muted hover:text-white"><X size={12} /></button>
   355	                                </div>
   356	                            </div>
   357	                            <pre className="p-3 text-xs text-green-300 font-mono overflow-auto max-h-[60vh] whitespace-pre-wrap">{fileContent.decoded_content || "[Binary]"}</pre>
   358	                        </div>
   359	                    ) : loading ? (
   360	                        <div className="flex justify-center py-10"><Loader2 className="animate-spin text-anton-accent" /></div>
   361	                    ) : (
   362	                        <div className="space-y-0.5">
   363	                            {tree.map((item) => (
   364	                                <button
   365	                                    key={item.id || item.name}
   366	                                    onClick={() => {
   367	                                        if (item.type === "tree") {
   368	                                            setPath(item.path);
   369	                                        } else {
   370	                                            openFile(item.path);
   371	                                        }
   372	                                    }}
   373	                                    className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-anton-card transition text-left"
   374	                                >
   375	                                    {item.type === "tree" ? <Folder size={14} className="text-blue-400 shrink-0" /> : <File size={14} className="text-anton-muted shrink-0" />}
   376	                                    <span className="text-sm truncate">{item.name}</span>
   377	                                </button>
   378	                            ))}
   379	                            {!tree.length && <p className="text-anton-muted text-center py-6 text-sm">Empty directory.</p>}
   380	                        </div>
   381	                    )}
   382	                </>
   383	            )}
   384	
   385	            {subTab === "pipelines" && (
   386	                <div className="space-y-2">
   387	                    <button onClick={() => gitlabTriggerPipeline(token, project.id, branch).then(loadPipelines)} className="px-3 py-1.5 bg-green-600 rounded-lg text-xs text-white flex items-center gap-1.5 hover:bg-green-700">
   388	                        <Play size={12} /> Run Pipeline
   389	                    </button>
   390	                    {pipelines.map((p) => (
   391	                        <div key={p.id} className="bg-anton-card border border-anton-border rounded-lg px-3 py-2 flex items-center gap-3">
   392	                            <span className={`w-2 h-2 rounded-full shrink-0 ${p.status === "success" ? "bg-green-500" : p.status === "failed" ? "bg-red-500" : p.status === "running" ? "bg-blue-500 animate-pulse" : "bg-yellow-500"}`} />
   393	                            <div className="min-w-0 flex-1">
   394	                                <span className="text-xs font-mono">#{p.id}</span>
   395	                                <span className="text-xs text-anton-muted ml-2">{p.ref}</span>
   396	                            </div>
   397	                            <span className="text-[10px] text-anton-muted">{p.status}</span>
   398	                        </div>
   399	                    ))}
   400	                    {!pipelines.length && <p className="text-anton-muted text-center py-6 text-sm">No pipelines yet.</p>}
   401	                </div>
   402	            )}
   403	        </div>
   404	    );
   405	}
   406	
   407	// ══════════════════════════════════════════════════════
   408	//  Operations Tab (THE APPROVAL QUEUE)
   409	// ══════════════════════════════════════════════════════
   410	function OperationsTab({ token }) {
   411	    const [ops, setOps] = useState([]);
   412	    const [filter, setFilter] = useState("pending");
   413	    const [loading, setLoading] = useState(false);
   414	    const [expandedOp, setExpandedOp] = useState(null);
   415	
   416	    const load = useCallback(async () => {
   417	        setLoading(true);
   418	        try { setOps(await gitlabListOperations(token, filter)); } catch { setOps([]); }
   419	        setLoading(false);
   420	    }, [token, filter]);
   421	
   422	    useEffect(() => { load(); }, [load]);
   423	
   424	    async function handleApprove(id) {
   425	        try { await gitlabApproveOperation(token, id); load(); } catch { /* ignore */ }
   426	    }
   427	    async function handleReject(id) {
   428	        try { await gitlabRejectOperation(token, id); load(); } catch { /* ignore */ }
   429	    }
   430	
   431	    return (
   432	        <div className="space-y-4">
   433	            <div className="flex gap-1.5">
   434	                {["pending", "executed", "failed", "rejected", "all"].map((f) => (
   435	                    <button key={f} onClick={() => setFilter(f)}
   436	                        className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${filter === f ? "bg-orange-500/20 text-orange-400" : "text-anton-muted hover:text-white hover:bg-anton-card"}`}>
   437	                        {f.charAt(0).toUpperCase() + f.slice(1)}
   438	                    </button>
   439	                ))}
   440	                <button onClick={load} className="ml-auto p-1.5 hover:bg-anton-card rounded-lg text-anton-muted"><RefreshCw size={14} /></button>
   441	            </div>
   442	
   443	            {loading ? (
   444	                <div className="flex justify-center py-10"><Loader2 className="animate-spin text-anton-accent" /></div>
   445	            ) : (
   446	                <div className="space-y-2">
   447	                    {ops.map((op) => (
   448	                        <div key={op.id} className={`bg-anton-card border rounded-xl overflow-hidden ${STATUS_STYLES[op.status] || "border-anton-border"}`}>
   449	                            <button onClick={() => setExpandedOp(expandedOp === op.id ? null : op.id)} className="w-full px-4 py-3 flex items-center gap-3 text-left">
   450	                                <span className={`text-xs font-bold uppercase px-2 py-0.5 rounded ${STATUS_STYLES[op.status]}`}>{op.status}</span>
   451	                                <div className="min-w-0 flex-1">
   452	                                    <div className="text-sm font-medium">{op.operation_type.replace(/_/g, " ")}</div>
   453	                                    <div className="text-[10px] text-anton-muted">{op.project_name || `Project #${op.project_id}`} • {op.branch || "main"} • {new Date(op.created_at).toLocaleString()}</div>
   454	                                </div>
   455	                                {expandedOp === op.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
   456	                            </button>
   457	
   458	                            {expandedOp === op.id && (
   459	                                <div className="px-4 pb-3 border-t border-anton-border/50 pt-3 space-y-3 animate-fade-in">
   460	                                    <div className="bg-anton-bg rounded-lg p-3">
   461	                                        <p className="text-[10px] text-anton-muted mb-1 font-bold">PAYLOAD</p>
   462	                                        <pre className="text-xs font-mono text-green-300 whitespace-pre-wrap max-h-60 overflow-y-auto">{JSON.stringify(op.payload, null, 2)}</pre>
   463	                                    </div>
   464	
   465	                                    {op.result && (
   466	                                        <div className="bg-anton-bg rounded-lg p-3">
   467	                                            <p className="text-[10px] text-anton-muted mb-1 font-bold">RESULT</p>
   468	                                            <pre className="text-xs font-mono text-blue-300 whitespace-pre-wrap max-h-40 overflow-y-auto">{typeof op.result === "string" ? op.result : JSON.stringify(op.result, null, 2)}</pre>
   469	                                        </div>
   470	                                    )}
   471	
   472	                                    {op.status === "pending" && (
   473	                                        <div className="flex gap-2">
   474	                                            <button onClick={() => handleApprove(op.id)} className="px-4 py-2 bg-green-600 rounded-lg text-sm text-white flex items-center gap-1.5 hover:bg-green-700">
   475	                                                <CheckCircle size={14} /> Approve & Execute
   476	                                            </button>
   477	                                            <button onClick={() => handleReject(op.id)} className="px-4 py-2 bg-red-600 rounded-lg text-sm text-white flex items-center gap-1.5 hover:bg-red-700">
   478	                                                <XCircle size={14} /> Reject
   479	                                            </button>
   480	                                        </div>
   481	                                    )}
   482	                                </div>
   483	                            )}
   484	                        </div>
   485	                    ))}
   486	                    {!ops.length && <p className="text-anton-muted text-center py-10 text-sm">No operations found.</p>}
   487	                </div>
   488	            )}
   489	        </div>
   490	    );
   491	}
   492	
   493	// ══════════════════════════════════════════════════════
   494	//  Audit Tab
   495	// ══════════════════════════════════════════════════════
   496	function AuditTab({ token }) {
   497	    const [entries, setEntries] = useState([]);
   498	    const [page, setPage] = useState(1);
   499	    const [total, setTotal] = useState(0);
   500	    const [loading, setLoading] = useState(false);
   501	
   502	    useEffect(() => {
   503	        (async () => {
   504	            setLoading(true);
   505	            try {
   506	                const r = await gitlabAuditLog(token, page);
   507	                setEntries(r.entries || []);
   508	                setTotal(r.total || 0);
   509	            } catch { setEntries([]); }
   510	            setLoading(false);
   511	        })();
   512	    }, [token, page]);
   513	
   514	    return (
   515	        <div className="space-y-4">
   516	            <h2 className="text-sm font-bold text-anton-muted">{total} total audit entries</h2>
   517	            {loading ? (
   518	                <div className="flex justify-center py-10"><Loader2 className="animate-spin text-anton-accent" /></div>
   519	            ) : (
   520	                <div className="space-y-1">
   521	                    {entries.map((e) => (
   522	                        <div key={e.id} className="bg-anton-card border border-anton-border rounded-lg px-3 py-2 flex items-start gap-3">
   523	                            <Clock size={12} className="text-anton-muted mt-0.5 shrink-0" />
   524	                            <div className="min-w-0 flex-1">
   525	                                <div className="text-xs font-medium text-orange-400">{e.action}</div>
   526	                                {e.details && <p className="text-[10px] text-anton-muted mt-0.5">{e.details}</p>}
   527	                            </div>
   528	                            <span className="text-[10px] text-anton-muted whitespace-nowrap">{new Date(e.created_at).toLocaleString()}</span>
   529	                        </div>
   530	                    ))}
   531	                    {entries.length === 50 && (
   532	                        <div className="flex justify-center pt-2">
   533	                            <button onClick={() => setPage((p) => p + 1)} className="text-xs text-orange-400 hover:underline">Load more</button>
   534	                        </div>
   535	                    )}
   536	                </div>
   537	            )}
   538	        </div>
   539	    );
   540	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [051]: frontend/src/pages/GitLabPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [052/60]: frontend/src/pages/KnowledgePage.jsx
│ LANGUAGE: jsx | LINES: 384 | SIZE: 20241 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useRef } from "react";
     2	import { useApp } from "../store";
     3	import { useNavigate } from "react-router-dom";
     4	import {
     5	    listKnowledgeBases,
     6	    createKnowledgeBase,
     7	    getKnowledgeBase,
     8	    updateKnowledgeBase,
     9	    deleteKnowledgeBase,
    10	    deleteKnowledgeDocument,
    11	    uploadDocuments,
    12	} from "../api";
    13	import {
    14	    BookOpen, Plus, Trash2, FileText, Upload, ArrowLeft, Edit3,
    15	    Check, X, ChevronRight, Database, Hash, Type, Calendar,
    16	    AlertTriangle, Loader2, Search, Flame, RefreshCw,
    17	} from "lucide-react";
    18	
    19	function fmtSize(b) {
    20	    if (!b) return "0 B";
    21	    if (b < 1024) return b + " B";
    22	    if (b < 1048576) return (b / 1024).toFixed(1) + " KB";
    23	    return (b / 1048576).toFixed(1) + " MB";
    24	}
    25	
    26	function fmtDate(s) {
    27	    if (!s) return "";
    28	    try {
    29	        return new Date(s).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
    30	    } catch {
    31	        return s;
    32	    }
    33	}
    34	
    35	export default function KnowledgePage() {
    36	    const { state } = useApp();
    37	    const navigate = useNavigate();
    38	    const token = state.token;
    39	
    40	    const [kbs, setKbs] = useState([]);
    41	    const [selectedKb, setSelectedKb] = useState(null);
    42	    const [loading, setLoading] = useState(true);
    43	    const [error, setError] = useState("");
    44	
    45	    // Create KB state
    46	    const [showCreate, setShowCreate] = useState(false);
    47	    const [newName, setNewName] = useState("");
    48	    const [newDesc, setNewDesc] = useState("");
    49	    const [creating, setCreating] = useState(false);
    50	
    51	    // Edit KB state
    52	    const [editing, setEditing] = useState(false);
    53	    const [editName, setEditName] = useState("");
    54	    const [editDesc, setEditDesc] = useState("");
    55	    const [saving, setSaving] = useState(false);
    56	
    57	    // Upload state
    58	    const [uploading, setUploading] = useState(false);
    59	    const [uploadResult, setUploadResult] = useState(null);
    60	    const fileRef = useRef(null);
    61	
    62	    // Delete doc state
    63	    const [deletingDocId, setDeletingDocId] = useState(null);
    64	    const [confirmDeleteKb, setConfirmDeleteKb] = useState(false);
    65	
    66	    // Search
    67	    const [docSearch, setDocSearch] = useState("");
    68	
    69	    useEffect(() => {
    70	        loadKbs();
    71	    }, []);
    72	
    73	    async function loadKbs() {
    74	        setLoading(true);
    75	        setError("");
    76	        try {
    77	            const data = await listKnowledgeBases(token);
    78	            setKbs(data);
    79	        } catch (e) {
    80	            setError(e.message);
    81	        } finally {
    82	            setLoading(false);
    83	        }
    84	    }
    85	
    86	    async function loadKbDetail(kbId) {
    87	        setError("");
    88	        try {
    89	            const data = await getKnowledgeBase(token, kbId);
    90	            setSelectedKb(data);
    91	        } catch (e) {
    92	            setError(e.message);
    93	        }
    94	    }
    95	
    96	    async function handleCreate() {
    97	        if (!newName.trim()) return;
    98	        setCreating(true);
    99	        setError("");
   100	        try {
   101	            await createKnowledgeBase(token, newName.trim(), newDesc.trim());
   102	            setNewName("");
   103	            setNewDesc("");
   104	            setShowCreate(false);
   105	            await loadKbs();
   106	        } catch (e) {
   107	            setError(e.message);
   108	        } finally {
   109	            setCreating(false);
   110	        }
   111	    }
   112	
   113	    async function handleDeleteKb() {
   114	        if (!selectedKb) return;
   115	        setError("");
   116	        try {
   117	            await deleteKnowledgeBase(token, selectedKb.id);
   118	            setSelectedKb(null);
   119	            setConfirmDeleteKb(false);
   120	            await loadKbs();
   121	        } catch (e) {
   122	            setError(e.message);
   123	        }
   124	    }
   125	
   126	    async function handleSaveEdit() {
   127	        if (!selectedKb) return;
   128	        setSaving(true);
   129	        setError("");
   130	        try {
   131	            await updateKnowledgeBase(token, selectedKb.id, {
   132	                name: editName.trim() || selectedKb.name,
   133	                description: editDesc.trim(),
   134	            });
   135	            setEditing(false);
   136	            await loadKbDetail(selectedKb.id);
   137	            await loadKbs();
   138	        } catch (e) {
   139	            setError(e.message);
   140	        } finally {
   141	            setSaving(false);
   142	        }
   143	    }
   144	
   145	    async function handleUpload(e) {
   146	        const files = Array.from(e.target.files || []);
   147	        if (!files.length || !selectedKb) return;
   148	        e.target.value = "";
   149	        setUploading(true);
   150	        setUploadResult(null);
   151	        setError("");
   152	        try {
   153	            const result = await uploadDocuments(token, selectedKb.id, files);
   154	            setUploadResult(result);
   155	            await loadKbDetail(selectedKb.id);
   156	            await loadKbs();
   157	        } catch (err) {
   158	            setError(err.message);
   159	        } finally {
   160	            setUploading(false);
   161	        }
   162	    }
   163	
   164	    async function handleDeleteDoc(docId) {
   165	        if (!selectedKb) return;
   166	        setDeletingDocId(docId);
   167	        setError("");
   168	        try {
   169	            await deleteKnowledgeDocument(token, selectedKb.id, docId);
   170	            await loadKbDetail(selectedKb.id);
   171	            await loadKbs();
   172	        } catch (e) {
   173	            setError(e.message);
   174	        } finally {
   175	            setDeletingDocId(null);
   176	        }
   177	    }
   178	
   179	    function openKb(kb) {
   180	        setSelectedKb(null);
   181	        setEditing(false);
   182	        setUploadResult(null);
   183	        setDocSearch("");
   184	        setConfirmDeleteKb(false);
   185	        loadKbDetail(kb.id);
   186	    }
   187	
   188	    function goBack() {
   189	        setSelectedKb(null);
   190	        setEditing(false);
   191	        setUploadResult(null);
   192	        setDocSearch("");
   193	        setConfirmDeleteKb(false);
   194	    }
   195	
   196	    const filteredDocs = (selectedKb?.documents || []).filter((d) =>
   197	        d.filename.toLowerCase().includes(docSearch.toLowerCase())
   198	    );
   199	
   200	    // ─── KB Detail View ───
   201	    if (selectedKb) {
   202	        return (
   203	            <div className="h-dvh flex flex-col bg-anton-bg text-anton-text">
   204	                {/* Header */}
   205	                <div className="border-b border-anton-border bg-anton-surface px-4 py-3 flex items-center gap-3">
   206	                    <button onClick={goBack} className="text-anton-muted hover:text-white transition"><ArrowLeft size={20} /></button>
   207	                    <div className="flex-1 min-w-0">
   208	                        {editing ? (
   209	                            <div className="flex items-center gap-2">
   210	                                <input value={editName} onChange={(e) => setEditName(e.target.value)} className="bg-anton-card border border-anton-border rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-anton-accent flex-1" placeholder="Name" />
   211	                                <button onClick={handleSaveEdit} disabled={saving} className="p-1.5 rounded-lg bg-anton-accent text-white hover:opacity-80 transition disabled:opacity-50">{saving ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}</button>
   212	                                <button onClick={() => setEditing(false)} className="p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"><X size={14} /></button>
   213	                            </div>
   214	                        ) : (
   215	                            <div className="flex items-center gap-2">
   216	                                <h1 className="text-lg font-bold text-white truncate">{selectedKb.name}</h1>
   217	                                <button onClick={() => { setEditName(selectedKb.name); setEditDesc(selectedKb.description || ""); setEditing(true); }} className="text-anton-muted hover:text-anton-accent transition"><Edit3 size={14} /></button>
   218	                            </div>
   219	                        )}
   220	                        {!editing && selectedKb.description && <p className="text-xs text-anton-muted truncate mt-0.5">{selectedKb.description}</p>}
   221	                    </div>
   222	                    <button onClick={() => navigate("/")} className="text-anton-muted hover:text-white transition text-sm">← Chat</button>
   223	                </div>
   224	
   225	                {/* Edit description row */}
   226	                {editing && (
   227	                    <div className="px-4 py-2 border-b border-anton-border bg-anton-surface">
   228	                        <textarea value={editDesc} onChange={(e) => setEditDesc(e.target.value)} placeholder="Description (optional)" rows={2} className="w-full bg-anton-card border border-anton-border rounded-lg px-3 py-2 text-white text-sm resize-none focus:outline-none focus:border-anton-accent" />
   229	                    </div>
   230	                )}
   231	
   232	                {error && <div className="px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-red-400 text-sm flex items-center gap-2"><AlertTriangle size={14} />{error}</div>}
   233	
   234	                {/* Stats bar */}
   235	                <div className="px-4 py-2 border-b border-anton-border bg-anton-surface/50 flex items-center gap-4 text-xs text-anton-muted flex-wrap">
   236	                    <span className="flex items-center gap-1"><FileText size={12} />{selectedKb.document_count} docs</span>
   237	                    <span className="flex items-center gap-1"><Hash size={12} />{selectedKb.chunk_count} chunks</span>
   238	                    <span className="flex items-center gap-1"><Type size={12} />{(selectedKb.estimated_tokens || 0).toLocaleString()} est. tokens</span>
   239	                    <span className="flex items-center gap-1"><Calendar size={12} />{fmtDate(selectedKb.created_at)}</span>
   240	                </div>
   241	
   242	                {/* Actions bar */}
   243	                <div className="px-4 py-2 border-b border-anton-border bg-anton-surface/30 flex items-center gap-2 flex-wrap">
   244	                    <button onClick={() => fileRef.current?.click()} disabled={uploading} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-anton-accent text-white text-sm hover:opacity-80 transition disabled:opacity-50">
   245	                        {uploading ? <Loader2 size={14} className="animate-spin" /> : <Upload size={14} />}
   246	                        {uploading ? "Uploading…" : "Upload Files"}
   247	                    </button>
   248	                    <input ref={fileRef} type="file" multiple className="hidden" accept=".pdf,.txt,.md,.py,.js,.ts,.jsx,.tsx,.cs,.java,.cpp,.c,.h,.go,.rs,.rb,.php,.html,.css,.json,.yaml,.yml,.xml,.toml,.csv,.sql,.sh,.swift,.kt,.lua,.gd,.dart,.vue,.svelte,.log,.doc,.docx" onChange={handleUpload} />
   249	                    <button onClick={() => loadKbDetail(selectedKb.id)} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-anton-border text-anton-muted text-sm hover:text-white hover:bg-anton-card transition"><RefreshCw size={14} /> Refresh</button>
   250	                    <div className="flex-1" />
   251	                    {confirmDeleteKb ? (
   252	                        <div className="flex items-center gap-2">
   253	                            <span className="text-red-400 text-xs">Delete entire KB?</span>
   254	                            <button onClick={handleDeleteKb} className="px-3 py-1.5 rounded-lg bg-red-600 text-white text-sm hover:opacity-80 transition">Yes, Delete</button>
   255	                            <button onClick={() => setConfirmDeleteKb(false)} className="px-3 py-1.5 rounded-lg border border-anton-border text-anton-muted text-sm hover:text-white transition">Cancel</button>
   256	                        </div>
   257	                    ) : (
   258	                        <button onClick={() => setConfirmDeleteKb(true)} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-red-500/30 text-red-400 text-sm hover:bg-red-500/10 transition"><Trash2 size={14} /> Delete KB</button>
   259	                    )}
   260	                </div>
   261	
   262	                {/* Upload result */}
   263	                {uploadResult && (
   264	                    <div className="px-4 py-2 border-b border-anton-border bg-green-500/5">
   265	                        <p className="text-xs text-green-400 font-semibold mb-1">Upload complete: {uploadResult.total_chunks_added} chunks from {uploadResult.total_files} file(s)</p>
   266	                        {uploadResult.files?.map((f, i) => (
   267	                            <p key={i} className={`text-xs ${f.error ? "text-red-400" : "text-anton-muted"}`}>
   268	                                {f.filename}: {f.error ? `Error: ${f.error}` : `${f.chunks_added} chunks, ~${f.estimated_tokens?.toLocaleString()} tokens`}
   269	                            </p>
   270	                        ))}
   271	                    </div>
   272	                )}
   273	
   274	                {/* Search */}
   275	                {(selectedKb.documents || []).length > 3 && (
   276	                    <div className="px-4 py-2 border-b border-anton-border">
   277	                        <div className="relative">
   278	                            <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-anton-muted" />
   279	                            <input value={docSearch} onChange={(e) => setDocSearch(e.target.value)} placeholder="Search documents…" className="w-full bg-anton-card border border-anton-border rounded-lg pl-9 pr-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" />
   280	                        </div>
   281	                    </div>
   282	                )}
   283	
   284	                {/* Document list */}
   285	                <div className="flex-1 overflow-y-auto px-4 py-3">
   286	                    {filteredDocs.length === 0 ? (
   287	                        <div className="text-center py-16">
   288	                            <Database size={40} className="mx-auto text-anton-muted mb-3 opacity-50" />
   289	                            <p className="text-anton-muted text-sm">{docSearch ? "No documents match your search." : "No documents yet. Upload some files!"}</p>
   290	                        </div>
   291	                    ) : (
   292	                        <div className="space-y-1.5">
   293	                            {filteredDocs.map((doc) => (
   294	                                <div key={doc.id} className="flex items-center gap-3 px-3 py-2.5 rounded-lg border border-anton-border bg-anton-card hover:border-anton-accent/30 transition group">
   295	                                    <FileText size={16} className="text-anton-accent shrink-0" />
   296	                                    <div className="flex-1 min-w-0">
   297	                                        <p className="text-sm text-white truncate font-medium">{doc.filename}</p>
   298	                                        <p className="text-[11px] text-anton-muted">{fmtSize(doc.file_size)} · {doc.chunk_count} chunks · {fmtDate(doc.created_at)}</p>
   299	                                    </div>
   300	                                    <button
   301	                                        onClick={() => handleDeleteDoc(doc.id)}
   302	                                        disabled={deletingDocId === doc.id}
   303	                                        className="p-1.5 rounded-lg text-anton-muted hover:text-red-400 hover:bg-red-500/10 transition opacity-0 group-hover:opacity-100 disabled:opacity-50"
   304	                                        title="Remove document and its vector chunks"
   305	                                    >
   306	                                        {deletingDocId === doc.id ? <Loader2 size={14} className="animate-spin" /> : <Trash2 size={14} />}
   307	                                    </button>
   308	                                </div>
   309	                            ))}
   310	                        </div>
   311	                    )}
   312	                </div>
   313	            </div>
   314	        );
   315	    }
   316	
   317	    // ─── KB List View ───
   318	    return (
   319	        <div className="h-dvh flex flex-col bg-anton-bg text-anton-text">
   320	            {/* Header */}
   321	            <div className="border-b border-anton-border bg-anton-surface px-4 py-3 flex items-center gap-3">
   322	                <button onClick={() => navigate("/")} className="text-anton-muted hover:text-white transition"><ArrowLeft size={20} /></button>
   323	                <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center">
   324	                    <Flame size={18} className="text-white" />
   325	                </div>
   326	                <div className="flex-1">
   327	                    <h1 className="text-lg font-bold text-white">Knowledge Bases</h1>
   328	                    <p className="text-xs text-anton-muted">Manage your RAG document collections</p>
   329	                </div>
   330	                <button onClick={() => setShowCreate(!showCreate)} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition ${showCreate ? "bg-anton-card text-anton-muted" : "bg-anton-accent text-white hover:opacity-80"}`}>
   331	                    {showCreate ? <X size={14} /> : <Plus size={14} />}
   332	                    {showCreate ? "Cancel" : "New KB"}
   333	                </button>
   334	            </div>
   335	
   336	            {error && <div className="px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-red-400 text-sm flex items-center gap-2"><AlertTriangle size={14} />{error}</div>}
   337	
   338	            {/* Create form */}
   339	            {showCreate && (
   340	                <div className="px-4 py-3 border-b border-anton-border bg-anton-surface/50 space-y-2 animate-fade-in">
   341	                    <input value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="Knowledge base name" className="w-full bg-anton-card border border-anton-border rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-anton-accent" onKeyDown={(e) => e.key === "Enter" && handleCreate()} />
   342	                    <textarea value={newDesc} onChange={(e) => setNewDesc(e.target.value)} placeholder="Description (optional)" rows={2} className="w-full bg-anton-card border border-anton-border rounded-lg px-3 py-2 text-white text-sm resize-none focus:outline-none focus:border-anton-accent" />
   343	                    <button onClick={handleCreate} disabled={!newName.trim() || creating} className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-anton-accent text-white text-sm hover:opacity-80 transition disabled:opacity-50">
   344	                        {creating ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />} Create
   345	                    </button>
   346	                </div>
   347	            )}
   348	
   349	            {/* KB list */}
   350	            <div className="flex-1 overflow-y-auto px-4 py-3">
   351	                {loading ? (
   352	                    <div className="flex items-center justify-center py-20"><Loader2 size={24} className="text-anton-accent animate-spin" /></div>
   353	                ) : kbs.length === 0 ? (
   354	                    <div className="text-center py-20">
   355	                        <BookOpen size={48} className="mx-auto text-anton-muted mb-4 opacity-50" />
   356	                        <p className="text-anton-muted text-sm">No knowledge bases yet.</p>
   357	                        <p className="text-anton-muted text-xs mt-1">Create one to start uploading documents for RAG.</p>
   358	                    </div>
   359	                ) : (
   360	                    <div className="space-y-2">
   361	                        {kbs.map((kb) => (
   362	                            <button
   363	                                key={kb.id}
   364	                                onClick={() => openKb(kb)}
   365	                                className="w-full text-left flex items-center gap-3 px-4 py-3 rounded-xl border border-anton-border bg-anton-card hover:border-anton-accent/40 hover:bg-anton-card/80 transition group"
   366	                            >
   367	                                <div className="w-10 h-10 rounded-lg bg-anton-accent/10 flex items-center justify-center shrink-0">
   368	                                    <BookOpen size={18} className="text-anton-accent" />
   369	                                </div>
   370	                                <div className="flex-1 min-w-0">
   371	                                    <p className="text-sm text-white font-semibold truncate">{kb.name}</p>
   372	                                    {kb.description && <p className="text-xs text-anton-muted truncate">{kb.description}</p>}
   373	                                    <p className="text-[11px] text-anton-muted mt-0.5">
   374	                                        {kb.document_count} docs · {kb.chunk_count} chunks · ~{(kb.estimated_tokens || 0).toLocaleString()} tokens
   375	                                    </p>
   376	                                </div>
   377	                                <ChevronRight size={16} className="text-anton-muted group-hover:text-anton-accent transition shrink-0" />
   378	                            </button>
   379	                        ))}
   380	                    </div>
   381	                )}
   382	            </div>
   383	        </div>
   384	    );
   385	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [052]: frontend/src/pages/KnowledgePage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [053/60]: frontend/src/pages/LoginPage.jsx
│ LANGUAGE: jsx | LINES: 122 | SIZE: 4955 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import { useApp } from "../store";
     3	import { login, register } from "../api";
     4	import { Flame, Eye, EyeOff, Loader2 } from "lucide-react";
     5	
     6	export default function LoginPage() {
     7	  const { dispatch } = useApp();
     8	  const [isRegister, setIsRegister] = useState(false);
     9	  const [username, setUsername] = useState("");
    10	  const [email, setEmail] = useState("");
    11	  const [password, setPassword] = useState("");
    12	  const [showPw, setShowPw] = useState(false);
    13	  const [error, setError] = useState("");
    14	  const [loading, setLoading] = useState(false);
    15	
    16	  async function handleSubmit(e) {
    17	    e.preventDefault();
    18	    setError("");
    19	    setLoading(true);
    20	    try {
    21	      const res = isRegister
    22	        ? await register(username, email, password)
    23	        : await login(username, password);
    24	      dispatch({ type: "LOGIN", token: res.token, user: res.user });
    25	    } catch (err) {
    26	      setError(err.message || "Authentication failed");
    27	    } finally {
    28	      setLoading(false);
    29	    }
    30	  }
    31	
    32	  return (
    33	    <div className="h-full h-dvh flex items-center justify-center bg-anton-bg px-4 safe-top safe-bottom">
    34	      <div className="w-full max-w-sm">
    35	        {/* Logo */}
    36	        <div className="text-center mb-8">
    37	          <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
    38	            <Flame size={32} className="text-white" />
    39	          </div>
    40	          <h1 className="text-2xl font-bold text-white">Son of Anton</h1>
    41	          <p className="text-anton-muted text-sm mt-1">Avatar of All Elements of Code</p>
    42	        </div>
    43	
    44	        {/* Form */}
    45	        <form onSubmit={handleSubmit} className="space-y-4">
    46	          <div>
    47	            <label className="text-xs text-anton-muted mb-1.5 block">Username</label>
    48	            <input
    49	              type="text"
    50	              value={username}
    51	              onChange={(e) => setUsername(e.target.value)}
    52	              className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
    53	              placeholder="Enter username"
    54	              required
    55	              autoComplete="username"
    56	              autoCapitalize="off"
    57	            />
    58	          </div>
    59	
    60	          {isRegister && (
    61	            <div>
    62	              <label className="text-xs text-anton-muted mb-1.5 block">Email</label>
    63	              <input
    64	                type="email"
    65	                value={email}
    66	                onChange={(e) => setEmail(e.target.value)}
    67	                className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
    68	                placeholder="your@email.com"
    69	                required
    70	                autoComplete="email"
    71	              />
    72	            </div>
    73	          )}
    74	
    75	          <div>
    76	            <label className="text-xs text-anton-muted mb-1.5 block">Password</label>
    77	            <div className="relative">
    78	              <input
    79	                type={showPw ? "text" : "password"}
    80	                value={password}
    81	                onChange={(e) => setPassword(e.target.value)}
    82	                className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition"
    83	                placeholder="••••••••"
    84	                required
    85	                autoComplete={isRegister ? "new-password" : "current-password"}
    86	              />
    87	              <button
    88	                type="button"
    89	                onClick={() => setShowPw(!showPw)}
    90	                className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition p-1"
    91	              >
    92	                {showPw ? <EyeOff size={18} /> : <Eye size={18} />}
    93	              </button>
    94	            </div>
    95	          </div>
    96	
    97	          {error && (
    98	            <div className="bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5">
    99	              {error}
   100	            </div>
   101	          )}
   102	
   103	          <button
   104	            type="submit"
   105	            disabled={loading}
   106	            className="w-full py-3.5 bg-anton-accent text-white rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50 active:scale-[0.98] flex items-center justify-center gap-2"
   107	          >
   108	            {loading && <Loader2 size={18} className="animate-spin" />}
   109	            {isRegister ? "Create Account" : "Sign In"}
   110	          </button>
   111	
   112	          <button
   113	            type="button"
   114	            onClick={() => { setIsRegister(!isRegister); setError(""); }}
   115	            className="w-full text-center text-sm text-anton-muted hover:text-white transition py-2"
   116	          >
   117	            {isRegister ? "Already have an account? Sign in" : "Need an account? Register"}
   118	          </button>
   119	        </form>
   120	      </div>
   121	    </div>
   122	  );
   123	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [053]: frontend/src/pages/LoginPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [054/60]: frontend/src/store.jsx
│ LANGUAGE: jsx | LINES: 104 | SIZE: 2820 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { createContext, useContext, useReducer, useCallback } from "react";
     2	
     3	const AppContext = createContext(null);
     4	
     5	const initialState = {
     6	  token: localStorage.getItem("token") || null,
     7	  user: null,
     8	  chats: [],
     9	  activeChatId: null,
    10	  chatMessages: {},
    11	  activeStreams: {},
    12	  sidebarOpen: false,
    13	};
    14	
    15	function reducer(state, action) {
    16	  switch (action.type) {
    17	    case "LOGIN":
    18	      localStorage.setItem("token", action.token);
    19	      return { ...state, token: action.token, user: action.user };
    20	    case "LOGOUT":
    21	      localStorage.removeItem("token");
    22	      return { ...initialState, token: null };
    23	    case "SET_USER":
    24	      return { ...state, user: action.user };
    25	
    26	    case "SET_CHATS":
    27	      return { ...state, chats: action.chats };
    28	    case "SET_ACTIVE_CHAT":
    29	      return { ...state, activeChatId: action.chatId, sidebarOpen: false };
    30	    case "ADD_CHAT":
    31	      return {
    32	        ...state,
    33	        chats: [action.chat, ...state.chats],
    34	        activeChatId: action.chat.id,
    35	        sidebarOpen: false,
    36	      };
    37	    case "UPDATE_CHAT":
    38	      return {
    39	        ...state,
    40	        chats: state.chats.map((c) =>
    41	          c.id === action.chat.id ? { ...c, ...action.chat } : c
    42	        ),
    43	      };
    44	    case "REMOVE_CHAT": {
    45	      const remaining = state.chats.filter((c) => c.id !== action.chatId);
    46	      return {
    47	        ...state,
    48	        chats: remaining,
    49	        activeChatId:
    50	          state.activeChatId === action.chatId
    51	            ? remaining[0]?.id || null
    52	            : state.activeChatId,
    53	      };
    54	    }
    55	
    56	    case "SET_MESSAGES":
    57	      return {
    58	        ...state,
    59	        chatMessages: { ...state.chatMessages, [action.chatId]: action.messages },
    60	      };
    61	    case "ADD_MESSAGE": {
    62	      const prev = state.chatMessages[action.chatId] || [];
    63	      return {
    64	        ...state,
    65	        chatMessages: {
    66	          ...state.chatMessages,
    67	          [action.chatId]: [...prev, action.message],
    68	        },
    69	      };
    70	    }
    71	
    72	    case "SET_STREAMING":
    73	      return {
    74	        ...state,
    75	        activeStreams: action.streaming
    76	          ? { ...state.activeStreams, [action.chatId]: true }
    77	          : Object.fromEntries(
    78	              Object.entries(state.activeStreams).filter(([k]) => k !== action.chatId)
    79	            ),
    80	      };
    81	
    82	    case "SET_SIDEBAR_OPEN":
    83	      return { ...state, sidebarOpen: action.open };
    84	    case "TOGGLE_SIDEBAR":
    85	      return { ...state, sidebarOpen: !state.sidebarOpen };
    86	
    87	    default:
    88	      return state;
    89	  }
    90	}
    91	fveth t rtjtdudbftjndfj djdrjndfjnbed rt jrstjdrtj srdtjb bdrnjdfbj dfsgjdrf
    92	export function AppProvider({ children }) {
    93	  const [state, dispatch] = useReducer(reducer, initialState);
    94	  return (
    95	    <AppContext.Provider value={{ state, dispatch }}>
    96	      {children}
    97	    </AppContext.Provider>
    98	  );
    99	}
   100	
   101	export function useApp() {
   102	  const ctx = useContext(AppContext);
   103	  if (!ctx) throw new Error("useApp must be inside AppProvider");
   104	  return ctx;
   105	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [054]: frontend/src/store.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [055/60]: frontend/src/streamManager.js
│ LANGUAGE: javascript | LINES: 158 | SIZE: 5635 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import { streamMessage } from "./api";
     2	
     3	const _streams = new Map();
     4	const _listeners = new Map();
     5	let _dispatch = null;
     6	
     7	export function setDispatch(dispatch) {
     8	  _dispatch = dispatch;
     9	}
    10	
    11	export function getStreamData(chatId) {
    12	  const s = _streams.get(chatId);
    13	  if (!s) return { streaming: false, text: "", thinking: "", isThinking: false };
    14	  return { streaming: true, text: s.text, thinking: s.thinking, isThinking: s.isThinking };
    15	}
    16	
    17	export function isStreaming(chatId) {
    18	  return _streams.has(chatId);
    19	}
    20	
    21	export function subscribe(chatId, cb) {
    22	  if (!_listeners.has(chatId)) _listeners.set(chatId, new Set());
    23	  _listeners.get(chatId).add(cb);
    24	  return () => {
    25	    const s = _listeners.get(chatId);
    26	    if (s) { s.delete(cb); if (!s.size) _listeners.delete(chatId); }
    27	  };
    28	}
    29	
    30	function _notify(id) {
    31	  const s = _listeners.get(id);
    32	  if (s) s.forEach((cb) => cb());
    33	}
    34	
    35	export function abortStream(chatId) {
    36	  const s = _streams.get(chatId);
    37	  if (s) {
    38	    s.abortController.abort();
    39	    _streams.delete(chatId);
    40	    _notify(chatId);
    41	    if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false });
    42	  }
    43	}
    44	
    45	export function startStream({ token, chatId, body }) {
    46	  if (_streams.has(chatId)) return;
    47	  const ac = new AbortController();
    48	  _streams.set(chatId, { text: "", thinking: "", isThinking: false, abortController: ac });
    49	  if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: true });
    50	  _notify(chatId);
    51	
    52	  (async () => {
    53	    const s = _streams.get(chatId);
    54	    if (!s) return;
    55	    let usage = {};
    56	    let msgId = "";
    57	    try {
    58	      for await (const evt of streamMessage(token, chatId, body, ac.signal)) {
    59	        if (ac.signal.aborted || !_streams.has(chatId)) break;
    60	        _handleEvent(chatId, s, evt, (u) => { usage = u; }, (id) => { msgId = id; });
    61	      }
    62	      if (!ac.signal.aborted && _dispatch) {
    63	        _dispatch({
    64	          type: "ADD_MESSAGE", chatId, message: {
    65	            id: msgId || `gen-${Date.now()}`, role: "assistant", content: s.text,
    66	            thinking_content: s.thinking || null, input_tokens: usage.input_tokens || 0,
    67	            output_tokens: usage.output_tokens || 0, created_at: new Date().toISOString(), attachments: [],
    68	          }
    69	        });
    70	      }
    71	    } catch (err) {
    72	      if (!ac.signal.aborted && _dispatch) {
    73	        _dispatch({
    74	          type: "ADD_MESSAGE", chatId, message: {
    75	            id: `err-${Date.now()}`, role: "assistant", content: `**Error:** ${err.message}`,
    76	            created_at: new Date().toISOString(), attachments: [],
    77	          }
    78	        });
    79	      }
    80	    } finally {
    81	      _streams.delete(chatId);
    82	      _notify(chatId);
    83	      if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false });
    84	    }
    85	  })();
    86	}
    87	
    88	/**
    89	 * Reconnect to an ongoing background generation via GET /stream endpoint.
    90	 */
    91	export function reconnectStream({ token, chatId }) {
    92	  if (_streams.has(chatId)) return;
    93	  const ac = new AbortController();
    94	  _streams.set(chatId, { text: "", thinking: "", isThinking: false, abortController: ac });
    95	  if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: true });
    96	  _notify(chatId);
    97	
    98	  (async () => {
    99	    const s = _streams.get(chatId);
   100	    if (!s) return;
   101	    let usage = {};
   102	    let msgId = "";
   103	    try {
   104	      const res = await fetch(`/api/chats/${chatId}/stream`, {
   105	        headers: { Authorization: `Bearer ${token}` },
   106	        signal: ac.signal,
   107	      });
   108	      if (!res.ok) throw new Error("Reconnect failed");
   109	
   110	      const reader = res.body.getReader();
   111	      const decoder = new TextDecoder();
   112	      let buffer = "";
   113	      while (true) {
   114	        const { done, value } = await reader.read();
   115	        if (done) break;
   116	        buffer += decoder.decode(value, { stream: true });
   117	        const parts = buffer.split("\n\n");
   118	        buffer = parts.pop() || "";
   119	        for (const part of parts) {
   120	          const line = part.trim();
   121	          if (line.startsWith("data: ")) {
   122	            try {
   123	              const evt = JSON.parse(line.slice(6));
   124	              if (ac.signal.aborted || !_streams.has(chatId)) break;
   125	              _handleEvent(chatId, s, evt, (u) => { usage = u; }, (id) => { msgId = id; });
   126	            } catch { /* skip */ }
   127	          }
   128	        }
   129	      }
   130	      if (!ac.signal.aborted && s.text && _dispatch) {
   131	        _dispatch({
   132	          type: "ADD_MESSAGE", chatId, message: {
   133	            id: msgId || `gen-${Date.now()}`, role: "assistant", content: s.text,
   134	            thinking_content: s.thinking || null, input_tokens: usage.input_tokens || 0,
   135	            output_tokens: usage.output_tokens || 0, created_at: new Date().toISOString(), attachments: [],
   136	          }
   137	        });
   138	      }
   139	    } catch { /* reconnect failed, generation may be done */ }
   140	    finally {
   141	      _streams.delete(chatId);
   142	      _notify(chatId);
   143	      if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false });
   144	    }
   145	  })();
   146	}
   147	
   148	function _handleEvent(chatId, s, evt, setUsage, setMsgId) {
   149	  switch (evt.type) {
   150	    case "thinking_start": s.isThinking = true; _notify(chatId); break;
   151	    case "thinking_delta": s.thinking += evt.content; _notify(chatId); break;
   152	    case "thinking_end": s.isThinking = false; _notify(chatId); break;
   153	    case "text_delta": s.text += evt.content; _notify(chatId); break;
   154	    case "usage": setUsage({ input_tokens: evt.input_tokens, output_tokens: evt.output_tokens }); break;
   155	    case "title_update": if (_dispatch) _dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, title: evt.title } }); break;
   156	    case "done": setMsgId(evt.message_id); break;
   157	    case "error": s.text += `\n\n**Error:** ${evt.message}`; _notify(chatId); break;
   158	  }
   159	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [055]: frontend/src/streamManager.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [056/60]: frontend/tailwind.config.js
│ LANGUAGE: javascript | LINES: 26 | SIZE: 714 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	/** @type {import('tailwindcss').Config} */
     2	export default {
     3	  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
     4	  theme: {
     5	    extend: {
     6	      colors: {
     7	        "anton-bg": "#09090f",
     8	        "anton-surface": "#0f0f1a",
     9	        "anton-card": "#16162a",
    10	        "anton-border": "#1e1e3a",
    11	        "anton-text": "#e2e2ea",
    12	        "anton-muted": "#6b6b8a",
    13	        "anton-accent": "#e53e3e",
    14	        "anton-success": "#48bb78",
    15	        "anton-danger": "#e53e3e",
    16	      },
    17	      fontFamily: {
    18	        sans: ["Inter", "system-ui", "-apple-system", "sans-serif"],
    19	        mono: ["JetBrains Mono", "Fira Code", "monospace"],
    20	      },
    21	      screens: {
    22	        "xs": "400px",
    23	      },
    24	    },
    25	  },
    26	  plugins: [],
    27	};│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [056]: frontend/tailwind.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [057/60]: frontend/vite.config.js
│ LANGUAGE: javascript | LINES: 23 | SIZE: 596 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import { defineConfig } from "vite";
     2	import react from "@vitejs/plugin-react";
     3	
     4	export default defineConfig({
     5	  plugins: [react()],
     6	  server: {
     7	    proxy: {
     8	      "/api": "http://localhost:80",
     9	    },
    10	  },
    11	  build: {
    12	    // Content-hash all chunks so browsers fetch new versions
    13	    rollupOptions: {
    14	      output: {
    15	        entryFileNames: "assets/[name]-[hash].js",
    16	        chunkFileNames: "assets/[name]-[hash].js",
    17	        assetFileNames: "assets/[name]-[hash].[ext]",
    18	      },
    19	    },
    20	    // Generate a manifest for cache-busting verification
    21	    manifest: true,
    22	    sourcemap: false,
    23	  },
    24	});│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [057]: frontend/vite.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [058/60]: main.py
│ LANGUAGE: python | LINES: 16 | SIZE: 528 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# This is a sample Python script.
     2	
     3	# Press Shift+F10 to execute it or replace it with your code.
     4	# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
     5	
     6	
     7	def print_hi(name):
     8	    # Use a breakpoint in the code line below to debug your script.
     9	    print(f'Hi, {name}')  # Press Ctrl+F8 to toggle the breakpoint.
    10	
    11	
    12	# Press the green button in the gutter to run the script.
    13	if __name__ == '__main__':
    14	    print_hi('PyCharm')
    15	
    16	# See PyCharm help at https://www.jetbrains.com/help/pycharm/
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [058]: main.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [059/60]: requirements.txt
│ LANGUAGE: plaintext | LINES: 11 | SIZE: 213 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	fastapi==0.115.6
     2	uvicorn[standard]==0.34.0
     3	sqlalchemy==2.0.36
     4	pyjwt==2.10.1
     5	passlib[bcrypt]==1.7.4
     6	bcrypt==4.0.1
     7	python-multipart==0.0.20
     8	httpx==0.28.1
     9	chromadb==0.6.3
    10	PyPDF2==3.0.1
    11	pydantic==2.10.4
    12	Pillow==11.1.0│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [059]: requirements.txt


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [060/60]: warmup.py
│ LANGUAGE: python | LINES: 6 | SIZE: 221 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import chromadb
     2	
     3	client = chromadb.Client()
     4	col = client.create_collection(name="warmup")
     5	col.add(documents=["warmup embedding model"], ids=["id0"])
     6	client.delete_collection(name="warmup")
     7	print("Embedding model cached.")│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [060]: warmup.py



################################################################################
#                                                                              #
#                     ✅ END OF COMPLETE CODEBASE DUMP                         #
#                                                                              #
#  Total Files:  60                                                         #
#  Total Lines:  14820                                                      #
#  Total Size:   550KB                                                      #
#  Generated:    2026-03-29 17:26:19                                        #
#                                                                              #
#  This file contains EVERYTHING: source code, configs, env vars, Docker,      #
#  CI/CD, build tools, package manifests, docs — the complete picture.          #
#                                                                              #
################################################################################
