Commit 18d64a46 authored by Mahmoud Aglan's avatar Mahmoud Aglan

ko

parent 44c66e72
......@@ -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.14" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (son-of-anton)" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
<?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
#!/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
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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
......
......@@ -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)
......
......@@ -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: RenameChatBody, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
def update_chat(chat_id: str, body: UpdateChatBody, 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),
}
......
"""
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_documents(
kb_id: str,
file: UploadFile = File(...),
files: 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 {
"filename": filename,
"chunks_added": len(chunks),
"characters": len(text),
"estimated_tokens": len(text) // 4,
"files": results,
"total_files": len(results),
"total_chunks_added": total_new_chunks,
"total_characters": total_new_chars,
}
......
......@@ -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:
......
......@@ -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 uploadDocuments(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);
......
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 bottomRef = useRef(null);
const scrollContainerRef = 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"
}`}
......
......@@ -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
......@@ -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, uploadDocuments,
} 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, files) {
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)}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment