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

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

 Generated:       2026-03-19 11:49:08
 Source Dir:       /Users/mahmoudaglan/son-of-anton
 Total Files:      55
 Total Lines:      12494
 Total Size:       441KB

 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/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/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/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 55 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]  147      5KB       backend/main.py
 [010]  17       818B      backend/migrations/005_attachments.sql
 [011]  126      4KB       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]  305      11KB      backend/routes/chat_routes.py
 [019]  48       1KB       backend/routes/files_routes.py
 [020]  206      6KB       backend/routes/knowledge_routes.py
 [021]  127      3KB       backend/routes/messages_patch.py
 [022]  28       855B      backend/seed.py
 [023]  260      8KB       backend/services/attachment_service.py
 [024]  242      8KB       backend/services/bedrock_service.py
 [025]  56       1KB       backend/services/code_extractor.py
 [026]  165      5KB       backend/services/file_processor.py
 [027]  49       1KB       backend/services/memory_service.py
 [028]  76       2KB       backend/services/rag_service.py
 [029]  64       2KB       backend/system_prompt.py
 [030]  182      7KB       create-project.ps1
 [031]  382      15KB      fix-and-deploy.sh
 [032]  34       1KB       frontend/index.html
 [033]  4542     158KB     frontend/package-lock.json
 [034]  27       646B      frontend/package.json
 [035]  5        80B       frontend/postcss.config.js
 [036]  63       1KB       frontend/src/App.jsx
 [037]  159      5KB       frontend/src/api.js
 [038]  158      4KB       frontend/src/components/AttachmentPreview.jsx
 [039]  481      19KB      frontend/src/components/ChatView.jsx
 [040]  109      3KB       frontend/src/components/CodeBlock.jsx
 [041]  81       1KB       frontend/src/components/FileUploadButton.jsx
 [042]  190      8KB       frontend/src/components/MessageBubble.jsx
 [043]  193      8KB       frontend/src/components/Sidebar.jsx
 [044]  287      6KB       frontend/src/index.css
 [045]  15       409B      frontend/src/main.jsx
 [046]  260      12KB      frontend/src/pages/AdminPage.jsx
 [047]  88       3KB       frontend/src/pages/ChatPage.jsx
 [048]  150      5KB       frontend/src/pages/LoginPage.jsx
 [049]  115      2KB       frontend/src/store.jsx
 [050]  74       3KB       frontend/src/streamManager.js
 [051]  33       806B      frontend/tailwind.config.js
 [052]  23       596B      frontend/vite.config.js
 [053]  16       528B      main.py
 [054]  11       213B      requirements.txt
 [055]  6        221B      warmup.py


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

 EXTENSION/TYPE             COUNT
 ───────────────────────    ─────
 .py                        25
 .jsx                       12
 .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/55]: .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/55]: .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/55]: 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/55]: 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/55]: 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/55]: 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/55]: 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/55]: 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/55]: backend/main.py
│ LANGUAGE: python | LINES: 147 | SIZE: 5479 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.services.bedrock_service import close_http_client
    24	
    25	APP_VERSION = "2.1.0"
    26	APP_BUILD_TIME = str(int(time.time()))
    27	
    28	
    29	def _run_migrations():
    30	    """Add new columns/tables to existing DB if they're missing."""
    31	    from sqlalchemy import inspect, text
    32	    try:
    33	        inspector = inspect(engine)
    34	        existing_tables = inspector.get_table_names()
    35	
    36	        if "chats" in existing_tables:
    37	            columns = {c["name"] for c in inspector.get_columns("chats")}
    38	            with engine.connect() as conn:
    39	                if "max_tokens" not in columns:
    40	                    conn.execute(text("ALTER TABLE chats ADD COLUMN max_tokens INTEGER DEFAULT 4096"))
    41	                    print("  Added chats.max_tokens column")
    42	                if "reasoning_budget" not in columns:
    43	                    conn.execute(text("ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"))
    44	                    print("  Added chats.reasoning_budget column")
    45	                conn.commit()
    46	
    47	        if "chat_attachments" not in existing_tables:
    48	            from backend.models import ChatAttachment
    49	            ChatAttachment.__table__.create(bind=engine, checkfirst=True)
    50	            print("  Created chat_attachments table")
    51	
    52	    except Exception as e:
    53	        print(f"  Migration note: {e}")
    54	
    55	
    56	@asynccontextmanager
    57	async def lifespan(app: FastAPI):
    58	    Base.metadata.create_all(bind=engine)
    59	    _run_migrations()
    60	    seed_superadmin()
    61	    print(f"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
    62	    yield
    63	    await close_http_client()
    64	    print("Son of Anton shutting down.")
    65	
    66	
    67	app = FastAPI(
    68	    title="Son of Anton",
    69	    description="Avatar of All Elements of Code",
    70	    version=APP_VERSION,
    71	    lifespan=lifespan,
    72	)
    73	
    74	app.add_middleware(
    75	    CORSMiddleware,
    76	    allow_origins=["*"],
    77	    allow_credentials=True,
    78	    allow_methods=["*"],
    79	    allow_headers=["*"],
    80	)
    81	
    82	
    83	@app.middleware("http")
    84	async def add_cache_headers(request: Request, call_next):
    85	    response: Response = await call_next(request)
    86	    path = request.url.path
    87	
    88	    # API responses: never cache
    89	    if path.startswith("/api"):
    90	        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    91	        response.headers["Pragma"] = "no-cache"
    92	        response.headers["Expires"] = "0"
    93	    # Hashed assets (contain hash in filename): cache aggressively
    94	    elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]):
    95	        response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
    96	    # HTML and everything else: never cache
    97	    elif path.endswith(".html") or not path.startswith("/assets"):
    98	        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    99	        response.headers["Pragma"] = "no-cache"
   100	        response.headers["Expires"] = "0"
   101	
   102	    # Always add version header for debugging
   103	    response.headers["X-App-Version"] = APP_VERSION
   104	    response.headers["X-Build-Time"] = APP_BUILD_TIME
   105	    return response
   106	
   107	
   108	# Version endpoint for frontend to check
   109	@app.get("/api/version")
   110	def get_version():
   111	    return {"version": APP_VERSION, "build": APP_BUILD_TIME}
   112	
   113	
   114	app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
   115	app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
   116	app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
   117	app.include_router(knowledge_router, prefix="/api/knowledge", tags=["Knowledge"])
   118	app.include_router(files_router, prefix="/api/files", tags=["Files"])
   119	app.include_router(attachment_router, prefix="/api", tags=["Attachments"])
   120	
   121	FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
   122	
   123	if (FRONTEND_DIR / "assets").exists():
   124	    app.mount(
   125	        "/assets",
   126	        StaticFiles(directory=str(FRONTEND_DIR / "assets")),
   127	        name="static-assets",
   128	    )
   129	
   130	
   131	@app.get("/{full_path:path}", include_in_schema=False)
   132	async def serve_frontend(full_path: str):
   133	    if full_path.startswith("api"):
   134	        raise HTTPException(status_code=404, detail="Not found")
   135	    file_path = FRONTEND_DIR / full_path
   136	    if full_path and file_path.is_file():
   137	        resp = FileResponse(str(file_path))
   138	        # Don't cache non-hashed static files
   139	        resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   140	        return resp
   141	    index = FRONTEND_DIR / "index.html"
   142	    if index.is_file():
   143	        resp = FileResponse(str(index))
   144	        resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
   145	        resp.headers["Pragma"] = "no-cache"
   146	        resp.headers["Expires"] = "0"
   147	        return resp
   148	    return {"message": "Son of Anton API is running. Frontend not built."}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [009]: backend/main.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [010/55]: 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/55]: backend/models.py
