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

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

 Generated:       2026-03-17 16:26:29
 Source Dir:       /Users/mahmoudaglan/son-of-anton
 Total Files:      40
 Total Lines:      3686
 Total Size:       125KB

 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:
  .env.example
  DEPLOY_GUIDE.md
  Dockerfile
  backend/auth.py
  backend/config.py
  backend/database.py
  backend/main.py
  backend/models.py
  backend/routes/admin_routes.py
  backend/routes/auth_routes.py
  backend/routes/chat_routes.py
  backend/routes/files_routes.py
  backend/routes/knowledge_routes.py
  backend/seed.py
  backend/services/bedrock_service.py
  backend/services/code_extractor.py
  backend/services/memory_service.py
  backend/services/rag_service.py
  backend/system_prompt.py
  create-project.ps1
  frontend/index.html
  frontend/package.json
  frontend/postcss.config.js
  frontend/src/App.jsx
  frontend/src/api.js
  frontend/src/components/ChatView.jsx
  frontend/src/components/CodeBlock.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/tailwind.config.js
  frontend/vite.config.js
  main.py
  requirements.txt
  warmup.py


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

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

 INDEX   LINES    SIZE      FILE PATH
 -----   -----    ------    ----------------------------------------------
 [001]  30       983B      .env.example
 [002]  23       749B      DEPLOY_GUIDE.md
 [003]  41       1KB       Dockerfile
 [004]  66       2KB       backend/auth.py
 [005]  31       1KB       backend/config.py
 [006]  28       777B      backend/database.py
 [007]  78       2KB       backend/main.py
 [008]  99       3KB       backend/models.py
 [009]  129      4KB       backend/routes/admin_routes.py
 [010]  90       2KB       backend/routes/auth_routes.py
 [011]  280      9KB       backend/routes/chat_routes.py
 [012]  48       1KB       backend/routes/files_routes.py
 [013]  176      5KB       backend/routes/knowledge_routes.py
 [014]  28       855B      backend/seed.py
 [015]  242      8KB       backend/services/bedrock_service.py
 [016]  56       1KB       backend/services/code_extractor.py
 [017]  60       1KB       backend/services/memory_service.py
 [018]  76       2KB       backend/services/rag_service.py
 [019]  64       2KB       backend/system_prompt.py
 [020]  182      7KB       create-project.ps1
 [021]  18       835B      frontend/index.html
 [022]  27       646B      frontend/package.json
 [023]  5        80B       frontend/postcss.config.js
 [024]  21       543B      frontend/src/App.jsx
 [025]  163      5KB       frontend/src/api.js
 [026]  338      12KB      frontend/src/components/ChatView.jsx
 [027]  109      3KB       frontend/src/components/CodeBlock.jsx
 [028]  135      5KB       frontend/src/components/MessageBubble.jsx
 [029]  265      11KB      frontend/src/components/Sidebar.jsx
 [030]  137      2KB       frontend/src/index.css
 [031]  15       409B      frontend/src/main.jsx
 [032]  260      12KB      frontend/src/pages/AdminPage.jsx
 [033]  53       1KB       frontend/src/pages/ChatPage.jsx
 [034]  150      5KB       frontend/src/pages/LoginPage.jsx
 [035]  79       1KB       frontend/src/store.jsx
 [036]  38       1KB       frontend/tailwind.config.js
 [037]  14       269B      frontend/vite.config.js
 [038]  16       528B      main.py
 [039]  10       198B      requirements.txt
 [040]  6        221B      warmup.py


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

 EXTENSION/TYPE             COUNT
 ───────────────────────    ─────
 .py                        18
 .jsx                       10
 .js                        4
 .txt                       1
 .ps1                       1
 .md                        1
 .json                      1
 .html                      1
 .example                   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/40]: .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 [001]: .env.example


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [002/40]: 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 [002]: DEPLOY_GUIDE.md


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [003/40]: Dockerfile
│ LANGUAGE: dockerfile | LINES: 41 | SIZE: 1207 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	# ============================================
     2	# Stage 1: Build React Frontend
     3	# ============================================
     4	FROM node:20-alpine AS frontend-build
     5	
     6	WORKDIR /build/frontend
     7	COPY frontend/package.json frontend/package-lock.json* ./
     8	RUN npm install --legacy-peer-deps
     9	COPY frontend/ ./
    10	RUN npm run build
    11	
    12	# ============================================
    13	# Stage 2: Python Backend + Serve Frontend
    14	# ============================================
    15	FROM python:3.11-slim
    16	
    17	RUN apt-get update && apt-get install -y --no-install-recommends \
    18	    build-essential \
    19	    && rm -rf /var/lib/apt/lists/*
    20	
    21	WORKDIR /app
    22	
    23	COPY requirements.txt .
    24	RUN pip install --no-cache-dir -r requirements.txt
    25	
    26	COPY backend/ ./backend/
    27	
    28	COPY --from=frontend-build /build/frontend/dist ./frontend/dist
    29	
    30	# Warm up the ChromaDB embedding model so first request is fast
    31	# Using a separate script file to avoid all quoting issues
    32	COPY warmup.py /tmp/warmup.py
    33	RUN python /tmp/warmup.py && rm /tmp/warmup.py
    34	
    35	# Create persistent data directories
    36	RUN mkdir -p /data/chromadb /data/uploads
    37	
    38	ENV PYTHONUNBUFFERED=1
    39	
    40	EXPOSE 80
    41	
    42	CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "1"]│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [003]: Dockerfile


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [004/40]: 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 [004]: backend/auth.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [005/40]: backend/config.py
│ LANGUAGE: python | LINES: 31 | SIZE: 1065 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	
    27	DEFAULT_QUOTA: int = int(os.getenv("DEFAULT_QUOTA", "2000000"))
    28	MAX_UPLOAD_BYTES: int = int(os.getenv("MAX_UPLOAD_MB", "50")) * 1024 * 1024
    29	
    30	BEDROCK_ENDPOINT: str = (
    31	    f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
    32	)│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [005]: backend/config.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [006/40]: 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 [006]: backend/database.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [007/40]: backend/main.py
│ LANGUAGE: python | LINES: 78 | SIZE: 2703 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Son of Anton — Main FastAPI Application
     3	"""
     4	
     5	import os
     6	from pathlib import Path
     7	from contextlib import asynccontextmanager
     8	
     9	from fastapi import FastAPI, HTTPException
    10	from fastapi.staticfiles import StaticFiles
    11	from fastapi.responses import FileResponse
    12	from fastapi.middleware.cors import CORSMiddleware
    13	
    14	from backend.database import engine, Base
    15	from backend.seed import seed_superadmin
    16	from backend.routes.auth_routes import router as auth_router
    17	from backend.routes.chat_routes import router as chat_router
    18	from backend.routes.admin_routes import router as admin_router
    19	from backend.routes.knowledge_routes import router as knowledge_router
    20	from backend.routes.files_routes import router as files_router
    21	from backend.services.bedrock_service import close_http_client
    22	
    23	
    24	@asynccontextmanager
    25	async def lifespan(app: FastAPI):
    26	    # --- Startup ---
    27	    Base.metadata.create_all(bind=engine)
    28	    seed_superadmin()
    29	    print("🔥 Son of Anton is online.")
    30	    yield
    31	    # --- Shutdown ---
    32	    await close_http_client()
    33	    print("Son of Anton shutting down.")
    34	
    35	
    36	app = FastAPI(
    37	    title="Son of Anton",
    38	    description="Avatar of All Elements of Code",
    39	    version="1.0.0",
    40	    lifespan=lifespan,
    41	)
    42	
    43	app.add_middleware(
    44	    CORSMiddleware,
    45	    allow_origins=["*"],
    46	    allow_credentials=True,
    47	    allow_methods=["*"],
    48	    allow_headers=["*"],
    49	)
    50	
    51	# ── API Routes ────────────────────────────────────
    52	app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
    53	app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
    54	app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
    55	app.include_router(knowledge_router, prefix="/api/knowledge", tags=["Knowledge"])
    56	app.include_router(files_router, prefix="/api/files", tags=["Files"])
    57	
    58	# ── Serve Frontend ────────────────────────────────
    59	FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
    60	
    61	if (FRONTEND_DIR / "assets").exists():
    62	    app.mount(
    63	        "/assets",
    64	        StaticFiles(directory=str(FRONTEND_DIR / "assets")),
    65	        name="static-assets",
    66	    )
    67	
    68	
    69	@app.get("/{full_path:path}", include_in_schema=False)
    70	async def serve_frontend(full_path: str):
    71	    if full_path.startswith("api"):
    72	        raise HTTPException(status_code=404, detail="Not found")
    73	    file_path = FRONTEND_DIR / full_path
    74	    if full_path and file_path.is_file():
    75	        return FileResponse(str(file_path))
    76	    index = FRONTEND_DIR / "index.html"
    77	    if index.is_file():
    78	        return FileResponse(str(index))
    79	    return {"message": "Son of Anton API is running. Frontend not built."}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [007]: backend/main.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [008/40]: backend/models.py
│ LANGUAGE: python | LINES: 99 | SIZE: 3497 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	    created_at = Column(DateTime, default=datetime.utcnow)
    53	    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    54	
    55	    user = relationship("User", back_populates="chats")
    56	    messages = relationship(
    57	        "Message", back_populates="chat",
    58	        cascade="all,delete-orphan", order_by="Message.created_at",
    59	    )
    60	
    61	
    62	class Message(Base):
    63	    __tablename__ = "messages"
    64	
    65	    id = Column(String(36), primary_key=True, default=new_id)
    66	    chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
    67	    role = Column(String(20), nullable=False)
    68	    content = Column(Text, default="")
    69	    thinking_content = Column(Text, nullable=True)
    70	    input_tokens = Column(Integer, default=0)
    71	    output_tokens = Column(Integer, default=0)
    72	    created_at = Column(DateTime, default=datetime.utcnow)
    73	
    74	    chat = relationship("Chat", back_populates="messages")
    75	
    76	
    77	class KnowledgeBase(Base):
    78	    __tablename__ = "knowledge_bases"
    79	
    80	    id = Column(String(36), primary_key=True, default=new_id)
    81	    user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
    82	    name = Column(String(200), nullable=False)
    83	    description = Column(Text, default="")
    84	    document_count = Column(Integer, default=0)
    85	    chunk_count = Column(Integer, default=0)
    86	    total_characters = Column(BigInteger, default=0)
    87	    created_at = Column(DateTime, default=datetime.utcnow)
    88	
    89	
    90	class KnowledgeDocument(Base):
    91	    __tablename__ = "knowledge_documents"
    92	
    93	    id = Column(String(36), primary_key=True, default=new_id)
    94	    knowledge_base_id = Column(
    95	        String(36), ForeignKey("knowledge_bases.id", ondelete="CASCADE"), nullable=False,
    96	    )
    97	    filename = Column(String(200), nullable=False)
    98	    file_size = Column(Integer, default=0)
    99	    chunk_count = Column(Integer, default=0)
   100	    created_at = Column(DateTime, default=datetime.utcnow)│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [008]: backend/models.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [009/40]: 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 [009]: backend/routes/admin_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [010/40]: 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 [010]: backend/routes/auth_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [011/40]: backend/routes/chat_routes.py
│ LANGUAGE: python | LINES: 280 | SIZE: 9705 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Chat CRUD and message streaming.
     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
    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
    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	
    28	
    29	class RenameChatBody(BaseModel):
    30	    title: str
    31	
    32	
    33	class SendMessageBody(BaseModel):
    34	    content: str
    35	    model: Optional[str] = None
    36	    max_tokens: int = 4096
    37	    reasoning_budget: int = 0
    38	    knowledge_base_id: Optional[str] = None
    39	
    40	
    41	# ── Chat CRUD ─────────────────────────────────────
    42	
    43	@router.get("")
    44	def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    45	    chats = (
    46	        db.query(Chat)
    47	        .filter(Chat.user_id == user.id)
    48	        .order_by(Chat.updated_at.desc())
    49	        .all()
    50	    )
    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,
    58	        title=body.title,
    59	        model=body.model,
    60	        knowledge_base_id=body.knowledge_base_id,
    61	    )
    62	    db.add(chat)
    63	    db.commit()
    64	    db.refresh(chat)
    65	    return _chat_dict(chat)
    66	
    67	
    68	@router.get("/{chat_id}")
    69	def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    70	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    71	    if not chat:
    72	        raise HTTPException(404, "Chat not found")
    73	    return _chat_dict(chat)
    74	
    75	
    76	@router.put("/{chat_id}")
    77	def rename_chat(chat_id: str, body: RenameChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    78	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    79	    if not chat:
    80	        raise HTTPException(404)
    81	    chat.title = body.title
    82	    db.commit()
    83	    return _chat_dict(chat)
    84	
    85	
    86	@router.delete("/{chat_id}")
    87	def delete_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    88	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    89	    if not chat:
    90	        raise HTTPException(404)
    91	    db.delete(chat)
    92	    db.commit()
    93	    return {"ok": True}
    94	
    95	
    96	@router.get("/{chat_id}/messages")
    97	def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    98	    chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
    99	    if not chat:
   100	        raise HTTPException(404)
   101	    return [_msg_dict(m) for m in chat.messages]
   102	
   103	
   104	# ── Send Message (Streaming) ─────────────────────
   105	
   106	@router.post("/{chat_id}/messages")
   107	async def send_message(
   108	    chat_id: str,
   109	    body: SendMessageBody,
   110	    user: User = Depends(get_current_user),
   111	):
   112	    user_id = user.id
   113	
   114	    async def generate():
   115	        db = SessionLocal()
   116	        try:
   117	            chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
   118	            if not chat:
   119	                yield _sse({"type": "error", "message": "Chat not found"})
   120	                return
   121	
   122	            db_user = db.query(User).filter(User.id == user_id).first()
   123	
   124	            # Quota check
   125	            now = datetime.utcnow()
   126	            if db_user.quota_reset_date and now >= db_user.quota_reset_date:
   127	                db_user.tokens_used_this_month = 0
   128	                if now.month == 12:
   129	                    db_user.quota_reset_date = datetime(now.year + 1, 1, 1)
   130	                else:
   131	                    db_user.quota_reset_date = datetime(now.year, now.month + 1, 1)
   132	                db.commit()
   133	
   134	            if db_user.tokens_used_this_month >= db_user.quota_tokens_monthly:
   135	                yield _sse({"type": "error", "message": "Monthly token quota exceeded. Contact your admin."})
   136	                return
   137	
   138	            # Save user message
   139	            user_msg = Message(chat_id=chat_id, role="user", content=body.content)
   140	            db.add(user_msg)
   141	            db.commit()
   142	
   143	            # Build context
   144	            kb_id = body.knowledge_base_id or chat.knowledge_base_id
   145	            rag_context = None
   146	            if kb_id:
   147	                try:
   148	                    rag_context = rag_service.query(kb_id, body.content, n_results=8)
   149	                except Exception:
   150	                    pass
   151	
   152	            system_prompt = build_full_prompt(rag_context)
   153	            messages = memory_service.build_messages(chat, db)
   154	
   155	            model_id = body.model or chat.model
   156	            max_tokens = body.max_tokens
   157	            thinking_config = None
   158	            if body.reasoning_budget > 0:
   159	                thinking_config = {
   160	                    "enabled": True,
   161	                    "budget_tokens": body.reasoning_budget,
   162	                }
   163	                max_tokens = max_tokens + body.reasoning_budget
   164	
   165	            full_text = ""
   166	            full_thinking = ""
   167	            input_tokens = 0
   168	            output_tokens = 0
   169	            current_block_type = "text"
   170	
   171	            async for event in bedrock_service.stream_response(
   172	                messages=messages,
   173	                system_prompt=system_prompt,
   174	                model_id=model_id,
   175	                max_tokens=min(max_tokens, 65536),
   176	                thinking_config=thinking_config,
   177	            ):
   178	                evt_type = event.get("type", "")
   179	
   180	                if evt_type == "message_start":
   181	                    usage = event.get("message", {}).get("usage", {})
   182	                    input_tokens = usage.get("input_tokens", 0)
   183	
   184	                elif evt_type == "content_block_start":
   185	                    blk = event.get("content_block", {})
   186	                    current_block_type = blk.get("type", "text")
   187	                    if current_block_type == "thinking":
   188	                        yield _sse({"type": "thinking_start"})
   189	
   190	                elif evt_type == "content_block_delta":
   191	                    delta = event.get("delta", {})
   192	                    dt = delta.get("type", "")
   193	                    if dt == "thinking_delta":
   194	                        t = delta.get("thinking", "")
   195	                        full_thinking += t
   196	                        yield _sse({"type": "thinking_delta", "content": t})
   197	                    elif dt == "text_delta":
   198	                        t = delta.get("text", "")
   199	                        full_text += t
   200	                        yield _sse({"type": "text_delta", "content": t})
   201	
   202	                elif evt_type == "content_block_stop":
   203	                    if current_block_type == "thinking":
   204	                        yield _sse({"type": "thinking_end"})
   205	
   206	                elif evt_type == "message_delta":
   207	                    usage = event.get("usage", {})
   208	                    output_tokens = usage.get("output_tokens", 0)
   209	
   210	            # Save assistant message
   211	            assistant_msg = Message(
   212	                chat_id=chat_id,
   213	                role="assistant",
   214	                content=full_text,
   215	                thinking_content=full_thinking or None,
   216	                input_tokens=input_tokens,
   217	                output_tokens=output_tokens,
   218	            )
   219	            db.add(assistant_msg)
   220	
   221	            db_user.tokens_used_this_month += input_tokens + output_tokens
   222	            chat.updated_at = datetime.utcnow()
   223	            db.commit()
   224	
   225	            # Auto title
   226	            msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
   227	            if msg_count <= 2 and chat.title == "New Chat":
   228	                try:
   229	                    title = await _generate_title(body.content, full_text[:300])
   230	                    chat.title = title[:120]
   231	                    db.commit()
   232	                    yield _sse({"type": "title_update", "title": chat.title})
   233	                except Exception:
   234	                    pass
   235	
   236	            yield _sse({"type": "usage", "input_tokens": input_tokens, "output_tokens": output_tokens})
   237	            yield _sse({"type": "done", "message_id": assistant_msg.id})
   238	
   239	        except Exception as exc:
   240	            yield _sse({"type": "error", "message": str(exc)})
   241	        finally:
   242	            db.close()
   243	
   244	    return StreamingResponse(generate(), media_type="text/event-stream")
   245	
   246	
   247	async def _generate_title(user_msg: str, ai_msg: str) -> str:
   248	    from backend.config import FAST_MODEL
   249	    result = await bedrock_service.invoke_model_simple(
   250	        model_id=FAST_MODEL,
   251	        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.",
   252	        max_tokens=30,
   253	    )
   254	    return result.strip().strip('"').strip("'")
   255	
   256	
   257	def _sse(data: dict) -> str:
   258	    return f"data: {json.dumps(data)}\n\n"
   259	
   260	
   261	def _chat_dict(c: Chat) -> dict:
   262	    return {
   263	        "id": c.id,
   264	        "title": c.title,
   265	        "model": c.model,
   266	        "knowledge_base_id": c.knowledge_base_id,
   267	        "created_at": str(c.created_at),
   268	        "updated_at": str(c.updated_at),
   269	    }
   270	
   271	
   272	def _msg_dict(m: Message) -> dict:
   273	    return {
   274	        "id": m.id,
   275	        "role": m.role,
   276	        "content": m.content,
   277	        "thinking_content": m.thinking_content,
   278	        "input_tokens": m.input_tokens,
   279	        "output_tokens": m.output_tokens,
   280	        "created_at": str(m.created_at),
   281	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [011]: backend/routes/chat_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [012/40]: 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 [012]: backend/routes/files_routes.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [013/40]: backend/routes/knowledge_routes.py
│ LANGUAGE: python | LINES: 176 | SIZE: 5498 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Knowledge base management and document upload.
     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_document(
    77	    kb_id: str,
    78	    file: UploadFile = File(...),
    79	    user: User = Depends(get_current_user),
    80	    db: Session = Depends(get_db),
    81	):
    82	    kb = _get_kb(kb_id, user, db)
    83	    content_bytes = await file.read()
    84	
    85	    if len(content_bytes) > MAX_UPLOAD_BYTES:
    86	        raise HTTPException(413, f"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB")
    87	
    88	    filename = file.filename or "document.txt"
    89	    text = _extract_text(filename, content_bytes)
    90	
    91	    if not text.strip():
    92	        raise HTTPException(400, "Could not extract text from file")
    93	
    94	    chunks = _chunk_text(text, chunk_size=3000, overlap=300)
    95	
    96	    rag_service.add_documents(
    97	        collection_id=kb_id,
    98	        documents=chunks,
    99	        metadatas=[{"filename": filename, "chunk_index": i} for i in range(len(chunks))],
   100	    )
   101	
   102	    doc = KnowledgeDocument(
   103	        knowledge_base_id=kb_id,
   104	        filename=filename,
   105	        file_size=len(content_bytes),
   106	        chunk_count=len(chunks),
   107	    )
   108	    db.add(doc)
   109	
   110	    kb.document_count = (kb.document_count or 0) + 1
   111	    kb.chunk_count = (kb.chunk_count or 0) + len(chunks)
   112	    kb.total_characters = (kb.total_characters or 0) + len(text)
   113	    db.commit()
   114	
   115	    return {
   116	        "filename": filename,
   117	        "chunks_added": len(chunks),
   118	        "characters": len(text),
   119	        "estimated_tokens": len(text) // 4,
   120	    }
   121	
   122	
   123	def _get_kb(kb_id: str, user: User, db: Session) -> KnowledgeBase:
   124	    kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first()
   125	    if not kb:
   126	        raise HTTPException(404, "Knowledge base not found")
   127	    if kb.user_id and kb.user_id != user.id and user.role != "superadmin":
   128	        raise HTTPException(403, "Access denied")
   129	    return kb
   130	
   131	
   132	def _extract_text(filename: str, content: bytes) -> str:
   133	    lower = filename.lower()
   134	    if lower.endswith(".pdf"):
   135	        try:
   136	            from PyPDF2 import PdfReader
   137	            import io
   138	            reader = PdfReader(io.BytesIO(content))
   139	            return "\n\n".join(page.extract_text() or "" for page in reader.pages)
   140	        except Exception as e:
   141	            raise HTTPException(400, f"PDF extraction failed: {e}")
   142	    else:
   143	        try:
   144	            return content.decode("utf-8")
   145	        except UnicodeDecodeError:
   146	            return content.decode("latin-1")
   147	
   148	
   149	def _chunk_text(text: str, chunk_size: int = 3000, overlap: int = 300) -> list[str]:
   150	    chunks = []
   151	    start = 0
   152	    while start < len(text):
   153	        end = start + chunk_size
   154	        if end < len(text):
   155	            for sep in ["\n\n", "\n", ". ", " "]:
   156	                pos = text.rfind(sep, start + chunk_size // 2, end)
   157	                if pos > start:
   158	                    end = pos + len(sep)
   159	                    break
   160	        chunk = text[start:end].strip()
   161	        if chunk:
   162	            chunks.append(chunk)
   163	        start = end - overlap if end < len(text) else end
   164	    return chunks
   165	
   166	
   167	def _kb_dict(kb: KnowledgeBase) -> dict:
   168	    return {
   169	        "id": kb.id,
   170	        "name": kb.name,
   171	        "description": kb.description,
   172	        "document_count": kb.document_count or 0,
   173	        "chunk_count": kb.chunk_count or 0,
   174	        "total_characters": kb.total_characters or 0,
   175	        "estimated_tokens": (kb.total_characters or 0) // 4,
   176	        "created_at": str(kb.created_at),
   177	    }│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [013]: backend/routes/knowledge_routes.py


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


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


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [017/40]: backend/services/memory_service.py
│ LANGUAGE: python | LINES: 60 | SIZE: 1767 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	"""
     2	Build the `messages` list for the Bedrock/Anthropic API from chat history.
     3	
     4	Keeps the most recent messages that fit within a character budget
     5	(rough proxy for tokens — 1 token ≈ 4 chars).
     6	"""
     7	
     8	from sqlalchemy.orm import Session
     9	from backend.models import Chat, Message
    10	
    11	# ~180 000 tokens budget → ~720 000 characters
    12	MAX_CONTEXT_CHARS = 720_000
    13	
    14	
    15	def build_messages(chat: Chat, db: Session) -> list[dict]:
    16	    """
    17	    Return a list of {"role": ..., "content": ...} ready for the API.
    18	    Messages are oldest-first (chronological).
    19	    """
    20	    rows: list[Message] = (
    21	        db.query(Message)
    22	        .filter(Message.chat_id == chat.id)
    23	        .order_by(Message.created_at.asc())
    24	        .all()
    25	    )
    26	
    27	    if not rows:
    28	        return []
    29	
    30	    # --- trim from the oldest to fit budget ---
    31	    total_chars = sum(len(m.content or "") for m in rows)
    32	    idx = 0
    33	    while total_chars > MAX_CONTEXT_CHARS and idx < len(rows) - 2:
    34	        total_chars -= len(rows[idx].content or "")
    35	        idx += 1
    36	
    37	    trimmed = rows[idx:]
    38	
    39	    # Anthropic requires the first message to be role=user.
    40	    # If trimming left an assistant message at the front, skip it.
    41	    while trimmed and trimmed[0].role != "user":
    42	        trimmed = trimmed[1:]
    43	
    44	    # Build the final list — collapse consecutive same-role messages
    45	    result: list[dict] = []
    46	    for m in trimmed:
    47	        content = m.content or ""
    48	        if not content.strip():
    49	            continue
    50	
    51	        role = m.role
    52	        if role not in ("user", "assistant"):
    53	            continue
    54	
    55	        if result and result[-1]["role"] == role:
    56	            # Merge into previous
    57	            result[-1]["content"] += "\n" + content
    58	        else:
    59	            result.append({"role": role, "content": content})
    60	
    61	    return result│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [017]: backend/services/memory_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [018/40]: 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 [018]: backend/services/rag_service.py


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [019/40]: 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 [019]: backend/system_prompt.py


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [021/40]: frontend/index.html
│ LANGUAGE: html | LINES: 18 | SIZE: 835 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	<!DOCTYPE html>
     2	<html lang="en" class="dark">
     3	  <head>
     4	    <meta charset="UTF-8" />
     5	    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     6	    <title>Son of Anton</title>
     7	    <link rel="preconnect" href="https://fonts.googleapis.com" />
     8	    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
     9	    <link
    10	      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
    11	      rel="stylesheet"
    12	    />
    13	    <link rel="icon" 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>" />
    14	  </head>
    15	  <body class="bg-anton-bg text-anton-text font-sans">
    16	    <div id="root"></div>
    17	    <script type="module" src="/src/main.jsx"></script>
    18	  </body>
    19	</html>│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [021]: frontend/index.html


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


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [024/40]: frontend/src/App.jsx
│ LANGUAGE: jsx | LINES: 21 | SIZE: 543 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React from "react";
     2	import { Routes, Route, Navigate } from "react-router-dom";
     3	import { useApp } from "./store";
     4	import LoginPage from "./pages/LoginPage";
     5	import ChatPage from "./pages/ChatPage";
     6	import AdminPage from "./pages/AdminPage";
     7	
     8	export default function App() {
     9	  const { state } = useApp();
    10	  const loggedIn = !!state.token;
    11	
    12	  if (!loggedIn) {
    13	    return <LoginPage />;
    14	  }
    15	
    16	  return (
    17	    <Routes>
    18	      <Route path="/admin" element={<AdminPage />} />
    19	      <Route path="/*" element={<ChatPage />} />
    20	    </Routes>
    21	  );
    22	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [024]: frontend/src/App.jsx


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [026/40]: frontend/src/components/ChatView.jsx
│ LANGUAGE: jsx | LINES: 338 | SIZE: 12453 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState, useEffect, useRef, useCallback } from "react";
     2	import { useApp } from "../store";
     3	import {
     4	  getMessages, streamMessage, downloadZip, listKnowledgeBases,
     5	} from "../api";
     6	import MessageBubble from "./MessageBubble";
     7	import {
     8	  Send, Square, Settings2, X, Brain, BookOpen, ChevronDown,
     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	export default function ChatView({ chatId }) {
    17	  const { state, dispatch } = useApp();
    18	  const [messages, setMessages] = useState([]);
    19	  const [input, setInput] = useState("");
    20	  const [streaming, setStreaming] = useState(false);
    21	  const [showSettings, setShowSettings] = useState(false);
    22	  const [model, setModel] = useState(MODELS[0].id);
    23	  const [maxTokens, setMaxTokens] = useState(4096);
    24	  const [reasoningBudget, setReasoningBudget] = useState(0);
    25	  const [selectedKbId, setSelectedKbId] = useState(null);
    26	  const [kbs, setKbs] = useState([]);
    27	  const [streamText, setStreamText] = useState("");
    28	  const [streamThinking, setStreamThinking] = useState("");
    29	  const [isThinking, setIsThinking] = useState(false);
    30	
    31	  const bottomRef = useRef(null);
    32	  const inputRef = useRef(null);
    33	  const abortRef = useRef(null);
    34	
    35	  const scroll = useCallback(() => {
    36	    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
    37	  }, []);
    38	
    39	  useEffect(() => {
    40	    (async () => {
    41	      try {
    42	        const msgs = await getMessages(state.token, chatId);
    43	        setMessages(msgs);
    44	        const kbData = await listKnowledgeBases(state.token);
    45	        setKbs(kbData);
    46	      } catch { /* */ }
    47	    })();
    48	  }, [chatId, state.token]);
    49	
    50	  useEffect(scroll, [messages, streamText, streamThinking, scroll]);
    51	
    52	  useEffect(() => {
    53	    inputRef.current?.focus();
    54	  }, [chatId]);
    55	
    56	  async function handleSend() {
    57	    const content = input.trim();
    58	    if (!content || streaming) return;
    59	
    60	    const userMsg = { id: `tmp-${Date.now()}`, role: "user", content, created_at: new Date().toISOString() };
    61	    setMessages((p) => [...p, userMsg]);
    62	    setInput("");
    63	    setStreaming(true);
    64	    setStreamText("");
    65	    setStreamThinking("");
    66	    setIsThinking(false);
    67	
    68	    const ac = new AbortController();
    69	    abortRef.current = ac;
    70	
    71	    try {
    72	      const body = {
    73	        content,
    74	        model,
    75	        max_tokens: maxTokens,
    76	        reasoning_budget: reasoningBudget,
    77	        knowledge_base_id: selectedKbId,
    78	      };
    79	
    80	      let fullText = "";
    81	      let fullThinking = "";
    82	      let usage = {};
    83	      let msgId = "";
    84	
    85	      for await (const evt of streamMessage(state.token, chatId, body)) {
    86	        if (ac.signal.aborted) break;
    87	
    88	        switch (evt.type) {
    89	          case "thinking_start":
    90	            setIsThinking(true);
    91	            break;
    92	          case "thinking_delta":
    93	            fullThinking += evt.content;
    94	            setStreamThinking(fullThinking);
    95	            break;
    96	          case "thinking_end":
    97	            setIsThinking(false);
    98	            break;
    99	          case "text_delta":
   100	            fullText += evt.content;
   101	            setStreamText(fullText);
   102	            break;
   103	          case "usage":
   104	            usage = { input_tokens: evt.input_tokens, output_tokens: evt.output_tokens };
   105	            break;
   106	          case "title_update":
   107	            dispatch({ type: "UPDATE_CHAT", chat: { id: chatId, title: evt.title } });
   108	            break;
   109	          case "done":
   110	            msgId = evt.message_id;
   111	            break;
   112	          case "error":
   113	            fullText += `\n\n**Error:** ${evt.message}`;
   114	            setStreamText(fullText);
   115	            break;
   116	        }
   117	      }
   118	
   119	      const assistantMsg = {
   120	        id: msgId || `gen-${Date.now()}`,
   121	        role: "assistant",
   122	        content: fullText,
   123	        thinking_content: fullThinking || null,
   124	        input_tokens: usage.input_tokens || 0,
   125	        output_tokens: usage.output_tokens || 0,
   126	        created_at: new Date().toISOString(),
   127	      };
   128	      setMessages((p) => [...p, assistantMsg]);
   129	    } catch (err) {
   130	      setMessages((p) => [
   131	        ...p,
   132	        { id: `err-${Date.now()}`, role: "assistant", content: `**Error:** ${err.message}`, created_at: new Date().toISOString() },
   133	      ]);
   134	    } finally {
   135	      setStreamText("");
   136	      setStreamThinking("");
   137	      setStreaming(false);
   138	      abortRef.current = null;
   139	    }
   140	  }
   141	
   142	  function handleStop() {
   143	    abortRef.current?.abort();
   144	  }
   145	
   146	  function handleKeyDown(e) {
   147	    if (e.key === "Enter" && !e.shiftKey) {
   148	      e.preventDefault();
   149	      handleSend();
   150	    }
   151	  }
   152	
   153	  async function handleDownloadAll() {
   154	    const all = messages
   155	      .filter((m) => m.role === "assistant")
   156	      .map((m) => m.content)
   157	      .join("\n\n---\n\n");
   158	    if (all) {
   159	      try { await downloadZip(state.token, all); } catch { /* */ }
   160	    }
   161	  }
   162	
   163	  return (
   164	    <div className="flex-1 flex flex-col min-h-0">
   165	      {/* Messages */}
   166	      <div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
   167	        {messages.map((m) => (
   168	          <MessageBubble key={m.id} message={m} />
   169	        ))}
   170	
   171	        {/* Streaming in progress */}
   172	        {streaming && (streamThinking || streamText) && (
   173	          <MessageBubble
   174	            message={{
   175	              id: "streaming",
   176	              role: "assistant",
   177	              content: streamText,
   178	              thinking_content: streamThinking || null,
   179	            }}
   180	            isStreaming
   181	            isThinking={isThinking}
   182	          />
   183	        )}
   184	
   185	        {streaming && !streamText && !streamThinking && (
   186	          <div className="flex items-center gap-2 px-4 py-3 animate-fade-in">
   187	            <div className="flex gap-1">
   188	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "0ms" }} />
   189	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "150ms" }} />
   190	              <span className="w-2 h-2 bg-anton-accent rounded-full animate-bounce" style={{ animationDelay: "300ms" }} />
   191	            </div>
   192	            <span className="text-anton-muted text-sm">Son of Anton is thinking…</span>
   193	          </div>
   194	        )}
   195	
   196	        <div ref={bottomRef} />
   197	      </div>
   198	
   199	      {/* Input area */}
   200	      <div className="border-t border-anton-border bg-anton-surface p-4">
   201	        {/* Settings bar */}
   202	        {showSettings && (
   203	          <div className="mb-3 bg-anton-card border border-anton-border rounded-xl p-4 space-y-4 animate-fade-in">
   204	            <div className="flex items-center justify-between">
   205	              <h3 className="text-sm font-semibold text-white flex items-center gap-1.5">
   206	                <Settings2 size={14} className="text-anton-accent" /> Generation Settings
   207	              </h3>
   208	              <button onClick={() => setShowSettings(false)} className="text-anton-muted hover:text-white"><X size={14} /></button>
   209	            </div>
   210	
   211	            {/* Model */}
   212	            <div>
   213	              <label className="text-xs text-anton-muted mb-1 block">Model</label>
   214	              <select value={model} onChange={(e) => setModel(e.target.value)}
   215	                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"
   216	              >
   217	                {MODELS.map((m) => (
   218	                  <option key={m.id} value={m.id}>{m.label}</option>
   219	                ))}
   220	              </select>
   221	            </div>
   222	
   223	            {/* Max Tokens */}
   224	            <div>
   225	              <div className="flex justify-between text-xs mb-1">
   226	                <span className="text-anton-muted">Max Output Tokens</span>
   227	                <span className="text-anton-accent font-mono">{maxTokens.toLocaleString()}</span>
   228	              </div>
   229	              <input type="range" min={256} max={65536} step={256} value={maxTokens}
   230	                onChange={(e) => setMaxTokens(Number(e.target.value))}
   231	              />
   232	              <div className="flex justify-between text-[10px] text-anton-muted mt-0.5">
   233	                <span>256</span><span>64K</span>
   234	              </div>
   235	            </div>
   236	
   237	            {/* Reasoning Budget */}
   238	            <div>
   239	              <div className="flex justify-between text-xs mb-1">
   240	                <span className="text-anton-muted flex items-center gap-1">
   241	                  <Brain size={12} className="text-purple-400" /> Reasoning Budget
   242	                </span>
   243	                <span className="text-purple-400 font-mono">
   244	                  {reasoningBudget === 0 ? "Off" : reasoningBudget.toLocaleString()}
   245	                </span>
   246	              </div>
   247	              <input type="range" min={0} max={32000} step={500} value={reasoningBudget}
   248	                onChange={(e) => setReasoningBudget(Number(e.target.value))}
   249	              />
   250	              <div className="flex justify-between text-[10px] text-anton-muted mt-0.5">
   251	                <span>Off</span><span>32K tokens</span>
   252	              </div>
   253	            </div>
   254	
   255	            {/* Knowledge Base */}
   256	            <div>
   257	              <label className="text-xs text-anton-muted mb-1 flex items-center gap-1">
   258	                <BookOpen size={12} /> Knowledge Base (RAG)
   259	              </label>
   260	              <select value={selectedKbId || ""} onChange={(e) => setSelectedKbId(e.target.value || null)}
   261	                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"
   262	              >
   263	                <option value="">None</option>
   264	                {kbs.map((kb) => (
   265	                  <option key={kb.id} value={kb.id}>{kb.name} ({kb.document_count} docs)</option>
   266	                ))}
   267	              </select>
   268	            </div>
   269	          </div>
   270	        )}
   271	
   272	        <div className="flex items-end gap-2">
   273	          <button onClick={() => setShowSettings(!showSettings)}
   274	            className={`p-2.5 rounded-xl transition shrink-0 ${
   275	              showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card"
   276	            }`}
   277	          >
   278	            <Settings2 size={18} />
   279	          </button>
   280	
   281	          <div className="flex-1 relative">
   282	            <textarea
   283	              ref={inputRef}
   284	              value={input}
   285	              onChange={(e) => setInput(e.target.value)}
   286	              onKeyDown={handleKeyDown}
   287	              placeholder="Ask Son of Anton anything… (Shift+Enter for new line)"
   288	              rows={1}
   289	              style={{ maxHeight: "200px" }}
   290	              className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white text-sm resize-none focus:outline-none focus:border-anton-accent transition"
   291	              onInput={(e) => {
   292	                e.target.style.height = "auto";
   293	                e.target.style.height = Math.min(e.target.scrollHeight, 200) + "px";
   294	              }}
   295	            />
   296	          </div>
   297	
   298	          {streaming ? (
   299	            <button onClick={handleStop}
   300	              className="p-2.5 rounded-xl bg-anton-danger text-white hover:opacity-80 transition shrink-0"
   301	            >
   302	              <Square size={18} />
   303	            </button>
   304	          ) : (
   305	            <button onClick={handleSend} disabled={!input.trim()}
   306	              className="p-2.5 rounded-xl bg-anton-accent text-white hover:opacity-80 transition shrink-0 disabled:opacity-30 disabled:cursor-not-allowed"
   307	            >
   308	              <Send size={18} />
   309	            </button>
   310	          )}
   311	        </div>
   312	
   313	        {/* Quick info bar */}
   314	        <div className="flex items-center gap-3 mt-2 text-[11px] text-anton-muted">
   315	          <span>{MODELS.find((m) => m.id === model)?.label}</span>
   316	          <span>•</span>
   317	          <span>{maxTokens.toLocaleString()} max tokens</span>
   318	          {reasoningBudget > 0 && (
   319	            <>
   320	              <span>•</span>
   321	              <span className="text-purple-400">🧠 {reasoningBudget.toLocaleString()} reasoning</span>
   322	            </>
   323	          )}
   324	          {selectedKbId && (
   325	            <>
   326	              <span>•</span>
   327	              <span className="text-green-400">📚 RAG active</span>
   328	            </>
   329	          )}
   330	          {messages.some((m) => m.role === "assistant") && (
   331	            <button onClick={handleDownloadAll} className="ml-auto hover:text-anton-accent transition">
   332	              ⬇ Download all code
   333	            </button>
   334	          )}
   335	        </div>
   336	      </div>
   337	    </div>
   338	  );
   339	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [026]: frontend/src/components/ChatView.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [027/40]: 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 [027]: frontend/src/components/CodeBlock.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [028/40]: frontend/src/components/MessageBubble.jsx
