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
18d64a46
Commit
18d64a46
authored
Mar 17, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ko
parent
44c66e72
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
5127 additions
and
71 deletions
+5127
-71
misc.xml
.idea/misc.xml
+1
-1
son-of-anton.iml
.idea/son-of-anton.iml
+4
-2
CollectCode.sh
CollectCode.sh
+596
-0
FULL_CODEBASE.txt
FULL_CODEBASE.txt
+4285
-0
main.py
backend/main.py
+20
-0
models.py
backend/models.py
+2
-0
chat_routes.py
backend/routes/chat_routes.py
+31
-5
knowledge_routes.py
backend/routes/knowledge_routes.py
+64
-34
memory_service.py
backend/services/memory_service.py
+13
-4
api.js
frontend/src/api.js
+12
-3
ChatView.jsx
frontend/src/components/ChatView.jsx
+70
-14
MessageBubble.jsx
frontend/src/components/MessageBubble.jsx
+4
-2
Sidebar.jsx
frontend/src/components/Sidebar.jsx
+25
-6
No files found.
.idea/misc.xml
View file @
18d64a46
...
...
@@ -3,5 +3,5 @@
<component
name=
"Black"
>
<option
name=
"sdkName"
value=
"Python 3.14"
/>
</component>
<component
name=
"ProjectRootManager"
version=
"2"
project-jdk-name=
"Python 3.1
4
"
project-jdk-type=
"Python SDK"
/>
<component
name=
"ProjectRootManager"
version=
"2"
project-jdk-name=
"Python 3.1
1 (son-of-anton)
"
project-jdk-type=
"Python SDK"
/>
</project>
\ No newline at end of file
.idea/son-of-anton.iml
View file @
18d64a46
<?xml version="1.0" encoding="UTF-8"?>
<module
type=
"PYTHON_MODULE"
version=
"4"
>
<component
name=
"NewModuleRootManager"
>
<content
url=
"file://$MODULE_DIR$"
/>
<orderEntry
type=
"jdk"
jdkName=
"Python 3.14"
jdkType=
"Python SDK"
/>
<content
url=
"file://$MODULE_DIR$"
>
<excludeFolder
url=
"file://$MODULE_DIR$/.venv"
/>
</content>
<orderEntry
type=
"jdk"
jdkName=
"Python 3.11 (son-of-anton)"
jdkType=
"Python SDK"
/>
<orderEntry
type=
"sourceFolder"
forTests=
"false"
/>
</component>
</module>
\ No newline at end of file
CollectCode.sh
0 → 100644
View file @
18d64a46
#!/bin/zsh
# ============================================================================
# CODEBASE COLLECTOR — "Give the AI EVERYTHING"
# Recursively grabs every code file, config, .env, Docker stuff, scripts,
# and packs it into one beautiful file with a full file map.
# ============================================================================
setopt NULL_GLOB 2>/dev/null
# ── CONFIG ──────────────────────────────────────────────────────────────────
OUTPUT_FILE
=
"FULL_CODEBASE.txt"
TARGET_DIR
=
"
${
1
:-
.
}
"
MAX_FILE_SIZE_KB
=
500
TIMESTAMP
=
$(
date
"+%Y-%m-%d %H:%M:%S"
)
# ── COLORS ──────────────────────────────────────────────────────────────────
RED
=
'\033[0;31m'
GREEN
=
'\033[0;32m'
YELLOW
=
'\033[1;33m'
CYAN
=
'\033[0;36m'
BOLD
=
'\033[1m'
NC
=
'\033[0m'
# ── FILE EXTENSIONS TO INCLUDE ──────────────────────────────────────────────
CODE_EXTENSIONS
=(
py js ts jsx tsx go rs rb php java kt kts
scala c h cpp hpp cc cxx cs swift m mm
r R pl pm lua zig nim dart ex exs erl hrl
hs lhs ml mli fs fsx clj cljs cljc v sv
vhd vhdl sol move cairo
html htm css scss sass less styl
vue svelte astro mdx
sh bash zsh fish ps1 psm1 bat cmd
json jsonc json5 yaml yml toml ini cfg conf
xml xsl xslt plist hcl tf tfvars
md markdown txt rst adoc tex org
sql prisma graphql gql
gradle sbt cmake mk
gemspec podspec
env
editorconfig prettierrc eslintrc babelrc browserslistrc
stylelintrc commitlintrc
lock map csv tsv
proto thrift avsc
ipynb
ejs hbs handlebars pug jade njk twig jinja j2
)
# ── EXACT FILENAMES TO ALWAYS INCLUDE ───────────────────────────────────────
EXACT_FILENAMES
=(
Dockerfile Dockerfile.dev Dockerfile.prod Dockerfile.staging
docker-compose.yml docker-compose.yaml docker-compose.dev.yml
docker-compose.prod.yml docker-compose.override.yml
.dockerignore .gitignore .gitattributes .gitmodules
.editorconfig
.prettierrc .prettierrc.json .prettierrc.yml .prettierrc.yaml
.prettierrc.js .prettierrc.cjs .prettierrc.toml .prettier.config.js
.eslintrc .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml
.babelrc .babelrc.json .stylelintrc .stylelintrc.json
.env .env.local .env.development .env.production .env.staging
.env.test .env.example .env.sample .env.template
.flake8 .pylintrc .rubocop.yml .ruby-version .node-version .nvmrc
.python-version .tool-versions
Makefile makefile GNUmakefile Rakefile Gemfile Gemfile.lock
Pipfile Pipfile.lock Cargo.toml Cargo.lock go.mod go.sum
package.json package-lock.json yarn.lock pnpm-lock.yaml bun.lockb
tsconfig.json tsconfig.base.json tsconfig.build.json jsconfig.json
webpack.config.js webpack.config.ts vite.config.js vite.config.ts
rollup.config.js rollup.config.ts esbuild.config.js
next.config.js next.config.mjs next.config.ts
nuxt.config.js nuxt.config.ts svelte.config.js astro.config.mjs
tailwind.config.js tailwind.config.ts postcss.config.js postcss.config.cjs
jest.config.js jest.config.ts vitest.config.ts vitest.config.js
pytest.ini setup.py setup.cfg pyproject.toml
requirements.txt requirements-dev.txt requirements.in constraints.txt
tox.ini poetry.lock composer.json composer.lock
build.gradle build.gradle.kts settings.gradle settings.gradle.kts
pom.xml CMakeLists.txt meson.build
Justfile justfile Taskfile.yml Taskfile.yaml Earthfile Tiltfile Brewfile
Procfile Vagrantfile Jenkinsfile
cloudbuild.yaml serverless.yml serverless.yaml serverless.ts
fly.toml render.yaml app.yaml app.json vercel.json netlify.toml
firebase.json .firebaserc angular.json nx.json project.json workspace.json
lerna.json turbo.json .swcrc biome.json deno.json deno.jsonc import_map.json
renovate.json .releaserc .releaserc.json .releaserc.yml
CODEOWNERS LICENSE LICENSE.md LICENSE.txt
CHANGELOG.md CONTRIBUTING.md README.md README.rst TODO.md
)
# ── DIRECTORIES TO SKIP ─────────────────────────────────────────────────────
SKIP_DIRS
=(
node_modules .git .svn .hg
__pycache__ .pytest_cache .mypy_cache .ruff_cache .tox .nox
venv .venv .env_dir virtualenv .virtualenv
.idea .vscode .vs
dist build out target bin obj
.next .nuxt .output .svelte-kit .astro .vercel .netlify .serverless
.terraform .gradle .maven .cargo
vendor Pods DerivedData
.sass-cache .parcel-cache .turbo .cache
coverage .nyc_output htmlcov
.eggs .bundle
tmp temp logs
__MACOSX
)
# ── CHECK IF PATH IS INSIDE A SKIPPED DIR ──────────────────────────────────
is_in_skip_dir
()
{
local
filepath
=
"
$1
"
for
dir
in
"
${
SKIP_DIRS
[@]
}
"
;
do
if
[[
"
$filepath
"
==
*
"/
${
dir
}
/"
*
]]
||
[[
"
$filepath
"
==
*
"/
${
dir
}
"
]]
;
then
return
0
fi
# Also catch ./dir/ at the start
if
[[
"
$filepath
"
==
"
${
dir
}
/"
*
]]
||
[[
"
$filepath
"
==
"./
${
dir
}
/"
*
]]
;
then
return
0
fi
done
return
1
}
# ── CHECK IF FILE SHOULD BE INCLUDED ───────────────────────────────────────
should_include
()
{
local
filepath
=
"
$1
"
local
filename
=
"
${
filepath
:t
}
"
# zsh way to get basename
local
extension
=
"
${
filename
:e
}
"
# zsh way to get extension
# Check exact filename matches
for
exact
in
"
${
EXACT_FILENAMES
[@]
}
"
;
do
if
[[
"
$filename
"
==
"
$exact
"
]]
;
then
return
0
fi
done
# Catch .env.anything
if
[[
"
$filename
"
==
.env
*
]]
;
then
return
0
fi
# Catch Dockerfile.anything
if
[[
"
$filename
"
==
Dockerfile
*
]]
||
[[
"
$filename
"
==
dockerfile
*
]]
;
then
return
0
fi
# Catch docker-compose*.anything
if
[[
"
$filename
"
==
docker-compose
*
]]
;
then
return
0
fi
# Catch GitHub Actions / CI workflows
if
[[
"
$filepath
"
==
*
".github/"
*
]]
&&
[[
"
$extension
"
==
"yml"
||
"
$extension
"
==
"yaml"
]]
;
then
return
0
fi
# Check extension match
if
[[
-n
"
$extension
"
]]
;
then
for
ext
in
"
${
CODE_EXTENSIONS
[@]
}
"
;
do
if
[[
"
$extension
"
==
"
$ext
"
]]
;
then
return
0
fi
done
fi
return
1
}
# ── CHECK IF FILE IS BINARY ────────────────────────────────────────────────
is_binary
()
{
local
filepath
=
"
$1
"
local
mime
mime
=
$(
file
--mime-encoding
"
$filepath
"
2>/dev/null
)
if
[[
"
$mime
"
==
*
"binary"
*
]]
;
then
return
0
fi
return
1
}
# ── DETECT LANGUAGE FOR SYNTAX HINT ────────────────────────────────────────
get_language_hint
()
{
local
filename
=
"
${
1
:t
}
"
local
ext
=
"
${
filename
:e
}
"
# Exact name matches first
case
"
$filename
"
in
Dockerfile
*
)
echo
"dockerfile"
;
return
;;
Makefile|makefile|GNUmakefile
)
echo
"makefile"
;
return
;;
Jenkinsfile
)
echo
"groovy"
;
return
;;
Vagrantfile|Rakefile|Gemfile
)
echo
"ruby"
;
return
;;
*
.env
*
)
echo
"dotenv"
;
return
;;
esac
case
"
$ext
"
in
py
)
echo
"python"
;;
js
)
echo
"javascript"
;;
ts
)
echo
"typescript"
;;
jsx
)
echo
"jsx"
;;
tsx
)
echo
"tsx"
;;
go
)
echo
"go"
;;
rs
)
echo
"rust"
;;
rb
)
echo
"ruby"
;;
php
)
echo
"php"
;;
java
)
echo
"java"
;;
kt|kts
)
echo
"kotlin"
;;
scala
)
echo
"scala"
;;
c|h
)
echo
"c"
;;
cpp|hpp|cc
)
echo
"cpp"
;;
cs
)
echo
"csharp"
;;
swift
)
echo
"swift"
;;
html|htm
)
echo
"html"
;;
css
)
echo
"css"
;;
scss
)
echo
"scss"
;;
json|jsonc
)
echo
"json"
;;
yaml|yml
)
echo
"yaml"
;;
toml
)
echo
"toml"
;;
xml
)
echo
"xml"
;;
sql
)
echo
"sql"
;;
sh|bash
)
echo
"bash"
;;
zsh
)
echo
"zsh"
;;
md
)
echo
"markdown"
;;
graphql|gql
)
echo
"graphql"
;;
tf|hcl
)
echo
"hcl"
;;
lua
)
echo
"lua"
;;
dart
)
echo
"dart"
;;
ex|exs
)
echo
"elixir"
;;
vue
)
echo
"vue"
;;
svelte
)
echo
"svelte"
;;
prisma
)
echo
"prisma"
;;
sol
)
echo
"solidity"
;;
ini|cfg|conf
)
echo
"ini"
;;
proto
)
echo
"protobuf"
;;
r|R
)
echo
"r"
;;
*
)
echo
"plaintext"
;;
esac
}
# ── MAIN ────────────────────────────────────────────────────────────────────
echo
""
echo
"
${
BOLD
}${
CYAN
}
╔══════════════════════════════════════════════════════════════╗
${
NC
}
"
echo
"
${
BOLD
}${
CYAN
}
║ 🔥 CODEBASE COLLECTOR — GRAB EVERYTHING 🔥 ║
${
NC
}
"
echo
"
${
BOLD
}${
CYAN
}
╚══════════════════════════════════════════════════════════════╝
${
NC
}
"
echo
""
# Resolve to absolute path
ABS_TARGET
=
$(
cd
"
$TARGET_DIR
"
&&
pwd
)
echo
"
${
YELLOW
}
Target directory:
${
NC
}
$ABS_TARGET
"
echo
"
${
YELLOW
}
Output file:
${
NC
}
$OUTPUT_FILE
"
echo
"
${
YELLOW
}
Max file size:
${
NC
}
${
MAX_FILE_SIZE_KB
}
KB"
echo
""
# ── PHASE 1: COLLECT ALL FILE PATHS ────────────────────────────────────────
echo
"
${
CYAN
}
[1/4]
${
NC
}
🔍 Scanning for files..."
collected_files
=()
skipped_binary
=()
skipped_size
=()
skipped_self
=
0
total_scanned
=
0
# Simple and robust: just find ALL regular files, filter in the loop
while
IFS
=
read
-r
file
;
do
total_scanned
=
$((
total_scanned
+
1
))
# Skip our own output file
local_name
=
"
${
file
:t
}
"
if
[[
"
$local_name
"
==
"
$OUTPUT_FILE
"
]]
||
[[
"
$local_name
"
==
"CollectCode.sh"
]]
;
then
skipped_self
=
$((
skipped_self
+
1
))
continue
fi
# Make a relative path for checking skip dirs
relpath
=
"
${
file
#
$ABS_TARGET
/
}
"
# Check if in a skipped directory
if
is_in_skip_dir
"
$relpath
"
;
then
continue
fi
# Check if we care about this file
if
should_include
"
$file
"
;
then
# Check file size
local_size
=
$(
stat
-f
%z
"
$file
"
2>/dev/null
||
wc
-c
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
local_size_kb
=
$((
local_size
/
1024
))
if
[[
$local_size_kb
-gt
$MAX_FILE_SIZE_KB
]]
;
then
skipped_size+
=(
"
$relpath
(
${
local_size_kb
}
KB)"
)
continue
fi
# Check if binary
if
is_binary
"
$file
"
;
then
skipped_binary+
=(
"
$relpath
"
)
continue
fi
collected_files+
=(
"
$file
"
)
fi
done
< <
(
find
"
$ABS_TARGET
"
-type
f 2>/dev/null |
sort
)
echo
"
${
GREEN
}
✓ Scanned
${
total_scanned
}
total files
${
NC
}
"
echo
"
${
GREEN
}
✓ Found
${#
collected_files
[@]
}
files to include
${
NC
}
"
[[
${#
skipped_binary
[@]
}
-gt
0
]]
&&
echo
"
${
YELLOW
}
⊘ Skipped
${#
skipped_binary
[@]
}
binary files
${
NC
}
"
[[
${#
skipped_size
[@]
}
-gt
0
]]
&&
echo
"
${
YELLOW
}
⊘ Skipped
${#
skipped_size
[@]
}
files over
${
MAX_FILE_SIZE_KB
}
KB
${
NC
}
"
if
[[
${#
collected_files
[@]
}
-eq
0
]]
;
then
echo
""
echo
"
${
RED
}
✗ No files found!
${
NC
}
"
echo
""
echo
"
${
YELLOW
}
DEBUG: Listing first 20 files found by find:
${
NC
}
"
find
"
$ABS_TARGET
"
-type
f 2>/dev/null |
head
-20
|
while
read
f
;
do
relp
=
"
${
f
#
$ABS_TARGET
/
}
"
echo
"
$relp
"
done
echo
""
echo
"
${
YELLOW
}
If you see files above, the extension/name filter isn't matching them.
${
NC
}
"
echo
"
${
YELLOW
}
Check CODE_EXTENSIONS and EXACT_FILENAMES arrays in the script.
${
NC
}
"
exit
1
fi
# ── PHASE 2: ORGANIZE + STATS ──────────────────────────────────────────────
echo
"
${
CYAN
}
[2/4]
${
NC
}
📂 Building file map..."
total_lines
=
0
total_bytes
=
0
typeset
-A
ext_count
for
file
in
"
${
collected_files
[@]
}
"
;
do
lines
=
$(
wc
-l
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
bytes
=
$(
stat
-f
%z
"
$file
"
2>/dev/null
||
wc
-c
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
total_lines
=
$((
total_lines
+
lines
))
total_bytes
=
$((
total_bytes
+
bytes
))
fname
=
"
${
file
:t
}
"
ext
=
"
${
fname
:e
}
"
# For dotfiles without extension, use filename
if
[[
-z
"
$ext
"
]]
;
then
ext
=
"
$fname
"
fi
ext_count[
$ext
]=
$((
${
ext_count
[
$ext
]
:-
0
}
+
1
))
done
# Human-readable size
if
[[
$total_bytes
-gt
1048576
]]
;
then
total_size
=
"
$((
total_bytes
/
1048576
))
.
$((
(
total_bytes
%
1048576
)
*
10
/
1048576
))
MB"
elif
[[
$total_bytes
-gt
1024
]]
;
then
total_size
=
"
$((
total_bytes
/
1024
))
KB"
else
total_size
=
"
${
total_bytes
}
B"
fi
echo
"
${
GREEN
}
✓
${#
collected_files
[@]
}
files,
${
total_lines
}
lines,
${
total_size
}${
NC
}
"
echo
"
${
CYAN
}
[3/4]
${
NC
}
📊 Sorting file types..."
# Build sorted type breakdown string
type_breakdown
=
""
for
ext
in
"
${
(@k)ext_count
}
"
;
do
type_breakdown+
=
"
${
ext_count
[
$ext
]
}
.
${
ext
}
\n
"
done
type_breakdown
=
$(
echo
"
$type_breakdown
"
|
sort
-rn
)
echo
"
${
GREEN
}
✓ Found
${#
ext_count
[@]
}
different file types
${
NC
}
"
# ── PHASE 3: WRITE OUTPUT ──────────────────────────────────────────────────
echo
"
${
CYAN
}
[4/4]
${
NC
}
✍️ Writing
${
OUTPUT_FILE
}
..."
{
cat
<<
'
HEADER_ART
'
################################################################################
# #
# ██████╗ ██████╗ ██████╗ ███████╗██████╗ █████╗ ███████╗███████╗ #
# ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝██╔════╝ #
# ██║ ██║ ██║██║ ██║█████╗ ██████╔╝███████║███████╗█████╗ #
# ██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██╔══██║╚════██║██╔══╝ #
# ╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝██║ ██║███████║███████╗ #
# ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ #
# #
# COMPLETE CODEBASE DUMP — EVERY FILE, EVERY LINE #
# #
################################################################################
HEADER_ART
echo
""
echo
"=============================================================================="
echo
" PROJECT CODEBASE — FULL SOURCE DUMP"
echo
"=============================================================================="
echo
""
echo
" Generated:
$TIMESTAMP
"
echo
" Source Dir:
$ABS_TARGET
"
echo
" Total Files:
${#
collected_files
[@]
}
"
echo
" Total Lines:
${
total_lines
}
"
echo
" Total Size:
${
total_size
}
"
echo
""
echo
" THIS FILE CONTAINS THE COMPLETE CODEBASE INCLUDING:"
echo
" • All source code files (every language found)"
echo
" • All configuration files (json, yaml, toml, xml, ini, etc.)"
echo
" • All environment files (.env, .env.local, .env.production, etc.)"
echo
" • All Docker files (Dockerfile, docker-compose.yml, .dockerignore)"
echo
" • All CI/CD configs (Jenkinsfile, GitHub Actions, etc.)"
echo
" • All build configs (webpack, vite, tsconfig, Makefile, etc.)"
echo
" • All package manifests (package.json, Cargo.toml, go.mod, etc.)"
echo
" • All lock files (package-lock.json, yarn.lock, etc.)"
echo
" • All documentation (README, CHANGELOG, LICENSE, etc.)"
echo
" • All scripts (shell, python, etc.)"
echo
""
echo
" STRUCTURE OF THIS FILE:"
echo
" 1. DIRECTORY TREE"
echo
" 2. FILE MAP (indexed table of every file)"
echo
" 3. FILE TYPE BREAKDOWN (stats by extension)"
echo
" 4. SKIPPED FILES (binary/oversized — for transparency)"
echo
" 5. COMPLETE FILE CONTENTS (every file printed in full)"
echo
""
echo
"=============================================================================="
echo
""
echo
""
# ── SECTION 1: DIRECTORY TREE ───────────────────────────────────────────
echo
"╔══════════════════════════════════════════════════════════════════════════════╗"
echo
"║ SECTION 1: DIRECTORY TREE ║"
echo
"╚══════════════════════════════════════════════════════════════════════════════╝"
echo
""
if
command
-v
tree &>/dev/null
;
then
tree_ignore
=
$(
printf
"%s|"
"
${
SKIP_DIRS
[@]
}
"
)
tree_ignore
=
"
${
tree_ignore
%|
}
"
tree
-a
-I
"
$tree_ignore
"
--charset
=
utf-8
"
$ABS_TARGET
"
2>/dev/null
||
echo
"(tree output failed)"
else
echo
"(For a visual tree: brew install tree)"
echo
""
echo
"Collected file paths:"
for
file
in
"
${
collected_files
[@]
}
"
;
do
relpath
=
"
${
file
#
$ABS_TARGET
/
}
"
echo
"
$relpath
"
done
fi
echo
""
echo
""
# ── SECTION 2: FILE MAP ─────────────────────────────────────────────────
echo
"╔══════════════════════════════════════════════════════════════════════════════╗"
echo
"║ SECTION 2: FILE MAP — INDEXED LIST OF ALL
${#
collected_files
[@]
}
FILES"
echo
"╚══════════════════════════════════════════════════════════════════════════════╝"
echo
""
echo
" Each file below appears in SECTION 5 with full contents."
echo
" Use [###] index to jump to any file."
echo
""
printf
" %-6s %-7s %-8s %s
\n
"
"INDEX"
"LINES"
"SIZE"
"FILE PATH"
printf
" %-6s %-7s %-8s %s
\n
"
"-----"
"-----"
"------"
"----------------------------------------------"
idx
=
1
for
file
in
"
${
collected_files
[@]
}
"
;
do
relpath
=
"
${
file
#
$ABS_TARGET
/
}
"
lines
=
$(
wc
-l
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
bytes
=
$(
stat
-f
%z
"
$file
"
2>/dev/null
||
wc
-c
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
if
[[
$bytes
-gt
1024
]]
;
then
size
=
"
$((
bytes
/
1024
))
KB"
else
size
=
"
${
bytes
}
B"
fi
printf
" [%03d] %-7s %-8s %s
\n
"
"
$idx
"
"
$lines
"
"
$size
"
"
$relpath
"
idx
=
$((
idx
+
1
))
done
echo
""
echo
""
# ── SECTION 3: FILE TYPE BREAKDOWN ──────────────────────────────────────
echo
"╔══════════════════════════════════════════════════════════════════════════════╗"
echo
"║ SECTION 3: FILE TYPE BREAKDOWN ║"
echo
"╚══════════════════════════════════════════════════════════════════════════════╝"
echo
""
printf
" %-25s %s
\n
"
"EXTENSION/TYPE"
"COUNT"
printf
" %-25s %s
\n
"
"───────────────────────"
"─────"
echo
"
$type_breakdown
"
|
while
read
count ext
;
do
[[
-z
"
$count
"
]]
&&
continue
printf
" %-25s %s
\n
"
"
$ext
"
"
$count
"
done
echo
""
echo
""
# ── SECTION 4: SKIPPED FILES ────────────────────────────────────────────
if
[[
${#
skipped_binary
[@]
}
-gt
0
||
${#
skipped_size
[@]
}
-gt
0
]]
;
then
echo
"╔══════════════════════════════════════════════════════════════════════════════╗"
echo
"║ SECTION 4: SKIPPED FILES (listed for completeness) ║"
echo
"╚══════════════════════════════════════════════════════════════════════════════╝"
echo
""
if
[[
${#
skipped_binary
[@]
}
-gt
0
]]
;
then
echo
" Binary files (not text):"
for
f
in
"
${
skipped_binary
[@]
}
"
;
do
echo
" ⊘
$f
"
done
echo
""
fi
if
[[
${#
skipped_size
[@]
}
-gt
0
]]
;
then
echo
" Oversized files (>
${
MAX_FILE_SIZE_KB
}
KB):"
for
f
in
"
${
skipped_size
[@]
}
"
;
do
echo
" ⊘
$f
"
done
echo
""
fi
echo
""
fi
# ── SECTION 5: FILE CONTENTS ────────────────────────────────────────────
echo
"╔══════════════════════════════════════════════════════════════════════════════╗"
echo
"║ SECTION 5: COMPLETE FILE CONTENTS ║"
echo
"║ ║"
echo
"║ Every file printed in full with: ║"
echo
"║ • Clear start/end markers ║"
echo
"║ • File path, size, line count in the header ║"
echo
"║ • Language hint for syntax context ║"
echo
"╚══════════════════════════════════════════════════════════════════════════════╝"
echo
""
echo
""
idx
=
1
for
file
in
"
${
collected_files
[@]
}
"
;
do
relpath
=
"
${
file
#
$ABS_TARGET
/
}
"
lines
=
$(
wc
-l
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
bytes
=
$(
stat
-f
%z
"
$file
"
2>/dev/null
||
wc
-c
<
"
$file
"
2>/dev/null |
tr
-d
' '
)
lang
=
$(
get_language_hint
"
$file
"
)
echo
"┌──────────────────────────────────────────────────────────────────────────────"
echo
"│ 📄 FILE [
$(
printf
'%03d'
$idx
)
/
${#
collected_files
[@]
}
]:
$relpath
"
echo
"│ LANGUAGE:
$lang
| LINES:
$lines
| SIZE:
${
bytes
}
bytes"
echo
"├──────────────────────────────────────────────────────────────────────────────"
echo
"│"
if
[[
-s
"
$file
"
]]
;
then
cat
-n
"
$file
"
2>/dev/null
||
echo
"│ [ERROR: Could not read file]"
else
echo
"│ [EMPTY FILE]"
fi
echo
"│"
echo
"└──────────────────────────────────────────────────────────────────────────────"
echo
" ✅ END OF [
$(
printf
'%03d'
$idx
)
]:
$relpath
"
echo
""
echo
""
# Progress to stderr
if
((
idx % 25
==
0
))
;
then
echo
" ...
${
idx
}
/
${#
collected_files
[@]
}
files written ..."
>
&2
fi
idx
=
$((
idx
+
1
))
done
# ── FOOTER ──────────────────────────────────────────────────────────────
echo
""
echo
"################################################################################"
echo
"# #"
echo
"# ✅ END OF COMPLETE CODEBASE DUMP #"
echo
"# #"
printf
"# Total Files: %-58s #
\n
"
"
${#
collected_files
[@]
}
"
printf
"# Total Lines: %-58s #
\n
"
"
${
total_lines
}
"
printf
"# Total Size: %-58s #
\n
"
"
${
total_size
}
"
printf
"# Generated: %-58s #
\n
"
"
${
TIMESTAMP
}
"
echo
"# #"
echo
"# This file contains EVERYTHING: source code, configs, env vars, Docker, #"
echo
"# CI/CD, build tools, package manifests, docs — the complete picture. #"
echo
"# #"
echo
"################################################################################"
}
>
"
$OUTPUT_FILE
"
# ── DONE ────────────────────────────────────────────────────────────────────
output_bytes
=
$(
stat
-f
%z
"
$OUTPUT_FILE
"
2>/dev/null
||
wc
-c
<
"
$OUTPUT_FILE
"
|
tr
-d
' '
)
if
[[
$output_bytes
-gt
1048576
]]
;
then
output_human
=
"
$((
output_bytes
/
1048576
))
MB"
elif
[[
$output_bytes
-gt
1024
]]
;
then
output_human
=
"
$((
output_bytes
/
1024
))
KB"
else
output_human
=
"
${
output_bytes
}
B"
fi
echo
""
echo
"
${
BOLD
}${
GREEN
}
╔══════════════════════════════════════════════════════════════╗
${
NC
}
"
echo
"
${
BOLD
}${
GREEN
}
║ ✅ DONE — ALL COLLECTED ║
${
NC
}
"
echo
"
${
BOLD
}${
GREEN
}
╚══════════════════════════════════════════════════════════════╝
${
NC
}
"
echo
""
echo
"
${
BOLD
}
Output:
${
NC
}
$OUTPUT_FILE
"
echo
"
${
BOLD
}
Output size:
${
NC
}
$output_human
"
echo
"
${
BOLD
}
Files:
${
NC
}
${#
collected_files
[@]
}
"
echo
"
${
BOLD
}
Total lines:
${
NC
}
${
total_lines
}
"
echo
""
echo
"
${
YELLOW
}
Feed this to any AI and it'll know your ENTIRE codebase.
${
NC
}
"
echo
""
\ No newline at end of file
FULL_CODEBASE.txt
0 → 100644
View file @
18d64a46
This source diff could not be displayed because it is too large. You can
view the blob
instead.
backend/main.py
View file @
18d64a46
...
...
@@ -21,10 +21,30 @@ from backend.routes.files_routes import router as files_router
from
backend.services.bedrock_service
import
close_http_client
def
_run_migrations
():
"""Add new columns to existing tables if they're missing (lightweight migration)."""
from
sqlalchemy
import
inspect
,
text
try
:
inspector
=
inspect
(
engine
)
if
"chats"
in
inspector
.
get_table_names
():
columns
=
{
c
[
"name"
]
for
c
in
inspector
.
get_columns
(
"chats"
)}
with
engine
.
connect
()
as
conn
:
if
"max_tokens"
not
in
columns
:
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN max_tokens INTEGER DEFAULT 4096"
))
print
(
" ✅ Added chats.max_tokens column"
)
if
"reasoning_budget"
not
in
columns
:
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"
))
print
(
" ✅ Added chats.reasoning_budget column"
)
conn
.
commit
()
except
Exception
as
e
:
print
(
f
" ⚠️ Migration note: {e}"
)
@
asynccontextmanager
async
def
lifespan
(
app
:
FastAPI
):
# --- Startup ---
Base
.
metadata
.
create_all
(
bind
=
engine
)
_run_migrations
()
seed_superadmin
()
print
(
"🔥 Son of Anton is online."
)
yield
...
...
backend/models.py
View file @
18d64a46
...
...
@@ -49,6 +49,8 @@ class Chat(Base):
title
=
Column
(
String
(
200
),
default
=
"New Chat"
)
model
=
Column
(
String
(
100
),
default
=
"eu.anthropic.claude-opus-4-6-v1"
)
knowledge_base_id
=
Column
(
String
(
36
),
nullable
=
True
)
max_tokens
=
Column
(
Integer
,
default
=
4096
)
reasoning_budget
=
Column
(
Integer
,
default
=
0
)
created_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
)
updated_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
,
onupdate
=
datetime
.
utcnow
)
...
...
backend/routes/chat_routes.py
View file @
18d64a46
...
...
@@ -24,10 +24,16 @@ class CreateChatBody(BaseModel):
title
:
str
=
"New Chat"
model
:
str
=
"eu.anthropic.claude-opus-4-6-v1"
knowledge_base_id
:
Optional
[
str
]
=
None
max_tokens
:
int
=
4096
reasoning_budget
:
int
=
0
class
RenameChatBody
(
BaseModel
):
title
:
str
class
UpdateChatBody
(
BaseModel
):
title
:
Optional
[
str
]
=
None
model
:
Optional
[
str
]
=
None
max_tokens
:
Optional
[
int
]
=
None
reasoning_budget
:
Optional
[
int
]
=
None
knowledge_base_id
:
Optional
[
str
]
=
None
class
SendMessageBody
(
BaseModel
):
...
...
@@ -57,7 +63,9 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db
user_id
=
user
.
id
,
title
=
body
.
title
,
model
=
body
.
model
,
knowledge_base_id
=
body
.
knowledge_base_id
,
knowledge_base_id
=
body
.
knowledge_base_id
or
None
,
max_tokens
=
body
.
max_tokens
,
reasoning_budget
=
body
.
reasoning_budget
,
)
db
.
add
(
chat
)
db
.
commit
()
...
...
@@ -74,11 +82,21 @@ def get_chat(chat_id: str, user: User = Depends(get_current_user), db: Session =
@
router
.
put
(
"/{chat_id}"
)
def
rename_chat
(
chat_id
:
str
,
body
:
Renam
eChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
update_chat
(
chat_id
:
str
,
body
:
Updat
eChatBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
)
chat
.
title
=
body
.
title
if
body
.
title
is
not
None
:
chat
.
title
=
body
.
title
if
body
.
model
is
not
None
:
chat
.
model
=
body
.
model
if
body
.
max_tokens
is
not
None
:
chat
.
max_tokens
=
body
.
max_tokens
if
body
.
reasoning_budget
is
not
None
:
chat
.
reasoning_budget
=
body
.
reasoning_budget
if
body
.
knowledge_base_id
is
not
None
:
# Empty string means "clear the KB"
chat
.
knowledge_base_id
=
body
.
knowledge_base_id
or
None
db
.
commit
()
return
_chat_dict
(
chat
)
...
...
@@ -219,6 +237,12 @@ async def send_message(
db
.
add
(
assistant_msg
)
db_user
.
tokens_used_this_month
+=
input_tokens
+
output_tokens
# Persist the generation settings used for this message onto the chat
chat
.
model
=
model_id
chat
.
max_tokens
=
body
.
max_tokens
chat
.
reasoning_budget
=
body
.
reasoning_budget
chat
.
knowledge_base_id
=
body
.
knowledge_base_id
or
None
chat
.
updated_at
=
datetime
.
utcnow
()
db
.
commit
()
...
...
@@ -264,6 +288,8 @@ def _chat_dict(c: Chat) -> dict:
"title"
:
c
.
title
,
"model"
:
c
.
model
,
"knowledge_base_id"
:
c
.
knowledge_base_id
,
"max_tokens"
:
c
.
max_tokens
or
4096
,
"reasoning_budget"
:
c
.
reasoning_budget
or
0
,
"created_at"
:
str
(
c
.
created_at
),
"updated_at"
:
str
(
c
.
updated_at
),
}
...
...
backend/routes/knowledge_routes.py
View file @
18d64a46
"""
Knowledge base management and document upload.
Knowledge base management and document upload
(supports multiple files at once)
.
"""
from
pydantic
import
BaseModel
...
...
@@ -73,50 +73,80 @@ def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session =
@
router
.
post
(
"/{kb_id}/upload"
)
async
def
upload_document
(
async
def
upload_document
s
(
kb_id
:
str
,
file
:
UploadFile
=
File
(
...
),
file
s
:
list
[
UploadFile
]
=
File
(
...
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Upload one or more documents to a knowledge base."""
kb
=
_get_kb
(
kb_id
,
user
,
db
)
content_bytes
=
await
file
.
read
()
if
len
(
content_bytes
)
>
MAX_UPLOAD_BYTES
:
raise
HTTPException
(
413
,
f
"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB"
)
results
=
[]
total_new_docs
=
0
total_new_chunks
=
0
total_new_chars
=
0
filename
=
file
.
filename
or
"document.txt"
text
=
_extract_text
(
filename
,
content_bytes
)
if
not
text
.
strip
():
raise
HTTPException
(
400
,
"Could not extract text from file"
)
chunks
=
_chunk_text
(
text
,
chunk_size
=
3000
,
overlap
=
300
)
rag_service
.
add_documents
(
collection_id
=
kb_id
,
documents
=
chunks
,
metadatas
=
[{
"filename"
:
filename
,
"chunk_index"
:
i
}
for
i
in
range
(
len
(
chunks
))],
)
doc
=
KnowledgeDocument
(
knowledge_base_id
=
kb_id
,
filename
=
filename
,
file_size
=
len
(
content_bytes
),
chunk_count
=
len
(
chunks
),
)
db
.
add
(
doc
)
for
file
in
files
:
filename
=
file
.
filename
or
"document.txt"
try
:
content_bytes
=
await
file
.
read
()
if
len
(
content_bytes
)
>
MAX_UPLOAD_BYTES
:
results
.
append
({
"filename"
:
filename
,
"error"
:
f
"File too large. Max {MAX_UPLOAD_BYTES // 1024 // 1024}MB"
,
})
continue
text
=
_extract_text
(
filename
,
content_bytes
)
if
not
text
.
strip
():
results
.
append
({
"filename"
:
filename
,
"error"
:
"Could not extract text from file"
})
continue
chunks
=
_chunk_text
(
text
,
chunk_size
=
3000
,
overlap
=
300
)
rag_service
.
add_documents
(
collection_id
=
kb_id
,
documents
=
chunks
,
metadatas
=
[{
"filename"
:
filename
,
"chunk_index"
:
i
}
for
i
in
range
(
len
(
chunks
))],
)
doc
=
KnowledgeDocument
(
knowledge_base_id
=
kb_id
,
filename
=
filename
,
file_size
=
len
(
content_bytes
),
chunk_count
=
len
(
chunks
),
)
db
.
add
(
doc
)
total_new_docs
+=
1
total_new_chunks
+=
len
(
chunks
)
total_new_chars
+=
len
(
text
)
results
.
append
({
"filename"
:
filename
,
"chunks_added"
:
len
(
chunks
),
"characters"
:
len
(
text
),
"estimated_tokens"
:
len
(
text
)
//
4
,
})
except
HTTPException
as
e
:
results
.
append
({
"filename"
:
filename
,
"error"
:
str
(
e
.
detail
)})
except
Exception
as
e
:
results
.
append
({
"filename"
:
filename
,
"error"
:
str
(
e
)})
kb
.
document_count
=
(
kb
.
document_count
or
0
)
+
1
kb
.
chunk_count
=
(
kb
.
chunk_count
or
0
)
+
len
(
chunks
)
kb
.
total_characters
=
(
kb
.
total_characters
or
0
)
+
len
(
text
)
# Update KB aggregate stats
kb
.
document_count
=
(
kb
.
document_count
or
0
)
+
total_new_docs
kb
.
chunk_count
=
(
kb
.
chunk_count
or
0
)
+
total_new_chunks
kb
.
total_characters
=
(
kb
.
total_characters
or
0
)
+
total_new_chars
db
.
commit
()
return
{
"file
name"
:
filename
,
"
chunks_added"
:
len
(
chunk
s
),
"
characters"
:
len
(
text
)
,
"
estimated_tokens"
:
len
(
text
)
//
4
,
"file
s"
:
results
,
"
total_files"
:
len
(
result
s
),
"
total_chunks_added"
:
total_new_chunks
,
"
total_characters"
:
total_new_chars
,
}
...
...
backend/services/memory_service.py
View file @
18d64a46
...
...
@@ -3,31 +3,40 @@ Build the `messages` list for the Bedrock/Anthropic API from chat history.
Keeps the most recent messages that fit within a character budget
(rough proxy for tokens — 1 token ≈ 4 chars).
Limits the total number of DB rows loaded to prevent hangs on very long chats.
"""
from
sqlalchemy.orm
import
Session
from
backend.models
import
Chat
,
Message
# ~180 000 tokens budget → ~720 000 characters
MAX_CONTEXT_CHARS
=
720_000
# ~100 000 tokens budget → ~400 000 characters
MAX_CONTEXT_CHARS
=
400_000
# Hard cap: never load more than this many messages from the DB
MAX_MESSAGES
=
80
def
build_messages
(
chat
:
Chat
,
db
:
Session
)
->
list
[
dict
]:
"""
Return a list of {"role": ..., "content": ...} ready for the API.
Messages are oldest-first (chronological).
Only the most recent MAX_MESSAGES are considered, then trimmed by char budget.
"""
# Fetch the most recent N messages (descending) then reverse to chronological
rows
:
list
[
Message
]
=
(
db
.
query
(
Message
)
.
filter
(
Message
.
chat_id
==
chat
.
id
)
.
order_by
(
Message
.
created_at
.
asc
())
.
order_by
(
Message
.
created_at
.
desc
())
.
limit
(
MAX_MESSAGES
)
.
all
()
)
rows
.
reverse
()
if
not
rows
:
return
[]
# --- trim from the oldest to fit budget ---
# --- trim from the oldest to fit
character
budget ---
total_chars
=
sum
(
len
(
m
.
content
or
""
)
for
m
in
rows
)
idx
=
0
while
total_chars
>
MAX_CONTEXT_CHARS
and
idx
<
len
(
rows
)
-
2
:
...
...
frontend/src/api.js
View file @
18d64a46
...
...
@@ -37,8 +37,11 @@ export const listChats = (token) =>
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
export
const
updateChat
=
(
token
,
chatId
,
data
)
=>
request
(
"PUT"
,
`/chats/
${
chatId
}
`
,
token
,
data
);
export
const
renameChat
=
(
token
,
chatId
,
title
)
=>
request
(
"PUT"
,
`/chats/
${
chatId
}
`
,
token
,
{
title
});
updateChat
(
token
,
chatId
,
{
title
});
export
const
deleteChat
=
(
token
,
chatId
)
=>
request
(
"DELETE"
,
`/chats/
${
chatId
}
`
,
token
);
...
...
@@ -106,9 +109,11 @@ export const getKnowledgeBase = (token, kbId) =>
export
const
deleteKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
async
function
uploadDocument
(
token
,
kbId
,
file
)
{
export
async
function
uploadDocument
s
(
token
,
kbId
,
files
)
{
const
form
=
new
FormData
();
form
.
append
(
"file"
,
file
);
for
(
const
file
of
files
)
{
form
.
append
(
"files"
,
file
);
}
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
{
Authorization
:
`Bearer
${
token
}
`
},
...
...
@@ -121,6 +126,10 @@ export async function uploadDocument(token, kbId, file) {
return
res
.
json
();
}
// Backward-compat wrapper for single file
export
const
uploadDocument
=
(
token
,
kbId
,
file
)
=>
uploadDocuments
(
token
,
kbId
,
[
file
]);
/* ── Admin ─────────────────────────────────── */
export
const
adminStats
=
(
token
)
=>
request
(
"GET"
,
"/admin/stats"
,
token
);
...
...
frontend/src/components/ChatView.jsx
View file @
18d64a46
import
React
,
{
useState
,
useEffect
,
useRef
,
useCallback
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
getMessages
,
streamMessage
,
downloadZip
,
listKnowledgeBases
,
getMessages
,
streamMessage
,
downloadZip
,
listKnowledgeBases
,
updateChat
,
}
from
"../api"
;
import
MessageBubble
from
"./MessageBubble"
;
import
{
...
...
@@ -15,27 +15,47 @@ const MODELS = [
export
default
function
ChatView
({
chatId
})
{
const
{
state
,
dispatch
}
=
useApp
();
// ── Load persisted settings from the chat object ──
const
currentChat
=
state
.
chats
.
find
((
c
)
=>
c
.
id
===
chatId
);
const
[
messages
,
setMessages
]
=
useState
([]);
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
streaming
,
setStreaming
]
=
useState
(
false
);
const
[
showSettings
,
setShowSettings
]
=
useState
(
false
);
const
[
model
,
setModel
]
=
useState
(
MODELS
[
0
].
id
);
const
[
maxTokens
,
setMaxTokens
]
=
useState
(
4096
);
const
[
reasoningBudget
,
setReasoningBudget
]
=
useState
(
0
);
const
[
selectedKbId
,
setSelectedKbId
]
=
useState
(
null
);
const
[
model
,
setModel
]
=
useState
(
currentChat
?.
model
||
MODELS
[
0
].
id
);
const
[
maxTokens
,
setMaxTokens
]
=
useState
(
currentChat
?.
max_tokens
||
4096
);
const
[
reasoningBudget
,
setReasoningBudget
]
=
useState
(
currentChat
?.
reasoning_budget
??
0
);
const
[
selectedKbId
,
setSelectedKbId
]
=
useState
(
currentChat
?.
knowledge_base_id
||
null
);
const
[
kbs
,
setKbs
]
=
useState
([]);
const
[
streamText
,
setStreamText
]
=
useState
(
""
);
const
[
streamThinking
,
setStreamThinking
]
=
useState
(
""
);
const
[
isThinking
,
setIsThinking
]
=
useState
(
false
);
const
bottom
Ref
=
useRef
(
null
);
const
scrollContainer
Ref
=
useRef
(
null
);
const
inputRef
=
useRef
(
null
);
const
abortRef
=
useRef
(
null
);
const
shouldAutoScrollRef
=
useRef
(
true
);
const
rafRef
=
useRef
(
null
);
// ── Scroll helpers ──
function
handleContainerScroll
()
{
const
el
=
scrollContainerRef
.
current
;
if
(
!
el
)
return
;
const
{
scrollHeight
,
scrollTop
,
clientHeight
}
=
el
;
shouldAutoScrollRef
.
current
=
scrollHeight
-
scrollTop
-
clientHeight
<
200
;
}
const
scroll
=
useCallback
(()
=>
{
bottomRef
.
current
?.
scrollIntoView
({
behavior
:
"smooth"
});
const
scrollToBottom
=
useCallback
(()
=>
{
if
(
!
shouldAutoScrollRef
.
current
)
return
;
if
(
rafRef
.
current
)
return
;
// already scheduled
rafRef
.
current
=
requestAnimationFrame
(()
=>
{
const
el
=
scrollContainerRef
.
current
;
if
(
el
)
el
.
scrollTop
=
el
.
scrollHeight
;
rafRef
.
current
=
null
;
});
},
[]);
// ── Load messages & KBs on mount ──
useEffect
(()
=>
{
(
async
()
=>
{
try
{
...
...
@@ -47,12 +67,39 @@ export default function ChatView({ chatId }) {
})();
},
[
chatId
,
state
.
token
]);
useEffect
(
scroll
,
[
messages
,
streamText
,
streamThinking
,
scroll
]);
// Scroll when messages change or stream updates
useEffect
(
scrollToBottom
,
[
messages
,
streamText
,
streamThinking
,
scrollToBottom
]);
useEffect
(()
=>
{
inputRef
.
current
?.
focus
();
},
[
chatId
]);
// ── Save settings to backend ──
async
function
saveSettings
()
{
const
data
=
{
model
,
max_tokens
:
maxTokens
,
reasoning_budget
:
reasoningBudget
,
knowledge_base_id
:
selectedKbId
||
""
,
};
try
{
await
updateChat
(
state
.
token
,
chatId
,
data
);
dispatch
({
type
:
"UPDATE_CHAT"
,
chat
:
{
id
:
chatId
,
model
,
max_tokens
:
maxTokens
,
reasoning_budget
:
reasoningBudget
,
knowledge_base_id
:
selectedKbId
},
});
}
catch
{
/* ignore */
}
}
function
toggleSettings
()
{
const
closing
=
showSettings
;
setShowSettings
(
!
showSettings
);
if
(
closing
)
{
saveSettings
();
}
}
// ── Send message ──
async
function
handleSend
()
{
const
content
=
input
.
trim
();
if
(
!
content
||
streaming
)
return
;
...
...
@@ -64,6 +111,7 @@ export default function ChatView({ chatId }) {
setStreamText
(
""
);
setStreamThinking
(
""
);
setIsThinking
(
false
);
shouldAutoScrollRef
.
current
=
true
;
// Force scroll for user's own message
const
ac
=
new
AbortController
();
abortRef
.
current
=
ac
;
...
...
@@ -126,6 +174,12 @@ export default function ChatView({ chatId }) {
created_at
:
new
Date
().
toISOString
(),
};
setMessages
((
p
)
=>
[...
p
,
assistantMsg
]);
// Sync settings to store (backend already saved them from the message)
dispatch
({
type
:
"UPDATE_CHAT"
,
chat
:
{
id
:
chatId
,
model
,
max_tokens
:
maxTokens
,
reasoning_budget
:
reasoningBudget
,
knowledge_base_id
:
selectedKbId
},
});
}
catch
(
err
)
{
setMessages
((
p
)
=>
[
...
p
,
...
...
@@ -163,7 +217,11 @@ export default function ChatView({ chatId }) {
return
(
<
div
className=
"flex-1 flex flex-col min-h-0"
>
{
/* Messages */
}
<
div
className=
"flex-1 overflow-y-auto px-4 py-4 space-y-4"
>
<
div
ref=
{
scrollContainerRef
}
onScroll=
{
handleContainerScroll
}
className=
"flex-1 overflow-y-auto px-4 py-4 space-y-4"
>
{
messages
.
map
((
m
)
=>
(
<
MessageBubble
key=
{
m
.
id
}
message=
{
m
}
/>
))
}
...
...
@@ -192,8 +250,6 @@ export default function ChatView({ chatId }) {
<
span
className=
"text-anton-muted text-sm"
>
Son of Anton is thinking…
</
span
>
</
div
>
)
}
<
div
ref=
{
bottomRef
}
/>
</
div
>
{
/* Input area */
}
...
...
@@ -205,7 +261,7 @@ export default function ChatView({ chatId }) {
<
h3
className=
"text-sm font-semibold text-white flex items-center gap-1.5"
>
<
Settings2
size=
{
14
}
className=
"text-anton-accent"
/>
Generation Settings
</
h3
>
<
button
onClick=
{
()
=>
setShowSettings
(
false
)
}
className=
"text-anton-muted hover:text-white"
><
X
size=
{
14
}
/></
button
>
<
button
onClick=
{
toggleSettings
}
className=
"text-anton-muted hover:text-white"
><
X
size=
{
14
}
/></
button
>
</
div
>
{
/* Model */
}
...
...
@@ -270,7 +326,7 @@ export default function ChatView({ chatId }) {
)
}
<
div
className=
"flex items-end gap-2"
>
<
button
onClick=
{
()
=>
setShowSettings
(
!
showSettings
)
}
<
button
onClick=
{
toggleSettings
}
className=
{
`p-2.5 rounded-xl transition shrink-0 ${
showSettings ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white hover:bg-anton-card"
}`
}
...
...
frontend/src/components/MessageBubble.jsx
View file @
18d64a46
...
...
@@ -4,7 +4,7 @@ import remarkGfm from "remark-gfm";
import
CodeBlock
from
"./CodeBlock"
;
import
{
User
,
Flame
,
ChevronDown
,
ChevronRight
,
Brain
,
Copy
,
Check
}
from
"lucide-react"
;
export
default
function
MessageBubble
({
message
,
isStreaming
,
isThinking
})
{
const
MessageBubble
=
React
.
memo
(
function
MessageBubble
({
message
,
isStreaming
,
isThinking
})
{
const
{
role
,
content
,
thinking_content
,
input_tokens
,
output_tokens
}
=
message
;
const
isUser
=
role
===
"user"
;
const
[
showThinking
,
setShowThinking
]
=
useState
(
false
);
...
...
@@ -133,4 +133,6 @@ export default function MessageBubble({ message, isStreaming, isThinking }) {
)
}
</
div
>
);
}
\ No newline at end of file
});
export
default
MessageBubble
;
\ No newline at end of file
frontend/src/components/Sidebar.jsx
View file @
18d64a46
...
...
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import
{
useApp
}
from
"../store"
;
import
{
createChat
,
deleteChat
,
renameChat
,
listKnowledgeBases
,
createKnowledgeBase
,
deleteKnowledgeBase
,
uploadDocument
,
listKnowledgeBases
,
createKnowledgeBase
,
deleteKnowledgeBase
,
uploadDocument
s
,
}
from
"../api"
;
import
{
Plus
,
Trash2
,
Flame
,
LogOut
,
Shield
,
PanelLeftClose
,
PanelLeftOpen
,
...
...
@@ -20,6 +20,7 @@ export default function Sidebar({ onRefresh }) {
const
[
showNewKb
,
setShowNewKb
]
=
useState
(
false
);
const
[
expandedKb
,
setExpandedKb
]
=
useState
(
null
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
uploadCount
,
setUploadCount
]
=
useState
(
0
);
const
[
renamingId
,
setRenamingId
]
=
useState
(
null
);
const
[
renameVal
,
setRenameVal
]
=
useState
(
""
);
...
...
@@ -75,15 +76,25 @@ export default function Sidebar({ onRefresh }) {
}
catch
{
/* */
}
}
async
function
handleUpload
(
kbId
,
file
)
{
async
function
handleUpload
(
kbId
,
file
s
)
{
setUploading
(
true
);
setUploadCount
(
files
.
length
);
try
{
await
uploadDocument
(
state
.
token
,
kbId
,
file
);
const
result
=
await
uploadDocuments
(
state
.
token
,
kbId
,
files
);
// Check for per-file errors
const
errors
=
(
result
.
files
||
[]).
filter
((
f
)
=>
f
.
error
);
if
(
errors
.
length
>
0
)
{
alert
(
`Uploaded
${
result
.
files
.
length
-
errors
.
length
}
of
${
result
.
files
.
length
}
files.\n\nErrors:\n`
+
errors
.
map
((
e
)
=>
`•
${
e
.
filename
}
:
${
e
.
error
}
`
).
join
(
"
\n
"
)
);
}
loadKbs
();
}
catch
(
e
)
{
alert
(
e
.
message
);
}
finally
{
setUploading
(
false
);
setUploadCount
(
0
);
}
}
...
...
@@ -221,9 +232,17 @@ export default function Sidebar({ onRefresh }) {
</
div
>
<
label
className=
{
`flex items-center gap-1.5 px-2 py-1.5 rounded border border-dashed border-anton-border text-xs text-anton-muted hover:text-anton-accent hover:border-anton-accent transition cursor-pointer ${uploading ? "opacity-50 pointer-events-none" : ""}`
}
>
<
Upload
size=
{
12
}
/>
{
uploading
?
"Uploading…"
:
"Upload file (.txt, .pdf, .md, .json, .csv …)"
}
<
input
type=
"file"
className=
"hidden"
accept=
".txt,.md,.pdf,.json,.csv,.py,.js,.ts,.cs,.html,.css,.xml,.yaml,.yml,.toml"
onChange=
{
(
e
)
=>
e
.
target
.
files
[
0
]
&&
handleUpload
(
kb
.
id
,
e
.
target
.
files
[
0
])
}
{
uploading
?
`Uploading ${uploadCount} file${uploadCount !== 1 ? "s" : ""}…`
:
"Upload files (.txt, .pdf, .md, .json, .csv …)"
}
<
input
type=
"file"
className=
"hidden"
multiple
accept=
".txt,.md,.pdf,.json,.csv,.py,.js,.ts,.cs,.html,.css,.xml,.yaml,.yml,.toml"
onChange=
{
(
e
)
=>
{
if
(
e
.
target
.
files
&&
e
.
target
.
files
.
length
>
0
)
{
handleUpload
(
kb
.
id
,
Array
.
from
(
e
.
target
.
files
));
}
e
.
target
.
value
=
""
;
}
}
/>
</
label
>
<
button
onClick=
{
()
=>
handleDeleteKb
(
kb
.
id
)
}
...
...
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