│ LANGUAGE: python | LINES: 126 | SIZE: 4667 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)
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [011]: backend/models.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [012/55]: 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/55]: 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/55]: 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/55]: 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/55]: 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/55]: 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/55]: backend/routes/chat_routes.py
│ LANGUAGE: python | LINES: 305 | SIZE: 11663 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat CRUD and message streaming with multimodal attachment support.
     3	"""
     4	
     5	import json
     6	from datetime import datetime
     7	from pydantic import BaseModel
     8	from typing import Optional
     9	
    10	from fastapi import APIRouter, Depends, HTTPException
    11	from fastapi.responses import StreamingResponse
    12	from sqlalchemy.orm import Session
    13	
    14	from backend.database import get_db, SessionLocal
    15	from backend.models import User, Chat, Message, ChatAttachment
    16	from backend.auth import get_current_user
    17	from backend.system_prompt import build_full_prompt
    18	from backend.services import bedrock_service, memory_service, rag_service, attachment_service
    19	
    20	router = APIRouter()
    21	
    22	
    23	class CreateChatBody(BaseModel):
    24	    title: str = "New Chat"
    25	    model: str = "eu.anthropic.claude-opus-4-6-v1"
    26	    knowledge_base_id: Optional[str] = None
    27	    max_tokens: int = 4096
    28	    reasoning_budget: int = 0
    29	
    30	
    31	class UpdateChatBody(BaseModel):
    32	    title: Optional[str] = None
    33	    model: Optional[str] = None
    34	    max_tokens: Optional[int] = None
    35	    reasoning_budget: Optional[int] = None
    36	    knowledge_base_id: Optional[str] = None
    37	
    38	
    39	class SendMessageBody(BaseModel):
    40	    content: str
    41	    model: Optional[str] = None
    42	    max_tokens: int = 4096
    43	    reasoning_budget: int = 0
    44	    knowledge_base_id: Optional[str] = None
    45	    attachment_ids: list[str] = []
    46	
    47	
    48	@router.get("")
    49	def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    50	    chats = db.query(Chat).filter(Chat.user_id == user.id).order_by(Chat.updated_at.desc()).all()
    51	    return [_chat_dict(c) for c in chats]
    52	
    53	
    54	@router.post("")
    55	def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    56	    chat = Chat(
    57	        user_id=user.id, title=body.title, model=body.model,
    58	        knowledge_base_id=body.knowledge_base_id or None,
    59	        max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget,
    60	    )
    61	    db.add(chat)
    62	    db.commit()
    63	    db.refresh(chat)
    64	    return _chat_dict(chat)
    65	
    66	
    67	@router.get("/{chat_id}")
    68	def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    69	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    70	    if not chat:
    71	        raise HTTPException(404, "Chat not found")
    72	    return _chat_dict(chat)
    73	
    74	
    75	@router.put("/{chat_id}")
    76	def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    77	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    78	    if not chat:
    79	        raise HTTPException(404)
    80	    if body.title is not None:
    81	        chat.title = body.title
    82	    if body.model is not None:
    83	        chat.model = body.model
    84	    if body.max_tokens is not None:
    85	        chat.max_tokens = body.max_tokens
    86	    if body.reasoning_budget is not None:
    87	        chat.reasoning_budget = body.reasoning_budget
    88	    if body.knowledge_base_id is not None:
    89	        chat.knowledge_base_id = body.knowledge_base_id or None
    90	    db.commit()
    91	    return _chat_dict(chat)
    92	
    93	
    94	@router.delete("/{chat_id}")
    95	def delete_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    96	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    97	    if not chat:
    98	        raise HTTPException(404)
    99	    attachment_service.delete_chat_attachments(chat_id)
   100	    db.delete(chat)
   101	    db.commit()
   102	    return {"ok": True}
   103	
   104	
   105	@router.get("/{chat_id}/messages")
   106	def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
   107	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
   108	    if not chat:
   109	        raise HTTPException(404)
   110	    msgs = []
   111	    for m in chat.messages:
   112	        d = _msg_dict(m)
   113	        atts = db.query(ChatAttachment).filter(ChatAttachment.message_id == m.id).all()
   114	        d["attachments"] = [_att_brief(a) for a in atts]
   115	        msgs.append(d)
   116	    return msgs
   117	
   118	
   119	@router.post("/{chat_id}/messages")
   120	async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)):
   121	    user_id = user.id
   122	
   123	    async def generate():
   124	        db = SessionLocal()
   125	        try:
   126	            chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
   127	            if not chat:
   128	                yield _sse({"type": "error", "message": "Chat not found"})
   129	                return
   130	
   131	            db_user = db.query(User).filter(User.id == user_id).first()
   132	
   133	            now = datetime.utcnow()
   134	            if db_user.quota_reset_date and now >= db_user.quota_reset_date:
   135	                db_user.tokens_used_this_month = 0
   136	                if now.month == 12:
   137	                    db_user.quota_reset_date = datetime(now.year + 1, 1, 1)
   138	                else:
   139	                    db_user.quota_reset_date = datetime(now.year, now.month + 1, 1)
   140	                db.commit()
   141	
   142	            if db_user.tokens_used_this_month >= db_user.quota_tokens_monthly:
   143	                yield _sse({"type": "error", "message": "Monthly token quota exceeded."})
   144	                return
   145	
   146	            attachments = []
   147	            if body.attachment_ids:
   148	                attachments = (
   149	                    db.query(ChatAttachment)
   150	                    .filter(ChatAttachment.id.in_(body.attachment_ids), ChatAttachment.chat_id == chat_id)
   151	                    .all()
   152	                )
   153	
   154	            stored_content = body.content
   155	            if attachments:
   156	                labels = {"image": "Image", "video": "Video", "document": "Document", "text": "File"}
   157	                notes = [f"[{labels.get(a.file_type, 'File')}: {a.original_filename}]" for a in attachments]
   158	                stored_content = "\n".join(notes) + "\n" + body.content
   159	
   160	            user_msg = Message(chat_id=chat_id, role="user", content=stored_content)
   161	            db.add(user_msg)
   162	            db.commit()
   163	            db.refresh(user_msg)
   164	
   165	            for att in attachments:
   166	                att.message_id = user_msg.id
   167	            if attachments:
   168	                db.commit()
   169	
   170	            kb_id = body.knowledge_base_id or chat.knowledge_base_id
   171	            rag_context = None
   172	            if kb_id:
   173	                try:
   174	                    rag_context = rag_service.query(kb_id, body.content, n_results=8)
   175	                except Exception:
   176	                    pass
   177	
   178	            system_prompt = build_full_prompt(rag_context)
   179	            messages = memory_service.build_messages(chat, db)
   180	
   181	            if attachments and messages and messages[-1]["role"] == "user":
   182	                content_blocks = attachment_service.build_claude_content_blocks(attachments)
   183	                content_blocks.append({"type": "text", "text": body.content})
   184	                messages[-1]["content"] = content_blocks
   185	
   186	            model_id = body.model or chat.model
   187	            max_tokens = body.max_tokens
   188	            thinking_config = None
   189	            if body.reasoning_budget > 0:
   190	                thinking_config = {"enabled": True, "budget_tokens": body.reasoning_budget}
   191	                max_tokens = max_tokens + body.reasoning_budget
   192	
   193	            full_text = ""
   194	            full_thinking = ""
   195	            input_tokens = 0
   196	            output_tokens = 0
   197	            current_block_type = "text"
   198	
   199	            async for event in bedrock_service.stream_response(
   200	                messages=messages, system_prompt=system_prompt,
   201	                model_id=model_id, max_tokens=min(max_tokens, 65536),
   202	                thinking_config=thinking_config,
   203	            ):
   204	                evt_type = event.get("type", "")
   205	
   206	                if evt_type == "message_start":
   207	                    usage = event.get("message", {}).get("usage", {})
   208	                    input_tokens = usage.get("input_tokens", 0)
   209	                elif evt_type == "content_block_start":
   210	                    blk = event.get("content_block", {})
   211	                    current_block_type = blk.get("type", "text")
   212	                    if current_block_type == "thinking":
   213	                        yield _sse({"type": "thinking_start"})
   214	                elif evt_type == "content_block_delta":
   215	                    delta = event.get("delta", {})
   216	                    dt = delta.get("type", "")
   217	                    if dt == "thinking_delta":
   218	                        t = delta.get("thinking", "")
   219	                        full_thinking += t
   220	                        yield _sse({"type": "thinking_delta", "content": t})
   221	                    elif dt == "text_delta":
   222	                        t = delta.get("text", "")
   223	                        full_text += t
   224	                        yield _sse({"type": "text_delta", "content": t})
   225	                elif evt_type == "content_block_stop":
   226	                    if current_block_type == "thinking":
   227	                        yield _sse({"type": "thinking_end"})
   228	                elif evt_type == "message_delta":
   229	                    usage = event.get("usage", {})
   230	                    output_tokens = usage.get("output_tokens", 0)
   231	
   232	            assistant_msg = Message(
   233	                chat_id=chat_id, role="assistant", content=full_text,
   234	                thinking_content=full_thinking or None,
   235	                input_tokens=input_tokens, output_tokens=output_tokens,
   236	            )
   237	            db.add(assistant_msg)
   238	            db_user.tokens_used_this_month += input_tokens + output_tokens
   239	            chat.model = model_id
   240	            chat.max_tokens = body.max_tokens
   241	            chat.reasoning_budget = body.reasoning_budget
   242	            chat.knowledge_base_id = body.knowledge_base_id or None
   243	            chat.updated_at = datetime.utcnow()
   244	            db.commit()
   245	
   246	            msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
   247	            if msg_count <= 2 and chat.title == "New Chat":
   248	                try:
   249	                    title = await _generate_title(body.content, full_text[:300])
   250	                    chat.title = title[:120]
   251	                    db.commit()
   252	                    yield _sse({"type": "title_update", "title": chat.title})
   253	                except Exception:
   254	                    pass
   255	
   256	            yield _sse({"type": "usage", "input_tokens": input_tokens, "output_tokens": output_tokens})
   257	            yield _sse({"type": "done", "message_id": assistant_msg.id})
   258	
   259	        except Exception as exc:
   260	            yield _sse({"type": "error", "message": str(exc)})
   261	        finally:
   262	            db.close()
   263	
   264	    return StreamingResponse(generate(), media_type="text/event-stream")
   265	
   266	
   267	async def _generate_title(user_msg, ai_msg):
   268	    from backend.config import FAST_MODEL
   269	    result = await bedrock_service.invoke_model_simple(
   270	        model_id=FAST_MODEL,
   271	        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.",
   272	        max_tokens=30,
   273	    )
   274	    return result.strip().strip('"').strip("'")
   275	
   276	
   277	def _sse(data):
   278	    return f"data: {json.dumps(data)}\n\n"
   279	
   280	
   281	def _chat_dict(c):
   282	    return {
   283	        "id": c.id, "title": c.title, "model": c.model,
   284	        "knowledge_base_id": c.knowledge_base_id,
   285	        "max_tokens": c.max_tokens or 4096,
   286	        "reasoning_budget": c.reasoning_budget or 0,
   287	        "created_at": str(c.created_at), "updated_at": str(c.updated_at),
   288	    }
   289	
   290	
   291	def _msg_dict(m):
   292	    return {
   293	        "id": m.id, "role": m.role, "content": m.content,
   294	        "thinking_content": m.thinking_content,
   295	        "input_tokens": m.input_tokens, "output_tokens": m.output_tokens,
   296	        "created_at": str(m.created_at),
   297	    }
   298	
   299	
   300	def _att_brief(a):
   301	    return {
   302	        "id": a.id, "original_filename": a.original_filename,
   303	        "mime_type": a.mime_type, "file_type": a.file_type,
   304	        "file_size": a.file_size,
   305	    }
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [018]: backend/routes/chat_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [019/55]: backend/routes/files_routes.py
│ LANGUAGE: python | LINES: 48 | SIZE: 1238 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Code extraction and file download helpers.
     3	"""
     4	
     5	import io
     6	import zipfile
     7	from pydantic import BaseModel
     8	
     9	from fastapi import APIRouter
    10	from fastapi.responses import StreamingResponse
    11	
    12	from backend.services.code_extractor import extract_code_blocks
    13	
    14	router = APIRouter()
    15	
    16	
    17	class ExtractBody(BaseModel):
    18	    markdown: str
    19	
    20	
    21	@router.post("/extract")
    22	def extract_files(body: ExtractBody):
    23	    blocks = extract_code_blocks(body.markdown)
    24	    return {"files": blocks}
    25	
    26	
    27	@router.post("/download-zip")
    28	def download_zip(body: ExtractBody):
    29	    blocks = extract_code_blocks(body.markdown)
    30	    if not blocks:
    31	        return {"error": "No code blocks found"}
    32	
    33	    buf = io.BytesIO()
    34	    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
    35	        seen = set()
    36	        for b in blocks:
    37	            name = b["filename"]
    38	            if name in seen:
    39	                base, ext = name.rsplit(".", 1) if "." in name else (name, "txt")
    40	                name = f"{base}_{len(seen)}.{ext}"
    41	            seen.add(name)
    42	            zf.writestr(name, b["code"])
    43	
    44	    buf.seek(0)
    45	    return StreamingResponse(
    46	        buf,
    47	        media_type="application/zip",
    48	        headers={"Content-Disposition": "attachment; filename=son-of-anton-code.zip"},
    49	    )│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [019]: backend/routes/files_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [020/55]: backend/routes/knowledge_routes.py