│ LANGUAGE: jsx | LINES: 135 | SIZE: 5340 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 { User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check } from "lucide-react";
     6	
     7	export default function MessageBubble({ message, isStreaming, isThinking }) {
     8	  const { role, content, thinking_content, input_tokens, output_tokens } = message;
     9	  const isUser = role === "user";
    10	  const [showThinking, setShowThinking] = useState(false);
    11	  const [copied, setCopied] = useState(false);
    12	
    13	  function handleCopy() {
    14	    navigator.clipboard.writeText(content || "");
    15	    setCopied(true);
    16	    setTimeout(() => setCopied(false), 2000);
    17	  }
    18	
    19	  return (
    20	    <div className={`flex gap-3 animate-fade-in ${isUser ? "justify-end" : ""}`}>
    21	      {/* Avatar */}
    22	      {!isUser && (
    23	        <div className="shrink-0 mt-1">
    24	          <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">
    25	            <Flame size={16} className="text-white" />
    26	          </div>
    27	        </div>
    28	      )}
    29	
    30	      <div className={`max-w-[80%] ${isUser ? "order-first" : ""}`}>
    31	        {/* Thinking block */}
    32	        {thinking_content && (
    33	          <div className="mb-2">
    34	            <button onClick={() => setShowThinking(!showThinking)}
    35	              className="flex items-center gap-1.5 text-xs text-purple-400 hover:text-purple-300 transition mb-1"
    36	            >
    37	              <Brain size={12} />
    38	              {showThinking ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
    39	              {isThinking ? (
    40	                <span className="thinking-pulse">Reasoning…</span>
    41	              ) : (
    42	                <span>View reasoning</span>
    43	              )}
    44	            </button>
    45	            {(showThinking || isThinking) && (
    46	              <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">
    47	                {thinking_content}
    48	                {isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
    49	              </div>
    50	            )}
    51	          </div>
    52	        )}
    53	
    54	        {/* Message content */}
    55	        <div className={`rounded-2xl px-4 py-3 ${
    56	          isUser
    57	            ? "bg-anton-accent text-white rounded-br-md"
    58	            : "bg-anton-card border border-anton-border rounded-bl-md"
    59	        }`}>
    60	          {isUser ? (
    61	            <div className="text-sm whitespace-pre-wrap">{content}</div>
    62	          ) : (
    63	            <div className="prose-anton text-sm">
    64	              <ReactMarkdown
    65	                remarkPlugins={[remarkGfm]}
    66	                components={{
    67	                  code({ node, inline, className, children, ...props }) {
    68	                    const match = /language-(\S+)/.exec(className || "");
    69	                    const rawLang = match?.[1] || "";
    70	
    71	                    if (inline) {
    72	                      return (
    73	                        <code className={className} {...props}>
    74	                          {children}
    75	                        </code>
    76	                      );
    77	                    }
    78	
    79	                    // Parse "lang:filename" format
    80	                    let lang = rawLang;
    81	                    let filename = null;
    82	                    if (rawLang.includes(":")) {
    83	                      const idx = rawLang.indexOf(":");
    84	                      lang = rawLang.slice(0, idx);
    85	                      filename = rawLang.slice(idx + 1);
    86	                    }
    87	
    88	                    const code = String(children).replace(/\n$/, "");
    89	                    return (
    90	                      <CodeBlock language={lang} filename={filename} code={code} />
    91	                    );
    92	                  },
    93	                  // Make sure pre doesn't double-wrap
    94	                  pre({ children }) {
    95	                    return <>{children}</>;
    96	                  },
    97	                }}
    98	              >
    99	                {content || ""}
   100	              </ReactMarkdown>
   101	              {isStreaming && !isThinking && (
   102	                <span className="inline-block w-1.5 h-4 bg-anton-accent ml-0.5 animate-pulse" />
   103	              )}
   104	            </div>
   105	          )}
   106	        </div>
   107	
   108	        {/* Footer */}
   109	        {!isUser && !isStreaming && content && (
   110	          <div className="flex items-center gap-3 mt-1.5 px-1">
   111	            <button onClick={handleCopy}
   112	              className="flex items-center gap-1 text-[11px] text-anton-muted hover:text-white transition"
   113	            >
   114	              {copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
   115	              {copied ? "Copied" : "Copy"}
   116	            </button>
   117	            {(input_tokens > 0 || output_tokens > 0) && (
   118	              <span className="text-[11px] text-anton-muted">
   119	                {input_tokens?.toLocaleString()}↓ / {output_tokens?.toLocaleString()}↑ tokens
   120	              </span>
   121	            )}
   122	          </div>
   123	        )}
   124	      </div>
   125	
   126	      {/* User avatar */}
   127	      {isUser && (
   128	        <div className="shrink-0 mt-1">
   129	          <div className="w-8 h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center">
   130	            <User size={16} className="text-anton-muted" />
   131	          </div>
   132	        </div>
   133	      )}
   134	    </div>
   135	  );
   136	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [028]: frontend/src/components/MessageBubble.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [029/40]: frontend/src/components/Sidebar.jsx
│ LANGUAGE: jsx | LINES: 265 | SIZE: 11406 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useState } from "react";
     2	import { useNavigate } from "react-router-dom";
     3	import { useApp } from "../store";
     4	import {
     5	  createChat, deleteChat, renameChat,
     6	  listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase, uploadDocument,
     7	} from "../api";
     8	import {
     9	  Plus, Trash2, Flame, LogOut, Shield, PanelLeftClose, PanelLeftOpen,
    10	  MessageSquare, BookOpen, Upload, X, ChevronDown, ChevronRight, Edit2, Check,
    11	} from "lucide-react";
    12	
    13	export default function Sidebar({ onRefresh }) {
    14	  const { state, dispatch } = useApp();
    15	  const navigate = useNavigate();
    16	  const [tab, setTab] = useState("chats");         // "chats" | "knowledge"
    17	  const [kbs, setKbs] = useState([]);
    18	  const [kbLoaded, setKbLoaded] = useState(false);
    19	  const [newKbName, setNewKbName] = useState("");
    20	  const [showNewKb, setShowNewKb] = useState(false);
    21	  const [expandedKb, setExpandedKb] = useState(null);
    22	  const [uploading, setUploading] = useState(false);
    23	  const [renamingId, setRenamingId] = useState(null);
    24	  const [renameVal, setRenameVal] = useState("");
    25	
    26	  const open = state.sidebarOpen;
    27	
    28	  async function handleNewChat() {
    29	    try {
    30	      const chat = await createChat(state.token);
    31	      dispatch({ type: "ADD_CHAT", chat });
    32	      onRefresh();
    33	    } catch { /* */ }
    34	  }
    35	
    36	  async function handleDelete(id) {
    37	    try {
    38	      await deleteChat(state.token, id);
    39	      dispatch({ type: "REMOVE_CHAT", chatId: id });
    40	    } catch { /* */ }
    41	  }
    42	
    43	  async function handleRename(id) {
    44	    if (!renameVal.trim()) return;
    45	    try {
    46	      await renameChat(state.token, id, renameVal.trim());
    47	      dispatch({ type: "UPDATE_CHAT", chat: { id, title: renameVal.trim() } });
    48	      setRenamingId(null);
    49	    } catch { /* */ }
    50	  }
    51	
    52	  async function loadKbs() {
    53	    try {
    54	      const data = await listKnowledgeBases(state.token);
    55	      setKbs(data);
    56	      setKbLoaded(true);
    57	    } catch { /* */ }
    58	  }
    59	
    60	  async function handleCreateKb() {
    61	    if (!newKbName.trim()) return;
    62	    try {
    63	      await createKnowledgeBase(state.token, newKbName.trim());
    64	      setNewKbName("");
    65	      setShowNewKb(false);
    66	      loadKbs();
    67	    } catch { /* */ }
    68	  }
    69	
    70	  async function handleDeleteKb(id) {
    71	    if (!confirm("Delete this knowledge base?")) return;
    72	    try {
    73	      await deleteKnowledgeBase(state.token, id);
    74	      loadKbs();
    75	    } catch { /* */ }
    76	  }
    77	
    78	  async function handleUpload(kbId, file) {
    79	    setUploading(true);
    80	    try {
    81	      await uploadDocument(state.token, kbId, file);
    82	      loadKbs();
    83	    } catch (e) {
    84	      alert(e.message);
    85	    } finally {
    86	      setUploading(false);
    87	    }
    88	  }
    89	
    90	  function switchTab(t) {
    91	    setTab(t);
    92	    if (t === "knowledge" && !kbLoaded) loadKbs();
    93	  }
    94	
    95	  if (!open) {
    96	    return (
    97	      <div className="w-12 bg-anton-surface border-r border-anton-border flex flex-col items-center py-3 gap-3 shrink-0">
    98	        <button onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })} className="p-2 rounded-lg hover:bg-anton-card text-anton-muted hover:text-white transition">
    99	          <PanelLeftOpen size={18} />
   100	        </button>
   101	        <button onClick={handleNewChat} className="p-2 rounded-lg bg-anton-accent/20 text-anton-accent hover:bg-anton-accent/30 transition">
   102	          <Plus size={18} />
   103	        </button>
   104	      </div>
   105	    );
   106	  }
   107	
   108	  return (
   109	    <div className="w-72 bg-anton-surface border-r border-anton-border flex flex-col shrink-0">
   110	      {/* Header */}
   111	      <div className="p-3 border-b border-anton-border flex items-center justify-between">
   112	        <div className="flex items-center gap-2">
   113	          <Flame size={20} className="text-anton-accent" />
   114	          <span className="font-bold text-white text-sm">Son of Anton</span>
   115	        </div>
   116	        <button onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })} className="p-1.5 rounded-lg hover:bg-anton-card text-anton-muted hover:text-white transition">
   117	          <PanelLeftClose size={16} />
   118	        </button>
   119	      </div>
   120	
   121	      {/* Tabs */}
   122	      <div className="flex border-b border-anton-border">
   123	        {[
   124	          { key: "chats", label: "Chats", icon: MessageSquare },
   125	          { key: "knowledge", label: "Knowledge", icon: BookOpen },
   126	        ].map((t) => (
   127	          <button
   128	            key={t.key}
   129	            onClick={() => switchTab(t.key)}
   130	            className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-xs font-medium transition ${
   131	              tab === t.key ? "text-anton-accent border-b-2 border-anton-accent" : "text-anton-muted hover:text-white"
   132	            }`}
   133	          >
   134	            <t.icon size={13} />
   135	            {t.label}
   136	          </button>
   137	        ))}
   138	      </div>
   139	
   140	      {/* Content */}
   141	      <div className="flex-1 overflow-y-auto p-2 space-y-1">
   142	        {tab === "chats" && (
   143	          <>
   144	            <button onClick={handleNewChat}
   145	              className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg border border-dashed border-anton-border text-anton-muted hover:text-anton-accent hover:border-anton-accent transition text-sm"
   146	            >
   147	              <Plus size={15} /> New Chat
   148	            </button>
   149	
   150	            {state.chats.map((c) => (
   151	              <div
   152	                key={c.id}
   153	                className={`group flex items-center rounded-lg cursor-pointer transition ${
   154	                  state.activeChatId === c.id ? "bg-anton-accent/10 text-anton-accent" : "text-anton-text hover:bg-anton-card"
   155	                }`}
   156	              >
   157	                {renamingId === c.id ? (
   158	                  <div className="flex items-center gap-1 flex-1 p-1">
   159	                    <input value={renameVal} onChange={(e) => setRenameVal(e.target.value)}
   160	                      onKeyDown={(e) => e.key === "Enter" && handleRename(c.id)}
   161	                      autoFocus
   162	                      className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs focus:outline-none focus:border-anton-accent"
   163	                    />
   164	                    <button onClick={() => handleRename(c.id)} className="p-1 text-anton-success"><Check size={12} /></button>
   165	                    <button onClick={() => setRenamingId(null)} className="p-1 text-anton-muted"><X size={12} /></button>
   166	                  </div>
   167	                ) : (
   168	                  <>
   169	                    <button onClick={() => dispatch({ type: "SET_ACTIVE_CHAT", chatId: c.id })}
   170	                      className="flex-1 text-left px-3 py-2 text-sm truncate"
   171	                    >
   172	                      {c.title}
   173	                    </button>
   174	                    <div className="hidden group-hover:flex items-center pr-1 gap-0.5 shrink-0">
   175	                      <button onClick={() => { setRenamingId(c.id); setRenameVal(c.title); }}
   176	                        className="p-1 rounded hover:bg-anton-border text-anton-muted"><Edit2 size={11} /></button>
   177	                      <button onClick={() => handleDelete(c.id)}
   178	                        className="p-1 rounded hover:bg-red-500/20 text-anton-danger"><Trash2 size={11} /></button>
   179	                    </div>
   180	                  </>
   181	                )}
   182	              </div>
   183	            ))}
   184	          </>
   185	        )}
   186	
   187	        {tab === "knowledge" && (
   188	          <>
   189	            <button onClick={() => setShowNewKb(!showNewKb)}
   190	              className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg border border-dashed border-anton-border text-anton-muted hover:text-anton-accent hover:border-anton-accent transition text-sm"
   191	            >
   192	              <Plus size={15} /> New Knowledge Base
   193	            </button>
   194	
   195	            {showNewKb && (
   196	              <div className="flex gap-1 p-1">
   197	                <input value={newKbName} onChange={(e) => setNewKbName(e.target.value)}
   198	                  onKeyDown={(e) => e.key === "Enter" && handleCreateKb()}
   199	                  placeholder="Name…" autoFocus
   200	                  className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-1 text-white text-xs focus:outline-none focus:border-anton-accent"
   201	                />
   202	                <button onClick={handleCreateKb} className="px-2 py-1 bg-anton-accent rounded text-white text-xs">Add</button>
   203	              </div>
   204	            )}
   205	
   206	            {kbs.map((kb) => (
   207	              <div key={kb.id} className="rounded-lg border border-anton-border/50 overflow-hidden">
   208	                <button onClick={() => setExpandedKb(expandedKb === kb.id ? null : kb.id)}
   209	                  className="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-anton-card transition"
   210	                >
   211	                  {expandedKb === kb.id ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
   212	                  <BookOpen size={13} className="text-anton-accent shrink-0" />
   213	                  <span className="flex-1 truncate">{kb.name}</span>
   214	                  <span className="text-xs text-anton-muted">{kb.document_count} docs</span>
   215	                </button>
   216	
   217	                {expandedKb === kb.id && (
   218	                  <div className="px-3 pb-3 space-y-2 bg-anton-card/50">
   219	                    <div className="text-xs text-anton-muted space-y-0.5">
   220	                      <div>Chunks: {kb.chunk_count} &middot; ~{(kb.estimated_tokens / 1000).toFixed(0)}K tokens</div>
   221	                    </div>
   222	                    <label className={`flex items-center gap-1.5 px-2 py-1.5 rounded border border-dashed border-anton-border text-xs text-anton-muted hover:text-anton-accent hover:border-anton-accent transition cursor-pointer ${uploading ? "opacity-50 pointer-events-none" : ""}`}>
   223	                      <Upload size={12} />
   224	                      {uploading ? "Uploading…" : "Upload file (.txt, .pdf, .md, .json, .csv …)"}
   225	                      <input type="file" className="hidden" accept=".txt,.md,.pdf,.json,.csv,.py,.js,.ts,.cs,.html,.css,.xml,.yaml,.yml,.toml"
   226	                        onChange={(e) => e.target.files[0] && handleUpload(kb.id, e.target.files[0])}
   227	                      />
   228	                    </label>
   229	                    <button onClick={() => handleDeleteKb(kb.id)}
   230	                      className="flex items-center gap-1 text-xs text-anton-danger hover:underline">
   231	                      <Trash2 size={11} /> Delete KB
   232	                    </button>
   233	                  </div>
   234	                )}
   235	              </div>
   236	            ))}
   237	          </>
   238	        )}
   239	      </div>
   240	
   241	      {/* Footer */}
   242	      <div className="p-3 border-t border-anton-border space-y-2">
   243	        {state.user?.role === "superadmin" && (
   244	          <button onClick={() => navigate("/admin")}
   245	            className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-anton-muted hover:text-anton-accent hover:bg-anton-card transition"
   246	          >
   247	            <Shield size={15} /> Admin Panel
   248	          </button>
   249	        )}
   250	        <div className="flex items-center justify-between px-2">
   251	          <div>
   252	            <div className="text-sm font-medium text-white">{state.user?.username}</div>
   253	            <div className="text-xs text-anton-muted">
   254	              {((state.user?.tokens_used_this_month || 0) / 1000).toFixed(0)}K / {((state.user?.quota_tokens_monthly || 0) / 1000).toFixed(0)}K tokens
   255	            </div>
   256	          </div>
   257	          <button onClick={() => dispatch({ type: "LOGOUT" })}
   258	            className="p-2 rounded-lg text-anton-muted hover:text-anton-danger hover:bg-red-500/10 transition"
   259	          >
   260	            <LogOut size={16} />
   261	          </button>
   262	        </div>
   263	      </div>
   264	    </div>
   265	  );
   266	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [029]: frontend/src/components/Sidebar.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [030/40]: frontend/src/index.css
│ LANGUAGE: css | LINES: 137 | SIZE: 2769 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	@tailwind base;
     2	@tailwind components;
     3	@tailwind utilities;
     4	
     5	/* ── Globals ───────────────────────────────── */
     6	* {
     7	  scrollbar-width: thin;
     8	  scrollbar-color: #2a2a3a #0a0a0f;
     9	}
    10	*::-webkit-scrollbar {
    11	  width: 6px;
    12	}
    13	*::-webkit-scrollbar-track {
    14	  background: #0a0a0f;
    15	}
    16	*::-webkit-scrollbar-thumb {
    17	  background: #2a2a3a;
    18	  border-radius: 3px;
    19	}
    20	
    21	html,
    22	body,
    23	#root {
    24	  height: 100%;
    25	  margin: 0;
    26	  overflow: hidden;
    27	}
    28	
    29	/* ── Markdown prose adjustments ────────────── */
    30	.prose-anton h1,
    31	.prose-anton h2,
    32	.prose-anton h3 {
    33	  color: #f97316;
    34	  margin-top: 1em;
    35	  margin-bottom: 0.5em;
    36	  font-weight: 600;
    37	}
    38	.prose-anton h1 { font-size: 1.5rem; }
    39	.prose-anton h2 { font-size: 1.25rem; }
    40	.prose-anton h3 { font-size: 1.1rem; }
    41	
    42	.prose-anton p {
    43	  margin-bottom: 0.75em;
    44	  line-height: 1.7;
    45	}
    46	
    47	.prose-anton ul,
    48	.prose-anton ol {
    49	  margin-left: 1.5em;
    50	  margin-bottom: 0.75em;
    51	}
    52	.prose-anton li {
    53	  margin-bottom: 0.25em;
    54	}
    55	.prose-anton ul { list-style-type: disc; }
    56	.prose-anton ol { list-style-type: decimal; }
    57	
    58	.prose-anton a {
    59	  color: #f97316;
    60	  text-decoration: underline;
    61	}
    62	
    63	.prose-anton blockquote {
    64	  border-left: 3px solid #f97316;
    65	  padding-left: 1em;
    66	  color: #8888a0;
    67	  margin: 0.75em 0;
    68	}
    69	
    70	.prose-anton table {
    71	  border-collapse: collapse;
    72	  margin: 0.75em 0;
    73	  width: 100%;
    74	}
    75	.prose-anton th,
    76	.prose-anton td {
    77	  border: 1px solid #2a2a3a;
    78	  padding: 0.4em 0.75em;
    79	  text-align: left;
    80	}
    81	.prose-anton th {
    82	  background: #1a1a28;
    83	  font-weight: 600;
    84	}
    85	
    86	.prose-anton code:not(pre code) {
    87	  background: #1a1a28;
    88	  padding: 0.15em 0.4em;
    89	  border-radius: 4px;
    90	  font-size: 0.9em;
    91	  font-family: "JetBrains Mono", monospace;
    92	  color: #f97316;
    93	}
    94	
    95	/* ── Thinking block animation ──────────────── */
    96	@keyframes thinkPulse {
    97	  0%, 100% { opacity: 0.6; }
    98	  50%      { opacity: 1;   }
    99	}
   100	.thinking-pulse {
   101	  animation: thinkPulse 2s ease-in-out infinite;
   102	}
   103	
   104	/* ── Slider custom styling ─────────────────── */
   105	input[type="range"] {
   106	  -webkit-appearance: none;
   107	  appearance: none;
   108	  background: transparent;
   109	  width: 100%;
   110	}
   111	input[type="range"]::-webkit-slider-track {
   112	  height: 4px;
   113	  border-radius: 2px;
   114	  background: #2a2a3a;
   115	}
   116	input[type="range"]::-webkit-slider-thumb {
   117	  -webkit-appearance: none;
   118	  appearance: none;
   119	  height: 16px;
   120	  width: 16px;
   121	  border-radius: 50%;
   122	  background: #f97316;
   123	  margin-top: -6px;
   124	  cursor: pointer;
   125	}
   126	input[type="range"]::-moz-range-track {
   127	  height: 4px;
   128	  border-radius: 2px;
   129	  background: #2a2a3a;
   130	}
   131	input[type="range"]::-moz-range-thumb {
   132	  height: 16px;
   133	  width: 16px;
   134	  border-radius: 50%;
   135	  background: #f97316;
   136	  border: none;
   137	  cursor: pointer;
   138	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [030]: frontend/src/index.css


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [031/40]: 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 App from "./App";
     5	import { AppProvider } from "./store";
     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 [031]: frontend/src/main.jsx


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [033/40]: frontend/src/pages/ChatPage.jsx
│ LANGUAGE: jsx | LINES: 53 | SIZE: 1690 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	import React, { useEffect, useCallback } 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 } from "lucide-react";
     7	
     8	export default function ChatPage() {
     9	  const { state, dispatch } = useApp();
    10	
    11	  const loadChats = useCallback(async () => {
    12	    try {
    13	      const chats = await listChats(state.token);
    14	      dispatch({ type: "SET_CHATS", chats });
    15	    } catch {
    16	      /* ignore */
    17	    }
    18	  }, [state.token, dispatch]);
    19	
    20	  useEffect(() => {
    21	    loadChats();
    22	  }, [loadChats]);
    23	
    24	  return (
    25	    <div className="h-full flex">
    26	      <Sidebar onRefresh={loadChats} />
    27	
    28	      <main className="flex-1 flex flex-col min-w-0">
    29	        {state.activeChatId ? (
    30	          <ChatView key={state.activeChatId} chatId={state.activeChatId} />
    31	        ) : (
    32	          <EmptyState />
    33	        )}
    34	      </main>
    35	    </div>
    36	  );
    37	}
    38	
    39	function EmptyState() {
    40	  return (
    41	    <div className="flex-1 flex items-center justify-center p-8">
    42	      <div className="text-center animate-fade-in">
    43	        <div className="inline-flex items-center justify-center w-24 h-24 rounded-3xl bg-gradient-to-br from-anton-accent/20 to-transparent border border-anton-accent/20 mb-6">
    44	          <Flame size={44} className="text-anton-accent" />
    45	        </div>
    46	        <h2 className="text-2xl font-bold text-white mb-2">Son of Anton</h2>
    47	        <p className="text-anton-muted max-w-md">
    48	          Avatar of All Elements of Code. Create a new chat to begin — but bring
    49	          real questions, not that first-result-of-Google garbage.
    50	        </p>
    51	      </div>
    52	    </div>
    53	  );
    54	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [033]: frontend/src/pages/ChatPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [034/40]: 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 [034]: frontend/src/pages/LoginPage.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [035/40]: frontend/src/store.jsx
│ LANGUAGE: jsx | LINES: 79 | SIZE: 2034 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	/**
     2	 * Global state via React Context + useReducer
     3	 */
     4	
     5	import React, { createContext, useContext, useReducer, useEffect } from "react";
     6	
     7	const AppContext = createContext();
     8	
     9	const initialState = {
    10	  token: localStorage.getItem("soa_token") || null,
    11	  user: JSON.parse(localStorage.getItem("soa_user") || "null"),
    12	  chats: [],
    13	  activeChatId: null,
    14	  sidebarOpen: true,
    15	};
    16	
    17	function reducer(state, action) {
    18	  switch (action.type) {
    19	    case "LOGIN":
    20	      localStorage.setItem("soa_token", action.token);
    21	      localStorage.setItem("soa_user", JSON.stringify(action.user));
    22	      return { ...state, token: action.token, user: action.user };
    23	
    24	    case "LOGOUT":
    25	      localStorage.removeItem("soa_token");
    26	      localStorage.removeItem("soa_user");
    27	      return { ...initialState, token: null, user: null };
    28	
    29	    case "SET_CHATS":
    30	      return { ...state, chats: action.chats };
    31	
    32	    case "ADD_CHAT":
    33	      return {
    34	        ...state,
    35	        chats: [action.chat, ...state.chats],
    36	        activeChatId: action.chat.id,
    37	      };
    38	
    39	    case "UPDATE_CHAT": {
    40	      const updated = state.chats.map((c) =>
    41	        c.id === action.chat.id ? { ...c, ...action.chat } : c
    42	      );
    43	      return { ...state, chats: updated };
    44	    }
    45	
    46	    case "REMOVE_CHAT": {
    47	      const filtered = state.chats.filter((c) => c.id !== action.chatId);
    48	      return {
    49	        ...state,
    50	        chats: filtered,
    51	        activeChatId:
    52	          state.activeChatId === action.chatId
    53	            ? filtered[0]?.id || null
    54	            : state.activeChatId,
    55	      };
    56	    }
    57	
    58	    case "SET_ACTIVE_CHAT":
    59	      return { ...state, activeChatId: action.chatId };
    60	
    61	    case "TOGGLE_SIDEBAR":
    62	      return { ...state, sidebarOpen: !state.sidebarOpen };
    63	
    64	    default:
    65	      return state;
    66	  }
    67	}
    68	
    69	export function AppProvider({ children }) {
    70	  const [state, dispatch] = useReducer(reducer, initialState);
    71	  return (
    72	    <AppContext.Provider value={{ state, dispatch }}>
    73	      {children}
    74	    </AppContext.Provider>
    75	  );
    76	}
    77	
    78	export function useApp() {
    79	  return useContext(AppContext);
    80	}│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [035]: frontend/src/store.jsx


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [036/40]: frontend/tailwind.config.js
│ LANGUAGE: javascript | LINES: 38 | SIZE: 1033 bytes
├──────────────────────────────────────────────────────────────────────────────
│
     1	/** @type {import('tailwindcss').Config} */
     2	export default {
     3	  content: ["./index.html", "./src/**/*.{js,jsx}"],
     4	  theme: {
     5	    extend: {
     6	      colors: {
     7	        anton: {
     8	          bg: "#0a0a0f",
     9	          surface: "#12121a",
    10	          card: "#1a1a28",
    11	          border: "#2a2a3a",
    12	          accent: "#f97316",
    13	          accentDim: "#c2410c",
    14	          text: "#e4e4ef",
    15	          muted: "#8888a0",
    16	          user: "#1e293b",
    17	          assistant: "#15151f",
    18	          danger: "#ef4444",
    19	          success: "#22c55e",
    20	        },
    21	      },
    22	      fontFamily: {
    23	        sans: ['"Inter"', "system-ui", "sans-serif"],
    24	        mono: ['"JetBrains Mono"', '"Fira Code"', "monospace"],
    25	      },
    26	      animation: {
    27	        "pulse-slow": "pulse 3s cubic-bezier(0.4,0,0.6,1) infinite",
    28	        "fade-in": "fadeIn 0.3s ease-out",
    29	      },
    30	      keyframes: {
    31	        fadeIn: {
    32	          "0%": { opacity: 0, transform: "translateY(8px)" },
    33	          "100%": { opacity: 1, transform: "translateY(0)" },
    34	        },
    35	      },
    36	    },
    37	  },
    38	  plugins: [],
    39	};│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [036]: frontend/tailwind.config.js


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [037/40]: frontend/vite.config.js
│ LANGUAGE: javascript | LINES: 14 | SIZE: 269 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:8000",
     9	    },
    10	  },
    11	  build: {
    12	    outDir: "dist",
    13	    sourcemap: false,
    14	  },
    15	});│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [037]: frontend/vite.config.js


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


┌──────────────────────────────────────────────────────────────────────────────
│ 📄 FILE [039/40]: requirements.txt
│ LANGUAGE: plaintext | LINES: 10 | SIZE: 198 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│
└──────────────────────────────────────────────────────────────────────────────
   ✅ END OF [039]: requirements.txt


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



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