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
99edb79c
Commit
99edb79c
authored
Mar 19, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
cool on mobile
parent
c0e521e8
Changes
12
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1723 additions
and
1391 deletions
+1723
-1391
FULL_CODEBASE.txt
FULL_CODEBASE.txt
+1106
-791
main.py
backend/main.py
+49
-6
index.html
frontend/index.html
+32
-16
App.jsx
frontend/src/App.jsx
+7
-1
MessageBubble.jsx
frontend/src/components/MessageBubble.jsx
+27
-78
Sidebar.jsx
frontend/src/components/Sidebar.jsx
+144
-272
index.css
frontend/src/index.css
+227
-77
main.jsx
frontend/src/main.jsx
+1
-1
ChatPage.jsx
frontend/src/pages/ChatPage.jsx
+67
-67
store.jsx
frontend/src/store.jsx
+33
-56
tailwind.config.js
frontend/tailwind.config.js
+19
-24
vite.config.js
frontend/vite.config.js
+11
-2
No files found.
FULL_CODEBASE.txt
View file @
99edb79c
This source diff could not be displayed because it is too large. You can
view the blob
instead.
backend/main.py
View file @
99edb79c
...
...
@@ -3,10 +3,11 @@ Son of Anton — Main FastAPI Application
"""
import
os
import
time
from
pathlib
import
Path
from
contextlib
import
asynccontextmanager
from
fastapi
import
FastAPI
,
HTTPException
from
fastapi
import
FastAPI
,
HTTPException
,
Request
,
Response
from
fastapi.staticfiles
import
StaticFiles
from
fastapi.responses
import
FileResponse
from
fastapi.middleware.cors
import
CORSMiddleware
...
...
@@ -21,6 +22,9 @@ from backend.routes.files_routes import router as files_router
from
backend.routes.attachment_routes
import
router
as
attachment_router
from
backend.services.bedrock_service
import
close_http_client
APP_VERSION
=
"2.1.0"
APP_BUILD_TIME
=
str
(
int
(
time
.
time
()))
def
_run_migrations
():
"""Add new columns/tables to existing DB if they're missing."""
...
...
@@ -54,7 +58,7 @@ async def lifespan(app: FastAPI):
Base
.
metadata
.
create_all
(
bind
=
engine
)
_run_migrations
()
seed_superadmin
()
print
(
"Son of Anton
is online."
)
print
(
f
"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME})
is online."
)
yield
await
close_http_client
()
print
(
"Son of Anton shutting down."
)
...
...
@@ -63,7 +67,7 @@ async def lifespan(app: FastAPI):
app
=
FastAPI
(
title
=
"Son of Anton"
,
description
=
"Avatar of All Elements of Code"
,
version
=
"2.0.0"
,
version
=
APP_VERSION
,
lifespan
=
lifespan
,
)
...
...
@@ -75,6 +79,38 @@ app.add_middleware(
allow_headers
=
[
"*"
],
)
@
app
.
middleware
(
"http"
)
async
def
add_cache_headers
(
request
:
Request
,
call_next
):
response
:
Response
=
await
call_next
(
request
)
path
=
request
.
url
.
path
# API responses: never cache
if
path
.
startswith
(
"/api"
):
response
.
headers
[
"Cache-Control"
]
=
"no-store, no-cache, must-revalidate, max-age=0"
response
.
headers
[
"Pragma"
]
=
"no-cache"
response
.
headers
[
"Expires"
]
=
"0"
# Hashed assets (contain hash in filename): cache aggressively
elif
path
.
startswith
(
"/assets/"
)
and
any
(
c
in
path
for
c
in
[
".js"
,
".css"
]):
response
.
headers
[
"Cache-Control"
]
=
"public, max-age=31536000, immutable"
# HTML and everything else: never cache
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"
# Always add version header for debugging
response
.
headers
[
"X-App-Version"
]
=
APP_VERSION
response
.
headers
[
"X-Build-Time"
]
=
APP_BUILD_TIME
return
response
# Version endpoint for frontend to check
@
app
.
get
(
"/api/version"
)
def
get_version
():
return
{
"version"
:
APP_VERSION
,
"build"
:
APP_BUILD_TIME
}
app
.
include_router
(
auth_router
,
prefix
=
"/api/auth"
,
tags
=
[
"Auth"
])
app
.
include_router
(
chat_router
,
prefix
=
"/api/chats"
,
tags
=
[
"Chats"
])
app
.
include_router
(
admin_router
,
prefix
=
"/api/admin"
,
tags
=
[
"Admin"
])
...
...
@@ -98,8 +134,15 @@ async def serve_frontend(full_path: str):
raise
HTTPException
(
status_code
=
404
,
detail
=
"Not found"
)
file_path
=
FRONTEND_DIR
/
full_path
if
full_path
and
file_path
.
is_file
():
return
FileResponse
(
str
(
file_path
))
resp
=
FileResponse
(
str
(
file_path
))
# Don't cache non-hashed static files
resp
.
headers
[
"Cache-Control"
]
=
"no-store, no-cache, must-revalidate, max-age=0"
return
resp
index
=
FRONTEND_DIR
/
"index.html"
if
index
.
is_file
():
return
FileResponse
(
str
(
index
))
return
{
"message"
:
"Son of Anton API is running. Frontend not built."
}
resp
=
FileResponse
(
str
(
index
))
resp
.
headers
[
"Cache-Control"
]
=
"no-store, no-cache, must-revalidate, max-age=0"
resp
.
headers
[
"Pragma"
]
=
"no-cache"
resp
.
headers
[
"Expires"
]
=
"0"
return
resp
return
{
"message"
:
"Son of Anton API is running. Frontend not built."
}
\ No newline at end of file
frontend/index.html
View file @
99edb79c
<!DOCTYPE html>
<html
lang=
"en"
class=
"dark"
>
<head>
<meta
charset=
"UTF-8"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<title>
Son of Anton
</title>
<link
rel=
"preconnect"
href=
"https://fonts.googleapis.com"
/>
<link
rel=
"preconnect"
href=
"https://fonts.gstatic.com"
crossorigin
/>
<link
href=
"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel=
"stylesheet"
/>
<link
rel=
"icon"
href=
"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>"
/>
</head>
<body
class=
"bg-anton-bg text-anton-text font-sans"
>
<div
id=
"root"
></div>
<script
type=
"module"
src=
"/src/main.jsx"
></script>
</body>
<head>
<meta
charset=
"UTF-8"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>
Son of Anton
</title>
<!-- KILL BROWSER CACHE FOR THIS HTML -->
<meta
http-equiv=
"Cache-Control"
content=
"no-store, no-cache, must-revalidate, max-age=0"
/>
<meta
http-equiv=
"Pragma"
content=
"no-cache"
/>
<meta
http-equiv=
"Expires"
content=
"0"
/>
<!-- PWA / Mobile -->
<meta
name=
"apple-mobile-web-app-capable"
content=
"yes"
/>
<meta
name=
"apple-mobile-web-app-status-bar-style"
content=
"black-translucent"
/>
<meta
name=
"theme-color"
content=
"#09090f"
/>
<meta
name=
"mobile-web-app-capable"
content=
"yes"
/>
<link
rel=
"preconnect"
href=
"https://fonts.googleapis.com"
/>
<link
rel=
"preconnect"
href=
"https://fonts.gstatic.com"
crossorigin
/>
<link
href=
"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel=
"stylesheet"
/>
<link
rel=
"icon"
href=
"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>"
/>
</head>
<body
class=
"bg-anton-bg text-anton-text font-sans overscroll-none"
>
<div
id=
"root"
></div>
<script
type=
"module"
src=
"/src/main.jsx"
></script>
</body>
</html>
\ No newline at end of file
frontend/src/App.jsx
View file @
99edb79c
...
...
@@ -2,6 +2,7 @@ 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"
;
...
...
@@ -11,6 +12,11 @@ export default function App() {
const
{
state
,
dispatch
}
=
useApp
();
const
[
authChecked
,
setAuthChecked
]
=
useState
(
!
state
.
token
);
// Connect streamManager to store dispatch
useEffect
(()
=>
{
streamManager
.
setDispatch
(
dispatch
);
},
[
dispatch
]);
useEffect
(()
=>
{
if
(
!
state
.
token
)
{
setAuthChecked
(
true
);
...
...
@@ -34,7 +40,7 @@ export default function App() {
if
(
!
authChecked
)
{
return
(
<
div
className=
"h-
full
flex items-center justify-center bg-anton-bg"
>
<
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"
/>
...
...
frontend/src/components/MessageBubble.jsx
View file @
99edb79c
This diff is collapsed.
Click to expand it.
frontend/src/components/Sidebar.jsx
View file @
99edb79c
This diff is collapsed.
Click to expand it.
frontend/src/index.css
View file @
99edb79c
...
...
@@ -2,137 +2,287 @@
@tailwind
components
;
@tailwind
utilities
;
/* ── Globals ───────────────────────────────── */
*
{
scrollbar-width
:
thin
;
scrollbar-color
:
#2a2a3a
#0a0a0f
;
/* ═══════════════════════════════════════════ */
/* Use dvh for full-height on mobile */
/* ═══════════════════════════════════════════ */
:root
{
--color-anton-bg
:
#09090f
;
--color-anton-surface
:
#0f0f18
;
--color-anton-card
:
#161622
;
--color-anton-border
:
#1e1e30
;
--color-anton-text
:
#e2e2f0
;
--color-anton-muted
:
#6b6b8a
;
--color-anton-accent
:
#e63946
;
--color-anton-success
:
#2ecc71
;
--color-anton-danger
:
#e74c3c
;
/* safe area insets for notched phones */
--sat
:
env
(
safe-area-inset-top
,
0px
);
--sab
:
env
(
safe-area-inset-bottom
,
0px
);
--sal
:
env
(
safe-area-inset-left
,
0px
);
--sar
:
env
(
safe-area-inset-right
,
0px
);
}
*
::-webkit-scrollbar
{
width
:
6px
;
*,
*
::before
,
*
::after
{
box-sizing
:
border-box
;
-webkit-tap-highlight-color
:
transparent
;
}
*
::-webkit-scrollbar-track
{
background
:
#0a0a0f
;
html
{
-webkit-text-size-adjust
:
100%
;
text-size-adjust
:
100%
;
}
*
::-webkit-scrollbar-thumb
{
background
:
#2a2a3a
;
border-radius
:
3px
;
body
{
margin
:
0
;
padding
:
0
;
background
:
var
(
--color-anton-bg
);
color
:
var
(
--color-anton-text
);
font-family
:
"Inter"
,
-apple-system
,
BlinkMacSystemFont
,
"Segoe UI"
,
sans-serif
;
overflow
:
hidden
;
overscroll-behavior
:
none
;
/* Prevent pull-to-refresh on mobile */
-webkit-overflow-scrolling
:
touch
;
}
/* Fix iOS textarea zoom — font MUST be >= 16px */
textarea
,
input
,
select
{
font-size
:
16px
!important
;
}
@media
(
min-width
:
640px
)
{
textarea
,
input
,
select
{
font-size
:
14px
!important
;
}
}
html
,
body
,
#root
{
height
:
100
%
;
margin
:
0
;
height
:
100
dvh
;
width
:
100vw
;
overflow
:
hidden
;
}
/* ── Markdown prose adjustments ────────────── */
.prose-anton
h1
,
.prose-anton
h2
,
.prose-anton
h3
{
color
:
#f97316
;
margin-top
:
1em
;
margin-bottom
:
0.5em
;
font-weight
:
600
;
/* ═══════════════════════════════════════════ */
/* Scrollbar styling */
/* ═══════════════════════════════════════════ */
::-webkit-scrollbar
{
width
:
5px
;
height
:
5px
;
}
.prose-anton
h1
{
font-size
:
1.5rem
;
}
.prose-anton
h2
{
font-size
:
1.25rem
;
}
.prose-anton
h3
{
font-size
:
1.1rem
;
}
.prose-anton
p
{
margin-bottom
:
0.75em
;
::-webkit-scrollbar-track
{
background
:
transparent
;
}
::-webkit-scrollbar-thumb
{
background
:
var
(
--color-anton-border
);
border-radius
:
10px
;
}
::-webkit-scrollbar-thumb:hover
{
background
:
var
(
--color-anton-muted
);
}
/* Hide scrollbar on mobile for cleaner look */
@media
(
max-width
:
640px
)
{
::-webkit-scrollbar
{
width
:
2px
;
}
}
/* ═══════════════════════════════════════════ */
/* Animations */
/* ═══════════════════════════════════════════ */
@keyframes
fade-in
{
from
{
opacity
:
0
;
transform
:
translateY
(
6px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
.animate-fade-in
{
animation
:
fade-in
0.25s
ease-out
;
}
.thinking-pulse
{
animation
:
pulse
1.5s
ease-in-out
infinite
;
}
/* ═══════════════════════════════════════════ */
/* Prose (Markdown) styling */
/* ═══════════════════════════════════════════ */
.prose-anton
{
line-height
:
1.7
;
word-break
:
break-word
;
overflow-wrap
:
break-word
;
}
.prose-anton
p
{
margin
:
0.5em
0
;
}
.prose-anton
p
:first-child
{
margin-top
:
0
;
}
.prose-anton
p
:last-child
{
margin-bottom
:
0
;
}
.prose-anton
ul
,
.prose-anton
ol
{
margin-left
:
1.
5em
;
margin
-bottom
:
0.75em
;
padding-left
:
1.2
5em
;
margin
:
0.5em
0
;
}
.prose-anton
li
{
margin
-bottom
:
0.25em
;
margin
:
0.15em
0
;
}
.prose-anton
ul
{
list-style-type
:
disc
;
}
.prose-anton
ol
{
list-style-type
:
decimal
;
}
.prose-anton
a
{
color
:
#f97316
;
text-decoration
:
underline
;
.prose-anton
code
:not
(
pre
code
)
{
background
:
var
(
--color-anton-border
);
border-radius
:
4px
;
padding
:
0.15em
0.35em
;
font-family
:
"JetBrains Mono"
,
monospace
;
font-size
:
0.85em
;
color
:
#f0a0a0
;
word-break
:
break-all
;
}
.prose-anton
blockquote
{
border-left
:
3px
solid
#f97316
;
padding-left
:
1em
;
color
:
#8888a0
;
margin
:
0.75em
0
;
border-left
:
3px
solid
var
(
--color-anton-accent
);
padding-left
:
0.75em
;
margin
:
0.5em
0
;
color
:
var
(
--color-anton-muted
);
}
.prose-anton
a
{
color
:
var
(
--color-anton-accent
);
text-decoration
:
underline
;
text-underline-offset
:
2px
;
}
.prose-anton
table
{
border-collapse
:
collapse
;
margin
:
0.75em
0
;
font-size
:
0.85em
;
width
:
100%
;
display
:
block
;
overflow-x
:
auto
;
}
.prose-anton
th
,
.prose-anton
td
{
border
:
1px
solid
#2a2a3a
;
padding
:
0.4em
0.
75
em
;
border
:
1px
solid
var
(
--color-anton-border
)
;
padding
:
0.4em
0.
6
em
;
text-align
:
left
;
white-space
:
nowrap
;
}
.prose-anton
th
{
background
:
#1a1a28
;
background
:
var
(
--color-anton-card
)
;
font-weight
:
600
;
}
.prose-anton
code
:not
(
pre
code
)
{
background
:
#1a1a28
;
padding
:
0.15em
0.4em
;
border-radius
:
4px
;
font-
size
:
0.9em
;
font-family
:
"JetBrains Mono"
,
monospac
e
;
color
:
#f97316
;
.prose-anton
h1
,
.prose-anton
h2
,
.prose-anton
h3
,
.prose-anton
h4
{
font-
weight
:
600
;
color
:
whit
e
;
margin
:
0.75em
0
0.35em
;
}
/* ── Thinking block animation ──────────────── */
@keyframes
thinkPulse
{
0
%,
100
%
{
opacity
:
0.6
;
}
50
%
{
opacity
:
1
;
}
.prose-anton
h1
{
font-size
:
1.35em
;
}
.thinking-pulse
{
animation
:
thinkPulse
2s
ease-in-out
infinite
;
.prose-anton
h2
{
font-size
:
1.2em
;
}
/* ── Slider custom styling ─────────────────── */
.prose-anton
h3
{
font-size
:
1.05em
;
}
.prose-anton
hr
{
border
:
none
;
border-top
:
1px
solid
var
(
--color-anton-border
);
margin
:
1em
0
;
}
/* ═══════════════════════════════════════════ */
/* Range input styling */
/* ═══════════════════════════════════════════ */
input
[
type
=
"range"
]
{
-webkit-appearance
:
none
;
appearance
:
none
;
background
:
transparent
;
width
:
100%
;
}
input
[
type
=
"range"
]
::-webkit-slider-track
{
height
:
4px
;
border-radius
:
2px
;
background
:
#2a2a3a
;
border-radius
:
999px
;
background
:
var
(
--color-anton-border
);
outline
:
none
;
}
input
[
type
=
"range"
]
::-webkit-slider-thumb
{
-webkit-appearance
:
none
;
appearance
:
none
;
height
:
16px
;
width
:
16px
;
width
:
18px
;
height
:
18px
;
border-radius
:
50%
;
background
:
#f97316
;
margin-top
:
-6px
;
background
:
var
(
--color-anton-accent
);
cursor
:
pointer
;
border
:
2px
solid
var
(
--color-anton-bg
);
}
input
[
type
=
"range"
]
::-moz-range-track
{
height
:
4px
;
border-radius
:
2px
;
background
:
#2a2a3a
;
}
input
[
type
=
"range"
]
::-moz-range-thumb
{
height
:
16
px
;
width
:
16
px
;
width
:
18
px
;
height
:
18
px
;
border-radius
:
50%
;
background
:
#f97316
;
border
:
none
;
background
:
var
(
--color-anton-accent
);
cursor
:
pointer
;
border
:
2px
solid
var
(
--color-anton-bg
);
}
/* ═══════════════════════════════════════════ */
/* Mobile-specific utilities */
/* ═══════════════════════════════════════════ */
@supports
(
height
:
100
dvh
)
{
.h-dvh
{
height
:
100
dvh
;
}
}
@supports
not
(
height
:
100
dvh
)
{
.h-dvh
{
height
:
100vh
;
}
}
/* Ensure touch targets are at least 44px */
@media
(
max-width
:
640px
)
{
button
,
a
,
[
role
=
"button"
]
{
min-height
:
44px
;
min-width
:
44px
;
}
/* Exception for inline/tiny buttons */
.prose-anton
button
,
.text-
\
[
11px
\
]
button
{
min-height
:
auto
;
min-width
:
auto
;
}
}
\ No newline at end of file
frontend/src/main.jsx
View file @
99edb79c
import
React
from
"react"
;
import
ReactDOM
from
"react-dom/client"
;
import
{
BrowserRouter
}
from
"react-router-dom"
;
import
App
from
"./App"
;
import
{
AppProvider
}
from
"./store"
;
import
App
from
"./App"
;
import
"./index.css"
;
ReactDOM
.
createRoot
(
document
.
getElementById
(
"root"
)).
render
(
...
...
frontend/src/pages/ChatPage.jsx
View file @
99edb79c
import
React
,
{
useEffect
,
useCallback
}
from
"react"
;
import
React
,
{
useEffect
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
listChats
}
from
"../api"
;
import
Sidebar
from
"../components/Sidebar"
;
import
ChatView
from
"../components/ChatView"
;
import
{
Flame
,
Paperclip
,
Layers
,
Zap
}
from
"lucide-react"
;
import
{
Flame
,
MessageSquarePlus
,
Menu
}
from
"lucide-react"
;
export
default
function
ChatPage
()
{
const
{
state
,
dispatch
}
=
useApp
();
const
loadChats
=
useCallback
(
async
()
=>
{
try
{
const
chats
=
await
listChats
(
state
.
token
);
dispatch
({
type
:
"SET_CHATS"
,
chats
});
}
catch
{
/* ignore */
}
},
[
state
.
token
,
dispatch
]);
useEffect
(()
=>
{
loadChats
();
},
[
loadChats
]);
return
(
<
div
className=
"h-full flex"
>
<
Sidebar
onRefresh=
{
loadChats
}
/>
<
main
className=
"flex-1 flex flex-col min-w-0"
>
{
state
.
activeChatId
?
(
<
ChatView
key=
{
state
.
activeChatId
}
chatId=
{
state
.
activeChatId
}
/>
)
:
(
<
EmptyState
/>
)
}
</
main
>
</
div
>
);
}
(
async
()
=>
{
try
{
const
chats
=
await
listChats
(
state
.
token
);
dispatch
({
type
:
"SET_CHATS"
,
chats
});
}
catch
{
}
})();
},
[
state
.
token
,
dispatch
]);
function
EmptyState
()
{
return
(
<
div
className=
"flex-1 flex items-center justify-center p-8"
>
<
div
className=
"text-center animate-fade-in max-w-lg"
>
<
div
className=
"inline-flex items-center justify-center w-24 h-24 rounded-3xl bg-gradient-to-br from-anton-accent/20 to-transparent border border-anton-accent/20 mb-6"
>
<
Flame
size=
{
44
}
className=
"text-anton-accent"
/>
</
div
>
<
h2
className=
"text-2xl font-bold text-white mb-2"
>
Son of Anton
</
h2
>
<
p
className=
"text-anton-muted mb-6"
>
Avatar of All Elements of Code. Create a new chat to begin — but bring
real questions, not that first-result-of-Google garbage.
</
p
>
<
div
className=
"h-dvh flex overflow-hidden relative"
>
{
/* Mobile overlay backdrop */
}
{
state
.
sidebarOpen
&&
(
<
div
className=
"fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
onClick=
{
()
=>
dispatch
({
type
:
"CLOSE_SIDEBAR"
})
}
/>
)
}
<
div
className=
"grid grid-cols-1 sm:grid-cols-3 gap-3 text-left"
>
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
div
className=
"w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center"
>
<
Paperclip
size=
{
16
}
className=
"text-blue-400"
/>
</
div
>
<
span
className=
"text-sm font-medium text-white"
>
File Upload
</
span
>
</
div
>
<
p
className=
"text-xs text-anton-muted leading-relaxed"
>
Drop images, videos, PDFs, or code files directly into chat. AI describes and analyzes them.
</
p
>
</
div
>
{
/* Sidebar — slides in on mobile, always visible on desktop */
}
<
div
className=
{
`
fixed inset-y-0 left-0 z-40 w-72
transform transition-transform duration-300 ease-in-out
lg:relative lg:translate-x-0 lg:z-auto
${state.sidebarOpen ? "translate-x-0" : "-translate-x-full"}
`
}
>
<
Sidebar
/>
</
div
>
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
div
className=
"w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center"
>
<
Layers
size=
{
16
}
className=
"text-purple-400"
/>
</
div
>
<
span
className=
"text-sm font-medium text-white"
>
Parallel Chats
</
span
>
</
div
>
<
p
className=
"text-xs text-anton-muted leading-relaxed"
>
Run multiple conversations simultaneously. Switch between them while they stream.
</
p
>
{
/* Main content area */
}
<
div
className=
"flex-1 flex flex-col min-w-0"
>
{
/* Mobile top bar */
}
<
div
className=
"flex items-center gap-3 px-4 py-3 border-b border-anton-border bg-anton-surface lg:hidden"
>
<
button
onClick=
{
()
=>
dispatch
({
type
:
"TOGGLE_SIDEBAR"
})
}
className=
"p-2 -ml-2 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition active:scale-95"
>
<
Menu
size=
{
22
}
/>
</
button
>
<
div
className=
"flex items-center gap-2 flex-1 min-w-0"
>
<
Flame
size=
{
18
}
className=
"text-anton-accent shrink-0"
/>
<
span
className=
"text-sm font-semibold text-white truncate"
>
{
state
.
activeChatId
?
state
.
chats
.
find
((
c
)
=>
c
.
id
===
state
.
activeChatId
)?.
title
||
"Chat"
:
"Son of Anton"
}
</
span
>
</
div
>
</
div
>
<
div
className=
"bg-anton-surface border border-anton-border rounded-xl p-4"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
div
className=
"w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center"
>
<
Zap
size=
{
16
}
className=
"text-green-400"
/>
{
/* Chat view or empty state */
}
{
state
.
activeChatId
?
(
<
ChatView
chatId=
{
state
.
activeChatId
}
/>
)
:
(
<
div
className=
"flex-1 flex items-center justify-center p-6"
>
<
div
className=
"text-center max-w-md"
>
<
div
className=
"w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20 mb-6"
>
<
Flame
size=
{
40
}
className=
"text-white"
/>
</
div
>
<
span
className=
"text-sm font-medium text-white"
>
Full Code
</
span
>
<
h1
className=
"text-2xl font-bold text-white mb-2"
>
Son of Anton
</
h1
>
<
p
className=
"text-anton-muted text-sm mb-6 leading-relaxed"
>
Avatar of All Elements of Code. Select an existing chat from the sidebar
or create a new one to begin.
</
p
>
<
button
onClick=
{
()
=>
dispatch
({
type
:
"OPEN_SIDEBAR"
})
}
className=
"lg:hidden inline-flex items-center gap-2 px-5 py-2.5 bg-anton-accent text-white rounded-xl hover:opacity-90 transition active:scale-95 text-sm font-medium"
>
<
MessageSquarePlus
size=
{
16
}
/>
Open Chats
</
button
>
</
div
>
<
p
className=
"text-xs text-anton-muted leading-relaxed"
>
Production-ready code with syntax highlighting, download buttons, and ZIP export.
</
p
>
</
div
>
</
div
>
)
}
</
div
>
</
div
>
);
...
...
frontend/src/store.jsx
View file @
99edb79c
import
React
,
{
createContext
,
useContext
,
useReducer
,
useEffect
}
from
"react"
;
import
{
setDispatch
}
from
"./streamManager"
;
const
AppContext
=
createContext
();
const
AppContext
=
createContext
(
null
);
const
initialState
=
{
token
:
localStorage
.
getItem
(
"token"
)
||
null
,
user
:
JSON
.
parse
(
localStorage
.
getItem
(
"user"
)
||
"null"
)
,
user
:
null
,
chats
:
[],
activeChatId
:
null
,
sidebarOpen
:
true
,
chatMessages
:
{},
activeStreams
:
{},
sidebarOpen
:
false
,
// mobile sidebar toggle
};
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
"SET_TOKEN"
:
if
(
action
.
token
)
localStorage
.
setItem
(
"token"
,
action
.
token
);
else
localStorage
.
removeItem
(
"token"
);
localStorage
.
setItem
(
"token"
,
action
.
token
);
return
{
...
state
,
token
:
action
.
token
};
case
"SET_USER"
:
localStorage
.
setItem
(
"user"
,
JSON
.
stringify
(
action
.
user
));
return
{
...
state
,
user
:
action
.
user
};
case
"LOGOUT"
:
localStorage
.
removeItem
(
"token"
);
localStorage
.
removeItem
(
"user"
);
return
{
...
initialState
,
token
:
null
,
user
:
null
};
return
{
...
initialState
,
token
:
null
};
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
],
activeChatId
:
action
.
chat
.
id
,
sidebarOpen
:
false
,
};
case
"UPDATE_CHAT"
:
{
...
...
@@ -52,64 +46,52 @@ function reducer(state, action) {
return
{
...
state
,
chats
:
updated
};
}
case
"REMOVE_CHAT"
:
case
"DELETE_CHAT"
:
{
const
chatId
=
action
.
chatId
;
const
filtered
=
state
.
chats
.
filter
((
c
)
=>
c
.
id
!==
chatId
);
case
"REMOVE_CHAT"
:
{
const
filtered
=
state
.
chats
.
filter
((
c
)
=>
c
.
id
!==
action
.
chatId
);
const
newMessages
=
{
...
state
.
chatMessages
};
delete
newMessages
[
chatId
];
const
newStreams
=
{
...
state
.
activeStreams
};
delete
newStreams
[
chatId
];
delete
newMessages
[
action
.
chatId
];
return
{
...
state
,
chats
:
filtered
,
chatMessages
:
newMessages
,
activeStreams
:
newStreams
,
activeChatId
:
state
.
activeChatId
===
chatId
?
filtered
[
0
]?.
id
||
null
:
state
.
activeChatId
,
activeChatId
:
state
.
activeChatId
===
action
.
chatId
?
null
:
state
.
activeChatId
,
};
}
case
"SET_ACTIVE_CHAT"
:
return
{
...
state
,
activeChatId
:
action
.
chatId
};
case
"TOGGLE_SIDEBAR"
:
return
{
...
state
,
sidebarOpen
:
!
state
.
sidebarOpen
};
case
"SET_MESSAGES"
:
return
{
...
state
,
chatMessages
:
{
...
state
.
chatMessages
,
[
action
.
chatId
]:
action
.
messages
,
},
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"
:
{
case
"SET_STREAMING"
:
if
(
action
.
streaming
)
{
return
{
...
state
,
activeStreams
:
{
...
state
.
activeStreams
,
[
action
.
chatId
]:
true
},
};
return
{
...
state
,
activeStreams
:
{
...
state
.
activeStreams
,
[
action
.
chatId
]:
true
}
};
}
else
{
const
s
=
{
...
state
.
activeStreams
};
delete
s
[
action
.
chatId
];
return
{
...
state
,
activeStreams
:
s
};
}
const
next
=
{
...
state
.
activeStreams
};
delete
next
[
action
.
chatId
];
return
{
...
state
,
activeStreams
:
next
};
}
case
"TOGGLE_SIDEBAR"
:
return
{
...
state
,
sidebarOpen
:
!
state
.
sidebarOpen
};
case
"CLOSE_SIDEBAR"
:
return
{
...
state
,
sidebarOpen
:
false
};
case
"OPEN_SIDEBAR"
:
return
{
...
state
,
sidebarOpen
:
true
};
default
:
return
state
;
...
...
@@ -118,11 +100,6 @@ function reducer(state, action) {
export
function
AppProvider
({
children
})
{
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
initialState
);
useEffect
(()
=>
{
setDispatch
(
dispatch
);
},
[
dispatch
]);
return
(
<
AppContext
.
Provider
value=
{
{
state
,
dispatch
}
}
>
{
children
}
...
...
frontend/tailwind.config.js
View file @
99edb79c
/** @type {import('tailwindcss').Config} */
export
default
{
content
:
[
"./index.html"
,
"./src/**/*.{js,jsx}"
],
content
:
[
"./index.html"
,
"./src/**/*.{js,jsx,ts,tsx}"
],
darkMode
:
"class"
,
theme
:
{
extend
:
{
colors
:
{
anton
:
{
bg
:
"#0a0a0f"
,
surface
:
"#12121a"
,
card
:
"#1a1a28"
,
border
:
"#2a2a3a"
,
accent
:
"#f97316"
,
accentDim
:
"#c2410c"
,
text
:
"#e4e4ef"
,
muted
:
"#8888a0"
,
user
:
"#1e293b"
,
assistant
:
"#15151f"
,
danger
:
"#ef4444"
,
success
:
"#22c55e"
,
},
"anton-bg"
:
"#09090f"
,
"anton-surface"
:
"#0f0f18"
,
"anton-card"
:
"#161622"
,
"anton-border"
:
"#1e1e30"
,
"anton-text"
:
"#e2e2f0"
,
"anton-muted"
:
"#6b6b8a"
,
"anton-accent"
:
"#e63946"
,
"anton-success"
:
"#2ecc71"
,
"anton-danger"
:
"#e74c3c"
,
},
fontFamily
:
{
sans
:
[
'"Inter"'
,
"system-ui"
,
"sans-serif"
],
mono
:
[
'"JetBrains Mono"'
,
'"Fira Code"'
,
"monospace"
],
mono
:
[
'"JetBrains Mono"'
,
"monospace"
],
},
animation
:
{
"pulse-slow"
:
"pulse 3s cubic-bezier(0.4,0,0.6,1) infinite"
,
"fade-in"
:
"fadeIn 0.3s ease-out"
,
height
:
{
dvh
:
"100dvh"
,
},
keyframes
:
{
fadeIn
:
{
"0%"
:
{
opacity
:
0
,
transform
:
"translateY(8px)"
},
"100%"
:
{
opacity
:
1
,
transform
:
"translateY(0)"
},
}
,
minHeight
:
{
dvh
:
"100dvh"
,
},
screens
:
{
xs
:
"480px"
,
},
},
},
...
...
frontend/vite.config.js
View file @
99edb79c
...
...
@@ -5,11 +5,20 @@ export default defineConfig({
plugins
:
[
react
()],
server
:
{
proxy
:
{
"/api"
:
"http://localhost:80
00
"
,
"/api"
:
"http://localhost:80"
,
},
},
build
:
{
outDir
:
"dist"
,
// Content-hash all chunks so browsers fetch new versions
rollupOptions
:
{
output
:
{
entryFileNames
:
"assets/[name]-[hash].js"
,
chunkFileNames
:
"assets/[name]-[hash].js"
,
assetFileNames
:
"assets/[name]-[hash].[ext]"
,
},
},
// Generate a manifest for cache-busting verification
manifest
:
true
,
sourcemap
:
false
,
},
});
\ No newline at end of file
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