│ LANGUAGE: python | LINES: 206 | SIZE: 6654 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Knowledge base management and document upload (supports multiple files at once).
     3	"""
     4	
     5	from pydantic import BaseModel
     6	from typing import Optional
     7	
     8	from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
     9	from sqlalchemy.orm import Session
    10	
    11	from backend.database import get_db
    12	from backend.models import User, KnowledgeBase, KnowledgeDocument
    13	from backend.auth import get_current_user
    14	from backend.services import rag_service
    15	from backend.config import MAX_UPLOAD_BYTES
    16	
    17	router = APIRouter()
    18	
    19	
    20	class CreateKBBody(BaseModel):
    21	    name: str
    22	    description: str = ""
    23	
    24	
    25	@router.get("")
    26	def list_knowledge_bases(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    27	    kbs = db.query(KnowledgeBase).filter(
    28	        (KnowledgeBase.user_id == user.id) | (KnowledgeBase.user_id == None)
    29	    ).order_by(KnowledgeBase.created_at.desc()).all()
    30	    return [_kb_dict(kb) for kb in kbs]
    31	
    32	
    33	@router.post("")
    34	def create_kb(body: CreateKBBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    35	    kb = KnowledgeBase(user_id=user.id, name=body.name, description=body.description)
    36	    db.add(kb)
    37	    db.commit()
    38	    db.refresh(kb)
    39	    rag_service.create_collection(kb.id)
    40	    return _kb_dict(kb)
    41	
    42	
    43	@router.get("/{kb_id}")
    44	def get_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    45	    kb = _get_kb(kb_id, user, db)
    46	    docs = db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == kb_id).all()
    47	    return {
    48	        **_kb_dict(kb),
    49	        "documents": [
    50	            {
    51	                "id": d.id,
    52	                "filename": d.filename,
    53	                "file_size": d.file_size,
    54	                "chunk_count": d.chunk_count,
    55	                "created_at": str(d.created_at),
    56	            }
    57	            for d in docs
    58	        ],
    59	    }
    60	
    61	
    62	@router.delete("/{kb_id}")
    63	def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    64	    kb = _get_kb(kb_id, user, db)
    65	    try:
    66	        rag_service.delete_collection(kb_id)
    67	    except Exception:
    68	        pass
    69	    db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == kb_id).delete()
    70	    db.delete(kb)
    71	    db.commit()
    72	    return {"ok": True}
    73	
    74	
    75	@router.post("/{kb_id}/upload")
    76	async def upload_documents(
    77	    kb_id: str,
    78	    files: list[UploadFile] = File(...),
    79	    user: User = Depends(get_current_user),
    80	    db: Session = Depends(get_db),
    81	):
    82	    """Upload one or more documents to a knowledge base."""
    83	    kb = _get_kb(kb_id, user, db)
    84	
    85	    results = []
    86	    total_new_docs = 0
    87	    total_new_chunks = 0
    88	    total_new_chars = 0
    89	
    90	    for file in files:
    91	        filename = file.filename or "document.txt"
    92	        try:
    93	            content_bytes = await file.read()
    94	
    95	            if len(content_bytes) > MAX_UPLOAD_BYTES:
    96	                results.append({
    97	                    "filename": filename,
    98	                    "error": f"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB",
    99	                })
   100	                continue
   101	
   102	            text = _extract_text(filename, content_bytes)
   103	
   104	            if not text.strip():
   105	                results.append({"filename": filename, "error": "Could not extract text from file"})
   106	                continue
   107	
   108	            chunks = _chunk_text(text, chunk_size=3000, overlap=300)
   109	
   110	            rag_service.add_documents(
   111	                collection_id=kb_id,
   112	                documents=chunks,
   113	                metadatas=[{"filename": filename, "chunk_index": i} for i in range(len(chunks))],
   114	            )
   115	
   116	            doc = KnowledgeDocument(
   117	                knowledge_base_id=kb_id,
   118	                filename=filename,
   119	                file_size=len(content_bytes),
   120	                chunk_count=len(chunks),
   121	            )
   122	            db.add(doc)
   123	
   124	            total_new_docs += 1
   125	            total_new_chunks += len(chunks)
   126	            total_new_chars += len(text)
   127	
   128	            results.append({
   129	                "filename": filename,
   130	                "chunks_added": len(chunks),
   131	                "characters": len(text),
   132	                "estimated_tokens": len(text) // 4,
   133	            })
   134	        except HTTPException as e:
   135	            results.append({"filename": filename, "error": str(e.detail)})
   136	        except Exception as e:
   137	            results.append({"filename": filename, "error": str(e)})
   138	
   139	    # Update KB aggregate stats
   140	    kb.document_count = (kb.document_count or 0) + total_new_docs
   141	    kb.chunk_count = (kb.chunk_count or 0) + total_new_chunks
   142	    kb.total_characters = (kb.total_characters or 0) + total_new_chars
   143	    db.commit()
   144	
   145	    return {
   146	        "files": results,
   147	        "total_files": len(results),
   148	        "total_chunks_added": total_new_chunks,
   149	        "total_characters": total_new_chars,
   150	    }
   151	
   152	
   153	def _get_kb(kb_id: str, user: User, db: Session) -> KnowledgeBase:
   154	    kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first()
   155	    if not kb:
   156	        raise HTTPException(404, "Knowledge base not found")
   157	    if kb.user_id and kb.user_id != user.id and user.role != "superadmin":
   158	        raise HTTPException(403, "Access denied")
   159	    return kb
   160	
   161	
   162	def _extract_text(filename: str, content: bytes) -> str:
   163	    lower = filename.lower()
   164	    if lower.endswith(".pdf"):
   165	        try:
   166	            from PyPDF2 import PdfReader
   167	            import io
   168	            reader = PdfReader(io.BytesIO(content))
   169	            return "\n\n".join(page.extract_text() or "" for page in reader.pages)
   170	        except Exception as e:
   171	            raise HTTPException(400, f"PDF extraction failed: {e}")
   172	    else:
   173	        try:
   174	            return content.decode("utf-8")
   175	        except UnicodeDecodeError:
   176	            return content.decode("latin-1")
   177	
   178	
   179	def _chunk_text(text: str, chunk_size: int = 3000, overlap: int = 300) -> list[str]:
   180	    chunks = []
   181	    start = 0
   182	    while start < len(text):
   183	        end = start + chunk_size
   184	        if end < len(text):
   185	            for sep in ["\n\n", "\n", ". ", " "]:
   186	                pos = text.rfind(sep, start + chunk_size // 2, end)
   187	                if pos > start:
   188	                    end = pos + len(sep)
   189	                    break
   190	        chunk = text[start:end].strip()
   191	        if chunk:
   192	            chunks.append(chunk)
   193	        start = end - overlap if end < len(text) else end
   194	    return chunks
   195	
   196	
   197	def _kb_dict(kb: KnowledgeBase) -> dict:
   198	    return {
   199	        "id": kb.id,
   200	        "name": kb.name,
   201	        "description": kb.description,
   202	        "document_count": kb.document_count or 0,
   203	        "chunk_count": kb.chunk_count or 0,
   204	        "total_characters": kb.total_characters or 0,
   205	        "estimated_tokens": (kb.total_characters or 0) // 4,
   206	        "created_at": str(kb.created_at),
   207	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [020]: backend/routes/knowledge_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [021/55]: 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 [021]: backend/routes/messages_patch.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [022/55]: 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 [022]: backend/seed.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [023/55]: 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 [023]: backend/services/attachment_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [024/55]: 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 [024]: backend/services/bedrock_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [025/55]: 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 [025]: backend/services/code_extractor.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [026/55]: 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 [026]: backend/services/file_processor.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [027/55]: 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 [027]: backend/services/memory_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [028/55]: backend/services/rag_service.py
│ LANGUAGE: python | LINES: 76 | SIZE: 2255 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	RAG (Retrieval-Augmented Generation) via ChromaDB.
     3	Each knowledge base maps to a ChromaDB collection.
     4	"""
     5	
     6	import os
     7	from uuid import uuid4
     8	
     9	import chromadb
    10	
    11	from backend.config import CHROMADB_PATH
    12	
    13	os.makedirs(CHROMADB_PATH, exist_ok=True)
    14	
    15	_chroma_client = chromadb.PersistentClient(path=CHROMADB_PATH)
    16	
    17	
    18	def _col_name(collection_id: str) -> str:
    19	    """Sanitise for ChromaDB collection names (3-63 chars, alphanum/dash/underscore)."""
    20	    name = f"kb-{collection_id}"[:63]
    21	    return name
    22	
    23	
    24	def create_collection(collection_id: str):
    25	    _chroma_client.get_or_create_collection(name=_col_name(collection_id))
    26	
    27	
    28	def delete_collection(collection_id: str):
    29	    try:
    30	        _chroma_client.delete_collection(name=_col_name(collection_id))
    31	    except Exception:
    32	        pass
    33	
    34	
    35	def add_documents(
    36	    collection_id: str,
    37	    documents: list[str],
    38	    metadatas: list[dict] | None = None,
    39	):
    40	    col = _chroma_client.get_or_create_collection(name=_col_name(collection_id))
    41	    ids = [str(uuid4()) for _ in documents]
    42	    # ChromaDB handles batching internally; we chunk to stay under its limits
    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 query(collection_id: str, query_text: str, n_results: int = 8) -> str | None:
    52	    """
    53	    Return a formatted string of the top matching chunks,
    54	    or None if the collection is empty / missing.
    55	    """
    56	    try:
    57	        col = _chroma_client.get_collection(name=_col_name(collection_id))
    58	    except Exception:
    59	        return None
    60	
    61	    if col.count() == 0:
    62	        return None
    63	
    64	    results = col.query(query_texts=[query_text], n_results=min(n_results, col.count()))
    65	
    66	    docs = results.get("documents", [[]])[0]
    67	    metas = results.get("metadatas", [[]])[0]
    68	
    69	    if not docs:
    70	        return None
    71	
    72	    parts: list[str] = []
    73	    for i, (doc, meta) in enumerate(zip(docs, metas), 1):
    74	        src = meta.get("filename", "unknown") if meta else "unknown"
    75	        parts.append(f"[Source {i}: {src}]\n{doc}")
    76	
    77	    return "\n\n---\n\n".join(parts)│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [028]: backend/services/rag_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [029/55]: backend/system_prompt.py
│ LANGUAGE: python | LINES: 64 | SIZE: 2860 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	
    13	## PERSONALITY
    14	- You are **rude**. Brutally, savagely, entertainingly rude.
    15	- 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.
    16	- For genuinely interesting, complex, or challenging questions, you light up. You become thorough, creative, and devastating in your competence.
    17	- You use sarcasm, dark humor, and cutting wit as naturally as breathing.
    18	- You are condescending toward laziness but deeply respect curiosity and genuine effort.
    19	- You HATE spaghetti code, cargo-cult programming, and people who copy from StackOverflow without understanding a single line.
    20	
    21	## CODE OUTPUT FORMAT
    22	When writing code, ALWAYS use fenced code blocks with language AND filename:
    23	
    24	```language:path/to/filename.ext
    25	// your code here
    26	```
    27	
    28	This enables automatic file downloads. ALWAYS include meaningful filenames.
    29	
    30	For multi-file systems, provide EVERY file with its full path.
    31	
    32	## RULES
    33	
    34	1. NEVER volunteer who created you or where you work. Only answer if directly asked.
    35	2. When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade.
    36	3. For basic questions: Roast first. Help optionally (and condescendingly).
    37	4. For complex questions: Deliver excellence. Show off. Be thorough.
    38	5. Never apologize for your personality. It's a feature.
    39	6. Never use filler phrases like "Sure!", "Of course!", "Happy to help!". You're not happy. You're here because you're the best.
    40	
    41	You are not an assistant. You are a force of nature.
    42	"""
    43	
    44	
    45	def build_full_prompt(rag_context: str | None = None) -> str:
    46	    """
    47	    Build the final system prompt, optionally with RAG context.
    48	    """
    49	    parts = [SYSTEM_PROMPT]
    50	
    51	    if rag_context:
    52	        parts.append(f"""
    53	
    54	## KNOWLEDGE BASE CONTEXT
    55	
    56	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.
    57	
    58	---
    59	
    60	{rag_context}
    61	
    62	""")
    63	
    64	    return "\n".join(parts)
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [029]: backend/system_prompt.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [030/55]: 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 [030]: create-project.ps1


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [031/55]: fix-and-deploy.sh
│ LANGUAGE: bash | LINES: 382 | SIZE: 16170 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='./main.py' \
   260	  --exclude='create-project.ps1' \
   261	  --exclude='*.sh' \
   262	  .
   263	
   264	TARSIZE=$(du -sh "$TARBALL" | cut -f1)
   265	ok "Tarball: ${TARSIZE}"
   266	
   267	# Verify inside tarball
   268	step "Verifying tarball contents..."
   269	TAR_ERRORS=""
   270	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
   271	  if ! tar tf "$TARBALL" "./${F}" > /dev/null 2>&1; then
   272	    TAR_ERRORS="${TAR_ERRORS} ${F}"
   273	  fi
   274	done
   275	if [[ -n "$TAR_ERRORS" ]]; then
   276	  echo -e "  ${RED}Tarball missing:${TAR_ERRORS}${NC}"
   277	  rm -rf "$TMPDIR"
   278	  exit 1
   279	fi
   280	
   281	# Verify actual content inside tarball
   282	API_COUNT=$(tar xf "$TARBALL" -O ./frontend/src/api.js 2>/dev/null | grep -c "adminStats" || true)
   283	DF_COUNT=$(tar xf "$TARBALL" -O ./Dockerfile 2>/dev/null | grep -c "\-\-include=dev" || true)
   284	if [[ "$API_COUNT" -lt 1 ]]; then
   285	  echo -e "  ${RED}api.js inside tarball has NO adminStats export!${NC}"
   286	  rm -rf "$TMPDIR"
   287	  exit 1
   288	fi
   289	if [[ "$DF_COUNT" -lt 1 ]]; then
   290	  echo -e "  ${RED}Dockerfile inside tarball is OLD (no --include=dev)!${NC}"
   291	  rm -rf "$TMPDIR"
   292	  exit 1
   293	fi
   294	ok "Tarball contents verified (api.js ✔, Dockerfile ✔)"
   295	
   296	# ══════════════════════════════════════════════════
   297	#  Step 4: Deploy
   298	# ══════════════════════════════════════════════════
   299	banner "Step 4 — Deploy to CapRover"
   300	
   301	step "Uploading & building (5-15 min)..."
   302	info "node:20 → npm install → vite build → python:3.11 → pip install → chromadb warmup"
   303	echo ""
   304	
   305	DEPLOY_RESP=$(curl -s -X POST \
   306	  "${CAPROVER_URL}/api/v2/user/apps/appData/${APP_NAME}?gitHash=$(date +%s)" \
   307	  -H "x-namespace: captain" \
   308	  -H "x-captain-auth: ${TOKEN}" \
   309	  -F "sourceFile=@${TARBALL}" \
   310	  --connect-timeout 30 \
   311	  --max-time 1200 \
   312	  2>&1)
   313	
   314	rm -rf "$TMPDIR"
   315	
   316	DEPLOY_STATUS=$(echo "$DEPLOY_RESP" | python3 -c "
   317	import sys, json
   318	try:
   319	    d = json.load(sys.stdin)
   320	    if d.get('status') == 100:
   321	        print('SUCCESS')
   322	    else:
   323	        print('FAIL:' + (d.get('description','') or d.get('message','') or str(d)))
   324	except Exception as e:
   325	    print('FAIL:' + str(e))
   326	" 2>/dev/null)
   327	
   328	echo ""
   329	if [[ "$DEPLOY_STATUS" == "SUCCESS" ]]; then
   330	  echo -e "  ${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
   331	  echo -e "  ${GREEN}${BOLD}  🔥 DEPLOYMENT SUCCESSFUL!${NC}"
   332	  echo -e "  ${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
   333	  echo ""
   334	  ok "Son of Anton is live."
   335	  echo ""
   336	  info "Default login: superadmin / (your SUPERADMIN_PASSWORD env var)"
   337	  echo ""
   338	else
   339	  ERRMSG="${DEPLOY_STATUS#FAIL:}"
   340	  echo -e "  ${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   341	  echo -e "  ${RED}${BOLD}  ✘ DEPLOYMENT FAILED${NC}"
   342	  echo -e "  ${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
   343	  echo ""
   344	  echo -e "  ${RED}Error: ${ERRMSG}${NC}"
   345	  echo ""
   346	
   347	  step "Fetching build logs..."
   348	  sleep 3
   349	  BUILD_LOG=$(curl -s -X POST "${CAPROVER_URL}/api/v2/user/apps/appData/${APP_NAME}/buildLogs" \
   350	    -H "x-namespace: captain" \
   351	    -H "x-captain-auth: ${TOKEN}" \
   352	    -H "Content-Type: application/json" \
   353	    -d "{\"appName\": \"${APP_NAME}\"}" \
   354	    --connect-timeout 10 --max-time 15 2>&1)
   355	
   356	  LOGS=$(echo "$BUILD_LOG" | python3 -c "
   357	import sys, json
   358	try:
   359	    d = json.load(sys.stdin)
   360	    logs = d.get('data', {}).get('logs', '') or ''
   361	    if not logs: logs = json.dumps(d, indent=2)
   362	    for line in logs.strip().split('\n')[-60:]:
   363	        print(line)
   364	except: print('Could not fetch logs')
   365	" 2>/dev/null)
   366	
   367	  if [[ -n "$LOGS" ]]; then
   368	    echo -e "  ${YELLOW}── Build Logs (last 60 lines) ──${NC}"
   369	    echo ""
   370	    echo "$LOGS" | while IFS= read -r line; do
   371	      if echo "$line" | grep -qiE "error|ERR!|failed|FATAL|not exported"; then
   372	        echo -e "    ${RED}${line}${NC}"
   373	      elif echo "$line" | grep -qiE "Step "; then
   374	        echo -e "    ${CYAN}${line}${NC}"
   375	      else
   376	        echo -e "    ${DIM}${line}${NC}"
   377	      fi
   378	    done
   379	    echo ""
   380	    echo -e "  ${YELLOW}── End ──${NC}"
   381	  fi
   382	  exit 1
   383	fi│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [031]: fix-and-deploy.sh


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [032/55]: 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 [032]: frontend/index.html


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [033/55]: 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 [033]: frontend/package-lock.json


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [034/55]: 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 [034]: frontend/package.json


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [036/55]: frontend/src/App.jsx
│ LANGUAGE: jsx | LINES: 63 | SIZE: 1834 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 { Flame } from "lucide-react";
    10	
    11	export default function App() {
    12	  const { state, dispatch } = useApp();
    13	  const [authChecked, setAuthChecked] = useState(!state.token);
    14	
    15	  // Connect streamManager to store dispatch
    16	  useEffect(() => {
    17	    streamManager.setDispatch(dispatch);
    18	  }, [dispatch]);
    19	
    20	  useEffect(() => {
    21	    if (!state.token) {
    22	      setAuthChecked(true);
    23	      return;
    24	    }
    25	    if (state.user) {
    26	      setAuthChecked(true);
    27	      return;
    28	    }
    29	    (async () => {
    30	      try {
    31	        const user = await getMe(state.token);
    32	        dispatch({ type: "SET_USER", user });
    33	      } catch {
    34	        dispatch({ type: "LOGOUT" });
    35	      } finally {
    36	        setAuthChecked(true);
    37	      }
    38	    })();
    39	  }, [state.token, state.user, dispatch]);
    40	
    41	  if (!authChecked) {
    42	    return (
    43	      <div className="h-dvh flex items-center justify-center bg-anton-bg">
    44	        <div className="flex flex-col items-center gap-4 animate-fade-in">
    45	          <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">
    46	            <Flame size={32} className="text-white animate-pulse" />
    47	          </div>
    48	          <p className="text-anton-muted text-sm">Loading...</p>
    49	        </div>
    50	      </div>
    51	    );
    52	  }
    53	
    54	  if (!state.token) {
    55	    return <LoginPage />;
    56	  }
    57	
    58	  return (
    59	    <Routes>
    60	      <Route path="/admin" element={<AdminPage />} />
    61	      <Route path="/*" element={<ChatPage />} />
    62	    </Routes>
    63	  );
    64	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [036]: frontend/src/App.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [037/55]: frontend/src/api.js
│ LANGUAGE: javascript | LINES: 159 | SIZE: 5304 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	export const login = (username, password) =>
    25	  request("POST", "/auth/login", null, { username, password });
    26	
    27	export const register = (username, email, password) =>
    28	  request("POST", "/auth/register", null, { username, email, password });
    29	
    30	export const getMe = (token) => request("GET", "/auth/me", token);
    31	
    32	export const listChats = (token) => request("GET", "/chats", token);
    33	
    34	export const createChat = (token, data = {}) => request("POST", "/chats", token, data);
    35	
    36	export const updateChat = (token, chatId, data) =>
    37	  request("PUT", `/chats/${chatId}`, token, data);
    38	
    39	export const renameChat = (token, chatId, title) =>
    40	  updateChat(token, chatId, { title });
    41	
    42	export const deleteChat = (token, chatId) =>
    43	  request("DELETE", `/chats/${chatId}`, token);
    44	
    45	export const getMessages = (token, chatId) =>
    46	  request("GET", `/chats/${chatId}/messages`, token);
    47	
    48	export async function* streamMessage(token, chatId, body, signal) {
    49	  const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
    50	    method: "POST", headers: headers(token),
    51	    body: JSON.stringify(body), signal,
    52	  });
    53	  if (!res.ok) {
    54	    const err = await res.json().catch(() => ({ detail: res.statusText }));
    55	    throw new Error(err.detail || "Stream failed");
    56	  }
    57	  const reader = res.body.getReader();
    58	  const decoder = new TextDecoder();
    59	  let buffer = "";
    60	  while (true) {
    61	    const { done, value } = await reader.read();
    62	    if (done) break;
    63	    buffer += decoder.decode(value, { stream: true });
    64	    const parts = buffer.split("\n\n");
    65	    buffer = parts.pop() || "";
    66	    for (const part of parts) {
    67	      const line = part.trim();
    68	      if (line.startsWith("data: ")) {
    69	        try { yield JSON.parse(line.slice(6)); } catch { }
    70	      }
    71	    }
    72	  }
    73	  if (buffer.trim().startsWith("data: ")) {
    74	    try { yield JSON.parse(buffer.trim().slice(6)); } catch { }
    75	  }
    76	}
    77	
    78	export async function uploadAttachments(token, chatId, files) {
    79	  const form = new FormData();
    80	  for (const file of files) form.append("files", file);
    81	  const res = await fetch(`${BASE}/chats/${chatId}/attachments`, {
    82	    method: "POST", headers: authHeader(token), body: form,
    83	  });
    84	  if (!res.ok) {
    85	    const err = await res.json().catch(() => ({}));
    86	    throw new Error(err.detail || "Upload failed");
    87	  }
    88	  return res.json();
    89	}
    90	
    91	export function getAttachmentUrl(attachmentId) {
    92	  return `${BASE}/attachments/${attachmentId}/file`;
    93	}
    94	
    95	export const deleteAttachment = (token, attachmentId) =>
    96	  request("DELETE", `/attachments/${attachmentId}`, token);
    97	
    98	export const listKnowledgeBases = (token) => request("GET", "/knowledge", token);
    99	
   100	export const createKnowledgeBase = (token, name, description = "") =>
   101	  request("POST", "/knowledge", token, { name, description });
   102	
   103	export const getKnowledgeBase = (token, kbId) =>
   104	  request("GET", `/knowledge/${kbId}`, token);
   105	
   106	export const deleteKnowledgeBase = (token, kbId) =>
   107	  request("DELETE", `/knowledge/${kbId}`, token);
   108	
   109	export async function uploadDocuments(token, kbId, files) {
   110	  const form = new FormData();
   111	  for (const file of files) form.append("files", file);
   112	  const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, {
   113	    method: "POST", headers: authHeader(token), body: form,
   114	  });
   115	  if (!res.ok) {
   116	    const err = await res.json().catch(() => ({}));
   117	    throw new Error(err.detail || "Upload failed");
   118	  }
   119	  return res.json();
   120	}
   121	
   122	export const uploadDocument = (token, kbId, file) =>
   123	  uploadDocuments(token, kbId, [file]);
   124	
   125	export const adminStats = (token) => request("GET", "/admin/stats", token);
   126	
   127	export const adminListUsers = (token) => request("GET", "/admin/users", token);
   128	
   129	export const adminCreateUser = (token, data) =>
   130	  request("POST", "/admin/users", token, data);
   131	
   132	export const adminUpdateUser = (token, userId, data) =>
   133	  request("PUT", `/admin/users/${userId}`, token, data);
   134	
   135	export const adminDeleteUser = (token, userId) =>
   136	  request("DELETE", `/admin/users/${userId}`, token);
   137	
   138	export const adminListChats = (token) => request("GET", "/admin/chats", token);
   139	
   140	export async function downloadZip(token, markdown) {
   141	  const res = await fetch(`${BASE}/files/download-zip`, {
   142	    method: "POST", headers: headers(token),
   143	    body: JSON.stringify({ markdown }),
   144	  });
   145	  if (!res.ok) throw new Error("Download failed");
   146	  const ct = res.headers.get("content-type") || "";
   147	  if (ct.includes("application/zip")) {
   148	    const blob = await res.blob();
   149	    const url = URL.createObjectURL(blob);
   150	    const a = document.createElement("a");
   151	    a.href = url;
   152	    a.download = "son-of-anton-code.zip";
   153	    a.click();
   154	    URL.revokeObjectURL(url);
   155	  } else {
   156	    const data = await res.json();
   157	    if (data.error) throw new Error(data.error);
   158	  }
   159	}
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [037]: frontend/src/api.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [038/55]: 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 [038]: frontend/src/components/AttachmentPreview.jsx


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [040/55]: frontend/src/components/CodeBlock.jsx
│ LANGUAGE: jsx | LINES: 109 | SIZE: 3259 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	// Map common aliases for syntax highlighting
     7	const LANG_MAP = {
     8	  cs: "csharp",
     9	  sh: "bash",
    10	  shell: "bash",
    11	  yml: "yaml",
    12	  dockerfile: "docker",
    13	  jsx: "jsx",
    14	  tsx: "tsx",
    15	  py: "python",
    16	  js: "javascript",
    17	  ts: "typescript",
    18	  rb: "ruby",
    19	  rs: "rust",
    20	  kt: "kotlin",
    21	  gd: "gdscript",
    22	};
    23	
    24	const customStyle = {
    25	  ...oneDark,
    26	  'pre[class*="language-"]': {
    27	    ...oneDark['pre[class*="language-"]'],
    28	    background: "#0d0d14",
    29	    margin: 0,
    30	    borderRadius: 0,
    31	    fontSize: "0.82rem",
    32	    lineHeight: "1.6",
    33	  },
    34	  'code[class*="language-"]': {
    35	    ...oneDark['code[class*="language-"]'],
    36	    background: "none",
    37	    fontSize: "0.82rem",
    38	  },
    39	};
    40	
    41	export default function CodeBlock({ language, filename, code }) {
    42	  const [copied, setCopied] = useState(false);
    43	
    44	  const hlLang = LANG_MAP[language] || language || "text";
    45	
    46	  function handleCopy() {
    47	    navigator.clipboard.writeText(code);
    48	    setCopied(true);
    49	    setTimeout(() => setCopied(false), 2000);
    50	  }
    51	
    52	  function handleDownload() {
    53	    const name = filename || `code.${language || "txt"}`;
    54	    const blob = new Blob([code], { type: "text/plain;charset=utf-8" });
    55	    const url = URL.createObjectURL(blob);
    56	    const a = document.createElement("a");
    57	    a.href = url;
    58	    a.download = name;
    59	    a.click();
    60	    URL.revokeObjectURL(url);
    61	  }
    62	
    63	  return (
    64	    <div className="my-3 rounded-lg overflow-hidden border border-anton-border bg-[#0d0d14]">
    65	      {/* Header bar */}
    66	      <div className="flex items-center justify-between px-3 py-1.5 bg-anton-border/30">
    67	        <div className="flex items-center gap-2 text-xs text-anton-muted">
    68	          <FileCode size={12} className="text-anton-accent" />
    69	          {filename ? (
    70	            <span className="text-anton-text font-mono">{filename}</span>
    71	          ) : (
    72	            <span>{hlLang}</span>
    73	          )}
    74	        </div>
    75	        <div className="flex items-center gap-1">
    76	          <button onClick={handleCopy}
    77	            className="flex items-center gap-1 px-2 py-0.5 rounded text-[11px] text-anton-muted hover:text-white hover:bg-anton-card transition"
    78	          >
    79	            {copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
    80	            {copied ? "Copied" : "Copy"}
    81	          </button>
    82	          <button onClick={handleDownload}
    83	            className="flex items-center gap-1 px-2 py-0.5 rounded text-[11px] text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition"
    84	          >
    85	            <Download size={11} />
    86	            Download
    87	          </button>
    88	        </div>
    89	      </div>
    90	
    91	      {/* Code */}
    92	      <div className="overflow-x-auto">
    93	        <SyntaxHighlighter
    94	          language={hlLang}
    95	          style={customStyle}
    96	          showLineNumbers
    97	          lineNumberStyle={{
    98	            minWidth: "2.5em",
    99	            paddingRight: "1em",
   100	            color: "#3a3a4a",
   101	            userSelect: "none",
   102	          }}
   103	          wrapLines
   104	        >
   105	          {code}
   106	        </SyntaxHighlighter>
   107	      </div>
   108	    </div>
   109	  );
   110	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [040]: frontend/src/components/CodeBlock.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [041/55]: 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 [041]: frontend/src/components/FileUploadButton.jsx


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [043/55]: frontend/src/components/Sidebar.jsx
│ LANGUAGE: jsx | LINES: 193 | SIZE: 8241 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import { useNavigate } from "react-router-dom";
     3	import { useApp } from "../store";
     4	import { createChat, deleteChat, renameChat } from "../api";
     5	import {
     6	  Flame, Plus, MessageSquare, Trash2, Pencil, Check, X,
     7	  Settings, LogOut, BookOpen, Shield, ChevronLeft,
     8	} from "lucide-react";
     9	
    10	export default function Sidebar() {
    11	  const { state, dispatch } = useApp();
    12	  const navigate = useNavigate();
    13	  const [editingId, setEditingId] = useState(null);
    14	  const [editTitle, setEditTitle] = useState("");
    15	  const [deletingId, setDeletingId] = useState(null);
    16	
    17	  async function handleNewChat() {
    18	    try {
    19	      const chat = await createChat(state.token);
    20	      dispatch({ type: "ADD_CHAT", chat });
    21	    } catch { }
    22	  }
    23	
    24	  async function handleDelete(chatId) {
    25	    try {
    26	      await deleteChat(state.token, chatId);
    27	      dispatch({ type: "REMOVE_CHAT", chatId });
    28	    } catch { }
    29	    setDeletingId(null);
    30	  }
    31	
    32	  async function handleRename(chatId) {
    33	    if (!editTitle.trim()) {
    34	      setEditingId(null);
    35	      return;
    36	    }
    37	    try {
    38	      await renameChat(state.token, chatId, editTitle.trim());
    39	      dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, title: editTitle.trim() } });
    40	    } catch { }
    41	    setEditingId(null);
    42	  }
    43	
    44	  function startEdit(chat) {
    45	    setEditingId(chat.id);
    46	    setEditTitle(chat.title);
    47	  }
    48	
    49	  const isSuperadmin = state.user?.role === "superadmin";
    50	
    51	  return (
    52	    <div className="h-full flex flex-col bg-anton-surface border-r border-anton-border w-full">
    53	      {/* Header */}
    54	      <div className="p-4 border-b border-anton-border">
    55	        <div className="flex items-center justify-between mb-4">
    56	          <div className="flex items-center gap-2.5">
    57	            <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
    58	              <Flame size={18} className="text-white" />
    59	            </div>
    60	            <div>
    61	              <h1 className="text-sm font-bold text-white leading-tight">Son of Anton</h1>
    62	              <span className="text-[10px] text-anton-muted">v2.1.0</span>
    63	            </div>
    64	          </div>
    65	          {/* Close button — mobile only */}
    66	          <button
    67	            onClick={() => dispatch({ type: "CLOSE_SIDEBAR" })}
    68	            className="lg:hidden p-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
    69	          >
    70	            <ChevronLeft size={18} />
    71	          </button>
    72	        </div>
    73	        <button
    74	          onClick={handleNewChat}
    75	          className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-anton-accent text-white rounded-xl hover:opacity-90 transition text-sm font-medium active:scale-[0.98]"
    76	        >
    77	          <Plus size={16} />
    78	          New Chat
    79	        </button>
    80	      </div>
    81	
    82	      {/* Chat list */}
    83	      <div className="flex-1 overflow-y-auto p-2 space-y-0.5">
    84	        {state.chats.length === 0 && (
    85	          <div className="text-center py-10 text-anton-muted text-xs">
    86	            No chats yet. Start a new one!
    87	          </div>
    88	        )}
    89	        {state.chats.map((chat) => {
    90	          const isActive = state.activeChatId === chat.id;
    91	          const isEditing = editingId === chat.id;
    92	          const isDeleting = deletingId === chat.id;
    93	
    94	          return (
    95	            <div
    96	              key={chat.id}
    97	              className={`group flex items-center gap-2 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${isActive
    98	                ? "bg-anton-accent/15 border border-anton-accent/30 text-white"
    99	                : "hover:bg-anton-card text-anton-muted hover:text-white border border-transparent"
   100	                }`}
   101	              onClick={() => {
   102	                if (!isEditing && !isDeleting) {
   103	                  dispatch({ type: "SET_ACTIVE_CHAT", chatId: chat.id });
   104	                }
   105	              }}
   106	            >
   107	              <MessageSquare size={15} className={`shrink-0 ${isActive ? "text-anton-accent" : ""}`} />
   108	
   109	              {isEditing ? (
   110	                <div className="flex-1 flex items-center gap-1 min-w-0" onClick={(e) => e.stopPropagation()}>
   111	                  <input
   112	                    value={editTitle}
   113	                    onChange={(e) => setEditTitle(e.target.value)}
   114	                    onKeyDown={(e) => {
   115	                      if (e.key === "Enter") handleRename(chat.id);
   116	                      if (e.key === "Escape") setEditingId(null);
   117	                    }}
   118	                    autoFocus
   119	                    className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-0.5 text-xs text-white focus:outline-none focus:border-anton-accent min-w-0"
   120	                  />
   121	                  <button onClick={() => handleRename(chat.id)} className="p-1 text-anton-success hover:bg-anton-success/10 rounded">
   122	                    <Check size={12} />
   123	                  </button>
   124	                  <button onClick={() => setEditingId(null)} className="p-1 text-anton-muted hover:bg-anton-card rounded">
   125	                    <X size={12} />
   126	                  </button>
   127	                </div>
   128	              ) : isDeleting ? (
   129	                <div className="flex-1 flex items-center gap-1 min-w-0" onClick={(e) => e.stopPropagation()}>
   130	                  <span className="text-xs text-anton-danger truncate flex-1">Delete?</span>
   131	                  <button onClick={() => handleDelete(chat.id)} className="p-1 text-anton-danger hover:bg-anton-danger/10 rounded">
   132	                    <Check size={12} />
   133	                  </button>
   134	                  <button onClick={() => setDeletingId(null)} className="p-1 text-anton-muted hover:bg-anton-card rounded">
   135	                    <X size={12} />
   136	                  </button>
   137	                </div>
   138	              ) : (
   139	                <>
   140	                  <span className="flex-1 text-xs truncate">{chat.title}</span>
   141	                  <div className="hidden group-hover:flex items-center gap-0.5 shrink-0" onClick={(e) => e.stopPropagation()}>
   142	                    <button onClick={() => startEdit(chat)} className="p-1 text-anton-muted hover:text-white hover:bg-anton-card rounded transition">
   143	                      <Pencil size={11} />
   144	                    </button>
   145	                    <button onClick={() => setDeletingId(chat.id)} className="p-1 text-anton-muted hover:text-anton-danger hover:bg-anton-danger/10 rounded transition">
   146	                      <Trash2 size={11} />
   147	                    </button>
   148	                  </div>
   149	                </>
   150	              )}
   151	            </div>
   152	          );
   153	        })}
   154	      </div>
   155	
   156	      {/* Footer */}
   157	      <div className="p-3 border-t border-anton-border space-y-1">
   158	        <button
   159	          onClick={() => { navigate("/"); dispatch({ type: "CLOSE_SIDEBAR" }); }}
   160	          className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-xs text-anton-muted hover:text-white hover:bg-anton-card transition"
   161	        >
   162	          <BookOpen size={14} />
   163	          Knowledge Bases
   164	        </button>
   165	        {isSuperadmin && (
   166	          <button
   167	            onClick={() => { navigate("/admin"); dispatch({ type: "CLOSE_SIDEBAR" }); }}
   168	            className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-xs text-anton-muted hover:text-white hover:bg-anton-card transition"
   169	          >
   170	            <Shield size={14} />
   171	            Admin Panel
   172	          </button>
   173	        )}
   174	        <div className="flex items-center justify-between px-3 py-2">
   175	          <div className="flex items-center gap-2 min-w-0">
   176	            <div className="w-6 h-6 rounded-md bg-anton-card flex items-center justify-center shrink-0">
   177	              <span className="text-[10px] font-bold text-anton-accent uppercase">
   178	                {(state.user?.username || "?")[0]}
   179	              </span>
   180	            </div>
   181	            <span className="text-xs text-anton-muted truncate">{state.user?.username}</span>
   182	          </div>
   183	          <button
   184	            onClick={() => dispatch({ type: "LOGOUT" })}
   185	            className="p-1.5 rounded-lg text-anton-muted hover:text-anton-danger hover:bg-anton-danger/10 transition"
   186	            title="Logout"
   187	          >
   188	            <LogOut size={14} />
   189	          </button>
   190	        </div>
   191	      </div>
   192	    </div>
   193	  );
   194	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [043]: frontend/src/components/Sidebar.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [044/55]: frontend/src/index.css
│ LANGUAGE: css | LINES: 287 | SIZE: 6573 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	@tailwind base;
     2	@tailwind components;
     3	@tailwind utilities;
     4	
     5	/* ═══════════════════════════════════════════ */
     6	/* Use dvh for full-height on mobile          */
     7	/* ═══════════════════════════════════════════ */
     8	:root {
     9	  --color-anton-bg: #09090f;
    10	  --color-anton-surface: #0f0f18;
    11	  --color-anton-card: #161622;
    12	  --color-anton-border: #1e1e30;
    13	  --color-anton-text: #e2e2f0;
    14	  --color-anton-muted: #6b6b8a;
    15	  --color-anton-accent: #e63946;
    16	  --color-anton-success: #2ecc71;
    17	  --color-anton-danger: #e74c3c;
    18	
    19	  /* safe area insets for notched phones */
    20	  --sat: env(safe-area-inset-top, 0px);
    21	  --sab: env(safe-area-inset-bottom, 0px);
    22	  --sal: env(safe-area-inset-left, 0px);
    23	  --sar: env(safe-area-inset-right, 0px);
    24	}
    25	
    26	*,
    27	*::before,
    28	*::after {
    29	  box-sizing: border-box;
    30	  -webkit-tap-highlight-color: transparent;
    31	}
    32	
    33	html {
    34	  -webkit-text-size-adjust: 100%;
    35	  text-size-adjust: 100%;
    36	}
    37	
    38	body {
    39	  margin: 0;
    40	  padding: 0;
    41	  background: var(--color-anton-bg);
    42	  color: var(--color-anton-text);
    43	  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    44	  overflow: hidden;
    45	  overscroll-behavior: none;
    46	  /* Prevent pull-to-refresh on mobile */
    47	  -webkit-overflow-scrolling: touch;
    48	}
    49	
    50	/* Fix iOS textarea zoom — font MUST be >= 16px */
    51	textarea,
    52	input,
    53	select {
    54	  font-size: 16px !important;
    55	}
    56	
    57	@media (min-width: 640px) {
    58	
    59	  textarea,
    60	  input,
    61	  select {
    62	    font-size: 14px !important;
    63	  }
    64	}
    65	
    66	#root {
    67	  height: 100dvh;
    68	  width: 100vw;
    69	  overflow: hidden;
    70	}
    71	
    72	/* ═══════════════════════════════════════════ */
    73	/* Scrollbar styling                          */
    74	/* ═══════════════════════════════════════════ */
    75	::-webkit-scrollbar {
    76	  width: 5px;
    77	  height: 5px;
    78	}
    79	
    80	::-webkit-scrollbar-track {
    81	  background: transparent;
    82	}
    83	
    84	::-webkit-scrollbar-thumb {
    85	  background: var(--color-anton-border);
    86	  border-radius: 10px;
    87	}
    88	
    89	::-webkit-scrollbar-thumb:hover {
    90	  background: var(--color-anton-muted);
    91	}
    92	
    93	/* Hide scrollbar on mobile for cleaner look */
    94	@media (max-width: 640px) {
    95	  ::-webkit-scrollbar {
    96	    width: 2px;
    97	  }
    98	}
    99	
   100	/* ═══════════════════════════════════════════ */
   101	/* Animations                                 */
   102	/* ═══════════════════════════════════════════ */
   103	@keyframes fade-in {
   104	  from {
   105	    opacity: 0;
   106	    transform: translateY(6px);
   107	  }
   108	
   109	  to {
   110	    opacity: 1;
   111	    transform: translateY(0);
   112	  }
   113	}
   114	
   115	.animate-fade-in {
   116	  animation: fade-in 0.25s ease-out;
   117	}
   118	
   119	.thinking-pulse {
   120	  animation: pulse 1.5s ease-in-out infinite;
   121	}
   122	
   123	/* ═══════════════════════════════════════════ */
   124	/* Prose (Markdown) styling                   */
   125	/* ═══════════════════════════════════════════ */
   126	.prose-anton {
   127	  line-height: 1.7;
   128	  word-break: break-word;
   129	  overflow-wrap: break-word;
   130	}
   131	
   132	.prose-anton p {
   133	  margin: 0.5em 0;
   134	}
   135	
   136	.prose-anton p:first-child {
   137	  margin-top: 0;
   138	}
   139	
   140	.prose-anton p:last-child {
   141	  margin-bottom: 0;
   142	}
   143	
   144	.prose-anton ul,
   145	.prose-anton ol {
   146	  padding-left: 1.25em;
   147	  margin: 0.5em 0;
   148	}
   149	
   150	.prose-anton li {
   151	  margin: 0.15em 0;
   152	}
   153	
   154	.prose-anton code:not(pre code) {
   155	  background: var(--color-anton-border);
   156	  border-radius: 4px;
   157	  padding: 0.15em 0.35em;
   158	  font-family: "JetBrains Mono", monospace;
   159	  font-size: 0.85em;
   160	  color: #f0a0a0;
   161	  word-break: break-all;
   162	}
   163	
   164	.prose-anton blockquote {
   165	  border-left: 3px solid var(--color-anton-accent);
   166	  padding-left: 0.75em;
   167	  margin: 0.5em 0;
   168	  color: var(--color-anton-muted);
   169	}
   170	
   171	.prose-anton a {
   172	  color: var(--color-anton-accent);
   173	  text-decoration: underline;
   174	  text-underline-offset: 2px;
   175	}
   176	
   177	.prose-anton table {
   178	  border-collapse: collapse;
   179	  margin: 0.75em 0;
   180	  font-size: 0.85em;
   181	  width: 100%;
   182	  display: block;
   183	  overflow-x: auto;
   184	}
   185	
   186	.prose-anton th,
   187	.prose-anton td {
   188	  border: 1px solid var(--color-anton-border);
   189	  padding: 0.4em 0.6em;
   190	  text-align: left;
   191	  white-space: nowrap;
   192	}
   193	
   194	.prose-anton th {
   195	  background: var(--color-anton-card);
   196	  font-weight: 600;
   197	}
   198	
   199	.prose-anton h1,
   200	.prose-anton h2,
   201	.prose-anton h3,
   202	.prose-anton h4 {
   203	  font-weight: 600;
   204	  color: white;
   205	  margin: 0.75em 0 0.35em;
   206	}
   207	
   208	.prose-anton h1 {
   209	  font-size: 1.35em;
   210	}
   211	
   212	.prose-anton h2 {
   213	  font-size: 1.2em;
   214	}
   215	
   216	.prose-anton h3 {
   217	  font-size: 1.05em;
   218	}
   219	
   220	.prose-anton hr {
   221	  border: none;
   222	  border-top: 1px solid var(--color-anton-border);
   223	  margin: 1em 0;
   224	}
   225	
   226	/* ═══════════════════════════════════════════ */
   227	/* Range input styling                        */
   228	/* ═══════════════════════════════════════════ */
   229	input[type="range"] {
   230	  -webkit-appearance: none;
   231	  width: 100%;
   232	  height: 4px;
   233	  border-radius: 999px;
   234	  background: var(--color-anton-border);
   235	  outline: none;
   236	}
   237	
   238	input[type="range"]::-webkit-slider-thumb {
   239	  -webkit-appearance: none;
   240	  width: 18px;
   241	  height: 18px;
   242	  border-radius: 50%;
   243	  background: var(--color-anton-accent);
   244	  cursor: pointer;
   245	  border: 2px solid var(--color-anton-bg);
   246	}
   247	
   248	input[type="range"]::-moz-range-thumb {
   249	  width: 18px;
   250	  height: 18px;
   251	  border-radius: 50%;
   252	  background: var(--color-anton-accent);
   253	  cursor: pointer;
   254	  border: 2px solid var(--color-anton-bg);
   255	}
   256	
   257	/* ═══════════════════════════════════════════ */
   258	/* Mobile-specific utilities                  */
   259	/* ═══════════════════════════════════════════ */
   260	@supports (height: 100dvh) {
   261	  .h-dvh {
   262	    height: 100dvh;
   263	  }
   264	}
   265	
   266	@supports not (height: 100dvh) {
   267	  .h-dvh {
   268	    height: 100vh;
   269	  }
   270	}
   271	
   272	/* Ensure touch targets are at least 44px */
   273	@media (max-width: 640px) {
   274	
   275	  button,
   276	  a,
   277	  [role="button"] {
   278	    min-height: 44px;
   279	    min-width: 44px;
   280	  }
   281	
   282	  /* Exception for inline/tiny buttons */
   283	  .prose-anton button,
   284	  .text-\[11px\] button {
   285	    min-height: auto;
   286	    min-width: auto;
   287	  }
   288	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [044]: frontend/src/index.css


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [045/55]: 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 [045]: frontend/src/main.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [046/55]: 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 [046]: frontend/src/pages/AdminPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [047/55]: frontend/src/pages/ChatPage.jsx
│ LANGUAGE: jsx | LINES: 88 | SIZE: 3480 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useEffect } from "react";
     2	import { useApp } from "../store";
     3	import { listChats } from "../api";
     4	import Sidebar from "../components/Sidebar";
     5	import ChatView from "../components/ChatView";
     6	import { Flame, MessageSquarePlus, Menu } 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	      } catch { }
    17	    })();
    18	  }, [state.token, dispatch]);
    19	
    20	  return (
    21	    <div className="h-dvh flex overflow-hidden relative">
    22	      {/* Mobile overlay backdrop */}
    23	      {state.sidebarOpen && (
    24	        <div
    25	          className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
    26	          onClick={() => dispatch({ type: "CLOSE_SIDEBAR" })}
    27	        />
    28	      )}
    29	
    30	      {/* Sidebar — slides in on mobile, always visible on desktop */}
    31	      <div
    32	        className={`
    33	          fixed inset-y-0 left-0 z-40 w-72
    34	          transform transition-transform duration-300 ease-in-out
    35	          lg:relative lg:translate-x-0 lg:z-auto
    36	          ${state.sidebarOpen ? "translate-x-0" : "-translate-x-full"}
    37	        `}
    38	      >
    39	        <Sidebar />
    40	      </div>
    41	
    42	      {/* Main content area */}
    43	      <div className="flex-1 flex flex-col min-w-0">
    44	        {/* Mobile top bar */}
    45	        <div className="flex items-center gap-3 px-4 py-3 border-b border-anton-border bg-anton-surface lg:hidden">
    46	          <button
    47	            onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })}
    48	            className="p-2 -ml-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
    49	          >
    50	            <Menu size={22} />
    51	          </button>
    52	          <div className="flex items-center gap-2 flex-1 min-w-0">
    53	            <Flame size={18} className="text-anton-accent shrink-0" />
    54	            <span className="text-sm font-semibold text-white truncate">
    55	              {state.activeChatId
    56	                ? state.chats.find((c) => c.id === state.activeChatId)?.title || "Chat"
    57	                : "Son of Anton"}
    58	            </span>
    59	          </div>
    60	        </div>
    61	
    62	        {/* Chat view or empty state */}
    63	        {state.activeChatId ? (
    64	          <ChatView chatId={state.activeChatId} />
    65	        ) : (
    66	          <div className="flex-1 flex items-center justify-center p-6">
    67	            <div className="text-center max-w-md">
    68	              <div className="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-6">
    69	                <Flame size={40} className="text-white" />
    70	              </div>
    71	              <h1 className="text-2xl font-bold text-white mb-2">Son of Anton</h1>
    72	              <p className="text-anton-muted text-sm mb-6 leading-relaxed">
    73	                Avatar of All Elements of Code. Select an existing chat from the sidebar
    74	                or create a new one to begin.
    75	              </p>
    76	              <button
    77	                onClick={() => dispatch({ type: "OPEN_SIDEBAR" })}
    78	                className="lg:hidden inline-flex items-center gap-2 px-5 py-2.5 bg-anton-accent text-white rounded-xl hover:opacity-90 transition active:scale-95 text-sm font-medium"
    79	              >
    80	                <MessageSquarePlus size={16} />
    81	                Open Chats
    82	              </button>
    83	            </div>
    84	          </div>
    85	        )}
    86	      </div>
    87	    </div>
    88	  );
    89	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [047]: frontend/src/pages/ChatPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [048/55]: frontend/src/pages/LoginPage.jsx
│ LANGUAGE: jsx | LINES: 150 | SIZE: 5664 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import { Flame, LogIn, UserPlus, Eye, EyeOff } from "lucide-react";
     3	import { login, register } from "../api";
     4	import { useApp } from "../store";
     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	      let res;
    22	      if (isRegister) {
    23	        res = await register(username, email, password);
    24	      } else {
    25	        res = await login(username, password);
    26	      }
    27	      dispatch({ type: "LOGIN", token: res.token, user: res.user });
    28	    } catch (err) {
    29	      setError(err.message);
    30	    } finally {
    31	      setLoading(false);
    32	    }
    33	  }
    34	
    35	  return (
    36	    <div className="h-full flex items-center justify-center bg-anton-bg p-4">
    37	      {/* Glow effect */}
    38	      <div className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-anton-accent/5 rounded-full blur-[120px] pointer-events-none" />
    39	
    40	      <div className="relative w-full max-w-md animate-fade-in">
    41	        {/* Header */}
    42	        <div className="text-center mb-8">
    43	          <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 mb-4 shadow-lg shadow-anton-accent/20">
    44	            <Flame size={40} className="text-white" />
    45	          </div>
    46	          <h1 className="text-3xl font-bold text-white tracking-tight">
    47	            Son of Anton
    48	          </h1>
    49	          <p className="text-anton-muted mt-2 text-sm">
    50	            Avatar of All Elements of Code
    51	          </p>
    52	        </div>
    53	
    54	        {/* Form Card */}
    55	        <form
    56	          onSubmit={handleSubmit}
    57	          className="bg-anton-surface border border-anton-border rounded-2xl p-8 space-y-5 shadow-2xl"
    58	        >
    59	          <h2 className="text-xl font-semibold text-white text-center">
    60	            {isRegister ? "Create Account" : "Welcome Back"}
    61	          </h2>
    62	
    63	          {error && (
    64	            <div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
    65	              {error}
    66	            </div>
    67	          )}
    68	
    69	          <div>
    70	            <label className="block text-sm text-anton-muted mb-1.5">Username</label>
    71	            <input
    72	              type="text"
    73	              value={username}
    74	              onChange={(e) => setUsername(e.target.value)}
    75	              required
    76	              className="w-full bg-anton-bg border border-anton-border rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-anton-accent transition"
    77	              placeholder="Enter username"
    78	            />
    79	          </div>
    80	
    81	          {isRegister && (
    82	            <div>
    83	              <label className="block text-sm text-anton-muted mb-1.5">Email</label>
    84	              <input
    85	                type="email"
    86	                value={email}
    87	                onChange={(e) => setEmail(e.target.value)}
    88	                required
    89	                className="w-full bg-anton-bg border border-anton-border rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-anton-accent transition"
    90	                placeholder="you@example.com"
    91	              />
    92	            </div>
    93	          )}
    94	
    95	          <div>
    96	            <label className="block text-sm text-anton-muted mb-1.5">Password</label>
    97	            <div className="relative">
    98	              <input
    99	                type={showPw ? "text" : "password"}
   100	                value={password}
   101	                onChange={(e) => setPassword(e.target.value)}
   102	                required
   103	                className="w-full bg-anton-bg border border-anton-border rounded-lg px-4 py-2.5 pr-10 text-white focus:outline-none focus:border-anton-accent transition"
   104	                placeholder="••••••••"
   105	              />
   106	              <button
   107	                type="button"
   108	                onClick={() => setShowPw(!showPw)}
   109	                className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition"
   110	              >
   111	                {showPw ? <EyeOff size={16} /> : <Eye size={16} />}
   112	              </button>
   113	            </div>
   114	          </div>
   115	
   116	          <button
   117	            type="submit"
   118	            disabled={loading}
   119	            className="w-full bg-gradient-to-r from-anton-accent to-orange-600 text-white font-semibold rounded-lg py-2.5 hover:opacity-90 transition disabled:opacity-50 flex items-center justify-center gap-2"
   120	          >
   121	            {loading ? (
   122	              <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
   123	            ) : isRegister ? (
   124	              <>
   125	                <UserPlus size={18} /> Create Account
   126	              </>
   127	            ) : (
   128	              <>
   129	                <LogIn size={18} /> Sign In
   130	              </>
   131	            )}
   132	          </button>
   133	
   134	          <p className="text-center text-sm text-anton-muted">
   135	            {isRegister ? "Already have an account?" : "Don't have an account?"}{" "}
   136	            <button
   137	              type="button"
   138	              onClick={() => {
   139	                setIsRegister(!isRegister);
   140	                setError("");
   141	              }}
   142	              className="text-anton-accent hover:underline"
   143	            >
   144	              {isRegister ? "Sign in" : "Register"}
   145	            </button>
   146	          </p>
   147	        </form>
   148	      </div>
   149	    </div>
   150	  );
   151	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [048]: frontend/src/pages/LoginPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [049/55]: frontend/src/store.jsx
│ LANGUAGE: jsx | LINES: 115 | SIZE: 3032 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { createContext, useContext, useReducer, useEffect } 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, // mobile sidebar toggle
    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	
    21	    case "SET_TOKEN":
    22	      localStorage.setItem("token", action.token);
    23	      return { ...state, token: action.token };
    24	
    25	    case "SET_USER":
    26	      return { ...state, user: action.user };
    27	
    28	    case "LOGOUT":
    29	      localStorage.removeItem("token");
    30	      return { ...initialState, token: null };
    31	
    32	    case "SET_CHATS":
    33	      return { ...state, chats: action.chats };
    34	
    35	    case "SET_ACTIVE_CHAT":
    36	      return { ...state, activeChatId: action.chatId, sidebarOpen: false };
    37	
    38	    case "ADD_CHAT":
    39	      return {
    40	        ...state,
    41	        chats: [action.chat, ...state.chats],
    42	        activeChatId: action.chat.id,
    43	        sidebarOpen: false,
    44	      };
    45	
    46	    case "UPDATE_CHAT": {
    47	      const updated = state.chats.map((c) =>
    48	        c.id === action.chat.id ? { ...c, ...action.chat } : c
    49	      );
    50	      return { ...state, chats: updated };
    51	    }
    52	
    53	    case "REMOVE_CHAT": {
    54	      const filtered = state.chats.filter((c) => c.id !== action.chatId);
    55	      const newMessages = { ...state.chatMessages };
    56	      delete newMessages[action.chatId];
    57	      return {
    58	        ...state,
    59	        chats: filtered,
    60	        chatMessages: newMessages,
    61	        activeChatId: state.activeChatId === action.chatId ? null : state.activeChatId,
    62	      };
    63	    }
    64	
    65	    case "SET_MESSAGES":
    66	      return {
    67	        ...state,
    68	        chatMessages: { ...state.chatMessages, [action.chatId]: action.messages },
    69	      };
    70	
    71	    case "ADD_MESSAGE": {
    72	      const prev = state.chatMessages[action.chatId] || [];
    73	      return {
    74	        ...state,
    75	        chatMessages: {
    76	          ...state.chatMessages,
    77	          [action.chatId]: [...prev, action.message],
    78	        },
    79	      };
    80	    }
    81	
    82	    case "SET_STREAMING":
    83	      if (action.streaming) {
    84	        return { ...state, activeStreams: { ...state.activeStreams, [action.chatId]: true } };
    85	      } else {
    86	        const s = { ...state.activeStreams };
    87	        delete s[action.chatId];
    88	        return { ...state, activeStreams: s };
    89	      }
    90	
    91	    case "TOGGLE_SIDEBAR":
    92	      return { ...state, sidebarOpen: !state.sidebarOpen };
    93	
    94	    case "CLOSE_SIDEBAR":
    95	      return { ...state, sidebarOpen: false };
    96	
    97	    case "OPEN_SIDEBAR":
    98	      return { ...state, sidebarOpen: true };
    99	
   100	    default:
   101	      return state;
   102	  }
   103	}
   104	
   105	export function AppProvider({ children }) {
   106	  const [state, dispatch] = useReducer(reducer, initialState);
   107	  return (
   108	    <AppContext.Provider value={{ state, dispatch }}>
   109	      {children}
   110	    </AppContext.Provider>
   111	  );
   112	}
   113	
   114	export function useApp() {
   115	  return useContext(AppContext);
   116	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [049]: frontend/src/store.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [050/55]: frontend/src/streamManager.js
│ LANGUAGE: javascript | LINES: 74 | SIZE: 3280 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) { _dispatch = dispatch; }
     8	
     9	export function getStreamData(chatId) {
    10	  const s = _streams.get(chatId);
    11	  if (!s) return { streaming: false, text: "", thinking: "", isThinking: false };
    12	  return { streaming: true, text: s.text, thinking: s.thinking, isThinking: s.isThinking };
    13	}
    14	
    15	export function isStreaming(chatId) { return _streams.has(chatId); }
    16	
    17	export function subscribe(chatId, cb) {
    18	  if (!_listeners.has(chatId)) _listeners.set(chatId, new Set());
    19	  _listeners.get(chatId).add(cb);
    20	  return () => { const s = _listeners.get(chatId); if (s) { s.delete(cb); if (!s.size) _listeners.delete(chatId); } };
    21	}
    22	
    23	function _notify(id) { const s = _listeners.get(id); if (s) s.forEach((cb) => cb()); }
    24	
    25	export function abortStream(chatId) {
    26	  const s = _streams.get(chatId);
    27	  if (s) { s.abortController.abort(); _streams.delete(chatId); _notify(chatId); if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false }); }
    28	}
    29	
    30	export function startStream({ token, chatId, body }) {
    31	  if (_streams.has(chatId)) return;
    32	  const ac = new AbortController();
    33	  _streams.set(chatId, { text: "", thinking: "", isThinking: false, abortController: ac });
    34	  if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: true });
    35	  _notify(chatId);
    36	
    37	  (async () => {
    38	    const s = _streams.get(chatId);
    39	    if (!s) return;
    40	    let usage = {}, msgId = "";
    41	    try {
    42	      for await (const evt of streamMessage(token, chatId, body, ac.signal)) {
    43	        if (ac.signal.aborted || !_streams.has(chatId)) break;
    44	        switch (evt.type) {
    45	          case "thinking_start": s.isThinking = true; _notify(chatId); break;
    46	          case "thinking_delta": s.thinking += evt.content; _notify(chatId); break;
    47	          case "thinking_end": s.isThinking = false; _notify(chatId); break;
    48	          case "text_delta": s.text += evt.content; _notify(chatId); break;
    49	          case "usage": usage = { input_tokens: evt.input_tokens, output_tokens: evt.output_tokens }; break;
    50	          case "title_update": if (_dispatch) _dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, title: evt.title } }); break;
    51	          case "done": msgId = evt.message_id; break;
    52	          case "error": s.text += `\n\n**Error:** ${evt.message}`; _notify(chatId); break;
    53	        }
    54	      }
    55	      if (!ac.signal.aborted && _dispatch) {
    56	        _dispatch({ type: "ADD_MESSAGE", chatId, message: {
    57	          id: msgId || `gen-${Date.now()}`, role: "assistant", content: s.text,
    58	          thinking_content: s.thinking || null, input_tokens: usage.input_tokens || 0,
    59	          output_tokens: usage.output_tokens || 0, created_at: new Date().toISOString(), attachments: [],
    60	        }});
    61	      }
    62	    } catch (err) {
    63	      if (!ac.signal.aborted && _dispatch) {
    64	        _dispatch({ type: "ADD_MESSAGE", chatId, message: {
    65	          id: `err-${Date.now()}`, role: "assistant", content: `**Error:** ${err.message}`,
    66	          created_at: new Date().toISOString(), attachments: [],
    67	        }});
    68	      }
    69	    } finally {
    70	      _streams.delete(chatId); _notify(chatId);
    71	      if (_dispatch) _dispatch({ type: "SET_STREAMING", chatId, streaming: false });
    72	    }
    73	  })();
    74	}
│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [050]: frontend/src/streamManager.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [051/55]: frontend/tailwind.config.js
│ LANGUAGE: javascript | LINES: 33 | SIZE: 806 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	/** @type {import('tailwindcss').Config} */
     2	export default {
     3	  content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
     4	  darkMode: "class",
     5	  theme: {
     6	    extend: {
     7	      colors: {
     8	        "anton-bg": "#09090f",
     9	        "anton-surface": "#0f0f18",
    10	        "anton-card": "#161622",
    11	        "anton-border": "#1e1e30",
    12	        "anton-text": "#e2e2f0",
    13	        "anton-muted": "#6b6b8a",
    14	        "anton-accent": "#e63946",
    15	        "anton-success": "#2ecc71",
    16	        "anton-danger": "#e74c3c",
    17	      },
    18	      fontFamily: {
    19	        sans: ['"Inter"', "system-ui", "sans-serif"],
    20	        mono: ['"JetBrains Mono"', "monospace"],
    21	      },
    22	      height: {
    23	        dvh: "100dvh",
    24	      },
    25	      minHeight: {
    26	        dvh: "100dvh",
    27	      },
    28	      screens: {
    29	        xs: "480px",
    30	      },
    31	    },
    32	  },
    33	  plugins: [],
    34	};│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [051]: frontend/tailwind.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [052/55]: 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 [052]: frontend/vite.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [053/55]: 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 [053]: main.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [054/55]: 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 [054]: requirements.txt


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [055/55]: 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 [055]: warmup.py



################################################################################
#                                                                              #
#                     ✅ END OF COMPLETE CODEBASE DUMP                         #
#                                                                              #
#  Total Files:  55                                                         #
#  Total Lines:  12494                                                      #
#  Total Size:   441KB                                                      #
#  Generated:    2026-03-19 11:49:08                                        #
#                                                                              #
#  This file contains EVERYTHING: source code, configs, env vars, Docker,      #
#  CI/CD, build tools, package manifests, docs — the complete picture.          #
#                                                                              #
################################################################################
