Commit 459309cd authored by AGLANPC\aglan's avatar AGLANPC\aglan

fgg dfjdfghj dfkj df

parent e636c018
This diff is collapsed.
......@@ -3,22 +3,11 @@ 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",
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 = {
......@@ -28,19 +17,18 @@ const customStyle = {
background: "#0d0d14",
margin: 0,
borderRadius: 0,
fontSize: "0.82rem",
lineHeight: "1.6",
fontSize: "0.78rem",
lineHeight: "1.55",
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: "none",
fontSize: "0.82rem",
fontSize: "0.78rem",
},
};
export default function CodeBlock({ language, filename, code }) {
const [copied, setCopied] = useState(false);
const hlLang = LANG_MAP[language] || language || "text";
function handleCopy() {
......@@ -61,46 +49,44 @@ export default function CodeBlock({ language, filename, code }) {
}
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" />
<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">{filename}</span>
<span className="text-anton-text font-mono truncate text-[11px]">{filename}</span>
) : (
<span>{hlLang}</span>
<span className="text-[11px]">{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"
<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={11} className="text-anton-success" /> : <Copy size={11} />}
{copied ? "Copied" : "Copy"}
{copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
<span className="hidden sm:inline">{copied ? "Copied" : "Copy"}</span>
</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"
<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={11} />
Download
<Download size={10} />
<span className="hidden sm:inline">Download</span>
</button>
</div>
</div>
{/* Code */}
<div className="overflow-x-auto">
{/* Code with horizontal scroll */}
<div className="overflow-x-auto overscroll-x-contain -webkit-overflow-scrolling-touch">
<SyntaxHighlighter
language={hlLang}
style={customStyle}
showLineNumbers
lineNumberStyle={{
minWidth: "2.5em",
paddingRight: "1em",
color: "#3a3a4a",
userSelect: "none",
}}
wrapLines
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>
......
......@@ -5,10 +5,12 @@ import CodeBlock from "./CodeBlock";
import { getAttachmentUrl } from "../api";
import {
User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check,
Image, Film, FileText, ExternalLink, FileCode, File,
Image, Film, FileText, ExternalLink,
} from "lucide-react";
const FILE_TYPE_ICONS = { image: Image, video: Film, document: FileText, text: FileCode };
const FILE_TYPE_ICONS = {
image: Image, video: Film, document: FileText, text: FileText,
};
const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, isThinking, token }) {
const { role, content, thinking_content, input_tokens, output_tokens, attachments } = message;
......@@ -35,20 +37,20 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
</div>
)}
<div className={`max-w-[92%] sm:max-w-[80%] min-w-0 ${isUser ? "order-first" : ""}`}>
<div className={`min-w-0 ${isUser ? "max-w-[85%] sm:max-w-[75%]" : "max-w-[90%] sm:max-w-[80%]"}`}>
{/* 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"
className="flex items-center gap-1.5 text-xs text-purple-400 hover:text-purple-300 transition mb-1 min-h-[32px]"
>
<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-2.5 text-xs text-purple-300/80 font-mono whitespace-pre-wrap max-h-48 sm:max-h-60 overflow-y-auto">
<div className="bg-purple-500/5 border border-purple-500/20 rounded-lg p-2.5 sm:p-3 text-xs text-purple-300/80 font-mono whitespace-pre-wrap max-h-48 sm:max-h-60 overflow-y-auto overscroll-contain break-words">
{thinking_content}
{isThinking && <span className="inline-block w-1.5 h-4 bg-purple-400 ml-0.5 animate-pulse" />}
</div>
......@@ -60,22 +62,22 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
{hasAttachments && (
<div className="mb-2 flex flex-wrap gap-1.5">
{attachments.map((att) => {
const Icon = FILE_TYPE_ICONS[att.file_type] || File;
const Icon = FILE_TYPE_ICONS[att.file_type] || FileText;
const url = getAttachmentUrl(att.id);
if (att.file_type === "image") {
return (
<div key={att.id} className="relative group">
<div key={att.id} className="relative">
<img
src={`${url}?token=${token}`}
alt={att.original_filename}
className="max-w-[180px] sm:max-w-[260px] max-h-[140px] sm:max-h-[200px] rounded-lg border border-anton-border object-cover cursor-pointer hover:opacity-90 transition shadow-md"
className="max-w-[200px] sm:max-w-[240px] max-h-[160px] sm: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"; }}
/>
{expandedImage === att.id && (
<div
className="fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4 cursor-pointer"
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 sm:p-8 cursor-pointer"
onClick={() => setExpandedImage(null)}
>
<img
......@@ -85,7 +87,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
/>
</div>
)}
<div className="absolute bottom-1 left-1 bg-black/60 text-[8px] text-white px-1.5 py-0.5 rounded max-w-[90%] truncate">
<div className="absolute bottom-1 left-1 bg-black/60 text-[8px] text-white px-1 py-0.5 rounded">
{att.original_filename}
</div>
</div>
......@@ -98,14 +100,14 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
href={`${url}?token=${token}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 bg-anton-card border border-anton-border rounded-lg px-2.5 py-2 hover:border-anton-accent transition group max-w-[200px]"
className="flex items-center gap-2 bg-anton-card border border-anton-border rounded-lg px-2.5 py-2 hover:border-anton-accent transition group min-h-[44px]"
>
<Icon size={15} className="shrink-0 text-blue-400" />
<div className="min-w-0 flex-1">
<div className="text-[11px] text-white truncate">{att.original_filename}</div>
<Icon size={14} className="shrink-0 text-blue-400" />
<div className="min-w-0">
<div className="text-xs text-white truncate max-w-[120px] sm:max-w-[160px]">{att.original_filename}</div>
<div className="text-[9px] text-anton-muted">{(att.file_size / 1024).toFixed(0)}KB</div>
</div>
<ExternalLink size={11} className="text-anton-muted group-hover:text-anton-accent shrink-0" />
<ExternalLink size={10} className="text-anton-muted group-hover:text-anton-accent shrink-0" />
</a>
);
})}
......@@ -113,14 +115,15 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
)}
{/* Message bubble */}
<div className={`rounded-2xl px-3.5 py-2.5 sm:px-4 sm: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-3.5 py-2.5 sm:px-4 sm: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 break-words">{_stripPrefixes(content)}</div>
<div className="text-sm whitespace-pre-wrap break-words leading-relaxed">{_stripPrefixes(content)}</div>
) : (
<div className="prose-anton text-sm break-words">
<div className="prose-anton text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
......@@ -148,16 +151,19 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
)}
</div>
{/* Meta info */}
{/* Actions */}
{!isUser && !isStreaming && content && (
<div className="flex items-center gap-3 mt-1.5 px-1 flex-wrap">
<button onClick={handleCopy} className="flex items-center gap-1 text-[10px] sm:text-[11px] text-anton-muted hover:text-white transition">
{copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />}
<div className="flex items-center gap-3 mt-1 px-1">
<button
onClick={handleCopy}
className="flex items-center gap-1 text-[10px] text-anton-muted hover:text-white transition min-h-[28px]"
>
{copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
{copied ? "Copied" : "Copy"}
</button>
{(input_tokens > 0 || output_tokens > 0) && (
<span className="text-[10px] sm:text-[11px] text-anton-muted">
{input_tokens?.toLocaleString()}/ {output_tokens?.toLocaleString()}
<span className="text-[10px] text-anton-muted">
{input_tokens?.toLocaleString()}{output_tokens?.toLocaleString()}
</span>
)}
</div>
......
This diff is collapsed.
This diff is collapsed.
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useApp } from "../store";
import { listChats, createChat } from "../api";
import Sidebar from "../components/Sidebar";
import ChatView from "../components/ChatView";
import { Flame, BookOpen, Shield } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Flame, Menu, Plus, MessageSquare } from "lucide-react";
export default function ChatPage() {
const { state, dispatch } = useApp();
const navigate = useNavigate();
const [activeChatId, setActiveChatId] = useState(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
useEffect(() => {
(async () => {
try {
const chats = await listChats(state.token);
dispatch({ type: "SET_CHATS", chats });
if (chats.length > 0 && !activeChatId) {
setActiveChatId(chats[0].id);
if (!state.activeChatId && chats.length > 0) {
dispatch({ type: "SET_ACTIVE_CHAT", chatId: chats[0].id });
}
} catch { }
} catch { /* ignore */ }
})();
}, [state.token, dispatch]);
}, [state.token]);
async function handleNewChat() {
try {
const chat = await createChat(state.token);
dispatch({ type: "ADD_CHAT", chat });
setActiveChatId(chat.id);
setSidebarOpen(false);
} catch { }
}
function handleSelectChat(chatId) {
setActiveChatId(chatId);
setSidebarOpen(false);
} catch { /* ignore */ }
}
return (
<div className="h-dvh flex bg-anton-bg text-anton-text overflow-hidden">
{/* Sidebar */}
<Sidebar
activeChatId={activeChatId}
onSelectChat={handleSelectChat}
onNewChat={handleNewChat}
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>
<div className="h-full h-dvh flex overflow-hidden bg-anton-bg">
{/* Desktop sidebar */}
<div className="hidden sm:flex">
<Sidebar />
</div>
{/* Mobile sidebar overlay */}
{state.sidebarOpen && (
<>
<div
className="sm:hidden fixed inset-0 z-40 bg-black/60 animate-overlay-in"
onClick={() => dispatch({ type: "SET_SIDEBAR_OPEN", open: false })}
/>
<div className="sm:hidden fixed inset-y-0 left-0 z-50 w-[280px] animate-slide-in safe-top safe-bottom">
<Sidebar mobile onClose={() => dispatch({ type: "SET_SIDEBAR_OPEN", open: false })} />
</div>
</>
)}
{/* Main */}
<div className="flex-1 flex flex-col min-h-0 min-w-0">
{/* Top bar */}
<div className="border-b border-anton-border bg-anton-surface px-3 py-2 flex items-center gap-2">
<button onClick={() => setSidebarOpen(true)} className="sm:hidden p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0">
{/* Mobile header */}
<div className="sm:hidden flex items-center gap-2 px-3 py-2.5 border-b border-anton-border bg-anton-surface safe-top">
<button
onClick={() => dispatch({ type: "TOGGLE_SIDEBAR" })}
className="p-2 -ml-1 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
>
<Menu size={20} />
</button>
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center">
<Flame size={14} className="text-white" />
<div className="flex-1 min-w-0 flex items-center gap-2">
<div className="w-6 h-6 rounded-md bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shrink-0">
<Flame size={12} className="text-white" />
</div>
<span className="text-sm font-medium text-white truncate">
{state.chats.find((c) => c.id === state.activeChatId)?.title || "Son of Anton"}
</span>
</div>
<span className="text-sm font-semibold text-white truncate flex-1">
{state.chats.find((c) => c.id === activeChatId)?.title || "Son of Anton"}
</span>
<button
onClick={() => navigate("/knowledge")}
className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs text-anton-muted hover:text-green-400 hover:bg-green-500/10 transition"
title="Knowledge Bases"
onClick={handleNewChat}
className="p-2 -mr-1 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
>
<BookOpen size={14} />
<span className="hidden sm:inline">Knowledge</span>
<Plus size={20} />
</button>
{state.user?.role === "superadmin" && (
<button
onClick={() => navigate("/admin")}
className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition"
title="Admin Panel"
>
<Shield size={14} />
<span className="hidden sm:inline">Admin</span>
</button>
)}
</div>
{/* Chat area */}
{activeChatId ? (
<ChatView chatId={activeChatId} />
{/* Chat or empty state */}
{state.activeChatId ? (
<ChatView chatId={state.activeChatId} />
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-anton-accent/20">
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center max-w-sm">
<div className="w-16 h-16 mx-auto mb-4 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" />
</div>
<h2 className="text-xl font-bold text-white mb-2">Son of Anton</h2>
<p className="text-anton-muted text-sm mb-6">Avatar of All Elements of Code</p>
<button onClick={handleNewChat} className="px-6 py-2.5 rounded-xl bg-anton-accent text-white font-medium hover:opacity-80 transition">
Start a Chat
<p className="text-anton-muted text-sm mb-6">
Avatar of All Elements of Code
</p>
<button
onClick={handleNewChat}
className="inline-flex items-center gap-2 px-5 py-3 bg-anton-accent text-white rounded-xl font-medium hover:opacity-90 transition active:scale-95"
>
<MessageSquare size={18} /> Start a conversation
</button>
</div>
</div>
......
import React, { useState } from "react";
import { Flame, LogIn, UserPlus, Eye, EyeOff } from "lucide-react";
import { login, register } from "../api";
import { useApp } from "../store";
import { login, register } from "../api";
import { Flame, Eye, EyeOff, Loader2 } from "lucide-react";
export default function LoginPage() {
const { dispatch } = useApp();
......@@ -18,132 +18,104 @@ export default function LoginPage() {
setError("");
setLoading(true);
try {
let res;
if (isRegister) {
res = await register(username, email, password);
} else {
res = await login(username, password);
}
const res = isRegister
? await register(username, email, password)
: await login(username, password);
dispatch({ type: "LOGIN", token: res.token, user: res.user });
} catch (err) {
setError(err.message);
setError(err.message || "Authentication failed");
} 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="h-full h-dvh flex items-center justify-center bg-anton-bg px-4 safe-top safe-bottom">
<div className="w-full max-w-sm">
{/* Logo */}
<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 className="w-16 h-16 mx-auto mb-4 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" />
</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>
<h1 className="text-2xl font-bold text-white">Son of Anton</h1>
<p className="text-anton-muted text-sm mt-1">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>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-anton-muted mb-1.5">Username</label>
<label className="text-xs text-anton-muted mb-1.5 block">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"
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
placeholder="Enter username"
required
autoComplete="username"
autoCapitalize="off"
/>
</div>
{isRegister && (
<div>
<label className="block text-sm text-anton-muted mb-1.5">Email</label>
<label className="text-xs text-anton-muted mb-1.5 block">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 text-white focus:outline-none focus:border-anton-accent transition"
placeholder="your@email.com"
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"
autoComplete="email"
/>
</div>
)}
<div>
<label className="block text-sm text-anton-muted mb-1.5">Password</label>
<label className="text-xs text-anton-muted mb-1.5 block">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"
className="w-full bg-anton-card border border-anton-border rounded-xl px-4 py-3 pr-12 text-white focus:outline-none focus:border-anton-accent transition"
placeholder="••••••••"
required
autoComplete={isRegister ? "new-password" : "current-password"}
/>
<button
type="button"
onClick={() => setShowPw(!showPw)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition"
className="absolute right-3 top-1/2 -translate-y-1/2 text-anton-muted hover:text-white transition p-1"
>
{showPw ? <EyeOff size={16} /> : <Eye size={16} />}
{showPw ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{error && (
<div className="bg-anton-danger/10 border border-anton-danger/30 text-anton-danger text-sm rounded-lg px-3 py-2.5">
{error}
</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"
className="w-full py-3.5 bg-anton-accent text-white rounded-xl font-semibold hover:opacity-90 transition disabled:opacity-50 active:scale-[0.98] 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
</>
)}
{loading && <Loader2 size={18} className="animate-spin" />}
{isRegister ? "Create Account" : "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>
<button
type="button"
onClick={() => { setIsRegister(!isRegister); setError(""); }}
className="w-full text-center text-sm text-anton-muted hover:text-white transition py-2"
>
{isRegister ? "Already have an account? Sign in" : "Need an account? Register"}
</button>
</form>
</div>
</div>
......
import React, { createContext, useContext, useReducer, useEffect } from "react";
import React, { createContext, useContext, useReducer, useCallback } from "react";
const AppContext = createContext(null);
......@@ -6,10 +6,10 @@ const initialState = {
token: localStorage.getItem("token") || null,
user: null,
chats: [],
activeChatId: null,
chatMessages: {},
activeStreams: {},
sidebarOpen: false,
sidebarTab: "chats", // "chats" | "knowledge"
};
function reducer(state, action) {
......@@ -17,18 +17,23 @@ function reducer(state, action) {
case "LOGIN":
localStorage.setItem("token", action.token);
return { ...state, token: action.token, user: action.user };
case "SET_TOKEN":
localStorage.setItem("token", action.token);
return { ...state, token: action.token };
case "SET_USER":
return { ...state, user: action.user };
case "LOGOUT":
localStorage.removeItem("token");
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, sidebarOpen: false };
case "ADD_CHAT":
return { ...state, chats: [action.chat, ...state.chats] };
return {
...state,
chats: [action.chat, ...state.chats],
activeChatId: action.chat.id,
sidebarOpen: false,
};
case "UPDATE_CHAT":
return {
...state,
......@@ -36,47 +41,49 @@ function reducer(state, action) {
c.id === action.chat.id ? { ...c, ...action.chat } : c
),
};
case "REMOVE_CHAT":
case "REMOVE_CHAT": {
const remaining = state.chats.filter((c) => c.id !== action.chatId);
return {
...state,
chats: state.chats.filter((c) => c.id !== action.chatId),
chatMessages: (() => {
const m = { ...state.chatMessages };
delete m[action.chatId];
return m;
})(),
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 },
};
case "ADD_MESSAGE":
case "ADD_MESSAGE": {
const prev = state.chatMessages[action.chatId] || [];
return {
...state,
chatMessages: {
...state.chatMessages,
[action.chatId]: [
...(state.chatMessages[action.chatId] || []),
action.message,
],
[action.chatId]: [...prev, action.message],
},
};
}
case "SET_STREAMING":
return {
...state,
activeStreams: action.streaming
? { ...state.activeStreams, [action.chatId]: true }
: (() => {
const s = { ...state.activeStreams };
delete s[action.chatId];
return s;
})(),
: Object.fromEntries(
Object.entries(state.activeStreams).filter(([k]) => k !== action.chatId)
),
};
case "SET_SIDEBAR_OPEN":
return { ...state, sidebarOpen: action.open };
case "SET_SIDEBAR_TAB":
return { ...state, sidebarTab: action.tab };
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
default:
return state;
}
......@@ -93,6 +100,6 @@ export function AppProvider({ children }) {
export function useApp() {
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be used within AppProvider");
if (!ctx) throw new Error("useApp must be inside AppProvider");
return ctx;
}
\ No newline at end of file
......@@ -5,18 +5,21 @@ export default {
extend: {
colors: {
"anton-bg": "#09090f",
"anton-surface": "#0d0d14",
"anton-card": "#12121c",
"anton-border": "#1e1e2e",
"anton-text": "#e0e0e0",
"anton-muted": "#6b6b80",
"anton-accent": "#ff4444",
"anton-success": "#22c55e",
"anton-danger": "#ef4444",
"anton-surface": "#0f0f1a",
"anton-card": "#16162a",
"anton-border": "#1e1e3a",
"anton-text": "#e2e2ea",
"anton-muted": "#6b6b8a",
"anton-accent": "#e53e3e",
"anton-success": "#48bb78",
"anton-danger": "#e53e3e",
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "Consolas", "monospace"],
sans: ["Inter", "system-ui", "-apple-system", "sans-serif"],
mono: ["JetBrains Mono", "Fira Code", "monospace"],
},
screens: {
"xs": "400px",
},
},
},
......
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