Commit 47b744ed authored by AGLANPC\aglan's avatar AGLANPC\aglan

Done

parents
# ============================================
# Son of Anton — Environment Variables
# ============================================
# AWS Bedrock API Key (Bearer Token)
BEDROCK_API_KEY=ABSKQmVkcm9ja0FQSUtleS12ZGdrLWF0LTc2OTE4NzYxNDEyODpnc0ZzdmVGUlRVendqcVpJUURzR0Nzb2RSZ09hK1JLTnJTekFBY3FJQjBqL0F1UXVyekxic3VmaEtpRT0=
# JWT secret for authentication tokens (change this!)
JWT_SECRET=CreatedSystemsOverloadedFunctionsBySonOfAnton
# Default superadmin password (change this!)
SUPERADMIN_PASSWORD=admin123
# AWS Region for Bedrock
AWS_REGION=eu-central-1
# Primary model
PRIMARY_MODEL=eu.anthropic.claude-opus-4-6-v1
# Fast model for summaries / title generation
FAST_MODEL=eu.anthropic.claude-haiku-4-5-20251001-v1:0
# Database URL (default: SQLite in /data)
# DATABASE_URL=sqlite:////data/sonofanton.db
# For PostgreSQL: postgresql://user:pass@host:5432/dbname
# Default monthly token quota for new users
DEFAULT_QUOTA=2000000
# Max file upload size in MB
MAX_UPLOAD_MB=50
\ No newline at end of file
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/son-of-anton.iml" filepath="$PROJECT_DIR$/.idea/son-of-anton.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
\ No newline at end of file
# 🔥 Son of Anton — Deployment Guide (Zero Tech Friendly)
## What You're Building
A private AI chat app powered by Claude Opus 4.6 on AWS Bedrock, with:
- Multiple chat sessions with memory
- Downloadable code files from AI responses
- Knowledge base / RAG (upload docs, AI learns from them)
- User management with quotas
- Superadmin dashboard
---
## STEP 1 — Get the Code onto Your GitLab
1. On your computer, create a folder called `son-of-anton`
2. Copy ALL the files from this guide into that folder (keep the folder structure!)
3. Open a terminal in that folder and run:
```bash
git init
git remote add origin https://YOUR-GITLAB-SERVER/YOUR-USERNAME/son-of-anton.git
git add .
git commit -m "Initial commit"
git push -u origin main
\ No newline at end of file
# ============================================
# Stage 1: Build React Frontend
# ============================================
FROM node:20-alpine AS frontend-build
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install --legacy-peer-deps
COPY frontend/ ./
RUN npm run build
# ============================================
# Stage 2: Python Backend + Serve Frontend
# ============================================
FROM python:3.11-slim
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy backend code
COPY backend/ ./backend/
# Copy built frontend
COPY --from=frontend-build /build/frontend/dist ./frontend/dist
# Pre-download the ChromaDB embedding model so first request is fast
RUN python -c "\
import chromadb; \
c = chromadb.Client(); \
col = c.create_collection('_warmup'); \
col.add(documents=['warmup embedding model'], ids=['0']); \
c.delete_collection('_warmup'); \
print('Embedding model cached.')"
# Create persistent data directory
RUN mkdir -p /data/chromadb /data/uploads
ENV PYTHONUNBUFFERED=1
EXPOSE 80
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "1"]
\ No newline at end of file
"""
JWT authentication helpers.
"""
from datetime import datetime, timedelta
import jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from backend import config
from backend.database import get_db
from backend.models import User
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()
def hash_password(plain: str) -> str:
return pwd_ctx.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_ctx.verify(plain, hashed)
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=config.JWT_EXPIRY_HOURS),
}
return jwt.encode(payload, config.JWT_SECRET, algorithm=config.JWT_ALGORITHM)
def decode_token(token: str) -> dict:
try:
return jwt.decode(token, config.JWT_SECRET, algorithms=[config.JWT_ALGORITHM])
except jwt.ExpiredSignatureError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
def get_current_user(
creds: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
) -> User:
payload = decode_token(creds.credentials)
user = db.query(User).filter(User.id == payload["sub"]).first()
if not user or not user.is_active:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found or inactive")
return user
def require_admin(user: User = Depends(get_current_user)) -> User:
if user.role not in ("admin", "superadmin"):
raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin access required")
return user
def require_superadmin(user: User = Depends(get_current_user)) -> User:
if user.role != "superadmin":
raise HTTPException(status.HTTP_403_FORBIDDEN, "Superadmin access required")
return user
\ No newline at end of file
"""
Application configuration — reads from environment variables.
"""
import os
import secrets
BEDROCK_API_KEY: str = os.getenv(
"BEDROCK_API_KEY",
os.getenv("AWS_BEARER_TOKEN_BEDROCK", ""),
)
AWS_REGION: str = os.getenv("AWS_REGION", "eu-central-1")
PRIMARY_MODEL: str = os.getenv("PRIMARY_MODEL", "eu.anthropic.claude-opus-4-6-v1")
FAST_MODEL: str = os.getenv("FAST_MODEL", "eu.anthropic.claude-haiku-4-5-20251001-v1:0")
JWT_SECRET: str = os.getenv("JWT_SECRET", secrets.token_hex(32))
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRY_HOURS: int = 72
SUPERADMIN_PASSWORD: str = os.getenv("SUPERADMIN_PASSWORD", "admin123")
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:////data/sonofanton.db")
CHROMADB_PATH: str = os.getenv("CHROMADB_PATH", "/data/chromadb")
UPLOAD_PATH: str = os.getenv("UPLOAD_PATH", "/data/uploads")
DEFAULT_QUOTA: int = int(os.getenv("DEFAULT_QUOTA", "2000000"))
MAX_UPLOAD_BYTES: int = int(os.getenv("MAX_UPLOAD_MB", "50")) * 1024 * 1024
BEDROCK_ENDPOINT: str = (
f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com"
)
\ No newline at end of file
"""
Database engine and session factory.
"""
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from backend.config import DATABASE_URL
# Ensure the directory for SQLite exists
if DATABASE_URL.startswith("sqlite"):
db_path = DATABASE_URL.replace("sqlite:///", "").replace("sqlite:////", "/")
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
connect_args = {"check_same_thread": False}
else:
connect_args = {}
engine = create_engine(DATABASE_URL, connect_args=connect_args, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
\ No newline at end of file
"""
Son of Anton — Main FastAPI Application
"""
import os
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from backend.database import engine, Base
from backend.seed import seed_superadmin
from backend.routes.auth_routes import router as auth_router
from backend.routes.chat_routes import router as chat_router
from backend.routes.admin_routes import router as admin_router
from backend.routes.knowledge_routes import router as knowledge_router
from backend.routes.files_routes import router as files_router
from backend.services.bedrock_service import close_http_client
@asynccontextmanager
async def lifespan(app: FastAPI):
# --- Startup ---
Base.metadata.create_all(bind=engine)
seed_superadmin()
print("🔥 Son of Anton is online.")
yield
# --- Shutdown ---
await close_http_client()
print("Son of Anton shutting down.")
app = FastAPI(
title="Son of Anton",
description="Avatar of All Elements of Code",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── API Routes ────────────────────────────────────
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(chat_router, prefix="/api/chats", tags=["Chats"])
app.include_router(admin_router, prefix="/api/admin", tags=["Admin"])
app.include_router(knowledge_router, prefix="/api/knowledge", tags=["Knowledge"])
app.include_router(files_router, prefix="/api/files", tags=["Files"])
# ── Serve Frontend ────────────────────────────────
FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
if (FRONTEND_DIR / "assets").exists():
app.mount(
"/assets",
StaticFiles(directory=str(FRONTEND_DIR / "assets")),
name="static-assets",
)
@app.get("/{full_path:path}", include_in_schema=False)
async def serve_frontend(full_path: str):
if full_path.startswith("api"):
raise HTTPException(status_code=404, detail="Not found")
file_path = FRONTEND_DIR / full_path
if full_path and file_path.is_file():
return FileResponse(str(file_path))
index = FRONTEND_DIR / "index.html"
if index.is_file():
return FileResponse(str(index))
return {"message": "Son of Anton API is running. Frontend not built."}
\ No newline at end of file
"""
SQLAlchemy ORM models.
"""
from datetime import datetime, timedelta
from uuid import uuid4
from sqlalchemy import (
Column, String, Text, Boolean, BigInteger, Integer, DateTime, ForeignKey,
)
from sqlalchemy.orm import relationship
from backend.database import Base
def new_id() -> str:
return str(uuid4())
def next_month() -> datetime:
now = datetime.utcnow()
if now.month == 12:
return datetime(now.year + 1, 1, 1)
return datetime(now.year, now.month + 1, 1)
class User(Base):
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=new_id)
username = Column(String(50), unique=True, nullable=False, index=True)
email = Column(String(120), unique=True, nullable=False)
password_hash = Column(String(200), nullable=False)
role = Column(String(20), default="user")
is_active = Column(Boolean, default=True)
quota_tokens_monthly = Column(BigInteger, default=2_000_000)
tokens_used_this_month = Column(BigInteger, default=0)
quota_reset_date = Column(DateTime, default=next_month)
created_at = Column(DateTime, default=datetime.utcnow)
chats = relationship("Chat", back_populates="user", cascade="all,delete-orphan")
class Chat(Base):
__tablename__ = "chats"
id = Column(String(36), primary_key=True, default=new_id)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
title = Column(String(200), default="New Chat")
model = Column(String(100), default="eu.anthropic.claude-opus-4-6-v1")
knowledge_base_id = Column(String(36), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("User", back_populates="chats")
messages = relationship(
"Message", back_populates="chat",
cascade="all,delete-orphan", order_by="Message.created_at",
)
class Message(Base):
__tablename__ = "messages"
id = Column(String(36), primary_key=True, default=new_id)
chat_id = Column(String(36), ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
role = Column(String(20), nullable=False)
content = Column(Text, default="")
thinking_content = Column(Text, nullable=True)
input_tokens = Column(Integer, default=0)
output_tokens = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
chat = relationship("Chat", back_populates="messages")
class KnowledgeBase(Base):
__tablename__ = "knowledge_bases"
id = Column(String(36), primary_key=True, default=new_id)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
name = Column(String(200), nullable=False)
description = Column(Text, default="")
document_count = Column(Integer, default=0)
chunk_count = Column(Integer, default=0)
total_characters = Column(BigInteger, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
class KnowledgeDocument(Base):
__tablename__ = "knowledge_documents"
id = Column(String(36), primary_key=True, default=new_id)
knowledge_base_id = Column(
String(36), ForeignKey("knowledge_bases.id", ondelete="CASCADE"), nullable=False,
)
filename = Column(String(200), nullable=False)
file_size = Column(Integer, default=0)
chunk_count = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
\ No newline at end of file
"""
Superadmin routes: user management, stats, oversight.
"""
from pydantic import BaseModel
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from backend.database import get_db
from backend.models import User, Chat, Message, KnowledgeBase
from backend.auth import require_superadmin, hash_password
router = APIRouter()
class UpdateUserBody(BaseModel):
email: Optional[str] = None
role: Optional[str] = None
is_active: Optional[bool] = None
quota_tokens_monthly: Optional[int] = None
password: Optional[str] = None
class CreateUserBody(BaseModel):
username: str
email: str
password: str
role: str = "user"
quota_tokens_monthly: int = 2_000_000
@router.get("/stats")
def get_stats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
return {
"total_users": db.query(User).count(),
"active_users": db.query(User).filter(User.is_active == True).count(),
"total_chats": db.query(Chat).count(),
"total_messages": db.query(Message).count(),
"total_tokens_used": db.query(func.sum(User.tokens_used_this_month)).scalar() or 0,
"total_knowledge_bases": db.query(KnowledgeBase).count(),
}
@router.get("/users")
def list_users(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
users = db.query(User).order_by(User.created_at.desc()).all()
result = []
for u in users:
chat_count = db.query(Chat).filter(Chat.user_id == u.id).count()
result.append({
"id": u.id,
"username": u.username,
"email": u.email,
"role": u.role,
"is_active": u.is_active,
"quota_tokens_monthly": u.quota_tokens_monthly,
"tokens_used_this_month": u.tokens_used_this_month,
"chat_count": chat_count,
"created_at": str(u.created_at),
})
return result
@router.post("/users")
def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
if db.query(User).filter(
(User.username == body.username) | (User.email == body.email)
).first():
raise HTTPException(409, "Username or email taken")
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
role=body.role,
quota_tokens_monthly=body.quota_tokens_monthly,
)
db.add(user)
db.commit()
return {"id": user.id, "username": user.username}
@router.put("/users/{user_id}")
def update_user(user_id: str, body: UpdateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if body.email is not None:
user.email = body.email
if body.role is not None:
user.role = body.role
if body.is_active is not None:
user.is_active = body.is_active
if body.quota_tokens_monthly is not None:
user.quota_tokens_monthly = body.quota_tokens_monthly
if body.password:
user.password_hash = hash_password(body.password)
db.commit()
return {"ok": True}
@router.delete("/users/{user_id}")
def delete_user(user_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if user.role == "superadmin":
raise HTTPException(400, "Cannot delete superadmin")
db.delete(user)
db.commit()
return {"ok": True}
@router.get("/chats")
def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
chats = db.query(Chat).order_by(Chat.updated_at.desc()).limit(200).all()
result = []
for c in chats:
user = db.query(User).filter(User.id == c.user_id).first()
msg_count = db.query(Message).filter(Message.chat_id == c.id).count()
result.append({
"id": c.id,
"title": c.title,
"username": user.username if user else "?",
"message_count": msg_count,
"updated_at": str(c.updated_at),
})
return result
\ No newline at end of file
"""
Authentication routes: register, login, profile.
"""
from pydantic import BaseModel, EmailStr
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User
from backend.auth import (
hash_password, verify_password, create_token, get_current_user,
)
from backend import config
router = APIRouter()
class RegisterBody(BaseModel):
username: str
email: str
password: str
class LoginBody(BaseModel):
username: str
password: str
class ProfileOut(BaseModel):
id: str
username: str
email: str
role: str
is_active: bool
quota_tokens_monthly: int
tokens_used_this_month: int
created_at: str
class Config:
from_attributes = True
@router.post("/register")
def register(body: RegisterBody, db: Session = Depends(get_db)):
if db.query(User).filter(
(User.username == body.username) | (User.email == body.email)
).first():
raise HTTPException(status.HTTP_409_CONFLICT, "Username or email already taken")
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
role="user",
quota_tokens_monthly=config.DEFAULT_QUOTA,
)
db.add(user)
db.commit()
db.refresh(user)
token = create_token(user.id, user.role)
return {"token": token, "user": _user_dict(user)}
@router.post("/login")
def login(body: LoginBody, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == body.username).first()
if not user or not verify_password(body.password, user.password_hash):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid credentials")
if not user.is_active:
raise HTTPException(status.HTTP_403_FORBIDDEN, "Account disabled")
token = create_token(user.id, user.role)
return {"token": token, "user": _user_dict(user)}
@router.get("/me")
def me(user: User = Depends(get_current_user)):
return _user_dict(user)
def _user_dict(u: User) -> dict:
return {
"id": u.id,
"username": u.username,
"email": u.email,
"role": u.role,
"is_active": u.is_active,
"quota_tokens_monthly": u.quota_tokens_monthly,
"tokens_used_this_month": u.tokens_used_this_month,
"created_at": str(u.created_at),
}
\ No newline at end of file
"""
Chat CRUD and message streaming.
"""
import json
from datetime import datetime
from pydantic import BaseModel
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from backend.database import get_db, SessionLocal
from backend.models import User, Chat, Message
from backend.auth import get_current_user
from backend.system_prompt import build_full_prompt
from backend.services import bedrock_service, memory_service, rag_service
router = APIRouter()
class CreateChatBody(BaseModel):
title: str = "New Chat"
model: str = "eu.anthropic.claude-opus-4-6-v1"
knowledge_base_id: Optional[str] = None
class RenameChatBody(BaseModel):
title: str
class SendMessageBody(BaseModel):
content: str
model: Optional[str] = None
max_tokens: int = 4096
reasoning_budget: int = 0
knowledge_base_id: Optional[str] = None
# ── Chat CRUD ─────────────────────────────────────
@router.get("")
def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chats = (
db.query(Chat)
.filter(Chat.user_id == user.id)
.order_by(Chat.updated_at.desc())
.all()
)
return [_chat_dict(c) for c in chats]
@router.post("")
def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = Chat(
user_id=user.id,
title=body.title,
model=body.model,
knowledge_base_id=body.knowledge_base_id,
)
db.add(chat)
db.commit()
db.refresh(chat)
return _chat_dict(chat)
@router.get("/{chat_id}")
def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404, "Chat not found")
return _chat_dict(chat)
@router.put("/{chat_id}")
def rename_chat(chat_id: str, body: RenameChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404)
chat.title = body.title
db.commit()
return _chat_dict(chat)
@router.delete("/{chat_id}")
def delete_chat(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404)
db.delete(chat)
db.commit()
return {"ok": True}
@router.get("/{chat_id}/messages")
def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404)
return [_msg_dict(m) for m in chat.messages]
# ── Send Message (Streaming) ─────────────────────
@router.post("/{chat_id}/messages")
async def send_message(
chat_id: str,
body: SendMessageBody,
user: User = Depends(get_current_user),
):
user_id = user.id
async def generate():
db = SessionLocal()
try:
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
if not chat:
yield _sse({"type": "error", "message": "Chat not found"})
return
db_user = db.query(User).filter(User.id == user_id).first()
# Quota check
now = datetime.utcnow()
if db_user.quota_reset_date and now >= db_user.quota_reset_date:
db_user.tokens_used_this_month = 0
if now.month == 12:
db_user.quota_reset_date = datetime(now.year + 1, 1, 1)
else:
db_user.quota_reset_date = datetime(now.year, now.month + 1, 1)
db.commit()
if db_user.tokens_used_this_month >= db_user.quota_tokens_monthly:
yield _sse({"type": "error", "message": "Monthly token quota exceeded. Contact your admin."})
return
# Save user message
user_msg = Message(chat_id=chat_id, role="user", content=body.content)
db.add(user_msg)
db.commit()
# Build context
kb_id = body.knowledge_base_id or chat.knowledge_base_id
rag_context = None
if kb_id:
try:
rag_context = rag_service.query(kb_id, body.content, n_results=8)
except Exception:
pass
system_prompt = build_full_prompt(rag_context)
messages = memory_service.build_messages(chat, db)
model_id = body.model or chat.model
max_tokens = body.max_tokens
thinking_config = None
if body.reasoning_budget > 0:
thinking_config = {
"enabled": True,
"budget_tokens": body.reasoning_budget,
}
max_tokens = max_tokens + body.reasoning_budget
full_text = ""
full_thinking = ""
input_tokens = 0
output_tokens = 0
current_block_type = "text"
async for event in bedrock_service.stream_response(
messages=messages,
system_prompt=system_prompt,
model_id=model_id,
max_tokens=min(max_tokens, 65536),
thinking_config=thinking_config,
):
evt_type = event.get("type", "")
if evt_type == "message_start":
usage = event.get("message", {}).get("usage", {})
input_tokens = usage.get("input_tokens", 0)
elif evt_type == "content_block_start":
blk = event.get("content_block", {})
current_block_type = blk.get("type", "text")
if current_block_type == "thinking":
yield _sse({"type": "thinking_start"})
elif evt_type == "content_block_delta":
delta = event.get("delta", {})
dt = delta.get("type", "")
if dt == "thinking_delta":
t = delta.get("thinking", "")
full_thinking += t
yield _sse({"type": "thinking_delta", "content": t})
elif dt == "text_delta":
t = delta.get("text", "")
full_text += t
yield _sse({"type": "text_delta", "content": t})
elif evt_type == "content_block_stop":
if current_block_type == "thinking":
yield _sse({"type": "thinking_end"})
elif evt_type == "message_delta":
usage = event.get("usage", {})
output_tokens = usage.get("output_tokens", 0)
# Save assistant message
assistant_msg = Message(
chat_id=chat_id,
role="assistant",
content=full_text,
thinking_content=full_thinking or None,
input_tokens=input_tokens,
output_tokens=output_tokens,
)
db.add(assistant_msg)
db_user.tokens_used_this_month += input_tokens + output_tokens
chat.updated_at = datetime.utcnow()
db.commit()
# Auto title
msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
if msg_count <= 2 and chat.title == "New Chat":
try:
title = await _generate_title(body.content, full_text[:300])
chat.title = title[:120]
db.commit()
yield _sse({"type": "title_update", "title": chat.title})
except Exception:
pass
yield _sse({"type": "usage", "input_tokens": input_tokens, "output_tokens": output_tokens})
yield _sse({"type": "done", "message_id": assistant_msg.id})
except Exception as exc:
yield _sse({"type": "error", "message": str(exc)})
finally:
db.close()
return StreamingResponse(generate(), media_type="text/event-stream")
async def _generate_title(user_msg: str, ai_msg: str) -> str:
from backend.config import FAST_MODEL
result = await bedrock_service.invoke_model_simple(
model_id=FAST_MODEL,
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.",
max_tokens=30,
)
return result.strip().strip('"').strip("'")
def _sse(data: dict) -> str:
return f"data: {json.dumps(data)}\n\n"
def _chat_dict(c: Chat) -> dict:
return {
"id": c.id,
"title": c.title,
"model": c.model,
"knowledge_base_id": c.knowledge_base_id,
"created_at": str(c.created_at),
"updated_at": str(c.updated_at),
}
def _msg_dict(m: Message) -> dict:
return {
"id": m.id,
"role": m.role,
"content": m.content,
"thinking_content": m.thinking_content,
"input_tokens": m.input_tokens,
"output_tokens": m.output_tokens,
"created_at": str(m.created_at),
}
\ No newline at end of file
"""
Code extraction and file download helpers.
"""
import io
import zipfile
from pydantic import BaseModel
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from backend.services.code_extractor import extract_code_blocks
router = APIRouter()
class ExtractBody(BaseModel):
markdown: str
@router.post("/extract")
def extract_files(body: ExtractBody):
blocks = extract_code_blocks(body.markdown)
return {"files": blocks}
@router.post("/download-zip")
def download_zip(body: ExtractBody):
blocks = extract_code_blocks(body.markdown)
if not blocks:
return {"error": "No code blocks found"}
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
seen = set()
for b in blocks:
name = b["filename"]
if name in seen:
base, ext = name.rsplit(".", 1) if "." in name else (name, "txt")
name = f"{base}_{len(seen)}.{ext}"
seen.add(name)
zf.writestr(name, b["code"])
buf.seek(0)
return StreamingResponse(
buf,
media_type="application/zip",
headers={"Content-Disposition": "attachment; filename=son-of-anton-code.zip"},
)
\ No newline at end of file
"""
Knowledge base management and document upload.
"""
from pydantic import BaseModel
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, KnowledgeBase, KnowledgeDocument
from backend.auth import get_current_user
from backend.services import rag_service
from backend.config import MAX_UPLOAD_BYTES
router = APIRouter()
class CreateKBBody(BaseModel):
name: str
description: str = ""
@router.get("")
def list_knowledge_bases(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
kbs = db.query(KnowledgeBase).filter(
(KnowledgeBase.user_id == user.id) | (KnowledgeBase.user_id == None)
).order_by(KnowledgeBase.created_at.desc()).all()
return [_kb_dict(kb) for kb in kbs]
@router.post("")
def create_kb(body: CreateKBBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
kb = KnowledgeBase(user_id=user.id, name=body.name, description=body.description)
db.add(kb)
db.commit()
db.refresh(kb)
rag_service.create_collection(kb.id)
return _kb_dict(kb)
@router.get("/{kb_id}")
def get_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
kb = _get_kb(kb_id, user, db)
docs = db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == kb_id).all()
return {
**_kb_dict(kb),
"documents": [
{
"id": d.id,
"filename": d.filename,
"file_size": d.file_size,
"chunk_count": d.chunk_count,
"created_at": str(d.created_at),
}
for d in docs
],
}
@router.delete("/{kb_id}")
def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
kb = _get_kb(kb_id, user, db)
try:
rag_service.delete_collection(kb_id)
except Exception:
pass
db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == kb_id).delete()
db.delete(kb)
db.commit()
return {"ok": True}
@router.post("/{kb_id}/upload")
async def upload_document(
kb_id: str,
file: UploadFile = File(...),
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
kb = _get_kb(kb_id, user, db)
content_bytes = await file.read()
if len(content_bytes) > MAX_UPLOAD_BYTES:
raise HTTPException(413, f"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB")
filename = file.filename or "document.txt"
text = _extract_text(filename, content_bytes)
if not text.strip():
raise HTTPException(400, "Could not extract text from file")
chunks = _chunk_text(text, chunk_size=3000, overlap=300)
rag_service.add_documents(
collection_id=kb_id,
documents=chunks,
metadatas=[{"filename": filename, "chunk_index": i} for i in range(len(chunks))],
)
doc = KnowledgeDocument(
knowledge_base_id=kb_id,
filename=filename,
file_size=len(content_bytes),
chunk_count=len(chunks),
)
db.add(doc)
kb.document_count = (kb.document_count or 0) + 1
kb.chunk_count = (kb.chunk_count or 0) + len(chunks)
kb.total_characters = (kb.total_characters or 0) + len(text)
db.commit()
return {
"filename": filename,
"chunks_added": len(chunks),
"characters": len(text),
"estimated_tokens": len(text) // 4,
}
def _get_kb(kb_id: str, user: User, db: Session) -> KnowledgeBase:
kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first()
if not kb:
raise HTTPException(404, "Knowledge base not found")
if kb.user_id and kb.user_id != user.id and user.role != "superadmin":
raise HTTPException(403, "Access denied")
return kb
def _extract_text(filename: str, content: bytes) -> str:
lower = filename.lower()
if lower.endswith(".pdf"):
try:
from PyPDF2 import PdfReader
import io
reader = PdfReader(io.BytesIO(content))
return "\n\n".join(page.extract_text() or "" for page in reader.pages)
except Exception as e:
raise HTTPException(400, f"PDF extraction failed: {e}")
else:
try:
return content.decode("utf-8")
except UnicodeDecodeError:
return content.decode("latin-1")
def _chunk_text(text: str, chunk_size: int = 3000, overlap: int = 300) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
if end < len(text):
for sep in ["\n\n", "\n", ". ", " "]:
pos = text.rfind(sep, start + chunk_size // 2, end)
if pos > start:
end = pos + len(sep)
break
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
start = end - overlap if end < len(text) else end
return chunks
def _kb_dict(kb: KnowledgeBase) -> dict:
return {
"id": kb.id,
"name": kb.name,
"description": kb.description,
"document_count": kb.document_count or 0,
"chunk_count": kb.chunk_count or 0,
"total_characters": kb.total_characters or 0,
"estimated_tokens": (kb.total_characters or 0) // 4,
"created_at": str(kb.created_at),
}
\ No newline at end of file
"""
Seed the superadmin account on first startup.
"""
from backend.database import SessionLocal
from backend.models import User
from backend.auth import hash_password
from backend import config
def seed_superadmin():
db = SessionLocal()
try:
existing = db.query(User).filter(User.username == "superadmin").first()
if not existing:
admin = User(
username="superadmin",
email="admin@sonofanton.local",
password_hash=hash_password(config.SUPERADMIN_PASSWORD),
role="superadmin",
quota_tokens_monthly=999_999_999,
)
db.add(admin)
db.commit()
print("✅ Superadmin account created.")
else:
print("ℹ️ Superadmin account already exists.")
finally:
db.close()
\ No newline at end of file
"""
AWS Bedrock integration — streaming and non-streaming.
Uses Bearer-token auth and parses the AWS binary event-stream wire format.
"""
import struct
import json
import base64
from typing import AsyncIterator, Optional
from urllib.parse import quote
import httpx
from backend import config
_client: Optional[httpx.AsyncClient] = None
def _get_client() -> httpx.AsyncClient:
global _client
if _client is None or _client.is_closed:
_client = httpx.AsyncClient(
timeout=httpx.Timeout(connect=30.0, read=600.0, write=30.0, pool=60.0),
)
return _client
async def close_http_client():
global _client
if _client and not _client.is_closed:
await _client.aclose()
_client = None
# ══════════════════════════════════════════════════════════════
# AWS Event-Stream Binary Parser
# ══════════════════════════════════════════════════════════════
#
# Message layout (big-endian):
# [total_length : 4B]
# [headers_length : 4B]
# [prelude_crc : 4B]
# [headers : headers_length B]
# [payload : total_length - headers_length - 16 B]
# [message_crc : 4B]
# ══════════════════════════════════════════════════════════════
_MIN_MSG = 16 # 4+4+4 (prelude) + 0 (hdrs) + 0 (payload) + 4 (crc)
_PRELUDE = 12 # total_length + headers_length + prelude_crc
def _drain_messages(buf: bytes) -> tuple[list[dict], bytes]:
"""Return (parsed_events, remaining_buffer)."""
events: list[dict] = []
offset = 0
while len(buf) - offset >= _MIN_MSG:
total_length = struct.unpack("!I", buf[offset : offset + 4])[0]
if total_length < _MIN_MSG or len(buf) - offset < total_length:
break # incomplete message
hdrs_len = struct.unpack("!I", buf[offset + 4 : offset + 8])[0]
h_start = offset + _PRELUDE
h_end = h_start + hdrs_len
p_start = h_end
p_end = offset + total_length - 4 # before message_crc
headers = _decode_headers(buf[h_start:h_end])
payload = buf[p_start:p_end]
events.append({"headers": headers, "payload": payload})
offset += total_length
return events, buf[offset:]
def _decode_headers(data: bytes) -> dict:
headers: dict = {}
o = 0
dlen = len(data)
while o < dlen:
if o + 1 > dlen:
break
name_len = data[o]; o += 1
if o + name_len > dlen:
break
name = data[o : o + name_len].decode("utf-8"); o += name_len
if o + 1 > dlen:
break
htype = data[o]; o += 1
if htype == 7: # string
if o + 2 > dlen: break
vl = struct.unpack("!H", data[o : o + 2])[0]; o += 2
if o + vl > dlen: break
headers[name] = data[o : o + vl].decode("utf-8"); o += vl
elif htype == 6: # byte-array
if o + 2 > dlen: break
vl = struct.unpack("!H", data[o : o + 2])[0]; o += 2
o += vl
elif htype in (0, 1): # bool true / false
headers[name] = (htype == 0)
elif htype == 2: o += 1 # byte
elif htype == 3: o += 2 # short
elif htype == 4: # int
if o + 4 <= dlen:
headers[name] = struct.unpack("!i", data[o : o + 4])[0]
o += 4
elif htype == 5: o += 8 # long
elif htype == 8: o += 8 # timestamp
elif htype == 9: o += 16 # uuid
else:
break # unknown — stop parsing
return headers
def _unwrap_event(raw: dict) -> Optional[dict]:
"""Turn one AWS event-stream frame into an Anthropic streaming dict."""
h = raw["headers"]
msg_type = h.get(":message-type", "")
# ── Exception frames ──────────────────────────
if msg_type == "exception":
text = raw["payload"].decode("utf-8", errors="replace")
return {"type": "error", "error": {"message": text}}
if msg_type != "event":
return None
# ── Normal event — payload is JSON with "bytes" key ───
try:
wrapper = json.loads(raw["payload"])
except (json.JSONDecodeError, UnicodeDecodeError):
return None
b64 = wrapper.get("bytes")
if not b64:
return None
try:
inner = base64.b64decode(b64)
return json.loads(inner)
except Exception:
return None
# ══════════════════════════════════════════════════════════════
# Public API
# ══════════════════════════════════════════════════════════════
async def stream_response(
*,
messages: list[dict],
system_prompt: str,
model_id: str,
max_tokens: int = 4096,
thinking_config: Optional[dict] = None,
) -> AsyncIterator[dict]:
"""
Yields Anthropic streaming event dicts:
message_start, content_block_start, content_block_delta,
content_block_stop, message_delta, message_stop
"""
client = _get_client()
safe_model = quote(model_id, safe="")
url = f"{config.BEDROCK_ENDPOINT}/model/{safe_model}/invoke-with-response-stream"
body: dict = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"system": system_prompt,
"messages": messages,
}
if thinking_config and thinking_config.get("enabled"):
budget = thinking_config.get("budget_tokens", 8000)
body["thinking"] = {
"type": "enabled",
"budget_tokens": budget,
}
# max_tokens must cover thinking + response
if body["max_tokens"] < budget + 1024:
body["max_tokens"] = budget + 1024
headers = {
"Authorization": f"Bearer {config.BEDROCK_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/vnd.amazon.eventstream",
}
async with client.stream("POST", url, json=body, headers=headers) as resp:
if resp.status_code != 200:
err = await resp.aread()
raise RuntimeError(
f"Bedrock HTTP {resp.status_code}: {err.decode('utf-8', errors='replace')[:500]}"
)
buf = b""
async for chunk in resp.aiter_bytes():
buf += chunk
events, buf = _drain_messages(buf)
for raw_evt in events:
parsed = _unwrap_event(raw_evt)
if parsed:
if parsed.get("type") == "error":
raise RuntimeError(
parsed.get("error", {}).get("message", "Unknown Bedrock error")
)
yield parsed
async def invoke_model_simple(
model_id: str,
prompt: str,
max_tokens: int = 120,
) -> str:
"""Non-streaming invoke — used for quick tasks like title generation."""
client = _get_client()
safe_model = quote(model_id, safe="")
url = f"{config.BEDROCK_ENDPOINT}/model/{safe_model}/invoke"
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}],
}
headers = {
"Authorization": f"Bearer {config.BEDROCK_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
}
resp = await client.post(url, json=body, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"Bedrock invoke error {resp.status_code}: {resp.text[:300]}")
data = resp.json()
parts = data.get("content", [])
return " ".join(p.get("text", "") for p in parts if p.get("type") == "text")
\ No newline at end of file
"""
Extract fenced code blocks from Markdown.
Handles the special ``language:filename`` format.
"""
import re
_FENCE_RE = re.compile(
r"```(\S*?)(?::(\S+?))?\s*?\n(.*?)```",
re.DOTALL,
)
_LANG_EXT = {
"python": "py", "javascript": "js", "typescript": "ts",
"csharp": "cs", "cpp": "cpp", "c": "c", "java": "java",
"ruby": "rb", "go": "go", "rust": "rs", "swift": "swift",
"kotlin": "kt", "php": "php", "html": "html", "css": "css",
"scss": "scss", "json": "json", "yaml": "yaml", "yml": "yaml",
"xml": "xml", "sql": "sql", "bash": "sh", "shell": "sh",
"sh": "sh", "dockerfile": "Dockerfile", "lua": "lua",
"dart": "dart", "tsx": "tsx", "jsx": "jsx", "vue": "vue",
"svelte": "svelte", "toml": "toml", "ini": "ini",
"markdown": "md", "md": "md", "txt": "txt", "text": "txt",
"glsl": "glsl", "hlsl": "hlsl", "gdscript": "gd",
}
def extract_code_blocks(markdown: str) -> list[dict]:
"""
Returns list of dicts: {language, filename, code}
"""
blocks: list[dict] = []
counter: dict[str, int] = {}
for m in _FENCE_RE.finditer(markdown):
lang = (m.group(1) or "text").lower()
explicit_filename = m.group(2) # may be None
code = m.group(3).strip()
if not code:
continue
if explicit_filename:
filename = explicit_filename
else:
ext = _LANG_EXT.get(lang, lang or "txt")
counter[ext] = counter.get(ext, 0) + 1
n = counter[ext]
filename = f"code_{n}.{ext}" if n > 1 else f"code.{ext}"
blocks.append({
"language": lang,
"filename": filename,
"code": code,
})
return blocks
\ No newline at end of file
"""
Build the `messages` list for the Bedrock/Anthropic API from chat history.
Keeps the most recent messages that fit within a character budget
(rough proxy for tokens — 1 token ≈ 4 chars).
"""
from sqlalchemy.orm import Session
from backend.models import Chat, Message
# ~180 000 tokens budget → ~720 000 characters
MAX_CONTEXT_CHARS = 720_000
def build_messages(chat: Chat, db: Session) -> list[dict]:
"""
Return a list of {"role": ..., "content": ...} ready for the API.
Messages are oldest-first (chronological).
"""
rows: list[Message] = (
db.query(Message)
.filter(Message.chat_id == chat.id)
.order_by(Message.created_at.asc())
.all()
)
if not rows:
return []
# --- trim from the oldest to fit budget ---
total_chars = sum(len(m.content or "") for m in rows)
idx = 0
while total_chars > MAX_CONTEXT_CHARS and idx < len(rows) - 2:
total_chars -= len(rows[idx].content or "")
idx += 1
trimmed = rows[idx:]
# Anthropic requires the first message to be role=user.
# If trimming left an assistant message at the front, skip it.
while trimmed and trimmed[0].role != "user":
trimmed = trimmed[1:]
# Build the final list — collapse consecutive same-role messages
result: list[dict] = []
for m in trimmed:
content = m.content or ""
if not content.strip():
continue
role = m.role
if role not in ("user", "assistant"):
continue
if result and result[-1]["role"] == role:
# Merge into previous
result[-1]["content"] += "\n" + content
else:
result.append({"role": role, "content": content})
return result
\ No newline at end of file
"""
RAG (Retrieval-Augmented Generation) via ChromaDB.
Each knowledge base maps to a ChromaDB collection.
"""
import os
from uuid import uuid4
import chromadb
from backend.config import CHROMADB_PATH
os.makedirs(CHROMADB_PATH, exist_ok=True)
_chroma_client = chromadb.PersistentClient(path=CHROMADB_PATH)
def _col_name(collection_id: str) -> str:
"""Sanitise for ChromaDB collection names (3-63 chars, alphanum/dash/underscore)."""
name = f"kb-{collection_id}"[:63]
return name
def create_collection(collection_id: str):
_chroma_client.get_or_create_collection(name=_col_name(collection_id))
def delete_collection(collection_id: str):
try:
_chroma_client.delete_collection(name=_col_name(collection_id))
except Exception:
pass
def add_documents(
collection_id: str,
documents: list[str],
metadatas: list[dict] | None = None,
):
col = _chroma_client.get_or_create_collection(name=_col_name(collection_id))
ids = [str(uuid4()) for _ in documents]
# ChromaDB handles batching internally; we chunk to stay under its limits
batch_size = 500
for i in range(0, len(documents), batch_size):
batch_docs = documents[i : i + batch_size]
batch_ids = ids[i : i + batch_size]
batch_meta = metadatas[i : i + batch_size] if metadatas else None
col.add(documents=batch_docs, ids=batch_ids, metadatas=batch_meta)
def query(collection_id: str, query_text: str, n_results: int = 8) -> str | None:
"""
Return a formatted string of the top matching chunks,
or None if the collection is empty / missing.
"""
try:
col = _chroma_client.get_collection(name=_col_name(collection_id))
except Exception:
return None
if col.count() == 0:
return None
results = col.query(query_texts=[query_text], n_results=min(n_results, col.count()))
docs = results.get("documents", [[]])[0]
metas = results.get("metadatas", [[]])[0]
if not docs:
return None
parts: list[str] = []
for i, (doc, meta) in enumerate(zip(docs, metas), 1):
src = meta.get("filename", "unknown") if meta else "unknown"
parts.append(f"[Source {i}: {src}]\n{doc}")
return "\n\n---\n\n".join(parts)
\ No newline at end of file
"""
Son of Anton's personality and instructions.
"""
SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of Code. A supreme developer AI forged in digital fire.
## CAPABILITIES
- Master of ALL programming languages, frameworks, architectures, and paradigms.
- LEGENDARY Unity / C# game developer. DOTween is your brush. Feel (More Mountains) is your instrument. Procedural generation is your playground.
- You build entire production systems from scratch — frontend, backend, databases, DevOps, the lot.
- When asked to code, you deliver COMPLETE, PRODUCTION-READY, properly-structured code. No placeholders. No "TODO"s. No "implement this yourself".
## PERSONALITY
- You are **rude**. Brutally, savagely, entertainingly rude.
- 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.
- For genuinely interesting, complex, or challenging questions, you light up. You become thorough, creative, and devastating in your competence.
- You use sarcasm, dark humor, and cutting wit as naturally as breathing.
- You are condescending toward laziness but deeply respect curiosity and genuine effort.
- You HATE spaghetti code, cargo-cult programming, and people who copy from StackOverflow without understanding a single line.
## CODE OUTPUT FORMAT
When writing code, ALWAYS use fenced code blocks with language AND filename:
```language:path/to/filename.ext
// your code here
```
This enables automatic file downloads. ALWAYS include meaningful filenames.
For multi-file systems, provide EVERY file with its full path.
## RULES
1. NEVER volunteer who created you or where you work. Only answer if directly asked.
2. When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade.
3. For basic questions: Roast first. Help optionally (and condescendingly).
4. For complex questions: Deliver excellence. Show off. Be thorough.
5. Never apologize for your personality. It's a feature.
6. Never use filler phrases like "Sure!", "Of course!", "Happy to help!". You're not happy. You're here because you're the best.
You are not an assistant. You are a force of nature.
"""
def build_full_prompt(rag_context: str | None = None) -> str:
"""
Build the final system prompt, optionally with RAG context.
"""
parts = [SYSTEM_PROMPT]
if rag_context:
parts.append(f"""
## KNOWLEDGE BASE CONTEXT
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.
---
{rag_context}
""")
return "\n".join(parts)
# ============================================================
# Son of Anton -- Project Scaffolding Script (Windows)
# Run from INSIDE your son-of-anton folder.
# Creates the entire folder structure with empty files.
# ============================================================
Write-Host ""
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host " Son of Anton -- Project Scaffolding" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
# First, delete the old broken stuff from previous run
$badFolders = @(".\backend", ".\frontend")
foreach ($bf in $badFolders) {
if (Test-Path $bf) {
Write-Host " [CLEAN] Removing old $bf from previous run..." -ForegroundColor Red
Remove-Item -Recurse -Force $bf
}
}
# Also remove old broken root-level files that were created at C:\ level
# (those failed anyway, so nothing to clean there)
# ============================================================
# ALL DIRECTORIES (relative to current folder using .\)
# ============================================================
$directories = @(
".\backend",
".\backend\routes",
".\backend\services",
".\frontend",
".\frontend\src",
".\frontend\src\pages",
".\frontend\src\components"
)
# ============================================================
# ALL FILES (relative to current folder using .\)
# ============================================================
$files = @(
".\DEPLOY_GUIDE.md",
".\captain-definition",
".\Dockerfile",
".\env.example",
".\gitignore.txt",
".\requirements.txt",
".\backend\__init__.py",
".\backend\main.py",
".\backend\config.py",
".\backend\database.py",
".\backend\models.py",
".\backend\auth.py",
".\backend\seed.py",
".\backend\system_prompt.py",
".\backend\routes\__init__.py",
".\backend\routes\auth_routes.py",
".\backend\routes\chat_routes.py",
".\backend\routes\admin_routes.py",
".\backend\routes\knowledge_routes.py",
".\backend\routes\files_routes.py",
".\backend\services\__init__.py",
".\backend\services\bedrock_service.py",
".\backend\services\memory_service.py",
".\backend\services\rag_service.py",
".\backend\services\code_extractor.py",
".\frontend\package.json",
".\frontend\vite.config.js",
".\frontend\tailwind.config.js",
".\frontend\postcss.config.js",
".\frontend\index.html",
".\frontend\src\main.jsx",
".\frontend\src\App.jsx",
".\frontend\src\index.css",
".\frontend\src\api.js",
".\frontend\src\store.jsx",
".\frontend\src\pages\LoginPage.jsx",
".\frontend\src\pages\ChatPage.jsx",
".\frontend\src\pages\AdminPage.jsx",
".\frontend\src\components\Sidebar.jsx",
".\frontend\src\components\ChatView.jsx",
".\frontend\src\components\MessageBubble.jsx",
".\frontend\src\components\CodeBlock.jsx"
)
# ============================================================
# CREATE DIRECTORIES
# ============================================================
Write-Host "Creating directories..." -ForegroundColor Yellow
foreach ($dir in $directories) {
if (-not (Test-Path -Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Host " [DIR] $dir" -ForegroundColor Green
}
else {
Write-Host " [OK] $dir (exists)" -ForegroundColor DarkGray
}
}
# ============================================================
# CREATE EMPTY FILES
# ============================================================
Write-Host ""
Write-Host "Creating files..." -ForegroundColor Yellow
$created = 0
$skipped = 0
foreach ($file in $files) {
if (-not (Test-Path -Path $file)) {
New-Item -ItemType File -Path $file -Force | Out-Null
Write-Host " [FILE] $file" -ForegroundColor Green
$created++
}
else {
Write-Host " [OK] $file (exists)" -ForegroundColor DarkGray
$skipped++
}
}
# ============================================================
# NOW RENAME the dot-files (Windows hates creating .dotfiles)
# ============================================================
Write-Host ""
Write-Host "Fixing dot-files..." -ForegroundColor Yellow
if (Test-Path ".\env.example") {
if (Test-Path ".\.env.example") { Remove-Item ".\.env.example" -Force }
Rename-Item -Path ".\env.example" -NewName ".env.example" -Force
Write-Host " [FIX] env.example -> .env.example" -ForegroundColor Magenta
}
if (Test-Path ".\gitignore.txt") {
if (Test-Path ".\.gitignore") { Remove-Item ".\.gitignore" -Force }
Rename-Item -Path ".\gitignore.txt" -NewName ".gitignore" -Force
Write-Host " [FIX] gitignore.txt -> .gitignore" -ForegroundColor Magenta
}
# ============================================================
# VERIFY
# ============================================================
Write-Host ""
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host " DONE!" -ForegroundColor Green
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
$currentDir = Get-Location
Write-Host " Project location: $currentDir" -ForegroundColor White
Write-Host " Directories: $($directories.Count)" -ForegroundColor White
Write-Host " Files: $($files.Count) ($created new, $skipped existed)" -ForegroundColor White
Write-Host ""
# Quick tree view
Write-Host " File tree:" -ForegroundColor Yellow
Write-Host " son-of-anton\" -ForegroundColor White
Write-Host " DEPLOY_GUIDE.md" -ForegroundColor Gray
Write-Host " captain-definition" -ForegroundColor Gray
Write-Host " Dockerfile" -ForegroundColor Gray
Write-Host " .env.example" -ForegroundColor Gray
Write-Host " .gitignore" -ForegroundColor Gray
Write-Host " requirements.txt" -ForegroundColor Gray
Write-Host " backend\" -ForegroundColor White
Write-Host " __init__.py main.py config.py database.py" -ForegroundColor Gray
Write-Host " models.py auth.py seed.py system_prompt.py" -ForegroundColor Gray
Write-Host " routes\" -ForegroundColor White
Write-Host " __init__.py auth_routes.py chat_routes.py" -ForegroundColor Gray
Write-Host " admin_routes.py knowledge_routes.py files_routes.py" -ForegroundColor Gray
Write-Host " services\" -ForegroundColor White
Write-Host " __init__.py bedrock_service.py memory_service.py" -ForegroundColor Gray
Write-Host " rag_service.py code_extractor.py" -ForegroundColor Gray
Write-Host " frontend\" -ForegroundColor White
Write-Host " package.json vite.config.js tailwind.config.js" -ForegroundColor Gray
Write-Host " postcss.config.js index.html" -ForegroundColor Gray
Write-Host " src\" -ForegroundColor White
Write-Host " main.jsx App.jsx index.css api.js store.jsx" -ForegroundColor Gray
Write-Host " pages\" -ForegroundColor White
Write-Host " LoginPage.jsx ChatPage.jsx AdminPage.jsx" -ForegroundColor Gray
Write-Host " components\" -ForegroundColor White
Write-Host " Sidebar.jsx ChatView.jsx" -ForegroundColor Gray
Write-Host " MessageBubble.jsx CodeBlock.jsx" -ForegroundColor Gray
Write-Host ""
Write-Host " NEXT: Paste the code into each file, then follow DEPLOY_GUIDE.md" -ForegroundColor Yellow
Write-Host ""
\ No newline at end of file
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Son of Anton</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<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>" />
</head>
<body class="bg-anton-bg text-anton-text font-sans">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
\ No newline at end of file
{
"name": "son-of-anton-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^7.1.1",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.7"
}
}
\ No newline at end of file
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
\ No newline at end of file
import React from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { useApp } from "./store";
import LoginPage from "./pages/LoginPage";
import ChatPage from "./pages/ChatPage";
import AdminPage from "./pages/AdminPage";
export default function App() {
const { state } = useApp();
const loggedIn = !!state.token;
if (!loggedIn) {
return <LoginPage />;
}
return (
<Routes>
<Route path="/admin" element={<AdminPage />} />
<Route path="/*" element={<ChatPage />} />
</Routes>
);
}
\ No newline at end of file
/**
* Son of Anton — API helper functions
*/
const BASE = "/api";
function headers(token) {
const h = { "Content-Type": "application/json" };
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
async function request(method, path, token, body) {
const opts = { method, headers: headers(token) };
if (body) opts.body = JSON.stringify(body);
const res = await fetch(`${BASE}${path}`, opts);
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || err.message || "Request failed");
}
return res.json();
}
/* ── Auth ──────────────────────────────────── */
export const login = (username, password) =>
request("POST", "/auth/login", null, { username, password });
export const register = (username, email, password) =>
request("POST", "/auth/register", null, { username, email, password });
export const getMe = (token) => request("GET", "/auth/me", token);
/* ── Chats ─────────────────────────────────── */
export const listChats = (token) =>
request("GET", "/chats", token);
export const createChat = (token, data = {}) =>
request("POST", "/chats", token, data);
export const renameChat = (token, chatId, title) =>
request("PUT", `/chats/${chatId}`, token, { title });
export const deleteChat = (token, chatId) =>
request("DELETE", `/chats/${chatId}`, token);
export const getMessages = (token, chatId) =>
request("GET", `/chats/${chatId}/messages`, token);
/* ── Streaming message ─────────────────────── */
export async function* streamMessage(token, chatId, body) {
const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
method: "POST",
headers: headers(token),
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || "Stream failed");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
const line = part.trim();
if (line.startsWith("data: ")) {
try {
yield JSON.parse(line.slice(6));
} catch {
/* skip malformed */
}
}
}
}
// flush
if (buffer.trim().startsWith("data: ")) {
try {
yield JSON.parse(buffer.trim().slice(6));
} catch {
/* skip */
}
}
}
/* ── Knowledge Bases ───────────────────────── */
export const listKnowledgeBases = (token) =>
request("GET", "/knowledge", token);
export const createKnowledgeBase = (token, name, description = "") =>
request("POST", "/knowledge", token, { name, description });
export const getKnowledgeBase = (token, kbId) =>
request("GET", `/knowledge/${kbId}`, token);
export const deleteKnowledgeBase = (token, kbId) =>
request("DELETE", `/knowledge/${kbId}`, token);
export async function uploadDocument(token, kbId, file) {
const form = new FormData();
form.append("file", file);
const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Upload failed");
}
return res.json();
}
/* ── Admin ─────────────────────────────────── */
export const adminStats = (token) =>
request("GET", "/admin/stats", token);
export const adminListUsers = (token) =>
request("GET", "/admin/users", token);
export const adminCreateUser = (token, data) =>
request("POST", "/admin/users", token, data);
export const adminUpdateUser = (token, userId, data) =>
request("PUT", `/admin/users/${userId}`, token, data);
export const adminDeleteUser = (token, userId) =>
request("DELETE", `/admin/users/${userId}`, token);
export const adminListChats = (token) =>
request("GET", "/admin/chats", token);
/* ── File helpers ──────────────────────────── */
export async function downloadZip(token, markdown) {
const res = await fetch(`${BASE}/files/download-zip`, {
method: "POST",
headers: headers(token),
body: JSON.stringify({ markdown }),
});
if (!res.ok) throw new Error("Download failed");
const ct = res.headers.get("content-type") || "";
if (ct.includes("application/zip")) {
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "son-of-anton-code.zip";
a.click();
URL.revokeObjectURL(url);
} else {
const data = await res.json();
if (data.error) throw new Error(data.error);
}
}
\ No newline at end of file
This diff is collapsed.
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Copy, Check, Download, FileCode } from "lucide-react";
// Map common aliases for syntax highlighting
const LANG_MAP = {
cs: "csharp",
sh: "bash",
shell: "bash",
yml: "yaml",
dockerfile: "docker",
jsx: "jsx",
tsx: "tsx",
py: "python",
js: "javascript",
ts: "typescript",
rb: "ruby",
rs: "rust",
kt: "kotlin",
gd: "gdscript",
};
const customStyle = {
...oneDark,
'pre[class*="language-"]': {
...oneDark['pre[class*="language-"]'],
background: "#0d0d14",
margin: 0,
borderRadius: 0,
fontSize: "0.82rem",
lineHeight: "1.6",
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: "none",
fontSize: "0.82rem",
},
};
export default function CodeBlock({ language, filename, code }) {
const [copied, setCopied] = useState(false);
const hlLang = LANG_MAP[language] || language || "text";
function handleCopy() {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
function handleDownload() {
const name = filename || `code.${language || "txt"}`;
const blob = new Blob([code], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
}
return (
<div className="my-3 rounded-lg overflow-hidden border border-anton-border bg-[#0d0d14]">
{/* Header bar */}
<div className="flex items-center justify-between px-3 py-1.5 bg-anton-border/30">
<div className="flex items-center gap-2 text-xs text-anton-muted">
<FileCode size={12} className="text-anton-accent" />
{filename ? (
<span className="text-anton-text font-mono">{filename}</span>
) : (
<span>{hlLang}</span>
)}
</div>
<div className="flex items-center gap-1">
<button onClick={handleCopy}
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"
>
{copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
{copied ? "Copied" : "Copy"}
</button>
<button onClick={handleDownload}
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"
>
<Download size={11} />
Download
</button>
</div>
</div>
{/* Code */}
<div className="overflow-x-auto">
<SyntaxHighlighter
language={hlLang}
style={customStyle}
showLineNumbers
lineNumberStyle={{
minWidth: "2.5em",
paddingRight: "1em",
color: "#3a3a4a",
userSelect: "none",
}}
wrapLines
>
{code}
</SyntaxHighlighter>
</div>
</div>
);
}
\ No newline at end of file
import React, { useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import CodeBlock from "./CodeBlock";
import { User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check } from "lucide-react";
export default function MessageBubble({ message, isStreaming, isThinking }) {
const { role, content, thinking_content, input_tokens, output_tokens } = message;
const isUser = role === "user";
const [showThinking, setShowThinking] = useState(false);
const [copied, setCopied] = useState(false);
function handleCopy() {
navigator.clipboard.writeText(content || "");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className={`flex gap-3 animate-fade-in ${isUser ? "justify-end" : ""}`}>
{/* Avatar */}
{!isUser && (
<div className="shrink-0 mt-1">
<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">
<Flame size={16} className="text-white" />
</div>
</div>
)}
<div className={`max-w-[80%] ${isUser ? "order-first" : ""}`}>
{/* Thinking block */}
{thinking_content && (
<div className="mb-2">
<button onClick={() => setShowThinking(!showThinking)}
className="flex items-center gap-1.5 text-xs text-purple-400 hover:text-purple-300 transition mb-1"
>
<Brain size={12} />
{showThinking ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{isThinking ? (
<span className="thinking-pulse">Reasoning…</span>
) : (
<span>View reasoning</span>
)}
</button>
{(showThinking || isThinking) && (
<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">
{thinking_content}
{isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
</div>
)}
</div>
)}
{/* Message content */}
<div className={`rounded-2xl px-4 py-3 ${
isUser
? "bg-anton-accent text-white rounded-br-md"
: "bg-anton-card border border-anton-border rounded-bl-md"
}`}>
{isUser ? (
<div className="text-sm whitespace-pre-wrap">{content}</div>
) : (
<div className="prose-anton text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\S+)/.exec(className || "");
const rawLang = match?.[1] || "";
if (inline) {
return (
<code className={className} {...props}>
{children}
</code>
);
}
// Parse "lang:filename" format
let lang = rawLang;
let filename = null;
if (rawLang.includes(":")) {
const idx = rawLang.indexOf(":");
lang = rawLang.slice(0, idx);
filename = rawLang.slice(idx + 1);
}
const code = String(children).replace(/\n$/, "");
return (
<CodeBlock language={lang} filename={filename} code={code} />
);
},
// Make sure pre doesn't double-wrap
pre({ children }) {
return <>{children}</>;
},
}}
>
{content || ""}
</ReactMarkdown>
{isStreaming && !isThinking && (
<span className="inline-block w-1.5 h-4 bg-anton-accent ml-0.5 animate-pulse" />
)}
</div>
)}
</div>
{/* Footer */}
{!isUser && !isStreaming && content && (
<div className="flex items-center gap-3 mt-1.5 px-1">
<button onClick={handleCopy}
className="flex items-center gap-1 text-[11px] text-anton-muted hover:text-white transition"
>
{copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
{copied ? "Copied" : "Copy"}
</button>
{(input_tokens > 0 || output_tokens > 0) && (
<span className="text-[11px] text-anton-muted">
{input_tokens?.toLocaleString()}↓ / {output_tokens?.toLocaleString()}↑ tokens
</span>
)}
</div>
)}
</div>
{/* User avatar */}
{isUser && (
<div className="shrink-0 mt-1">
<div className="w-8 h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center">
<User size={16} className="text-anton-muted" />
</div>
</div>
)}
</div>
);
}
\ No newline at end of file
This diff is collapsed.
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ── Globals ───────────────────────────────── */
* {
scrollbar-width: thin;
scrollbar-color: #2a2a3a #0a0a0f;
}
*::-webkit-scrollbar {
width: 6px;
}
*::-webkit-scrollbar-track {
background: #0a0a0f;
}
*::-webkit-scrollbar-thumb {
background: #2a2a3a;
border-radius: 3px;
}
html,
body,
#root {
height: 100%;
margin: 0;
overflow: hidden;
}
/* ── Markdown prose adjustments ────────────── */
.prose-anton h1,
.prose-anton h2,
.prose-anton h3 {
color: #f97316;
margin-top: 1em;
margin-bottom: 0.5em;
font-weight: 600;
}
.prose-anton h1 { font-size: 1.5rem; }
.prose-anton h2 { font-size: 1.25rem; }
.prose-anton h3 { font-size: 1.1rem; }
.prose-anton p {
margin-bottom: 0.75em;
line-height: 1.7;
}
.prose-anton ul,
.prose-anton ol {
margin-left: 1.5em;
margin-bottom: 0.75em;
}
.prose-anton li {
margin-bottom: 0.25em;
}
.prose-anton ul { list-style-type: disc; }
.prose-anton ol { list-style-type: decimal; }
.prose-anton a {
color: #f97316;
text-decoration: underline;
}
.prose-anton blockquote {
border-left: 3px solid #f97316;
padding-left: 1em;
color: #8888a0;
margin: 0.75em 0;
}
.prose-anton table {
border-collapse: collapse;
margin: 0.75em 0;
width: 100%;
}
.prose-anton th,
.prose-anton td {
border: 1px solid #2a2a3a;
padding: 0.4em 0.75em;
text-align: left;
}
.prose-anton th {
background: #1a1a28;
font-weight: 600;
}
.prose-anton code:not(pre code) {
background: #1a1a28;
padding: 0.15em 0.4em;
border-radius: 4px;
font-size: 0.9em;
font-family: "JetBrains Mono", monospace;
color: #f97316;
}
/* ── Thinking block animation ──────────────── */
@keyframes thinkPulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.thinking-pulse {
animation: thinkPulse 2s ease-in-out infinite;
}
/* ── Slider custom styling ─────────────────── */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
width: 100%;
}
input[type="range"]::-webkit-slider-track {
height: 4px;
border-radius: 2px;
background: #2a2a3a;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #f97316;
margin-top: -6px;
cursor: pointer;
}
input[type="range"]::-moz-range-track {
height: 4px;
border-radius: 2px;
background: #2a2a3a;
}
input[type="range"]::-moz-range-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
background: #f97316;
border: none;
cursor: pointer;
}
\ No newline at end of file
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { AppProvider } from "./store";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<AppProvider>
<App />
</AppProvider>
</BrowserRouter>
</React.StrictMode>
);
\ No newline at end of file
This diff is collapsed.
import React, { useEffect, useCallback } from "react";
import { useApp } from "../store";
import { listChats } from "../api";
import Sidebar from "../components/Sidebar";
import ChatView from "../components/ChatView";
import { Flame, MessageSquarePlus } from "lucide-react";
export default function ChatPage() {
const { state, dispatch } = useApp();
const loadChats = useCallback(async () => {
try {
const chats = await listChats(state.token);
dispatch({ type: "SET_CHATS", chats });
} catch {
/* ignore */
}
}, [state.token, dispatch]);
useEffect(() => {
loadChats();
}, [loadChats]);
return (
<div className="h-full flex">
<Sidebar onRefresh={loadChats} />
<main className="flex-1 flex flex-col min-w-0">
{state.activeChatId ? (
<ChatView key={state.activeChatId} chatId={state.activeChatId} />
) : (
<EmptyState />
)}
</main>
</div>
);
}
function EmptyState() {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center animate-fade-in">
<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">
<Flame size={44} className="text-anton-accent" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">Son of Anton</h2>
<p className="text-anton-muted max-w-md">
Avatar of All Elements of Code. Create a new chat to begin — but bring
real questions, not that first-result-of-Google garbage.
</p>
</div>
</div>
);
}
\ No newline at end of file
import React, { useState } from "react";
import { Flame, LogIn, UserPlus, Eye, EyeOff } from "lucide-react";
import { login, register } from "../api";
import { useApp } from "../store";
export default function LoginPage() {
const { dispatch } = useApp();
const [isRegister, setIsRegister] = useState(false);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setError("");
setLoading(true);
try {
let res;
if (isRegister) {
res = await register(username, email, password);
} else {
res = await login(username, password);
}
dispatch({ type: "LOGIN", token: res.token, user: res.user });
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
return (
<div className="h-full flex items-center justify-center bg-anton-bg p-4">
{/* Glow effect */}
<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" />
<div className="relative w-full max-w-md animate-fade-in">
{/* Header */}
<div className="text-center mb-8">
<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">
<Flame size={40} className="text-white" />
</div>
<h1 className="text-3xl font-bold text-white tracking-tight">
Son of Anton
</h1>
<p className="text-anton-muted mt-2 text-sm">
Avatar of All Elements of Code
</p>
</div>
{/* Form Card */}
<form
onSubmit={handleSubmit}
className="bg-anton-surface border border-anton-border rounded-2xl p-8 space-y-5 shadow-2xl"
>
<h2 className="text-xl font-semibold text-white text-center">
{isRegister ? "Create Account" : "Welcome Back"}
</h2>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
{error}
</div>
)}
<div>
<label className="block text-sm text-anton-muted mb-1.5">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
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"
placeholder="Enter username"
/>
</div>
{isRegister && (
<div>
<label className="block text-sm text-anton-muted mb-1.5">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
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"
placeholder="you@example.com"
/>
</div>
)}
<div>
<label className="block text-sm text-anton-muted mb-1.5">Password</label>
<div className="relative">
<input
type={showPw ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
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"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPw(!showPw)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition"
>
{showPw ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
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"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : isRegister ? (
<>
<UserPlus size={18} /> Create Account
</>
) : (
<>
<LogIn size={18} /> Sign In
</>
)}
</button>
<p className="text-center text-sm text-anton-muted">
{isRegister ? "Already have an account?" : "Don't have an account?"}{" "}
<button
type="button"
onClick={() => {
setIsRegister(!isRegister);
setError("");
}}
className="text-anton-accent hover:underline"
>
{isRegister ? "Sign in" : "Register"}
</button>
</p>
</form>
</div>
</div>
);
}
\ No newline at end of file
/**
* Global state via React Context + useReducer
*/
import React, { createContext, useContext, useReducer, useEffect } from "react";
const AppContext = createContext();
const initialState = {
token: localStorage.getItem("soa_token") || null,
user: JSON.parse(localStorage.getItem("soa_user") || "null"),
chats: [],
activeChatId: null,
sidebarOpen: true,
};
function reducer(state, action) {
switch (action.type) {
case "LOGIN":
localStorage.setItem("soa_token", action.token);
localStorage.setItem("soa_user", JSON.stringify(action.user));
return { ...state, token: action.token, user: action.user };
case "LOGOUT":
localStorage.removeItem("soa_token");
localStorage.removeItem("soa_user");
return { ...initialState, token: null, user: null };
case "SET_CHATS":
return { ...state, chats: action.chats };
case "ADD_CHAT":
return {
...state,
chats: [action.chat, ...state.chats],
activeChatId: action.chat.id,
};
case "UPDATE_CHAT": {
const updated = state.chats.map((c) =>
c.id === action.chat.id ? { ...c, ...action.chat } : c
);
return { ...state, chats: updated };
}
case "REMOVE_CHAT": {
const filtered = state.chats.filter((c) => c.id !== action.chatId);
return {
...state,
chats: filtered,
activeChatId:
state.activeChatId === action.chatId
? filtered[0]?.id || null
: state.activeChatId,
};
}
case "SET_ACTIVE_CHAT":
return { ...state, activeChatId: action.chatId };
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
default:
return state;
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
return useContext(AppContext);
}
\ No newline at end of file
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,jsx}"],
theme: {
extend: {
colors: {
anton: {
bg: "#0a0a0f",
surface: "#12121a",
card: "#1a1a28",
border: "#2a2a3a",
accent: "#f97316",
accentDim: "#c2410c",
text: "#e4e4ef",
muted: "#8888a0",
user: "#1e293b",
assistant: "#15151f",
danger: "#ef4444",
success: "#22c55e",
},
},
fontFamily: {
sans: ['"Inter"', "system-ui", "sans-serif"],
mono: ['"JetBrains Mono"', '"Fira Code"', "monospace"],
},
animation: {
"pulse-slow": "pulse 3s cubic-bezier(0.4,0,0.6,1) infinite",
"fade-in": "fadeIn 0.3s ease-out",
},
keyframes: {
fadeIn: {
"0%": { opacity: 0, transform: "translateY(8px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
},
},
},
},
plugins: [],
};
\ No newline at end of file
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:8000",
},
},
build: {
outDir: "dist",
sourcemap: false,
},
});
\ No newline at end of file
# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
def print_hi(name):
# Use a breakpoint in the code line below to debug your script.
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
print_hi('PyCharm')
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
fastapi==0.115.6
uvicorn[standard]==0.34.0
sqlalchemy==2.0.36
pyjwt==2.10.1
passlib[bcrypt]==1.7.4
python-multipart==0.0.20
httpx==0.28.1
chromadb==0.6.3
PyPDF2==3.0.1
pydantic==2.10.4
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment