Commit 8bb1768b authored by Mahmoud Aglan's avatar Mahmoud Aglan

gooooo

parent f90d5b94
{
"permissions": {
"allow": [
"Bash(npx vite:*)",
"Bash(node -e ':*)",
"Bash(python3 -c \"import ast; ast.parse\\(open\\('backend/services/generation_manager.py'\\).read\\(\\)\\); print\\('Syntax OK'\\)\")",
"Bash(python3 -c \"import ast; ast.parse\\(open\\('backend/routes/auth_routes.py'\\).read\\(\\)\\); print\\('Syntax OK'\\)\")"
]
}
}
This diff is collapsed.
"""
Chat attachment upload, serve, and delete routes.
"""
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, Chat, ChatAttachment
from backend.auth import get_current_user, decode_token
from backend.services import attachment_service
from backend.config import MAX_ATTACHMENT_BYTES
router = APIRouter()
@router.post("/chats/{chat_id}/attachments")
async def upload_attachments(
chat_id: str,
files: list[UploadFile] = File(...),
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404, "Chat not found")
results = []
for file in files:
filename = file.filename or "file"
try:
content = await file.read()
if len(content) > MAX_ATTACHMENT_BYTES:
results.append({
"error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
})
continue
meta = attachment_service.save_attachment(
chat_id=chat_id,
filename=filename,
content=content,
content_type=file.content_type,
)
att = ChatAttachment(
id=meta["id"],
chat_id=chat_id,
filename=meta["filename"],
original_filename=meta["original_filename"],
mime_type=meta["mime_type"],
file_type=meta["file_type"],
file_size=meta["file_size"],
storage_path=meta["storage_path"],
text_extract=meta.get("text_extract"),
)
db.add(att)
db.commit()
db.refresh(att)
results.append(_att_dict(att))
except Exception as e:
results.append({"error": f"Failed to upload {filename}: {str(e)}"})
return {"attachments": results}
@router.get("/attachments/{attachment_id}/file")
def serve_attachment(
attachment_id: str,
token: Optional[str] = Query(None),
user: Optional[User] = Depends(_optional_current_user),
db: Session = Depends(get_db),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
# Try query param auth if header auth didn't work
if user is None and token:
try:
payload = decode_token(token)
user = db.query(User).filter(User.id == payload["sub"]).first()
except Exception:
pass
if user is None:
raise HTTPException(401, "Authentication required")
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404, "Attachment not found")
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403, "Access denied")
if not os.path.exists(att.storage_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
att.storage_path,
media_type=att.mime_type,
filename=att.original_filename,
)
@router.delete("/attachments/{attachment_id}")
def delete_attachment(
attachment_id: str,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete a single attachment."""
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404)
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403)
attachment_service.delete_attachment_file(att.storage_path)
db.delete(att)
db.commit()
return {"ok": True}
def _optional_current_user(
db: Session = Depends(get_db),
):
"""
A dependency that tries to get current user but returns None on failure.
This allows the endpoint to also accept ?token= query param.
"""
# This is a placeholder — the actual auth is handled in the route
# by checking both header and query param
return None
def _att_dict(att: ChatAttachment) -> dict:
return {
"id": att.id,
"chat_id": att.chat_id,
"message_id": att.message_id,
"filename": att.filename,
"original_filename": att.original_filename,
"mime_type": att.mime_type,
"file_type": att.file_type,
"file_size": att.file_size,
"created_at": str(att.created_at),
}
\ No newline at end of file
"""
Chat attachment upload, serve, and delete routes.
"""
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, Chat, ChatAttachment
from backend.auth import get_current_user, decode_token
from backend.services import attachment_service
from backend.config import MAX_ATTACHMENT_BYTES
router = APIRouter()
def _get_user_from_request(request: Request, db: Session, token_param: Optional[str] = None) -> User:
"""
Resolve user from either:
1. Authorization: Bearer <token> header
2. ?token=<token> query parameter (for img/video tags)
"""
raw_token = None
# Try header first
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
raw_token = auth_header[7:]
# Fall back to query param
if not raw_token and token_param:
raw_token = token_param
if not raw_token:
raise HTTPException(401, "Authentication required")
payload = decode_token(raw_token)
user = db.query(User).filter(User.id == payload["sub"]).first()
if not user or not user.is_active:
raise HTTPException(401, "User not found or inactive")
return user
@router.post("/chats/{chat_id}/attachments")
async def upload_attachments(
chat_id: str,
files: list[UploadFile] = File(...),
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404, "Chat not found")
results = []
for file in files:
filename = file.filename or "file"
try:
content = await file.read()
if len(content) > MAX_ATTACHMENT_BYTES:
results.append({
"error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
})
continue
meta = attachment_service.save_attachment(
chat_id=chat_id,
filename=filename,
content=content,
content_type=file.content_type,
)
att = ChatAttachment(
id=meta["id"],
chat_id=chat_id,
filename=meta["filename"],
original_filename=meta["original_filename"],
mime_type=meta["mime_type"],
file_type=meta["file_type"],
file_size=meta["file_size"],
storage_path=meta["storage_path"],
text_extract=meta.get("text_extract"),
)
db.add(att)
db.commit()
db.refresh(att)
results.append(_att_dict(att))
except Exception as e:
results.append({"error": f"Failed to upload {filename}: {str(e)}"})
return {"attachments": results}
@router.get("/attachments/{attachment_id}/file")
def serve_attachment(
attachment_id: str,
request: Request,
token: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
user = _get_user_from_request(request, db, token)
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404, "Attachment not found")
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403, "Access denied")
if not os.path.exists(att.storage_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
att.storage_path,
media_type=att.mime_type,
filename=att.original_filename,
)
@router.delete("/attachments/{attachment_id}")
def delete_attachment(
attachment_id: str,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete a single attachment."""
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404)
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403)
attachment_service.delete_attachment_file(att.storage_path)
db.delete(att)
db.commit()
return {"ok": True}
def _att_dict(att: ChatAttachment) -> dict:
return {
"id": att.id,
"chat_id": att.chat_id,
"message_id": att.message_id,
"filename": att.filename,
"original_filename": att.original_filename,
"mime_type": att.mime_type,
"file_type": att.file_type,
"file_size": att.file_size,
"created_at": str(att.created_at),
}
\ No newline at end of file
"""
Son of Anton — Attachment Upload Routes
Handles file uploads for chat messages.
"""
import os
import uuid
import shutil
from pathlib import Path
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form
from typing import List
from ..auth import get_current_user
from ..database import get_db
from ..services.file_processor import classify_media, validate_file
router = APIRouter(prefix="/chats/{chat_id}/attachments", tags=["attachments"])
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
@router.post("")
async def upload_attachments(
chat_id: str,
files: List[UploadFile] = File(...),
user=Depends(get_current_user),
db=Depends(get_db),
):
"""Upload one or more files to a chat. Returns attachment metadata."""
# Verify chat belongs to user
chat = await db.fetchrow(
"SELECT id, user_id FROM chats WHERE id = $1", uuid.UUID(chat_id)
)
if not chat:
raise HTTPException(404, "Chat not found")
if str(chat["user_id"]) != str(user["id"]):
raise HTTPException(403, "Not your chat")
if len(files) > 10:
raise HTTPException(400, "Maximum 10 files per upload")
results = []
for f in files:
# Read file content to get size
content = await f.read()
size = len(content)
# Validate
ok, error = validate_file(f.filename or "file", f.content_type or "", size)
if not ok:
raise HTTPException(400, f"File '{f.filename}': {error}")
media_type = classify_media(f.content_type or "")
# Generate unique storage filename
ext = Path(f.filename or "file").suffix or ".bin"
storage_name = f"{uuid.uuid4().hex}{ext}"
chat_dir = Path(UPLOAD_DIR) / chat_id
chat_dir.mkdir(parents=True, exist_ok=True)
storage_path = f"{chat_id}/{storage_name}"
full_path = chat_dir / storage_name
# Write file
full_path.write_bytes(content)
# Insert DB record
att = await db.fetchrow(
"""
INSERT INTO attachments (chat_id, user_id, filename, original_filename, mime_type, file_size, media_type, storage_path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, filename, original_filename, mime_type, file_size, media_type, created_at
""",
uuid.UUID(chat_id),
user["id"],
storage_name,
f.filename or "file",
f.content_type or "application/octet-stream",
size,
media_type,
storage_path,
)
results.append({
"id": str(att["id"]),
"filename": att["original_filename"],
"mime_type": att["mime_type"],
"file_size": att["file_size"],
"media_type": att["media_type"],
"created_at": att["created_at"].isoformat(),
})
return results
@router.get("")
async def list_attachments(
chat_id: str,
user=Depends(get_current_user),
db=Depends(get_db),
):
"""List all attachments for a chat."""
chat = await db.fetchrow(
"SELECT id, user_id FROM chats WHERE id = $1", uuid.UUID(chat_id)
)
if not chat:
raise HTTPException(404, "Chat not found")
if str(chat["user_id"]) != str(user["id"]):
raise HTTPException(403, "Not your chat")
rows = await db.fetch(
"""
SELECT id, original_filename as filename, mime_type, file_size, media_type, message_id, created_at
FROM attachments WHERE chat_id = $1 ORDER BY created_at
""",
uuid.UUID(chat_id),
)
return [dict(r) for r in rows]
@router.get("/{attachment_id}/preview")
async def preview_attachment(
chat_id: str,
attachment_id: str,
user=Depends(get_current_user),
db=Depends(get_db),
):
"""Return raw file content for preview (images mainly)."""
from fastapi.responses import FileResponse
att = await db.fetchrow(
"""
SELECT a.*, c.user_id as chat_owner
FROM attachments a JOIN chats c ON a.chat_id = c.id
WHERE a.id = $1 AND a.chat_id = $2
""",
uuid.UUID(attachment_id),
uuid.UUID(chat_id),
)
if not att:
raise HTTPException(404, "Attachment not found")
if str(att["chat_owner"]) != str(user["id"]):
raise HTTPException(403, "Not your attachment")
full_path = Path(UPLOAD_DIR) / att["storage_path"]
if not full_path.exists():
raise HTTPException(404, "File not found on disk")
return FileResponse(
path=str(full_path),
media_type=att["mime_type"],
filename=att["original_filename"],
)
\ No newline at end of file
......@@ -2,6 +2,8 @@
Authentication routes: register, login, profile — with permissions and registration toggle.
"""
from datetime import datetime
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
......@@ -84,6 +86,11 @@ def login(body: LoginBody, db: Session = Depends(get_db)):
@router.get("/me")
def me(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
now = datetime.utcnow()
if user.quota_reset_date and now >= user.quota_reset_date:
user.tokens_used_this_month = 0
user.quota_reset_date = datetime(now.year + 1, 1, 1) if now.month == 12 else datetime(now.year, now.month + 1, 1)
db.commit()
perms = get_user_permissions(user.id, db)
return _user_dict(user, perms)
......
......@@ -352,7 +352,7 @@ async def commit_from_chat(
except gitlab_service.GitLabError as e:
# Check if it's a "already exists" type error (file was committed despite error)
error_detail = str(e.detail).lower()
if "a]ready exists" in error_detail or "already been taken" in error_detail:
if "already exists" in error_detail or "already been taken" in error_detail:
# Files might have been committed — retry with update action
for a in actions:
if a["action"] == "create":
......
"""
SON OF ANTON — INTEGRATION PATCH FOR YOUR EXISTING MESSAGE ROUTE
This is NOT a standalone file. These are the functions and code blocks
you need to ADD to your existing message-sending route (the one that
handles POST /chats/{chat_id}/messages and streams SSE back).
--- STEP 1: Add these imports at the top of your messages route file ---
"""
# Add to your imports:
import os
from ..services.file_processor import build_content_blocks_for_attachments
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
"""
--- STEP 2: In your request body model/parsing, add attachment_ids ---
Your existing body probably looks like:
{ content, model, max_tokens, reasoning_budget, knowledge_base_id }
Add:
attachment_ids: list[str] = []
So it becomes:
{ content, model, max_tokens, reasoning_budget, knowledge_base_id, attachment_ids }
"""
"""
--- STEP 3: Where you build the Claude messages array, replace the simple
text content with a content block array when attachments exist ---
BEFORE (you probably have something like):
user_message_content = body.content
# or
messages_for_claude.append({"role": "user", "content": body.content})
AFTER (replace with):
"""
async def build_user_content(db, body_content: str, attachment_ids: list, chat_id: str):
"""
Build Claude content blocks for a user message.
If there are attachments, returns a list of content blocks.
If no attachments, returns the plain text string (backward compatible).
"""
if not attachment_ids:
return [{"text": body_content}]
import uuid as uuid_mod
# Fetch attachment records
att_uuids = [uuid_mod.UUID(aid) for aid in attachment_ids]
attachments = await db.fetch(
"""
SELECT id, filename, original_filename, mime_type, file_size, media_type, storage_path
FROM attachments
WHERE id = ANY($1) AND chat_id = $2
""",
att_uuids,
uuid_mod.UUID(chat_id),
)
attachments = [dict(a) for a in attachments]
# Build content blocks: text first, then file blocks
content_blocks = []
# Add the text message
if body_content.strip():
content_blocks.append({"text": body_content})
# Add file content blocks
file_blocks = build_content_blocks_for_attachments(attachments, UPLOAD_DIR)
content_blocks.extend(file_blocks)
# If no text was provided, add a default prompt
if not body_content.strip():
content_blocks.insert(0, {"text": "Please describe and analyze the attached file(s)."})
# Link attachments to the message (do after message is saved)
return content_blocks
"""
--- STEP 4: In your Claude API call, use the content blocks ---
Instead of:
{"role": "user", "content": "user text here"}
Use:
{"role": "user", "content": content_blocks}
Where content_blocks comes from build_user_content() above.
--- STEP 5: After saving the user message to DB, link attachments ---
"""
async def link_attachments_to_message(db, attachment_ids: list, message_id):
"""Call this after inserting the user message into your messages table."""
import uuid as uuid_mod
if attachment_ids:
await db.execute(
"UPDATE attachments SET message_id = $1 WHERE id = ANY($2)",
message_id,
[uuid_mod.UUID(aid) for aid in attachment_ids],
)
"""
--- STEP 6: Register the attachments router in your main app file ---
In your main.py or app.py, add:
from .routes.attachments import router as attachments_router
app.include_router(attachments_router, prefix="/api")
--- STEP 7: Also serve uploaded files statically (optional, for image previews) ---
from fastapi.staticfiles import StaticFiles
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
--- DONE. That's it for the backend. ---
"""
\ No newline at end of file
"""
Son of Anton — File Processor
Handles classification, validation, and Claude content-block generation
for uploaded files (images, videos, documents).
"""
import base64
import mimetypes
from pathlib import Path
# Claude Bedrock supported formats
IMAGE_FORMATS = {"image/jpeg", "image/png", "image/gif", "image/webp"}
VIDEO_FORMATS = {"video/mp4", "video/webm", "video/mov", "video/mpeg", "video/mkv",
"video/x-matroska", "video/quicktime", "video/x-flv",
"video/x-ms-wmv", "video/3gpp"}
DOCUMENT_FORMATS = {
"application/pdf": "pdf",
"text/csv": "csv",
"application/msword": "doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/vnd.ms-excel": "xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
"text/html": "html",
"text/plain": "txt",
"text/markdown": "md",
}
# Max sizes (bytes)
MAX_IMAGE_SIZE = 20 * 1024 * 1024 # 20MB
MAX_VIDEO_SIZE = 100 * 1024 * 1024 # 100MB (Claude limit ~25MB for video in message)
MAX_DOCUMENT_SIZE = 50 * 1024 * 1024 # 50MB
ALLOWED_MIMES = IMAGE_FORMATS | VIDEO_FORMATS | set(DOCUMENT_FORMATS.keys())
def classify_media(mime_type: str) -> str:
"""Classify a MIME type into a media category."""
if mime_type in IMAGE_FORMATS:
return "image"
if mime_type in VIDEO_FORMATS:
return "video"
if mime_type in DOCUMENT_FORMATS:
return "document"
if mime_type and mime_type.startswith("text/"):
return "text"
return "unknown"
def get_max_size(media_type: str) -> int:
"""Return max allowed file size in bytes for a media type."""
return {
"image": MAX_IMAGE_SIZE,
"video": MAX_VIDEO_SIZE,
"document": MAX_DOCUMENT_SIZE,
"text": MAX_DOCUMENT_SIZE,
}.get(media_type, MAX_DOCUMENT_SIZE)
def validate_file(filename: str, content_type: str, size: int) -> tuple[bool, str]:
"""Validate an uploaded file. Returns (ok, error_message)."""
if not content_type:
guessed, _ = mimetypes.guess_type(filename)
content_type = guessed or "application/octet-stream"
media_type = classify_media(content_type)
if media_type == "unknown":
return False, f"Unsupported file type: {content_type}. Supported: images, videos, PDF, Office docs, text files."
max_size = get_max_size(media_type)
if size > max_size:
return False, f"File too large ({size / 1024 / 1024:.1f}MB). Max for {media_type}: {max_size / 1024 / 1024:.0f}MB."
return True, ""
def mime_to_claude_format(mime_type: str, media_type: str) -> str:
"""Convert MIME type to Claude's format string."""
if media_type == "image":
return mime_type.split("/")[1] # jpeg, png, gif, webp
if media_type == "video":
mapping = {
"video/mp4": "mp4",
"video/webm": "webm",
"video/quicktime": "mov",
"video/mov": "mov",
"video/mpeg": "mpeg",
"video/mkv": "mkv",
"video/x-matroska": "mkv",
"video/x-flv": "flv",
"video/x-ms-wmv": "wmv",
"video/3gpp": "three_gp",
}
return mapping.get(mime_type, "mp4")
if media_type == "document":
return DOCUMENT_FORMATS.get(mime_type, "txt")
return "txt"
def build_content_block(file_path: str, mime_type: str, media_type: str, original_filename: str) -> dict:
"""
Build a Claude Converse API content block for a file.
Returns a dict that goes directly into the message content array.
"""
path = Path(file_path)
if not path.exists():
return {"text": f"[Attachment missing: {original_filename}]"}
file_bytes = path.read_bytes()
fmt = mime_to_claude_format(mime_type, media_type)
if media_type == "image":
return {
"image": {
"format": fmt,
"source": {
"bytes": file_bytes
}
}
}
elif media_type == "video":
return {
"video": {
"format": fmt,
"source": {
"bytes": file_bytes
}
}
}
elif media_type == "document":
return {
"document": {
"format": fmt,
"name": Path(original_filename).stem[:200],
"source": {
"bytes": file_bytes
}
}
}
elif media_type == "text":
try:
text_content = file_bytes.decode("utf-8", errors="replace")
except Exception:
text_content = "[Could not decode text file]"
return {
"text": f"--- Content of {original_filename} ---\n{text_content}\n--- End of {original_filename} ---"
}
else:
return {"text": f"[Unsupported attachment: {original_filename}]"}
def build_content_blocks_for_attachments(attachments: list, upload_dir: str) -> list[dict]:
"""
Given a list of attachment DB records, build all Claude content blocks.
"""
blocks = []
for att in attachments:
file_path = str(Path(upload_dir) / att["storage_path"])
block = build_content_block(
file_path=file_path,
mime_type=att["mime_type"],
media_type=att["media_type"],
original_filename=att["original_filename"],
)
blocks.append(block)
return blocks
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
import { request } from "./client";
// Stats & users
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);
// App settings
export const adminGetAppSettings = (token) =>
request("GET", "/admin/app-settings", token);
export const adminUpdateAppSettings = (token, data) =>
request("PUT", "/admin/app-settings", token, data);
// Models
export const adminGetModels = (token) => request("GET", "/admin/models", token);
// Permission defaults
export const adminGetPermissionDefaults = (token) =>
request("GET", "/admin/permissions/defaults", token);
export const adminUpdatePermissionDefaults = (token, data) =>
request("PUT", "/admin/permissions/defaults", token, data);
export const adminApplyDefaults = (token) =>
request("POST", "/admin/permissions/apply-defaults", token);
// User permissions
export const adminGetUserPermissions = (token, userId) =>
request("GET", `/admin/users/${userId}/permissions`, token);
export const adminUpdateUserPermissions = (token, userId, data) =>
request("PUT", `/admin/users/${userId}/permissions`, token, data);
import { request, authHeader, BASE } from "./client";
export async function uploadAttachments(token, chatId, files) {
const form = new FormData();
for (const file of files) form.append("files", file);
const res = await fetch(`${BASE}/chats/${chatId}/attachments`, {
method: "POST", headers: authHeader(token), body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Upload failed");
}
return res.json();
}
export function getAttachmentUrl(attachmentId) {
return `${BASE}/attachments/${attachmentId}/file`;
}
export const deleteAttachment = (token, attachmentId) =>
request("DELETE", `/attachments/${attachmentId}`, token);
import { request } from "./client";
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);
export const getRegistrationStatus = () =>
request("GET", "/auth/registration-status", null);
import { request, headers, BASE } from "./client";
export const listChats = (token) => request("GET", "/chats", token);
export const createChat = (token, data = {}) =>
request("POST", "/chats", token, data);
export const updateChat = (token, chatId, data) =>
request("PUT", `/chats/${chatId}`, token, data);
export const renameChat = (token, chatId, title) =>
updateChat(token, chatId, { title });
export const deleteChat = (token, chatId) =>
request("DELETE", `/chats/${chatId}`, token);
export const getMessages = (token, chatId) =>
request("GET", `/chats/${chatId}/messages`, token);
export const checkGenerating = (token, chatId) =>
request("GET", `/chats/${chatId}/generating`, token);
export async function* streamMessage(token, chatId, body, signal) {
const res = await fetch(`${BASE}/chats/${chatId}/messages`, {
method: "POST", headers: headers(token),
body: JSON.stringify(body), signal,
});
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 { }
}
}
}
if (buffer.trim().startsWith("data: ")) {
try { yield JSON.parse(buffer.trim().slice(6)); } catch { }
}
}
// User-facing GitLab integration (routes under /chats/)
export const listLinkedRepos = (token) =>
request("GET", "/chats/available-repos", token);
export const getRepoBranches = (token, repoId) =>
request("GET", `/chats/repos/${repoId}/branches`, token);
export const commitFromChat = (token, chatId, data) =>
request("POST", `/chats/${chatId}/commit`, token, data);
export const refreshRepoContext = (token, chatId) =>
request("POST", `/chats/${chatId}/refresh-repo`, token);
const BASE = "/api";
export function headers(token) {
const h = { "Content-Type": "application/json" };
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
export function authHeader(token) {
return token ? { Authorization: `Bearer ${token}` } : {};
}
export 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();
}
export { BASE };
import { headers, BASE } from "./client";
export async function exportPptx(token, markdown, title) {
const res = await fetch(`${BASE}/export/pptx`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a"); a.href = url;
a.download = `${(title || "presentation").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "presentation"}.pptx`;
a.click(); URL.revokeObjectURL(url);
}
export async function exportDocx(token, markdown, title) {
const res = await fetch(`${BASE}/export/docx`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a"); a.href = url;
a.download = `${(title || "document").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "document"}.docx`;
a.click(); URL.revokeObjectURL(url);
}
export async function downloadZip(token, markdown, title) {
const res = await fetch(`${BASE}/files/download-zip`, {
method: "POST", headers: headers(token),
body: JSON.stringify({ markdown, title }),
});
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);
}
}
import { request } from "./client";
// Connection settings
export const gitlabGetSettings = (token) =>
request("GET", "/gitlab/settings", token);
export const gitlabUpdateSettings = (token, data) =>
request("PUT", "/gitlab/settings", token, data);
export const gitlabTestConnection = (token) =>
request("POST", "/gitlab/test-connection", token);
// Projects
export const gitlabSearchProjects = (token, search, owned) =>
request("GET", `/gitlab/projects?search=${encodeURIComponent(search || "")}&owned=${owned || false}`, token);
export const gitlabCreateProject = (token, data) =>
request("POST", "/gitlab/projects", token, data);
// Linked repos
export const gitlabListRepos = (token) =>
request("GET", "/gitlab/repos", token);
export const gitlabLinkRepo = (token, gitlabProjectId) =>
request("POST", "/gitlab/repos", token, { gitlab_project_id: gitlabProjectId });
export const gitlabUnlinkRepo = (token, repoId) =>
request("DELETE", `/gitlab/repos/${repoId}`, token);
export const gitlabAnalyzeRepo = (token, repoId) =>
request("POST", `/gitlab/repos/${repoId}/analyze`, token);
export const gitlabGetRepoMap = (token, repoId) =>
request("GET", `/gitlab/repos/${repoId}/map`, token);
// Tree & files
export const gitlabGetTree = (token, repoId, path, ref) =>
request("GET", `/gitlab/repos/${repoId}/tree?path=${encodeURIComponent(path || "")}&ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabGetFile = (token, repoId, path, ref) =>
request("GET", `/gitlab/repos/${repoId}/file?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(ref || "")}`, token);
// Branches & commits
export const gitlabGetBranches = (token, repoId) =>
request("GET", `/gitlab/repos/${repoId}/branches`, token);
export const gitlabCommitFiles = (token, repoId, data) =>
request("POST", `/gitlab/repos/${repoId}/commit`, token, data);
export const gitlabCommitSingle = (token, repoId, data) =>
request("POST", `/gitlab/repos/${repoId}/commit-single`, token, data);
export const gitlabCreateBranch = (token, repoId, data) =>
request("POST", `/gitlab/repos/${repoId}/branches`, token, data);
export const gitlabCreateMergeRequest = (token, repoId, data) =>
request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data);
// Actions
export const gitlabListActions = (token, status) =>
request("GET", `/gitlab/actions?status=${status || "pending"}`, token);
export const gitlabCreateAction = (token, data) =>
request("POST", "/gitlab/actions", token, data);
export const gitlabApproveAction = (token, actionId) =>
request("POST", `/gitlab/actions/${actionId}/approve`, token);
export const gitlabRejectAction = (token, actionId) =>
request("POST", `/gitlab/actions/${actionId}/reject`, token);
// Barrel re-export — every domain module in one place.
// Consumers import from "../api" and get everything.
// Each module is small and self-contained, so AI edits
// to one domain can't break another domain's exports.
export { login, register, getMe, getRegistrationStatus } from "./auth";
export {
listChats, createChat, updateChat, renameChat, deleteChat,
getMessages, checkGenerating, streamMessage,
listLinkedRepos, getRepoBranches, commitFromChat, refreshRepoContext,
} from "./chats";
export { uploadAttachments, getAttachmentUrl, deleteAttachment } from "./attachments";
export {
listKnowledgeBases, createKnowledgeBase, getKnowledgeBase,
updateKnowledgeBase, deleteKnowledgeBase,
listKnowledgeDocuments, deleteKnowledgeDocument,
uploadDocuments, uploadDocument,
} from "./knowledge";
export {
gitlabGetSettings, gitlabUpdateSettings, gitlabTestConnection,
gitlabSearchProjects, gitlabCreateProject,
gitlabListRepos, gitlabLinkRepo, gitlabUnlinkRepo,
gitlabAnalyzeRepo, gitlabGetRepoMap,
gitlabGetTree, gitlabGetFile,
gitlabGetBranches, gitlabCommitFiles, gitlabCommitSingle,
gitlabCreateBranch, gitlabCreateMergeRequest,
gitlabListActions, gitlabCreateAction,
gitlabApproveAction, gitlabRejectAction,
} from "./gitlab";
export {
adminStats, adminListUsers, adminCreateUser, adminUpdateUser, adminDeleteUser,
adminListChats, adminGetAppSettings, adminUpdateAppSettings, adminGetModels,
adminGetPermissionDefaults, adminUpdatePermissionDefaults, adminApplyDefaults,
adminGetUserPermissions, adminUpdateUserPermissions,
} from "./admin";
export { exportPptx, exportDocx, downloadZip } from "./exports";
import { request, authHeader, BASE } from "./client";
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 updateKnowledgeBase = (token, kbId, data) =>
request("PUT", `/knowledge/${kbId}`, token, data);
export const deleteKnowledgeBase = (token, kbId) =>
request("DELETE", `/knowledge/${kbId}`, token);
export const listKnowledgeDocuments = (token, kbId) =>
request("GET", `/knowledge/${kbId}/documents`, token);
export const deleteKnowledgeDocument = (token, kbId, docId) =>
request("DELETE", `/knowledge/${kbId}/documents/${docId}`, token);
export async function uploadDocuments(token, kbId, files) {
const form = new FormData();
for (const file of files) form.append("files", file);
const res = await fetch(`${BASE}/knowledge/${kbId}/upload`, {
method: "POST", headers: authHeader(token), body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Upload failed");
}
return res.json();
}
export const uploadDocument = (token, kbId, file) =>
uploadDocuments(token, kbId, [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