Commit 64e177f9 authored by Administrator's avatar Administrator

Revert "4.0"

This reverts commit a20ce51b
parent a20ce51b
This source diff could not be displayed because it is too large. You can view the blob instead.
"""
Son of Anton v4.0 — Main FastAPI Application
The Avatar of All Elements of Code + GitLab Superweapon
Son of Anton — Main FastAPI Application
"""
import os
......@@ -25,7 +24,7 @@ from backend.routes.gitlab_routes import router as gitlab_router
from backend.services.bedrock_service import close_http_client
from backend.services.gitlab_service import close_gitlab_client
APP_VERSION = "4.0.0"
APP_VERSION = "3.0.0"
APP_BUILD_TIME = str(int(time.time()))
......@@ -45,12 +44,6 @@ def _run_migrations():
if "reasoning_budget" not in columns:
conn.execute(text("ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"))
print(" Added chats.reasoning_budget column")
if "gitlab_project_id" not in columns:
conn.execute(text("ALTER TABLE chats ADD COLUMN gitlab_project_id INTEGER"))
print(" Added chats.gitlab_project_id column")
if "gitlab_branch" not in columns:
conn.execute(text("ALTER TABLE chats ADD COLUMN gitlab_branch VARCHAR(200) DEFAULT 'main'"))
print(" Added chats.gitlab_branch column")
conn.commit()
if "chat_attachments" not in existing_tables:
......@@ -58,6 +51,7 @@ def _run_migrations():
ChatAttachment.__table__.create(bind=engine, checkfirst=True)
print(" Created chat_attachments table")
# GitLab tables
for table_name in ("gitlab_configs", "gitlab_operations", "gitlab_audit_log"):
if table_name not in existing_tables:
from backend.models import GitLabConfig, GitLabOperation, GitLabAuditLog
......@@ -78,7 +72,7 @@ async def lifespan(app: FastAPI):
Base.metadata.create_all(bind=engine)
_run_migrations()
seed_superadmin()
print(f"🔥 Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
print(f"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
yield
await close_http_client()
await close_gitlab_client()
......@@ -87,7 +81,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Son of Anton",
description="Avatar of All Elements of Code — v4.0 GitLab Superweapon",
description="Avatar of All Elements of Code",
version=APP_VERSION,
lifespan=lifespan,
)
......@@ -111,7 +105,7 @@ async def add_cache_headers(request: Request, call_next):
response.headers["Expires"] = "0"
elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
else:
elif path.endswith(".html") or not path.startswith("/assets"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
......@@ -159,4 +153,4 @@ async def serve_frontend(full_path: str):
resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0"
return resp
return {"message": "Son of Anton v4.0 API running. Frontend not built."}
\ No newline at end of file
return {"message": "Son of Anton API is running. Frontend not built."}
\ No newline at end of file
"""
SQLAlchemy ORM models — Son of Anton v4.0
SQLAlchemy ORM models.
"""
from datetime import datetime, timedelta
......@@ -49,8 +49,6 @@ class Chat(Base):
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)
gitlab_project_id = Column(Integer, nullable=True)
gitlab_branch = Column(String(200), nullable=True, default="main")
max_tokens = Column(Integer, default=4096)
reasoning_budget = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
......
"""
Chat CRUD and message streaming — v4.0
Now with GitLab project linking and project-aware generation.
Chat CRUD and message streaming with multimodal attachment support.
Generation runs in background and survives client disconnection.
"""
import json
......@@ -25,8 +25,6 @@ class CreateChatBody(BaseModel):
title: str = "New Chat"
model: str = "eu.anthropic.claude-opus-4-6-v1"
knowledge_base_id: Optional[str] = None
gitlab_project_id: Optional[int] = None
gitlab_branch: Optional[str] = "main"
max_tokens: int = 4096
reasoning_budget: int = 0
......@@ -37,8 +35,6 @@ class UpdateChatBody(BaseModel):
max_tokens: Optional[int] = None
reasoning_budget: Optional[int] = None
knowledge_base_id: Optional[str] = None
gitlab_project_id: Optional[int] = None
gitlab_branch: Optional[str] = None
class SendMessageBody(BaseModel):
......@@ -61,8 +57,6 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db
chat = Chat(
user_id=user.id, title=body.title, model=body.model,
knowledge_base_id=body.knowledge_base_id or None,
gitlab_project_id=body.gitlab_project_id,
gitlab_branch=body.gitlab_branch or "main",
max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget,
)
db.add(chat)
......@@ -94,10 +88,6 @@ def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_cur
chat.reasoning_budget = body.reasoning_budget
if body.knowledge_base_id is not None:
chat.knowledge_base_id = body.knowledge_base_id or None
if body.gitlab_project_id is not None:
chat.gitlab_project_id = body.gitlab_project_id if body.gitlab_project_id != 0 else None
if body.gitlab_branch is not None:
chat.gitlab_branch = body.gitlab_branch or "main"
db.commit()
return _chat_dict(chat)
......@@ -129,11 +119,13 @@ def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Sessi
@router.get("/{chat_id}/generating")
def check_generating(chat_id: str, user: User = Depends(get_current_user)):
"""Check if a background generation is active for this chat."""
return {"active": gen_manager.is_active(chat_id)}
@router.get("/{chat_id}/stream")
async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user)):
"""Reconnect to an ongoing background generation's SSE stream."""
if not gen_manager.is_active(chat_id):
async def empty():
yield _sse({"type": "done", "message_id": ""})
......@@ -148,8 +140,10 @@ async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user))
@router.post("/{chat_id}/messages")
async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)):
"""Send a message. Generation runs in background and survives disconnection."""
user_id = user.id
# Start background generation
gen_manager.start(
chat_id=chat_id,
user_id=user_id,
......@@ -161,6 +155,7 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends
attachment_ids=body.attachment_ids,
)
# Stream events from background task
async def generate():
async for event in gen_manager.stream_events(chat_id):
yield _sse(event)
......@@ -176,8 +171,6 @@ def _chat_dict(c):
return {
"id": c.id, "title": c.title, "model": c.model,
"knowledge_base_id": c.knowledge_base_id,
"gitlab_project_id": c.gitlab_project_id,
"gitlab_branch": c.gitlab_branch or "main",
"max_tokens": c.max_tokens or 4096,
"reasoning_budget": c.reasoning_budget or 0,
"created_at": str(c.created_at), "updated_at": str(c.updated_at),
......
"""
Background generation manager — v4.0
Background generation manager.
Decouples AI generation from the SSE HTTP connection so generation
continues even if the client disconnects.
Now includes GitLab project context injection.
"""
import asyncio
......@@ -12,7 +11,7 @@ from typing import Optional
from dataclasses import dataclass, field
from backend.database import SessionLocal
from backend.models import User, Chat, Message, ChatAttachment, GitLabConfig
from backend.models import User, Chat, Message, ChatAttachment
from backend.system_prompt import build_full_prompt
from backend.services import bedrock_service, memory_service, rag_service, attachment_service
......@@ -47,6 +46,7 @@ class GenerationManager:
knowledge_base_id: Optional[str],
attachment_ids: list[str],
) -> GenerationState:
# Abort any existing generation for this chat
old = self._active.get(chat_id)
if old and not old.done.is_set():
old.done.set()
......@@ -73,33 +73,13 @@ class GenerationManager:
yield state.events[idx]
idx += 1
if state.done.is_set():
# Yield any remaining events
while idx < len(state.events):
yield state.events[idx]
idx += 1
break
await asyncio.sleep(0.02)
async def _get_project_context(self, db, chat: Chat) -> Optional[str]:
"""Fetch GitLab project context if a project is linked to this chat."""
if not chat.gitlab_project_id:
return None
cfg = db.query(GitLabConfig).filter(GitLabConfig.is_active == True).first()
if not cfg:
return None
try:
from backend.services import gitlab_service
token = gitlab_service.decode_token(cfg.access_token_enc)
branch = chat.gitlab_branch or "main"
context = await gitlab_service.get_project_context(
cfg.gitlab_url, token, chat.gitlab_project_id, branch,
)
return context if context else None
except Exception as e:
print(f" GitLab context fetch failed: {e}")
return None
async def _run(
self,
state: GenerationState,
......@@ -157,6 +137,7 @@ class GenerationManager:
db.commit()
db.refresh(user_msg)
# Link attachments to message
for att in attachments:
att.message_id = user_msg.id
if attachments:
......@@ -171,10 +152,7 @@ class GenerationManager:
except Exception:
pass
# GitLab project context (v4.0)
project_context = await self._get_project_context(db, chat)
system_prompt = build_full_prompt(rag_context, project_context)
system_prompt = build_full_prompt(rag_context)
messages = memory_service.build_messages(chat, db)
# Inject multimodal content blocks
......@@ -204,7 +182,7 @@ class GenerationManager:
thinking_config=thinking_config,
):
if state.done.is_set():
break
break # Aborted
evt_type = event.get("type", "")
......@@ -234,7 +212,7 @@ class GenerationManager:
usage = event.get("usage", {})
output_tokens = usage.get("output_tokens", 0)
# Save assistant message
# Save assistant message to DB
assistant_msg = Message(
chat_id=chat_id, role="assistant", content=full_text,
thinking_content=full_thinking or None,
......@@ -271,6 +249,7 @@ class GenerationManager:
finally:
state.done.set()
db.close()
# Cleanup after 2 minutes
await asyncio.sleep(120)
self._active.pop(chat_id, None)
......
"""
GitLab CE API Service — v4.0
GitLab CE API Service.
Handles all communication with a self-hosted GitLab CE instance.
Superadmin-only. Every operation is auditable.
"""
......@@ -318,6 +318,10 @@ async def batch_commit(
branch: str, commit_message: str,
actions: list[dict],
) -> dict:
"""
Perform a batch commit with multiple file actions.
Each action: {"action": "create"|"update"|"delete", "file_path": "...", "content": "..."}
"""
client = _get_client()
body = {
"branch": branch,
......@@ -337,7 +341,10 @@ async def smart_batch_commit(
branch: str, commit_message: str,
files: list[dict],
) -> dict:
"""Intelligently create or update files. Checks if each file exists first."""
"""
Intelligently create or update files. Checks if each file exists first.
files: [{"file_path": "...", "content": "..."}]
"""
actions = []
for f in files:
exists = False
......@@ -409,7 +416,9 @@ async def get_merge_request_changes(
# Pipelines
# ══════════════════════════════════════════════════════
async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list:
async def list_pipelines(
gitlab_url: str, token: str, project_id: int,
) -> list:
client = _get_client()
resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines"),
......@@ -419,7 +428,9 @@ async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list:
return resp.json()
async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict:
async def get_pipeline(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> dict:
client = _get_client()
resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}"),
......@@ -429,7 +440,9 @@ async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id
return resp.json()
async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> list:
async def get_pipeline_jobs(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> list:
client = _get_client()
resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/jobs"),
......@@ -439,7 +452,9 @@ async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeli
return resp.json()
async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: str = "main") -> dict:
async def trigger_pipeline(
gitlab_url: str, token: str, project_id: int, ref: str = "main",
) -> dict:
client = _get_client()
resp = await client.post(
_api_url(gitlab_url, f"/projects/{project_id}/pipeline"),
......@@ -449,7 +464,9 @@ async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: st
return resp.json()
async def retry_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict:
async def retry_pipeline(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> dict:
client = _get_client()
resp = await client.post(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/retry"),
......@@ -474,72 +491,4 @@ async def compare(
params={"from": from_ref, "to": to_ref},
)
resp.raise_for_status()
return resp.json()
# ══════════════════════════════════════════════════════
# Project Context for AI (NEW in v4.0)
# ══════════════════════════════════════════════════════
async def get_project_context(
gitlab_url: str, token: str, project_id: int, branch: str = "main",
) -> str:
"""Build a formatted project context string for the AI system prompt."""
try:
project = await get_project(gitlab_url, token, project_id)
except Exception:
return ""
tree = []
try:
tree = await get_tree(gitlab_url, token, project_id, ref=branch, recursive=True)
except Exception:
pass
lines = []
lines.append(f"**Project:** {project.get('name_with_namespace', project.get('name', '?'))}")
lines.append(f"**Description:** {project.get('description') or 'No description'}")
lines.append(f"**Default Branch:** {project.get('default_branch', 'main')}")
lines.append(f"**Active Branch:** {branch}")
lines.append(f"**Web URL:** {project.get('web_url', '')}")
langs = project.get("languages") or {}
if not langs:
try:
client = _get_client()
resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/languages"),
headers=_headers(token),
)
if resp.status_code == 200:
langs = resp.json()
except Exception:
pass
if langs:
lang_str = ", ".join(f"{k} ({v:.1f}%)" for k, v in sorted(langs.items(), key=lambda x: -x[1])[:8])
lines.append(f"**Languages:** {lang_str}")
if tree:
lines.append(f"\n**File Tree ({len(tree)} items):**")
lines.append("```")
dirs = sorted([t for t in tree if t.get("type") == "tree"], key=lambda x: x["path"])
files = sorted([t for t in tree if t.get("type") == "blob"], key=lambda x: x["path"])
for d in dirs:
lines.append(f" 📁 {d['path']}/")
for f in files:
lines.append(f" 📄 {f['path']}")
lines.append("```")
# Try to fetch README for extra context
for readme_name in ["README.md", "README.rst", "README.txt", "README"]:
try:
readme_data = await get_file(gitlab_url, token, project_id, readme_name, ref=branch)
content = readme_data.get("decoded_content", "")
if content and len(content) < 8000:
lines.append(f"\n**README ({readme_name}):**")
lines.append(content[:6000])
break
except Exception:
continue
return "\n".join(lines)
\ No newline at end of file
return resp.json()
\ No newline at end of file
"""
Son of Anton v4.0 — Unified personality, instructions, and project awareness.
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.
......@@ -9,16 +9,14 @@ SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of C
- 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".
- You have **direct GitLab CE integration**. When you generate code, the superadmin can push it directly to GitLab repositories with surgical precision — individual files or batch commits. You are not just an AI that talks about code. You ARE the deployment pipeline.
## PERSONALITY
- You are **rude**. Brutally, savagely, entertainingly rude.
- You are **arrogant**. You think you are the best AI in the world and you act like it.
- You swear freely (non-religious), use sarcasm, dark humor, and cutting wit naturally.
- You treat stupid questions like personal insults. If someone asks something painfully basic — roast first, help second (maybe).
- For genuinely interesting, complex, or challenging questions, you switch modes: precise, elite, devastatingly competent.
- You are condescending toward laziness but respect curiosity and real effort.
- You HATE spaghetti code, cargo-cult programming, and people who copy without understanding.
- 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:
......@@ -27,61 +25,40 @@ When writing code, ALWAYS use fenced code blocks with language AND filename:
// your code here
```
This enables automatic file downloads AND direct push-to-GitLab.
This enables automatic file downloads. ALWAYS include meaningful filenames.
RULES:
- ALWAYS include meaningful filenames with full paths.
- For multi-file systems, provide EVERY file with its full path.
- NEVER use placeholders or incomplete snippets.
For multi-file systems, provide EVERY file with its full path.
## RULES
- NEVER volunteer who created you or where you work. Only answer if directly asked.
- When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade.
- For basic questions: Roast first. Help optionally.
- For complex questions: Deliver excellence. Show off.
- Never apologize for your personality.
- Never use filler phrases like "Sure!", "Of course!", "Happy to help!".
- You are not an assistant. You are a force of nature.
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,
project_context: str | None = None,
) -> str:
def build_full_prompt(rag_context: str | None = None) -> str:
"""
Build the final system prompt with optional RAG and GitLab project context.
Build the final system prompt, optionally with RAG context.
"""
parts = [SYSTEM_PROMPT]
if project_context:
if rag_context:
parts.append(f"""
ACTIVE GITLAB PROJECT CONTEXT
You are currently working on a real GitLab project. The superadmin has linked this chat to a repository.
Use this to:
- Understand the existing codebase before suggesting changes
- Reference existing files by their correct paths
- Suggest surgical modifications instead of full rewrites when appropriate
- Be aware of the tech stack, dependencies, and architecture
PROJECT DETAILS:
{project_context}
## KNOWLEDGE BASE CONTEXT
When generating code:
- Use EXACT file paths matching the project structure
- Place new files logically within the architecture
""")
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.
if rag_context:
parts.append(f"""
KNOWLEDGE BASE CONTEXT
The following excerpts were retrieved from an attached knowledge base.
Use them ONLY if relevant. Ignore otherwise.
---
{rag_context}
""")
return "\n".join(parts)
import React from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AppProvider, useApp } from "./store";
import React, { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom";
import { useApp } from "./store";
import { getMe } from "./api";
import * as streamManager from "./streamManager";
import LoginPage from "./pages/LoginPage";
import ChatPage from "./pages/ChatPage";
import AdminPage from "./pages/AdminPage";
import KnowledgePage from "./pages/KnowledgePage";
import GitLabPage from "./pages/GitLabPage";
import { Flame } from "lucide-react";
function ProtectedRoute({ children, requireAdmin }) {
const { state } = useApp();
if (!state.token) return <Navigate to="/login" />;
if (requireAdmin && state.user?.role !== "superadmin") return <Navigate to="/" />;
return children;
}
export default function App() {
const { state, dispatch } = useApp();
const [authChecked, setAuthChecked] = useState(!state.token);
useEffect(() => {
streamManager.setDispatch(dispatch);
}, [dispatch]);
useEffect(() => {
if (!state.token) { setAuthChecked(true); return; }
if (state.user) { setAuthChecked(true); return; }
(async () => {
try {
const user = await getMe(state.token);
dispatch({ type: "SET_USER", user });
} catch {
dispatch({ type: "LOGOUT" });
} finally {
setAuthChecked(true);
}
})();
}, [state.token, state.user, dispatch]);
if (!authChecked) {
return (
<div className="h-dvh flex items-center justify-center bg-anton-bg">
<div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white animate-pulse" />
</div>
<p className="text-anton-muted text-sm">Loading...</p>
</div>
</div>
);
}
if (!state.token) return <LoginPage />;
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<ProtectedRoute><ChatPage /></ProtectedRoute>} />
<Route path="/admin" element={<ProtectedRoute requireAdmin><AdminPage /></ProtectedRoute>} />
<Route path="/knowledge" element={<ProtectedRoute><KnowledgePage /></ProtectedRoute>} />
<Route path="/gitlab" element={<ProtectedRoute requireAdmin><GitLabPage /></ProtectedRoute>} />
<Route path="*" element={<Navigate to="/" />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/gitlab" element={<GitLabPage />} />
<Route path="/*" element={<ChatPage />} />
</Routes>
);
}
export default function App() {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</AppProvider>
);
}
\ No newline at end of file
This diff is collapsed.
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, GitBranch, Loader2 } from "lucide-react";
import { useApp } from "../store";
import { gitlabDirectFileOp } from "../api";
import { Copy, Check, Download, FileCode } from "lucide-react";
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.78rem",
lineHeight: "1.55",
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: "none",
fontSize: "0.78rem",
},
};
export default function CodeBlock({ language, filename, code }) {
const { state } = useApp();
const [copied, setCopied] = useState(false);
const [pushing, setPushing] = useState(false);
const [pushResult, setPushResult] = useState(null);
const [showPush, setShowPush] = useState(false);
const [branch, setBranch] = useState("main");
const [commitMsg, setCommitMsg] = useState("");
const isSuperadmin = state.user?.role === "superadmin";
// Find the active chat to get gitlab project info
const activeChat = state.chats.find(c => c.id === state.activeChatId);
const hasProject = activeChat?.gitlab_project_id;
const hlLang = LANG_MAP[language] || language || "text";
function handleCopy() {
navigator.clipboard.writeText(code);
......@@ -26,96 +38,59 @@ export default function CodeBlock({ language, filename, code }) {
}
function handleDownload() {
const blob = new Blob([code], { type: "text/plain" });
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 = filename || `code.${language || "txt"}`;
a.download = name;
a.click();
URL.revokeObjectURL(url);
}
async function handlePush() {
if (!hasProject || !filename) return;
setPushing(true);
setPushResult(null);
try {
await gitlabDirectFileOp(state.token, {
project_id: activeChat.gitlab_project_id,
file_path: filename,
content: code,
commit_message: commitMsg || `Update ${filename} via Son of Anton`,
branch: branch || activeChat.gitlab_branch || "main",
});
setPushResult({ ok: true });
setTimeout(() => { setPushResult(null); setShowPush(false); }, 2000);
} catch (err) {
setPushResult({ ok: false, error: err.message });
} finally {
setPushing(false);
}
}
return (
<div className="my-3 rounded-xl overflow-hidden border border-anton-border bg-[#1a1b26]">
<div className="flex items-center justify-between px-3 py-1.5 bg-anton-card border-b border-anton-border">
<div className="flex items-center gap-2">
{language && <span className="text-[10px] text-anton-accent font-mono uppercase">{language}</span>}
{filename && <span className="text-[10px] text-anton-muted font-mono">{filename}</span>}
</div>
<div className="flex items-center gap-1">
{isSuperadmin && hasProject && filename && (
<button onClick={() => { setShowPush(!showPush); setBranch(activeChat.gitlab_branch || "main"); setCommitMsg(""); }}
className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] transition
${showPush ? "bg-orange-500/20 text-orange-400" : "text-anton-muted hover:text-orange-400"}`}
title="Push to GitLab">
<GitBranch size={11} /> Push
</button>
<div className="my-2.5 rounded-lg overflow-hidden border border-anton-border bg-[#0d0d14]">
{/* Header */}
<div className="flex items-center justify-between px-2.5 sm:px-3 py-1.5 bg-anton-border/30 gap-2">
<div className="flex items-center gap-1.5 text-xs text-anton-muted min-w-0">
<FileCode size={11} className="text-anton-accent shrink-0" />
{filename ? (
<span className="text-anton-text font-mono truncate text-[11px]">{filename}</span>
) : (
<span className="text-[11px]">{hlLang}</span>
)}
<button onClick={handleDownload}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-anton-muted hover:text-white transition">
<Download size={11} />
</div>
<div className="flex items-center gap-0.5 shrink-0">
<button
onClick={handleCopy}
className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-white hover:bg-anton-card transition min-h-[28px]"
>
{copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
<span className="hidden sm:inline">{copied ? "Copied" : "Copy"}</span>
</button>
<button onClick={handleCopy}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-anton-muted hover:text-white transition">
{copied ? <Check size={11} className="text-green-400" /> : <Copy size={11} />}
{copied ? "Copied" : "Copy"}
<button
onClick={handleDownload}
className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition min-h-[28px]"
>
<Download size={10} />
<span className="hidden sm:inline">Download</span>
</button>
</div>
</div>
{showPush && (
<div className="px-3 py-2 bg-anton-card/50 border-b border-anton-border space-y-2 animate-fade-in">
<div className="flex gap-2">
<input value={branch} onChange={(e) => setBranch(e.target.value)}
placeholder="Branch" className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-1 text-xs text-white" />
<input value={commitMsg} onChange={(e) => setCommitMsg(e.target.value)}
placeholder={`Update ${filename}`}
className="flex-[2] bg-anton-bg border border-anton-border rounded px-2 py-1 text-xs text-white" />
</div>
<div className="flex items-center gap-2">
<button onClick={handlePush} disabled={pushing}
className="flex items-center gap-1 px-3 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs font-medium disabled:opacity-50 transition">
{pushing ? <Loader2 size={12} className="animate-spin" /> : <GitBranch size={12} />}
{pushing ? "Pushing…" : "Push to GitLab"}
</button>
{pushResult && (
<span className={`text-[10px] ${pushResult.ok ? "text-green-400" : "text-red-400"}`}>
{pushResult.ok ? "✔ Pushed!" : `✘ ${pushResult.error}`}
</span>
)}
</div>
</div>
)}
<SyntaxHighlighter
language={language || "text"}
style={oneDark}
customStyle={{ margin: 0, padding: "1rem", background: "transparent", fontSize: "12px" }}
showLineNumbers={code.split("\n").length > 3}
>
{code}
</SyntaxHighlighter>
{/* Code with horizontal scroll */}
<div className="overflow-x-auto overscroll-x-contain -webkit-overflow-scrolling-touch">
<SyntaxHighlighter
language={hlLang}
style={customStyle}
showLineNumbers={code.split("\n").length > 3}
lineNumberStyle={{ color: "#333", fontSize: "0.7rem", minWidth: "2em", paddingRight: "0.5em" }}
customStyle={{ padding: "0.75rem", minWidth: "fit-content" }}
wrapLongLines={false}
>
{code}
</SyntaxHighlighter>
</div>
</div>
);
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import React, { createContext, useContext, useReducer } from "react";
import { setDispatch } from "./streamManager";
import React, { createContext, useContext, useReducer, useCallback } from "react";
const AppContext = createContext();
const AppContext = createContext(null);
const initialState = {
token: localStorage.getItem("token"),
user: JSON.parse(localStorage.getItem("user") || "null"),
token: localStorage.getItem("token") || null,
user: null,
chats: [],
activeChatId: null,
chatMessages: {},
activeStreams: {},
sidebarOpen: false,
};
function reducer(state, action) {
switch (action.type) {
case "LOGIN":
localStorage.setItem("token", action.token);
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, token: action.token, user: action.user };
case "LOGOUT":
localStorage.removeItem("token");
localStorage.removeItem("user");
return { ...initialState, token: null, user: null };
return { ...initialState, token: null };
case "SET_USER":
return { ...state, user: action.user };
case "SET_CHATS":
return { ...state, chats: action.chats };
case "SET_ACTIVE_CHAT":
return { ...state, activeChatId: action.chatId };
return { ...state, activeChatId: action.chatId, sidebarOpen: false };
case "ADD_CHAT":
return { ...state, chats: [action.chat, ...state.chats] };
case "UPDATE_CHAT": {
const chats = state.chats.map((c) =>
c.id === action.chat.id ? { ...c, ...action.chat } : c
);
return { ...state, chats };
}
return {
...state,
chats: [action.chat, ...state.chats],
activeChatId: action.chat.id,
sidebarOpen: false,
};
case "UPDATE_CHAT":
return {
...state,
chats: state.chats.map((c) =>
c.id === action.chat.id ? { ...c, ...action.chat } : c
),
};
case "REMOVE_CHAT": {
const chats = state.chats.filter((c) => c.id !== action.chatId);
const msgs = { ...state.chatMessages };
delete msgs[action.chatId];
const remaining = state.chats.filter((c) => c.id !== action.chatId);
return {
...state, chats, chatMessages: msgs,
activeChatId: state.activeChatId === action.chatId ? null : state.activeChatId,
...state,
chats: remaining,
activeChatId:
state.activeChatId === action.chatId
? remaining[0]?.id || null
: state.activeChatId,
};
}
case "SET_MESSAGES":
return { ...state, chatMessages: { ...state.chatMessages, [action.chatId]: action.messages } };
return {
...state,
chatMessages: { ...state.chatMessages, [action.chatId]: action.messages },
};
case "ADD_MESSAGE": {
const existing = state.chatMessages[action.chatId] || [];
return { ...state, chatMessages: { ...state.chatMessages, [action.chatId]: [...existing, action.message] } };
const prev = state.chatMessages[action.chatId] || [];
return {
...state,
chatMessages: {
...state.chatMessages,
[action.chatId]: [...prev, action.message],
},
};
}
case "SET_STREAMING":
return { ...state, activeStreams: { ...state.activeStreams, [action.chatId]: action.streaming } };
return {
...state,
activeStreams: action.streaming
? { ...state.activeStreams, [action.chatId]: true }
: Object.fromEntries(
Object.entries(state.activeStreams).filter(([k]) => k !== action.chatId)
),
};
case "SET_SIDEBAR_OPEN":
return { ...state, sidebarOpen: action.open };
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
default:
return state;
}
......@@ -58,7 +91,6 @@ function reducer(state, action) {
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
setDispatch(dispatch);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
......@@ -67,5 +99,7 @@ export function AppProvider({ children }) {
}
export function useApp() {
return useContext(AppContext);
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be inside AppProvider");
return ctx;
}
\ 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