Commit 17ddd732 authored by AGLANPC\aglan's avatar AGLANPC\aglan

ghvntumyutj mtyj mtyjthy nv

parent f8727239
This diff is collapsed.
......@@ -109,18 +109,12 @@ def update_settings(body: SettingsBody, admin: User = Depends(require_superadmin
@router.post("/test-connection")
async def test_connection(body: SettingsBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
url = body.gitlab_url.rstrip("/")
if not body.gitlab_token or body.gitlab_token == "UNCHANGED":
s = db.query(GitLabSettings).first()
token = s.gitlab_token if s else ""
else:
token = body.gitlab_token
if not url or not token:
raise HTTPException(400, "GitLab URL and token not provided")
async def test_connection(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
s = db.query(GitLabSettings).first()
if not s or not s.gitlab_url or not s.gitlab_token:
raise HTTPException(400, "GitLab URL and token not configured")
try:
result = await gitlab_service.test_connection(url, token)
result = await gitlab_service.test_connection(s.gitlab_url, s.gitlab_token)
return result
except gitlab_service.GitLabError as e:
raise HTTPException(e.status_code, f"Connection failed: {e.detail}")
......
This diff is collapsed.
This diff is collapsed.
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Copy, Check, Download, GitBranch, Loader2, CheckCircle2, XCircle } from "lucide-react";
import { gitlabFileExists, gitlabCommitSingle } from "../api";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Copy, Check, Download, GitCommitVertical } from "lucide-react";
const COMMIT_STATES = { idle: "idle", checking: "checking", committing: "committing", success: "success", error: "error" };
export default function CodeBlock({ language, filename, code, linkedRepo, token }) {
export default function CodeBlock({ language, filename, code, linkedRepo, onCommit }) {
const [copied, setCopied] = useState(false);
const [commitState, setCommitState] = useState(COMMIT_STATES.idle);
const [commitMsg, setCommitMsg] = useState("");
const [commitError, setCommitError] = useState("");
const [showCommitForm, setShowCommitForm] = useState(false);
function handleCopy() {
navigator.clipboard.writeText(code);
......@@ -29,119 +22,43 @@ export default function CodeBlock({ language, filename, code, linkedRepo, token
URL.revokeObjectURL(url);
}
function toggleCommitForm() {
if (!showCommitForm) {
setCommitMsg(`Update ${filename || "file"} via Son of Anton`);
setCommitError("");
setCommitState(COMMIT_STATES.idle);
}
setShowCommitForm(!showCommitForm);
}
async function handleCommit() {
if (!linkedRepo || !token || !filename) return;
setCommitState(COMMIT_STATES.checking);
setCommitError("");
try {
const exists = await gitlabFileExists(token, linkedRepo.id, filename, linkedRepo.default_branch);
const action = exists ? "update" : "create";
setCommitState(COMMIT_STATES.committing);
await gitlabCommitSingle(token, linkedRepo.id, {
branch: linkedRepo.default_branch,
file_path: filename,
content: code,
commit_message: commitMsg || `${action === "create" ? "Create" : "Update"} ${filename} via Son of Anton`,
action,
});
setCommitState(COMMIT_STATES.success);
setTimeout(() => { setCommitState(COMMIT_STATES.idle); setShowCommitForm(false); }, 2500);
} catch (err) {
setCommitState(COMMIT_STATES.error);
setCommitError(err.message || "Commit failed");
}
function handleCommit() {
if (!onCommit || !filename) return;
onCommit(filename, code, "update");
}
const canCommit = linkedRepo && filename && token;
return (
<div className="rounded-lg overflow-hidden border border-anton-border my-3">
<div className="my-3 rounded-xl overflow-hidden border border-anton-border bg-[#1a1b26]">
{/* Header */}
<div className="flex items-center justify-between bg-[#1e1e2e] px-3 py-2 gap-2">
<span className="text-xs text-anton-muted font-mono truncate min-w-0">
{filename || language || "code"}
</span>
<div className="flex items-center gap-1 shrink-0">
{canCommit && (
<button onClick={toggleCommitForm} title="Commit to repo"
className={`flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition ${showCommitForm
? "bg-green-500/20 text-green-400"
: "text-anton-muted hover:text-green-400 hover:bg-green-500/10"
}`}>
<GitBranch size={12} />
<span className="hidden sm:inline">Push</span>
</button>
)}
{filename && (
<button onClick={handleDownload} title="Download file"
className="p-1.5 rounded text-anton-muted hover:text-white hover:bg-white/5 transition">
<Download size={13} />
<div className="flex items-center justify-between px-3 py-1.5 bg-anton-surface border-b border-anton-border">
<div className="flex items-center gap-2 min-w-0">
{language && <span className="text-[10px] text-anton-accent font-mono uppercase">{language}</span>}
{filename && <span className="text-[10px] text-anton-muted truncate">{filename}</span>}
</div>
<div className="flex items-center gap-0.5 shrink-0">
{linkedRepo && filename && (
<button onClick={handleCommit} title={`Commit to ${linkedRepo.name}`}
className="flex items-center gap-1 px-2 py-1 text-[10px] text-orange-400 hover:bg-orange-400/10 rounded transition">
<GitCommitVertical size={11} /> Commit
</button>
)}
<button onClick={handleCopy} title="Copy code"
className="p-1.5 rounded text-anton-muted hover:text-white hover:bg-white/5 transition">
{copied ? <Check size={13} className="text-green-400" /> : <Copy size={13} />}
<button onClick={handleDownload} className="p-1.5 text-anton-muted hover:text-white transition" title="Download">
<Download size={12} />
</button>
<button onClick={handleCopy} className="p-1.5 text-anton-muted hover:text-white transition" title="Copy">
{copied ? <Check size={12} className="text-green-400" /> : <Copy size={12} />}
</button>
</div>
</div>
{/* Commit Form */}
{showCommitForm && canCommit && (
<div className="bg-[#16162a] border-b border-anton-border px-3 py-2.5 space-y-2 animate-fade-in">
<div className="flex items-center gap-2 text-[11px]">
<span className="text-anton-muted">Repo:</span>
<span className="text-green-400 font-mono">{linkedRepo.name}</span>
<span className="text-anton-muted"></span>
<span className="text-blue-400 font-mono">{linkedRepo.default_branch}</span>
</div>
<input
type="text" value={commitMsg}
onChange={(e) => setCommitMsg(e.target.value)}
placeholder="Commit message..."
className="w-full bg-anton-bg border border-anton-border rounded px-2.5 py-1.5 text-xs text-white placeholder-anton-muted focus:outline-none focus:border-green-500/50"
/>
<div className="flex items-center gap-2">
<button onClick={handleCommit}
disabled={commitState === COMMIT_STATES.checking || commitState === COMMIT_STATES.committing}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-green-600 hover:bg-green-500 text-white text-xs font-medium transition disabled:opacity-50 disabled:cursor-not-allowed">
{commitState === COMMIT_STATES.checking && <><Loader2 size={12} className="animate-spin" /> Checking...</>}
{commitState === COMMIT_STATES.committing && <><Loader2 size={12} className="animate-spin" /> Committing...</>}
{commitState === COMMIT_STATES.success && <><CheckCircle2 size={12} /> Committed!</>}
{commitState === COMMIT_STATES.error && <><XCircle size={12} /> Retry</>}
{commitState === COMMIT_STATES.idle && <><GitBranch size={12} /> Commit</>}
</button>
<button onClick={toggleCommitForm} className="px-3 py-1.5 rounded text-xs text-anton-muted hover:text-white transition">
Cancel
</button>
{commitState === COMMIT_STATES.success && (
<span className="text-[11px] text-green-400">✓ Pushed to {linkedRepo.default_branch}</span>
)}
</div>
{commitError && (
<div className="text-[11px] text-red-400 bg-red-500/10 rounded px-2 py-1">{commitError}</div>
)}
</div>
)}
{/* Code */}
<SyntaxHighlighter
language={language || "text"}
style={vscDarkPlus}
customStyle={{ margin: 0, padding: "1rem", fontSize: "0.8rem", background: "#1a1a2e", maxHeight: "500px" }}
showLineNumbers
lineNumberStyle={{ minWidth: "2.5em", paddingRight: "1em", color: "#555" }}
style={oneDark}
customStyle={{ margin: 0, padding: "12px 16px", fontSize: "12px", lineHeight: "1.5", background: "transparent" }}
showLineNumbers={code.split("\n").length > 3}
lineNumberStyle={{ color: "#555", fontSize: "10px", paddingRight: "12px" }}
wrapLongLines
>
{code}
</SyntaxHighlighter>
......
......@@ -8,22 +8,16 @@ import {
Image, Film, FileText, ExternalLink,
} from "lucide-react";
const FILE_TYPE_ICONS = {
image: Image, video: Film, document: FileText, text: FileText,
};
const FILE_TYPE_ICONS = { image: Image, video: Film, document: FileText, text: FileText };
const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, isThinking, token, linkedRepo }) {
const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, isThinking, token, linkedRepo, onCommit }) {
const { role, content, thinking_content, input_tokens, output_tokens, attachments } = message;
const isUser = role === "user";
const [showThinking, setShowThinking] = useState(false);
const [copied, setCopied] = useState(false);
const [expandedImage, setExpandedImage] = useState(null);
function handleCopy() {
navigator.clipboard.writeText(content || "");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
function handleCopy() { navigator.clipboard.writeText(content || ""); setCopied(true); setTimeout(() => setCopied(false), 2000); }
const hasAttachments = attachments && attachments.length > 0;
......@@ -40,16 +34,14 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
<div className={`max-w-[80%] ${isUser ? "order-first" : ""}`}>
{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">
<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" />}
{thinking_content}{isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
</div>
)}
</div>
......@@ -57,7 +49,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
{hasAttachments && (
<div className="mb-2 flex flex-wrap gap-2">
{attachments.map((att) => {
{attachments.map(att => {
const Icon = FILE_TYPE_ICONS[att.file_type] || FileText;
const url = getAttachmentUrl(att.id);
if (att.file_type === "image") {
......@@ -65,18 +57,13 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
<div key={att.id} className="relative group">
<img src={`${url}?token=${token}`} alt={att.original_filename}
className="max-w-[240px] max-h-[200px] rounded-lg border border-anton-border object-cover cursor-pointer hover:opacity-90 transition"
onClick={() => setExpandedImage(expandedImage === att.id ? null : att.id)}
onError={(e) => { e.target.style.display = "none"; }} />
onClick={() => setExpandedImage(expandedImage === att.id ? null : att.id)} onError={e => { e.target.style.display = "none"; }} />
{expandedImage === att.id && (
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-8 cursor-pointer"
onClick={() => setExpandedImage(null)}>
<img src={`${url}?token=${token}`} alt={att.original_filename}
className="max-w-full max-h-full object-contain rounded-lg" />
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-8 cursor-pointer" onClick={() => setExpandedImage(null)}>
<img src={`${url}?token=${token}`} alt={att.original_filename} className="max-w-full max-h-full object-contain rounded-lg" />
</div>
)}
<div className="absolute bottom-1 left-1 bg-black/60 text-[9px] text-white px-1.5 py-0.5 rounded">
{att.original_filename}
</div>
<div className="absolute bottom-1 left-1 bg-black/60 text-[9px] text-white px-1.5 py-0.5 rounded">{att.original_filename}</div>
</div>
);
}
......@@ -95,8 +82,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
</div>
)}
<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"
}`}>
<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">{_stripPrefixes(content)}</div>
) : (
......@@ -107,28 +93,14 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
const rawLang = match?.[1] || "";
if (inline) return <code className={className} {...props}>{children}</code>;
let lang = rawLang, filename = null;
if (rawLang.includes(":")) {
const idx = rawLang.indexOf(":");
lang = rawLang.slice(0, idx);
filename = rawLang.slice(idx + 1);
}
return (
<CodeBlock
language={lang}
filename={filename}
code={String(children).replace(/\n$/, "")}
linkedRepo={linkedRepo}
token={token}
/>
);
if (rawLang.includes(":")) { const idx = rawLang.indexOf(":"); lang = rawLang.slice(0, idx); filename = rawLang.slice(idx + 1); }
return <CodeBlock language={lang} filename={filename} code={String(children).replace(/\n$/, "")} linkedRepo={linkedRepo} onCommit={onCommit} />;
},
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" />
)}
{isStreaming && !isThinking && <span className="inline-block w-1.5 h-4 bg-anton-accent ml-0.5 animate-pulse" />}
</div>
)}
</div>
......@@ -136,13 +108,10 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
{!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"}
{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>
<span className="text-[11px] text-anton-muted">{input_tokens?.toLocaleString()}↓ / {output_tokens?.toLocaleString()}</span>
)}
</div>
)}
......
This diff is collapsed.
......@@ -70,7 +70,7 @@ export default function GitLabPage() {
async function handleTest() {
setTesting(true); setTestResult(null);
try {
const r = await gitlabTestConnection(t, { gitlab_url: url, gitlab_token: token || "UNCHANGED" });
const r = await gitlabTestConnection(t);
setTestResult({ ok: true, msg: `Connected as ${r.name} (@${r.username})` });
} catch (e) { setTestResult({ ok: false, msg: e.message }); }
setTesting(false);
......
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