Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Son Of Anton
Commits
459309cd
Commit
459309cd
authored
Mar 23, 2026
by
AGLANPC\aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fgg dfjdfghj dfkj df
parent
e636c018
Changes
9
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
727 additions
and
687 deletions
+727
-687
ChatView.jsx
frontend/src/components/ChatView.jsx
+189
-54
CodeBlock.jsx
frontend/src/components/CodeBlock.jsx
+31
-45
MessageBubble.jsx
frontend/src/components/MessageBubble.jsx
+33
-27
Sidebar.jsx
frontend/src/components/Sidebar.jsx
+123
-221
index.css
frontend/src/index.css
+199
-166
ChatPage.jsx
frontend/src/pages/ChatPage.jsx
+58
-62
LoginPage.jsx
frontend/src/pages/LoginPage.jsx
+46
-74
store.jsx
frontend/src/store.jsx
+35
-28
tailwind.config.js
frontend/tailwind.config.js
+13
-10
No files found.
frontend/src/components/ChatView.jsx
View file @
459309cd
This diff is collapsed.
Click to expand it.
frontend/src/components/CodeBlock.jsx
View file @
459309cd
...
...
@@ -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.
82
rem"
,
lineHeight
:
"1.
6
"
,
fontSize
:
"0.
78
rem"
,
lineHeight
:
"1.
55
"
,
},
'code[class*="language-"]'
:
{
...
oneDark
[
'code[class*="language-"]'
],
background
:
"none"
,
fontSize
:
"0.
82
rem"
,
fontSize
:
"0.
78
rem"
,
},
};
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=
{
1
2
}
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=
{
1
1
}
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=
{
1
1
}
className=
"text-anton-success"
/>
:
<
Copy
size=
{
11
}
/>
}
{
copied
?
"Copied"
:
"Copy"
}
{
copied
?
<
Check
size=
{
1
0
}
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=
{
1
1
}
/>
Download
<
Download
size=
{
1
0
}
/>
<
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
>
...
...
frontend/src/components/MessageBubble.jsx
View file @
459309cd
...
...
@@ -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=
{
`m
ax-w-[92%] sm:max-w-[80%] min-w-0 ${isUser ? "order-first" : "
"}`
}
>
<
div
className=
{
`m
in-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
]
||
File
Text
;
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/8
5 flex items-center justify-center p-4
cursor-pointer"
className=
"fixed inset-0 z-50 bg-black/8
0 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 m
ax-w-[200
px]"
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 m
in-h-[44
px]"
>
<
Icon
size=
{
1
5
}
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=
{
1
4
}
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=
{
1
1
}
className=
"text-anton-muted group-hover:text-anton-accent shrink-0"
/>
<
ExternalLink
size=
{
1
0
}
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
>
...
...
frontend/src/components/Sidebar.jsx
View file @
459309cd
This diff is collapsed.
Click to expand it.
frontend/src/index.css
View file @
459309cd
This diff is collapsed.
Click to expand it.
frontend/src/pages/ChatPage.jsx
View file @
459309cd
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
>
...
...
frontend/src/pages/LoginPage.jsx
View file @
459309cd
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=
{
1
6
}
/>
:
<
Eye
size=
{
16
}
/>
}
{
showPw
?
<
EyeOff
size=
{
1
8
}
/>
:
<
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
>
...
...
frontend/src/store.jsx
View file @
459309cd
import
React
,
{
createContext
,
useContext
,
useReducer
,
use
Effect
}
from
"react"
;
import
React
,
{
createContext
,
useContext
,
useReducer
,
use
Callback
}
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
frontend/tailwind.config.js
View file @
459309cd
...
...
@@ -5,18 +5,21 @@ export default {
extend
:
{
colors
:
{
"anton-bg"
:
"#09090f"
,
"anton-surface"
:
"#0
d0d14
"
,
"anton-card"
:
"#1
2121c
"
,
"anton-border"
:
"#1e1e
2e
"
,
"anton-text"
:
"#e
0e0e0
"
,
"anton-muted"
:
"#6b6b8
0
"
,
"anton-accent"
:
"#
ff4444
"
,
"anton-success"
:
"#
22c55e
"
,
"anton-danger"
:
"#e
f4444
"
,
"anton-surface"
:
"#0
f0f1a
"
,
"anton-card"
:
"#1
6162a
"
,
"anton-border"
:
"#1e1e
3a
"
,
"anton-text"
:
"#e
2e2ea
"
,
"anton-muted"
:
"#6b6b8
a
"
,
"anton-accent"
:
"#
e53e3e
"
,
"anton-success"
:
"#
48bb78
"
,
"anton-danger"
:
"#e
53e3e
"
,
},
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"
,
},
},
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment