Compare commits

..

145 commits

Author SHA1 Message Date
d8d57edd84 feat(daisyui): added that and fixed eslint
Some checks failed
Build and Deploy / Build Images and Deploy to Server (push) Has been cancelled
jeez theres a lot of garbage code in here
2025-11-07 11:26:36 -06:00
d6378b8eb1
fix(repo): we start over, its really that time
Some checks failed
Build and Deploy / Build Images and Deploy to Server (push) Has been cancelled
2025-10-25 13:28:40 -05:00
Christbru
8ed519b89a fix broken reference 2025-10-19 12:51:20 -05:00
Christbru
3781e4507a Fix deployment, verified with officer as ok 2025-10-19 12:25:58 -05:00
Christbru
3c8d08dc03 Fix files 2025-10-19 11:59:13 -05:00
Christbru
18081a28ba Fix worker 2025-10-19 11:49:47 -05:00
Christbru
995cfbd9b0 Merge branch 'rust-dev' 2025-10-19 11:37:43 -05:00
Christbru
95ce7b343c Fixing 2025-10-19 11:35:28 -05:00
Christbru
44b6faaeb9 Fix routing 2025-10-19 11:32:41 -05:00
Christbru
4a2a9a7489 Correct react implementation of rust backend 2025-10-19 11:26:17 -05:00
Christbru
9035e00da3 Patch files not being passed properly 2025-10-19 11:12:06 -05:00
Christbru
b0d4eb86c1 Merge branch 'rust-dev' 2025-10-19 11:03:27 -05:00
Christbru
94d3a90438 Add fallback workflow 2025-10-19 10:59:32 -05:00
Christbru
7a5cbc3549 Cleared old table usage 2025-10-19 10:48:12 -05:00
Christbru
feb87873fb Correct user uid for container app execution 2025-10-19 10:35:47 -05:00
Christbru
1412f8ac1f Fix permissions issue 2025-10-19 10:29:45 -05:00
Christbru
ee41c3fbd3 Correct demo data volume access 2025-10-19 10:20:22 -05:00
Christbru
18b96382f2 Correct demo data importing. Add significant debugging. 2025-10-19 10:04:34 -05:00
Christbru
e479439bb4 Remove c from ci due to lock file issues 2025-10-19 09:43:37 -05:00
Christbru
606c1dff53 Prep worker management corrections and debug 2025-10-19 09:40:59 -05:00
5c16d12f77 Merge branch 'stuff' 2025-10-19 09:24:02 -05:00
eb48d61dcf feature: chatgpt 2025-10-19 09:22:48 -05:00
yenminh269
586e77de2f Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-19 09:07:34 -05:00
94f87b9fee fixed heaer 2025-10-19 09:06:48 -05:00
c46fdc5f5e fixed heaer 2025-10-19 08:49:17 -05:00
Christbru
933f6297a8 Merge branch 'rust-dev' 2025-10-19 08:39:15 -05:00
Christbru
60f7e644ef Cleared daisyui 2025-10-19 08:37:15 -05:00
Minh Ho
c67cbb148b
Merge pull request #2 from devaine/mincy
"making the delete button works"
2025-10-19 08:32:22 -05:00
yenminh269
a6d9d8b5e8 Merge branch 'main' of https://github.com/devaine/CodeRED-Astra into mincy 2025-10-19 08:31:24 -05:00
Christbru
9888e2dd0a Merge branch 'rust-dev' 2025-10-19 08:25:26 -05:00
Christbru
3fc2beed58 Correcting demo data import and the button interaction 2025-10-19 08:24:07 -05:00
yenminh269
d1cddf2690 "making the delete button works" 2025-10-19 08:22:59 -05:00
yenminh269
ded3e57e29 Merge branch 'main' of https://github.com/devaine/CodeRED-Astra into mincy 2025-10-19 08:17:53 -05:00
yenminh269
3f893e71b0 "add index.js" 2025-10-19 08:17:22 -05:00
337703c4a0 Merge branch 'diagram' 2025-10-19 08:12:29 -05:00
Christbru
893e857fd5 Remove unneeded powershell script for local development prep 2025-10-19 08:12:12 -05:00
542ba32cda feat(down-button): added down button func
Now you don't have to scroll all the way down yay
2025-10-19 08:11:54 -05:00
Christbru
ba4444cc11 Merge branch 'rust-dev' 2025-10-19 08:03:46 -05:00
Christbru
724fb69c87 Correct for wildcard tightening 2025-10-19 07:56:32 -05:00
309e0f6b73 Merge branch 'main' into diagram 2025-10-19 07:52:47 -05:00
8917a4d1a5 feat(filelist): added file list ui
Drop down menu that shows it all
2025-10-19 07:52:13 -05:00
Christbru
1bd7a716fd Updated package lock to clear npm ci with github action 2025-10-19 07:43:09 -05:00
Christbru
4ddd3e2dce Allow rust-dev to trigger deployment to prevent frequent main merging. 2025-10-19 07:34:46 -05:00
Christbru
d4c4fb2fe7 Attempt to fix server side for react handling 2025-10-19 07:33:47 -05:00
Christbru
9ff012dd1d Preparing proper gemini integrations for the api handlers. Bridging database building gaps in the flow. 2025-10-19 07:22:13 -05:00
Christbru
a9dd6c032e Corrected sql communication issues causing rust panic 2025-10-19 07:00:47 -05:00
Christbru
db99d625c4 Merge remote-tracking branch 'origin/mincy' 2025-10-19 06:18:51 -05:00
Christbru
38532056e5 Corrected storage issue by adding a docker volume for file persistance 2025-10-19 06:18:36 -05:00
yenminh269
7b0e86ea1b fix syntax 2025-10-19 06:16:07 -05:00
Christbru
e44bc95a4e Merge remote-tracking branch 'origin/diagram' 2025-10-19 06:02:01 -05:00
Christbru
de632411a8 Merge branch 'rust-dev' 2025-10-19 05:57:52 -05:00
Christbru
a03969e497 Prepared demo files and demo explanation file. Added debug only button to trigger demo file ingest on the server to queue and prepare the files. Added small expressjs server for talking between the web app and the rust engine containers. 2025-10-19 05:55:41 -05:00
Christbru
54169c2b93 Merge branch 'rust-dev' 2025-10-19 05:02:20 -05:00
Christbru
381b7b8858 Implement file information processing queue system and Vector Graph preperation 2025-10-19 05:02:01 -05:00
7785047976 feat(file input): adding feature to add and remove files
Next up is to create a seperate menu that will show all the files.
Probably as a seperate frame. Needs to be scrollable and can use similar
ui as before
2025-10-19 04:32:32 -05:00
Christbru
b61b6fa884 Merge branch 'rust-dev' 2025-10-19 04:14:20 -05:00
Christbru
8cda296143 Bringing build docker to latest to mitigate known security vulnerabilities 2025-10-19 04:11:59 -05:00
Christbru
af2367d8f3 Merge branch 'rust-dev' 2025-10-19 04:03:57 -05:00
Christbru
1912ab2e53 Add qdrant docker to server build file 2025-10-19 03:54:11 -05:00
Christbru
da6ab3a782 Add and prepare rust worker management system for file information processing and knowledge base framework 2025-10-19 03:53:02 -05:00
yenminh269
11e7d9b140 reverting to original padding 2025-10-19 03:24:19 -05:00
yenminh269
ed079e9b2c change the padding; git testing and learning, will revert 2025-10-19 03:07:17 -05:00
Christbru
af82b71657 Add environment key to build task list. 2025-10-19 01:47:50 -05:00
Christbru
855fea6b66 Patch allowedHosts 2025-10-19 01:34:32 -05:00
Christbru
d037f85d83 Add domain to allowed hosts 2025-10-19 01:32:16 -05:00
Christbru
533e223a39 Migrate to host attachment. 2025-10-19 01:15:32 -05:00
Christbru
16eb5d3775 Migrate web app access through cloudflare tunnel at astrachat.christbru.services 2025-10-19 00:53:34 -05:00
Christbru
5eebd85357 Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-19 00:41:19 -05:00
Christbru
dde800c31a Attach web-app to direct ip 2025-10-19 00:41:17 -05:00
b8a087945e style(markdown): last thing i swear 2025-10-19 00:24:58 -05:00
7ef0c87ded Merge branch 'diagram' 2025-10-19 00:01:38 -05:00
607b25aa62 refactor(markdown): changed markdown conf loc 2025-10-19 00:00:08 -05:00
Christbru
529c1eef2b Eliminate deploytest branch and remove the database url from the info logs 2025-10-18 23:57:20 -05:00
Christbru
ea38d51e25 Eliminate deploytest branch as it is no longer needed. 2025-10-18 23:57:18 -05:00
Christbru
840767adff Merge branch 'deploytest' 2025-10-18 23:55:46 -05:00
6fd667edb2 feat(markdown): made gemini render in markdown 2025-10-18 23:48:04 -05:00
Christbru
493d6644f2 Attach to internet interface directly due to wildcard attaching to already in use internal port 2025-10-18 23:38:38 -05:00
Christbru
40a3f96df4 Move port opening to proper container 2025-10-18 23:37:21 -05:00
Christbru
50ca52e781 Add wildcard ip attachment to bind to all interfaces. 2025-10-18 23:35:55 -05:00
Christbru
721296c7f1 Clear improper --no-cache flag and make minor change to attempt to get github to clear the bugged cache for that file 2025-10-18 23:32:35 -05:00
Christbru
c95ccda282 Expose mysql port to internet for dev purposes during hackathon 2025-10-18 23:24:32 -05:00
76cb0e0536
feat: adding gemini stuff 2025-10-18 23:23:11 -05:00
Christbru
01c1594df1 Force no build cache to clear the bad placeholder file 2025-10-18 23:22:00 -05:00
af7a246e77 feat(skematic): added skematic button 2025-10-18 23:20:00 -05:00
Christbru
d2c6fe7aec Bring chain versions up to proper version and bring dependencies up to current version. 2025-10-18 22:25:55 -05:00
41d0451cce style(chat): fixed chat layout + added shadows 2025-10-18 22:18:18 -05:00
Christbru
86b878cd60 Correct improper dummy file being built and served. 2025-10-18 22:13:58 -05:00
Christbru
ed31642cd3 Fix format issue. 2025-10-18 22:09:21 -05:00
Christbru
833b304002 Correct container execution user to appropriate account on the server 2025-10-18 22:06:55 -05:00
Minh Ho
b214242d38
Merge pull request #1 from devaine/mincy
"add delete button functionality"
2025-10-18 21:51:07 -05:00
yenminh269
0f12533873 "add delete button functionality" 2025-10-18 21:47:00 -05:00
3661874789 style(footer+header): made both sticky 2025-10-18 21:39:02 -05:00
Christbru
47fc78057d Fix deployment: properly stop containers before recreating
- Add compose down before pull/up to avoid container name conflicts
- Improve toolchain caching: only install if not already present
- Add log directory mount for easy error monitoring
- Scope Buildx cache per image for better hit rates
2025-10-18 21:37:47 -05:00
Christbru
88f79356f2 Correct log implementations 2025-10-18 21:31:49 -05:00
Christbru
17679c609e Pull errors out into the main system for easy tailing 2025-10-18 21:21:48 -05:00
yenminh269
2d7637ebcf Merge: Resolve conflicts by accepting incoming changes 2025-10-18 21:18:46 -05:00
Christbru
6df73ca465 Migrate to non root container for best practice and to clear security warnings. 2025-10-18 21:13:03 -05:00
Christbru
a32e0dd474 Deploy: fix GHCR auth and compose warnings
- Pass RUNNER envs to remote SSH action so docker login ghcr.io works
- Remove obsolete `version` from docker-compose.prod.yml to avoid warnings
2025-10-18 21:08:20 -05:00
Christbru
61f2cef7a8 rust-engine: improve Docker caching
- Install pinned toolchain from rust-toolchain.toml in a cacheable layer
- Remove target cache mount from warm-up step so deps persist in image layers
- Keep final build using only registry/git mounts for speed and artifact persistence
2025-10-18 21:03:27 -05:00
Christbru
5ca801bdd1 Switch to GITHUB_TOKEN allowing only the action to trigger an update. Not necessary if public but doesn't harm anything and can cover if private. 2025-10-18 20:54:55 -05:00
Christbru
c9186ea923 Update the deploy 2025-10-18 20:47:35 -05:00
Christbru
9cd450e849 Add key to pull private repo data for cloud execution 2025-10-18 20:47:25 -05:00
yenminh269
4989d30ec3 edit add, delete button 2025-10-18 20:42:09 -05:00
Christbru
e8a971e879 Fix rust version mismatch issues 2025-10-18 20:32:30 -05:00
848f27b29b Merge branch 'buttons' into diagram 2025-10-18 20:32:20 -05:00
c9327b3ec3 feat(buttons): added flame and down button 2025-10-18 20:31:35 -05:00
Christbru
2075c252f6 Correct key usage back to normal 2025-10-18 20:19:56 -05:00
Christbru
47bcd0aa5f Fix cache build issue. 2025-10-18 20:08:42 -05:00
d044eb5b06
feat: added basic gemini functionality 2025-10-18 20:06:10 -05:00
5e0de6e894 style(colors): changed colors
using slate and grey colors for the most part with violot bc why not
2025-10-18 20:04:02 -05:00
Christbru
f730609f62 Patch ssh handling for authentication. 2025-10-18 20:03:27 -05:00
Christbru
60ac72d41f Only delete the main if it is the dummy file for dependency caching 2025-10-18 19:39:05 -05:00
Christbru
cfd2bac2ae Rustup already installed 2025-10-18 19:28:11 -05:00
Christbru
fc3e2b92e9 Revert cargo lock ignore 2025-10-18 19:25:56 -05:00
Christbru
0eca3ae87c Add multiple layers to mitigate dependency issues. 2025-10-18 19:15:33 -05:00
71c87e8468 style(everything): fuck you 2025-10-18 19:15:31 -05:00
Christbru
734ecf8293 Correct docker setup. 2025-10-18 19:11:34 -05:00
Christbru
151a354518 Remove problematic caching systems 2025-10-18 19:03:11 -05:00
Christbru
066bab50ba Provide dummy main file to get through dependency validation then pass source files in to take advantage of github action caching to significantly speed up pushes. 2025-10-18 18:59:55 -05:00
Christbru
eb17df8788 Merge branch 'deploytest' of https://github.com/devaine/CodeRED-Astra into deploytest 2025-10-18 18:54:21 -05:00
Christbru
e49a90cf9f Update rust docker to account for regenerating lock file. 2025-10-18 18:54:18 -05:00
Christian
0b4125f3ca
Delete rust-engine/Cargo.lock
Remove old lock file now that gitignore no longer tracks it.
2025-10-18 18:51:28 -05:00
Christbru
59c9d1d271 Force the rust server to generate it's own lock file. 2025-10-18 18:49:40 -05:00
Christbru
f3ecc01385 Corrected cargo dependency versions. 2025-10-18 18:40:03 -05:00
Christbru
72b08a7bce Reverted to stable version of dependencies. 2025-10-18 18:39:23 -05:00
yenminh269
807d1f27ac Merge branch 'main' of https://github.com/devaine/CodeRED-Astra into mincy 2025-10-18 18:36:17 -05:00
Christbru
413c9766b9 Correct cargo file issue. 2025-10-18 18:36:17 -05:00
yenminh269
3bcd4087de style send button 2025-10-18 18:35:52 -05:00
Christbru
691d2f09fd Switching off to other branch for testing deployment. main no longer triggers deployment at the moment. 2025-10-18 18:31:23 -05:00
c4fc66e520 deleted that stupid default vite svg 2025-10-18 18:29:53 -05:00
Christbru
e3e2b2b501 Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-18 18:26:34 -05:00
Christbru
f424a319ba Updated version to current Cargo. 2025-10-18 18:26:18 -05:00
e9e5804ea1 Merge branch 'eslint' 2025-10-18 18:23:17 -05:00
Christbru
32b7cc5ccb Fixed rust dependency issues. 2025-10-18 18:21:38 -05:00
42a012ca9c fix(eslint): installed one dep for eslint 2025-10-18 18:21:29 -05:00
Christbru
97d373ed87 Correct chat-header import error 2025-10-18 18:14:57 -05:00
Christbru
657a46f601 Fix button structure issue 2025-10-18 18:11:46 -05:00
Christbru
55898b7bdd Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-18 18:09:13 -05:00
Christbru
5778b63e8e Correcting branch case issue for deployment building. 2025-10-18 18:07:39 -05:00
yenminh269
074ad0c3c3 "fix the absolute path in index.jsx" 2025-10-18 18:04:33 -05:00
Christbru
fa78ffc1b7 Converts repository name to lowercase which is necessary for the build to work with docker container registry. 2025-10-18 17:53:58 -05:00
yenminh269
dc8203967a Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-18 17:48:05 -05:00
yenminh269
5a9da85f07 Merge branch 'main' of https://github.com/devaine/CodeRED-Astra 2025-10-18 17:46:54 -05:00
Christbru
64a70357eb Removed complex "Extract metadata" tagging which was causing build failures. Replaced it with simpler but still effective method. 2025-10-18 17:46:39 -05:00
yenminh269
3df311634d "combine button components" 2025-10-18 17:44:11 -05:00
46 changed files with 10243 additions and 10692 deletions

View file

@ -2,14 +2,12 @@
name: Build and Deploy
# This workflow runs only on pushes to the 'main' branch
on:
push:
branches: ["main"]
branches: ["main", "rust-dev"]
jobs:
build-and-deploy:
# Set permissions for the job to read contents and write to GitHub Packages
permissions:
contents: read
packages: write
@ -21,6 +19,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set repo name to lowercase
id: repo_name
run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
@ -28,53 +30,81 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# --- NEW STEP TO FIX THE CACHING ERROR ---
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}/web-app
ghcr.io/${{ github.repository }}/rust-engine
- name: Create web-app .env file
run: echo 'GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}' > web-app/.env
# --- Build and push one image for each service ---
- name: Build and push web-app image 🚀
uses: docker/build-push-action@v6
with:
context: ./web-app
push: true
tags: ${{ steps.meta.outputs.tags_web-app }}
labels: ${{ steps.meta.outputs.labels_web-app }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/${{ steps.repo_name.outputs.name }}/web-app:${{ github.sha }}
cache-from: type=gha,scope=web-app
cache-to: type=gha,mode=max,scope=web-app
- name: Build and push Rust engine image ⚙️
uses: docker/build-push-action@v6
with:
context: ./rust-engine
push: true
tags: ${{ steps.meta.outputs.tags_rust-engine }}
labels: ${{ steps.meta.outputs.labels_rust-engine }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/${{ steps.repo_name.outputs.name }}/rust-engine:${{ github.sha }}
cache-from: type=gha,scope=rust-engine
cache-to: type=gha,mode=max,scope=rust-engine
# --- Deploy the new images to your server ---
- name: Deploy to server via SSH ☁️
- name: Ensure remote deploy directory exists
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/codered-astra
mkdir -p /home/github-actions/codered-astra
- name: Upload compose files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "docker-compose.yml,docker-compose.prod.yml,rust-engine/demo-data"
target: "/home/github-actions/codered-astra/"
- name: Deploy to server via SSH ☁️
uses: appleboy/ssh-action@v1.0.3
env:
RUNNER_GH_ACTOR: ${{ github.actor }}
RUNNER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
# pass selected env vars to the remote shell so docker login works
envs: RUNNER_GITHUB_TOKEN,RUNNER_GH_ACTOR
debug: true
script: |
cd /home/github-actions/codered-astra
chmod -R o+rX rust-engine/demo-data
# wrapper to support both Docker Compose v2 and legacy v1
compose() { docker compose "$@" || docker-compose "$@"; }
# Log in to GHCR using the run's GITHUB_TOKEN so compose can pull images.
if [ -n "$RUNNER_GITHUB_TOKEN" ] && [ -n "$RUNNER_GH_ACTOR" ]; then
echo "$RUNNER_GITHUB_TOKEN" | docker login ghcr.io -u "$RUNNER_GH_ACTOR" --password-stdin || true
fi
export REPO_NAME_LOWER='${{ steps.repo_name.outputs.name }}'
export GEMINI_API_KEY='${{ secrets.GEMINI_API_KEY }}'
export MYSQL_DATABASE='${{ secrets.MYSQL_DATABASE }}'
export MYSQL_USER='${{ secrets.MYSQL_USER }}'
export MYSQL_PASSWORD='${{ secrets.MYSQL_PASSWORD }}'
export MYSQL_ROOT_PASSWORD='${{ secrets.MYSQL_ROOT_PASSWORD }}'
export IMAGE_TAG=${{ github.sha }}
docker-compose pull
docker-compose up -d --force-recreate
# Stop and remove old containers before pulling new images
compose -f docker-compose.prod.yml down
# Clear previous logs for a clean deployment log
: > ~/astra-logs/astra-errors.log || true
compose -f docker-compose.prod.yml pull
compose -f docker-compose.prod.yml up -d
# Security hygiene: remove GHCR credentials after pulling
docker logout ghcr.io || true

View file

@ -1,75 +0,0 @@
# CodeRED-Astra 🚀
A hackathon-ready project with React frontend and Rust backend engine.
## Quick Start
```bash
# 1. Setup environment
cp .env.example .env
# Edit .env with your credentials
# 2. Start everything with Docker
docker-compose up --build
# 3. Access your app
# Frontend: http://localhost
# API: http://localhost:8000
# Database Admin: http://127.0.0.1:8080
```
## Development
**Frontend (React + Vite)**:
```bash
cd web-app
npm install
npm run dev # http://localhost:5173
```
**Backend (Rust)**:
```bash
cd rust-engine
cargo run # http://localhost:8000
```
## Architecture
- **Frontend**: React 18 + Vite + Tailwind CSS
- **Backend**: Rust + Warp + SQLx
- **Database**: MySQL 8.0 + phpMyAdmin
- **API**: RESTful endpoints with CORS enabled
- **Docker**: Full containerization for easy deployment
## Project Structure
```
├── web-app/ # React frontend
│ ├── src/
│ │ ├── App.jsx # Main component
│ │ └── main.jsx # Entry point
│ └── Dockerfile
├── rust-engine/ # Rust backend
│ ├── src/
│ │ └── main.rs # API server
│ └── Dockerfile
├── docker-compose.yml # Full stack orchestration
└── .env.example # Environment template
```
## Team Workflow
- **Frontend devs**: Work in `web-app/src/`, use `/api/*` for backend calls
- **Backend devs**: Work in `rust-engine/src/`, add endpoints to main.rs
- **Database**: Access phpMyAdmin at http://127.0.0.1:8080
## Features
✅ Hot reload for both frontend and backend
✅ Automatic API proxying from React to Rust
✅ Database connection with graceful fallback
✅ CORS configured for cross-origin requests
✅ Production-ready Docker containers
✅ Health monitoring and status dashboard
Ready for your hackathon! See `DEVELOPMENT.md` for detailed setup instructions.

View file

@ -13,6 +13,8 @@ services:
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
- RUST_ENGINE_URL=http://rust-engine:8000
- GEMINI_API_KEY=${GEMINI_API_KEY}
volumes:
- rust-storage:/app/storage:ro
depends_on:
- mysql # <-- Updated dependency
- rust-engine
@ -23,8 +25,16 @@ services:
restart: always
environment:
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
- ASTRA_STORAGE=/app/storage
- DEMO_DATA_DIR=/app/demo-data
- QDRANT_URL=http://qdrant:6333
- GEMINI_API_KEY=${GEMINI_API_KEY}
volumes:
- rust-storage:/app/storage
- ./rust-engine/demo-data:/app/demo-data:ro
depends_on:
- mysql # <-- Updated dependency
- mysql
- qdrant
# --- Key Changes are in this section ---
mysql: # <-- Renamed service for clarity
@ -49,5 +59,18 @@ services:
depends_on:
- mysql
qdrant:
image: qdrant/qdrant:latest
restart: unless-stopped
ports:
- "127.0.0.1:6333:6333"
volumes:
- qdrant-data:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334
# expose to rust-engine via service name 'qdrant'
volumes:
mysql-data: # Renamed volume for clarity (optional but good practice)
mysql-data: # Renamed volume for clarity (optional but good practice)
qdrant-data:
rust-storage:

9
frontend/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm i
COPY . .
RUN npm run format && npm run build
EXPOSE 3000
CMD ["node", "server.mjs"]

1
frontend/README.md Normal file
View file

@ -0,0 +1 @@

35
frontend/eslint.config.js Normal file
View file

@ -0,0 +1,35 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
export default [
{
ignores: ["dist/**"],
},
js.configs.recommended,
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
},
},
];

View file

@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/src/index.css" rel="stylesheet" />
<title>codered-astra</title>
</head>
<body>

9245
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

55
frontend/package.json Normal file
View file

@ -0,0 +1,55 @@
{
"name": "codered-astra",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite --host 0.0.0.0 --port 3000",
"host": "vite --host 0.0.0.0 --port 3000",
"preview": "vite preview --host 0.0.0.0 --port 3000",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"clean-dist": "find apps/ -type d -name 'dist' -print0 | xargs -r0 -- rm -r",
"clean-all": "find apps/ -type d -name 'dist' -print0 | xargs -r0 -- rm -r && find . -path ./node_modules -prune -o -name 'node_modules' | xargs rm -rf "
},
"license": "ISC",
"dependencies": {
"@google/genai": "^1.25.0",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react-swc": "^3.7.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"class-variance-authority": "^0.7.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"helmet": "^8.1.0",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"node-fetch": "^3.3.2",
"pg": "^8.16.3",
"react": "^19.2.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router": "^7.9.4",
"react-router-dom": "^7.9.4",
"shadcn-ui": "^0.9.5",
"vite-jsconfig-paths": "^2.0.1"
},
"packageManager": ">=npm@10.9.0",
"devDependencies": {
"@eslint/js": "^9.38.0",
"daisyui": "^5.4.7",
"eslint": "^9.38.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.4.0",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.14",
"vite": "^7.1.10"
}
}

View file

@ -3,8 +3,9 @@ import ChatLayout from "src/components/layouts/chat-layout";
function App() {
return (
<div className="min-h-screen bg-slate-900 text-white flex items-center justify-center p-6">
<div className="dark min-h-screen bg-gray-950 text-white flex justify-center pt-12">
<ChatLayout />
<div></div>
</div>
);
}

View file

@ -0,0 +1,244 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import ChatHeader from "src/components/ui/chat/chat-header";
import ChatWindow from "src/components/ui/chat/chat-window";
import MessageInput from "src/components/ui/chat/message-input";
import {
createQuery,
getQueryResult,
getQueryStatus,
listFiles,
} from "src/lib/api";
const createId = () =>
globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random()}`;
const INTRO_MESSAGE = {
id: "intro",
role: "assistant",
content:
"Ask me about the demo PDFs and I'll respond with the best matches pulled from the processed files.",
};
export default function ChatLayout() {
const [messages, setMessages] = useState([INTRO_MESSAGE]);
const [isProcessing, setIsProcessing] = useState(false);
const [files, setFiles] = useState([]);
const [errorToast, setErrorToast] = useState("");
const pollAbortRef = useRef(null);
const showError = useCallback((message) => {
setErrorToast(message);
window.setTimeout(() => setErrorToast(""), 5000);
}, []);
const refreshFiles = useCallback(async () => {
try {
const latest = await listFiles();
setFiles(latest);
} catch (error) {
showError(error.message ?? "Failed to load files");
}
}, [showError]);
useEffect(() => {
refreshFiles();
}, [refreshFiles]);
useEffect(() => {
return () => {
if (pollAbortRef.current) {
pollAbortRef.current.aborted = true;
}
};
}, []);
const buildAssistantMarkdown = useCallback((result) => {
if (!result || typeof result !== "object") {
return "I could not find a response for that request.";
}
const finalAnswer = result.final_answer?.trim();
const relationships = result.relationships?.trim();
const relatedFiles = Array.isArray(result.related_files)
? result.related_files
: [];
const fileLines = relatedFiles
.filter((f) => f && typeof f === "object")
.map((file) => {
const filename = file.filename || file.id || "download";
const storageUrl = file.storage_url || `/storage/${filename}`;
const linkTarget = storageUrl.startsWith("/storage/")
? `/storage/${encodeURIComponent(storageUrl.replace("/storage/", ""))}`
: storageUrl;
const description = file.description?.trim();
const score =
typeof file.score === "number"
? ` _(score: ${file.score.toFixed(3)})_`
: "";
const detail = description ? `${description}` : "";
return `- [${filename}](${linkTarget})${detail}${score}`;
});
let content =
finalAnswer ||
"I could not determine an answer from the indexed documents yet.";
if (fileLines.length) {
content += `\n\n**Related Files**\n${fileLines.join("\n")}`;
}
if (relationships && relationships !== finalAnswer) {
content += `\n\n---\n${relationships}`;
}
if (!fileLines.length && (!finalAnswer || finalAnswer.length < 10)) {
content +=
"\n\n_No analyzed documents matched yet. Try seeding demo data or wait for processing to finish._";
}
return content;
}, []);
const waitForResult = useCallback(async (id) => {
const abortState = { aborted: false };
pollAbortRef.current = abortState;
const timeoutMs = 120_000;
const intervalMs = 1_500;
const started = Date.now();
while (!abortState.aborted) {
if (Date.now() - started > timeoutMs) {
throw new Error("Timed out waiting for the query to finish");
}
const statusPayload = await getQueryStatus(id);
const status = statusPayload?.status;
if (status === "Completed") {
const resultPayload = await getQueryResult(id);
return resultPayload?.result;
}
if (status === "Failed") {
const resultPayload = await getQueryResult(id);
const reason = resultPayload?.result?.error || "Query failed";
throw new Error(reason);
}
if (status === "Cancelled") {
throw new Error("Query was cancelled");
}
if (status === "not_found") {
throw new Error("Query was not found");
}
await new Promise((resolve) => window.setTimeout(resolve, intervalMs));
}
throw new Error("Query polling was aborted");
}, []);
const handleSend = useCallback(
async (text) => {
if (isProcessing) {
showError("Please wait for the current response to finish.");
return;
}
const userEntry = {
id: createId(),
role: "user",
content: text,
};
setMessages((prev) => [...prev, userEntry]);
const placeholderId = createId();
setMessages((prev) => [
...prev,
{
id: placeholderId,
role: "assistant",
content: "_Analyzing indexed documents..._",
pending: true,
},
]);
setIsProcessing(true);
try {
const payload = { q: text, top_k: 5 };
const created = await createQuery(payload);
const result = await waitForResult(created.id);
const content = buildAssistantMarkdown(result);
setMessages((prev) =>
prev.map((message) =>
message.id === placeholderId
? { ...message, content, pending: false }
: message,
),
);
} catch (error) {
const message = error?.message || "Something went wrong.";
setMessages((prev) =>
prev.map((entry) =>
entry.id === placeholderId
? {
...entry,
content: `⚠️ ${message}`,
pending: false,
error: true,
}
: entry,
),
);
showError(message);
} finally {
pollAbortRef.current = null;
setIsProcessing(false);
refreshFiles();
}
},
[
isProcessing,
showError,
refreshFiles,
waitForResult,
buildAssistantMarkdown,
],
);
const handleDeleteAll = useCallback(() => {
if (!window.confirm("Delete all messages?")) {
return;
}
setMessages([INTRO_MESSAGE]);
}, []);
const latestFileSummary = useMemo(() => {
if (!files.length) return "No files indexed yet.";
const pending = files.filter((f) => f.pending_analysis).length;
const ready = files.length - pending;
return `${ready} ready • ${pending} processing`;
}, [files]);
return (
<div className="flex flex-col flex-start w-full max-w-3xl gap-4 p-4">
<ChatHeader
onClear={handleDeleteAll}
busy={isProcessing}
fileSummary={latestFileSummary}
errorMessage={errorToast}
/>
<ChatWindow messages={messages} />
<MessageInput onSend={handleSend} disabled={isProcessing} />
</div>
);
}

View file

@ -0,0 +1,19 @@
import { Flame } from "lucide-react";
import { motion } from "motion/react";
export default function FlameButton({ onClick, disabled = false }) {
return (
<motion.button
onClick={onClick}
className={`bg-gray-700 p-2 rounded-2xl border-2 border-gray-600 ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
}`}
whileHover={disabled ? undefined : { scale: 1.1 }}
whileTap={disabled ? undefined : { scale: 0.9 }}
disabled={disabled}
style={{ opacity: disabled ? 0.5 : 1 }}
>
<Flame />
</motion.button>
);
}

View file

@ -0,0 +1,24 @@
import React from "react";
import { ArrowDown } from "lucide-react";
import { motion } from "motion/react";
export default function DownButton({ onClick }) {
function handleClick(e) {
if (onClick) return onClick(e);
// default behavior: scroll to bottom of page smoothly
const doc = document.documentElement;
const top = Math.max(doc.scrollHeight, document.body.scrollHeight);
window.scrollTo({ top, behavior: "smooth" });
}
return (
<motion.button
onClick={handleClick}
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<ArrowDown />
</motion.button>
);
}

View file

@ -0,0 +1,33 @@
import React, { forwardRef, useRef } from "react";
import { motion } from "motion/react";
// Hidden file input that exposes an open() method via ref
const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) {
const inputRef = useRef(null);
React.useImperativeHandle(ref, () => ({
open: () => inputRef.current && inputRef.current.click(),
}));
function handleFiles(e) {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
if (onFiles) onFiles(files);
if (inputRef.current) inputRef.current.value = null;
}
return (
<motion.input
ref={inputRef}
type="file"
accept="image/*,application/pdf"
multiple
onChange={handleFiles}
className="file-input hidden"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
/>
);
});
export default SchematicButton;

View file

@ -0,0 +1,89 @@
import React, { useEffect, useMemo, useState } from "react";
import { motion } from "motion/react";
import { Rocket } from "lucide-react";
import DeleteButton from "src/components/ui/button/delete-button";
import SchematicButton from "src/components/ui/button/schematic-button";
export default function ChatHeader({
title = "Title of Chat",
onClear,
busy = false,
fileSummary,
errorMessage,
}) {
const isDebug = useMemo(() => {
const p = new URLSearchParams(window.location.search);
return p.get("debug") === "1";
}, []);
const [ingesting, setIngesting] = useState(false);
const [toast, setToast] = useState("");
const [externalToast, setExternalToast] = useState("");
useEffect(() => {
if (!errorMessage) return;
setExternalToast(errorMessage);
const timer = window.setTimeout(() => setExternalToast(""), 5000);
return () => window.clearTimeout(timer);
}, [errorMessage]);
async function triggerDemoIngest() {
try {
setIngesting(true);
const res = await fetch("/api/files/import-demo", { method: "POST" });
const json = await res.json().catch(() => ({}));
const imported = json.imported ?? "?";
const skipped = json.skipped ?? "?";
const summary = `Imported: ${imported}, Skipped: ${skipped}`;
setToast(json.error ? `${summary} - ${json.error}` : summary);
setTimeout(() => setToast(""), 4000);
} catch (e) {
setToast("Import failed");
setTimeout(() => setToast(""), 4000);
} finally {
setIngesting(false);
}
}
return (
<div className="w-full flex justify-center">
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
<div className="flex justify-between items-center gap-4">
<SchematicButton />
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
{title}
</h1>
{fileSummary && (
<div className="text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-3 py-1">
{fileSummary}
</div>
)}
<DeleteButton onClick={onClear} disabled={busy} />
{isDebug && (
<motion.button
onClick={triggerDemoIngest}
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
disabled={ingesting}
>
<Rocket size={16} />
{ingesting ? "Seeding…" : "Seed Demo Data"}
</motion.button>
)}
</div>
</div>
{toast && (
<div className="mt-2 text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-2 py-1 inline-block">
{toast}
</div>
)}
{externalToast && (
<div className="mt-2 text-xs text-red-300 bg-red-900/40 border border-red-700 rounded px-2 py-1 inline-block">
{externalToast}
</div>
)}
</header>
</div>
);
}

View file

@ -0,0 +1,44 @@
import React, { useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { MARKDOWN_COMPONENTS } from "src/config/markdown";
function MessageBubble({ message }) {
const isUser = message.role === "user";
const isError = !!message.error;
return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"} py-2`}>
<div
className={`p-3 rounded-xl ${isUser ? "bg-indigo-600 text-white rounded-tr-sm" : "bg-gray-700 text-slate-100 rounded-tl-sm"} ${isError ? "border border-red-500/60 bg-red-900/50" : ""}`}
>
{isUser ? (
<div className="text-sm">{message.content}</div>
) : (
<ReactMarkdown components={MARKDOWN_COMPONENTS}>
{message.content}
</ReactMarkdown>
)}
</div>
</div>
);
}
export default function ChatWindow({ messages }) {
const bottomRef = useRef(null);
useEffect(() => {
if (bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [messages]);
return (
<div className="flex-1 overflow-auto px-2 pt-4 pb-32">
<div className="">
{messages.map((m, i) => (
<MessageBubble key={m.id ?? i} message={m} />
))}
<div ref={bottomRef} />
</div>
</div>
);
}

View file

@ -0,0 +1,119 @@
import React, { useState, useRef, useEffect } from "react";
import DownButton from "src/components/ui/button/down-button";
import { motion } from "motion/react";
import { BotMessageSquare } from "lucide-react";
export default function MessageInput({ onSend, disabled = false }) {
const [text, setText] = useState("");
const textareaRef = useRef(null);
useEffect(() => {
// ensure correct initial height
if (textareaRef.current) textareaRef.current.style.height = "auto";
}, []);
async function handleSubmit(e) {
e.preventDefault();
if (!text.trim() || disabled) return;
onSend(text.trim());
// create query on backend
try {
if (onMessage)
onMessage("assistant", "Queued: sending request to server...");
const createRes = await fetch(`/api/query/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ q: text, top_k: 5 }),
});
const createJson = await createRes.json();
const id = createJson.id;
if (!id) throw new Error("no id returned");
// poll status
let status = "Queued";
if (onMessage) onMessage("assistant", `Status: ${status}`);
while (status !== "Completed" && status !== "Failed") {
await new Promise((r) => setTimeout(r, 1000));
const sRes = await fetch(`/api/query/status?id=${id}`);
const sJson = await sRes.json();
status = sJson.status;
if (onMessage) onMessage("assistant", `Status: ${status}`);
if (status === "Cancelled") break;
}
if (status === "Completed") {
const resultRes = await fetch(`/api/query/result?id=${id}`);
const resultJson = await resultRes.json();
const final =
resultJson?.result?.final_answer ||
JSON.stringify(resultJson?.result || {});
if (onMessage) onMessage("assistant", final);
} else {
if (onMessage)
onMessage("assistant", `Query status ended as: ${status}`);
}
} catch (err) {
console.error(err);
if (onMessage) onMessage("assistant", `Error: ${err.message}`);
}
setText("");
}
return (
<div className="w-full flex justify-center">
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
<div className="flex flex-col gap-4">
<div>
<DownButton></DownButton>
</div>
<form
onSubmit={handleSubmit}
className="bg-gray-900 rounded-2xl border-2 border-gray-800 shadow-lg shadow-indigo-600"
>
<div className="flex p-2 shadow-xl items-center">
<textarea
ref={textareaRef}
value={text}
onChange={(e) => {
if (disabled) return;
setText(e.target.value);
// auto-resize
const ta = textareaRef.current;
if (ta) {
ta.style.height = "auto";
ta.style.height = `${ta.scrollHeight}px`;
}
}}
onKeyDown={(e) => {
// Enter to submit, Shift+Enter for newline
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
}}
placeholder="Type a message..."
rows={1}
className="flex-1 mx-2 rounded-md shadow-2sx border-none focus:border-none focus:outline-none resize-none overflow-auto max-h-40"
disabled={disabled}
/>
<motion.button
type="submit"
className={`flex gap-2 px-4 py-2 bg-gray-700 rounded-xl ml-4 items-center ${
disabled ? "cursor-not-allowed" : ""
}`}
whileHover={disabled ? undefined : { scale: 1.1 }}
whileTap={disabled ? undefined : { scale: 0.9 }}
disabled={disabled}
style={{ opacity: disabled ? 0.5 : 1 }}
>
<BotMessageSquare />
</motion.button>
</div>
</form>
</div>
</footer>
</div>
);
}

View file

@ -0,0 +1,83 @@
import React, { useRef, useState } from "react";
import SchematicButton from "src/components/ui/button/schematic-button";
import { motion } from "motion/react";
import { Menu } from "lucide-react";
import { X } from "lucide-react";
import { FilePlus2 } from "lucide-react";
export default function FileList() {
const pickerRef = useRef(null);
const [open, setOpen] = useState(false);
const [files, setFiles] = useState([]);
function handleAdd() {
if (pickerRef.current && pickerRef.current.open) pickerRef.current.open();
}
function handleFiles(selected) {
setFiles((s) => [...s, ...selected]);
setOpen(true);
}
function removeFile(i) {
setFiles((s) => s.filter((_, idx) => idx !== i));
}
return (
<div className="h-full flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<motion.button
onClick={() => setOpen((v) => !v)}
className="p-2 rounded-xl bg-gray-700 border-2 border-gray-600"
aria-expanded={open}
whileHover={{ scale: 1.1 }}
whileTab={{ scale: 0.9 }}
>
{open ? <X /> : <Menu />}
</motion.button>
</div>
{open && (
<div className="fixed left-1/2 top-24 transform -translate-x-1/2 z-50 w-full max-w-3xl px-4">
<div className="bg-gray-900 border-2 border-gray-800 rounded-2xl p-4 shadow-lg overflow-auto">
<div className="flex items-center justify-between mb-2 pr-1">
<div className="text-lg font-medium">Files</div>
<div>
<motion.button
onClick={handleAdd}
className="w-full bg-gray-700 text-sm p-2 rounded-full border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<FilePlus2 />
</motion.button>
<SchematicButton ref={pickerRef} onFiles={handleFiles} />
</div>
</div>
<div className="flex flex-col gap-2">
{files.length === 0 ? (
<div className="text-md text-slate-400">No files added</div>
) : (
files.map((f, i) => (
<div
key={i}
className="flex items-center justify-between bg-gray-800 p-2 rounded-lg text-sm"
>
<span className="truncate max-w-[24rem]">{f.name}</span>
<button
onClick={() => removeFile(i)}
className="text-xs bg-gray-700 rounded-full p-2"
>
<X />
</button>
</div>
))
)}
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,60 @@
export const MARKDOWN_COMPONENTS = {
h1: ({ node, ...props }) => (
<h1 className="text-xl font-semibold mt-2 mb-1" {...props} />
),
h2: ({ node, ...props }) => (
<h2 className="text-lg font-semibold mt-2 mb-1" {...props} />
),
h3: ({ node, ...props }) => (
<h3 className="text-md font-semibold mt-2 mb-1" {...props} />
),
p: ({ node, ...props }) => (
<p className="text-sm leading-relaxed mb-2" {...props} />
),
a: ({ node, href, ...props }) => (
<a
href={href}
className="text-indigo-300 hover:underline"
target="_blank"
rel="noopener noreferrer"
{...props}
/>
),
code: ({ node, inline, className, children, ...props }) => {
if (inline) {
return (
<code
className={`bg-slate-800 px-1 py-0.5 rounded text-sm ${className || ""}`}
{...props}
>
{children}
</code>
);
}
return (
<pre
className="bg-slate-800 p-2 rounded overflow-auto text-sm"
{...props}
>
<code className={className || ""}>{children}</code>
</pre>
);
},
blockquote: ({ node, ...props }) => (
<blockquote
className="border-l-2 border-slate-600 pl-4 italic text-slate-200 my-2"
{...props}
/>
),
ul: ({ node, ...props }) => (
<ul className="list-disc list-inside ml-4 mb-2 text-sm" {...props} />
),
ol: ({ node, ...props }) => (
<ol className="list-decimal list-inside ml-4 mb-2 text-sm" {...props} />
),
li: ({ node, ...props }) => <li className="mb-1 text-sm" {...props} />,
strong: ({ node, ...props }) => (
<strong className="font-semibold" {...props} />
),
em: ({ node, ...props }) => <em className="italic" {...props} />,
};

View file

@ -0,0 +1,47 @@
import { GoogleGenAI } from "@google/genai"
import fs from "fs"
const ai = new GoogleGenAI({ apiKey: import.meta.env.GEMINI_API_KEY })
async function uploadLocalPDFs() {
var pdfList = fs.readdirSync("public/pdfs")
// Upload each file in /public
pdfList.forEach(async (path) => {
console.log("file names: " + path)
console.log("file names: " + path.slice(0, path.length - 4))
console.log("UPLOADING")
const file = await ai.files.upload({
file: "public/pdfs/" + path,
config: {
displayName: path.slice(0, path.length - 4)
}
})
console.log("FETCHING: public/pdfs/" + path)
// Wait for the file to be processed
let getFile = await ai.files.get({
name: file.name
})
while (getFile.state === "PROCESSING") {
let getFile = await ai.files.get({
name: file.name
})
console.log(`Current file status: ${getFile.state}`)
console.log("File is currently processing, retrying in 5 seconds")
await new Promise((resolve) => {
setTimeout(resolve, 5000) // Checks every 5 seconds
})
// Error handling
if (getFile.state === "FAILED") {
throw new Error("File has failed to process!")
}
return file
}
})
}

23
frontend/src/index.css Normal file
View file

@ -0,0 +1,23 @@
@import "tailwindcss";
@import "daisyui";
.dark {
--paragraph: 235, 236, 239;
--background: 15, 16, 26;
--primary: 158, 166, 214;
--secondary: 35, 50, 133;
--accent: 52, 75, 223;
background: rgba(var(--background));
}
body {
margin: 0;
font-family:
ui-sans-serif,
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial;
}

View file

@ -0,0 +1,5 @@
import type { Config } from "tailwindcss";
export default {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
} satisfies Config;

26
frontend/vite.config.js Normal file
View file

@ -0,0 +1,26 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import jsconfigPaths from "vite-jsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
try {
process.loadEnvFile(".env")
} catch (error) {
console.log("Env file not found!\n" + error)
}
export default defineConfig({
plugins: [tailwindcss(), react(), jsconfigPaths()],
resolve: {
alias: {
src: "/src",
},
},
// Defines envrionmental files across all src code b/c prefix is usually "VITE"
define: {
'import.meta.env.GEMINI_API_KEY': JSON.stringify(process.env.GEMINI_API_KEY),
},
preview: {
allowedHosts: ["astrachat.christbru.services"]
}
});

2390
rust-engine/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
[package]
name = "rust-engine"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
warp = { version = "0.4.2", features = ["server"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "mysql", "chrono"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
dotenv = "0.15"
cors = "0.1.0"
anyhow = "1.0"

View file

@ -1,30 +0,0 @@
# rust-engine/Dockerfile
# --- Stage 1: Builder ---
FROM rust:1.82-slim AS builder
WORKDIR /usr/src/app
# Install build dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy Cargo files for dependency caching
COPY Cargo.toml Cargo.lock ./
# Create a dummy src/main.rs for dependency build
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release && rm src/main.rs
# Copy source code and build
COPY src ./src
RUN cargo build --release
# --- Stage 2: Final Image ---
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/app/target/release/rust-engine /usr/local/bin/rust-engine
EXPOSE 8000
CMD ["rust-engine"]

View file

@ -1,132 +0,0 @@
use std::env;
use warp::Filter;
use sqlx::mysql::MySqlPool;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
#[derive(Debug, Serialize, Deserialize)]
struct HealthResponse {
status: String,
timestamp: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
message: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::fmt::init();
// Load environment variables
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "mysql://astraadmin:password@mysql:3306/astra".to_string());
info!("Starting Rust Engine...");
info!("Connecting to database: {}", database_url);
// Connect to database
let pool = match MySqlPool::connect(&database_url).await {
Ok(pool) => {
info!("Successfully connected to database");
pool
}
Err(e) => {
warn!("Failed to connect to database: {}. Starting without DB connection.", e);
// In a hackathon setting, we might want to continue without DB for initial testing
return start_server_without_db().await;
}
};
// CORS configuration
let cors = warp::cors()
.allow_any_origin()
.allow_headers(vec!["content-type", "authorization"])
.allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]);
// Health check endpoint
let health = warp::path("health")
.and(warp::get())
.map(|| {
let response = HealthResponse {
status: "healthy".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
};
warp::reply::json(&ApiResponse {
success: true,
data: Some(response),
message: None,
})
});
// API routes - you'll expand these for your hackathon needs
let api = warp::path("api")
.and(
health.or(
// Add more routes here as needed
warp::path("version")
.and(warp::get())
.map(|| {
warp::reply::json(&ApiResponse {
success: true,
data: Some("1.0.0"),
message: Some("Rust Engine API".to_string()),
})
})
)
);
let routes = api
.with(cors)
.with(warp::log("rust_engine"));
info!("Rust Engine started on http://0.0.0.0:8000");
warp::serve(routes)
.run(([0, 0, 0, 0], 8000))
.await;
Ok(())
}
async fn start_server_without_db() -> Result<(), Box<dyn std::error::Error>> {
info!("Starting server in DB-less mode for development");
let cors = warp::cors()
.allow_any_origin()
.allow_headers(vec!["content-type", "authorization"])
.allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]);
let health = warp::path("health")
.and(warp::get())
.map(|| {
let response = HealthResponse {
status: "healthy (no db)".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
};
warp::reply::json(&ApiResponse {
success: true,
data: Some(response),
message: Some("Running without database connection".to_string()),
})
});
let routes = warp::path("api")
.and(health)
.with(cors)
.with(warp::log("rust_engine"));
info!("Rust Engine started on http://0.0.0.0:8000 (DB-less mode)");
warp::serve(routes)
.run(([0, 0, 0, 0], 8000))
.await;
Ok(())
}

View file

@ -1,15 +0,0 @@
FROM node:23-alpine
COPY . /codered-astra
WORKDIR /codered-astra
RUN npm i
EXPOSE 3000
RUN npm run format
RUN npm run build
CMD ["npm", "run", "host"]

View file

@ -1,16 +0,0 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View file

@ -1,27 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{js,jsx}"],
extends: [
js.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
rules: {
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
},
},
]);

View file

@ -1,6 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./"
},
"include": ["src"]
}

7692
web-app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,45 +0,0 @@
{
"name": "codered-astra",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite",
"host": "vite host",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"clean-dist": "find apps/ -type d -name 'dist' -print0 | xargs -r0 -- rm -r",
"clean-all": "find apps/ -type d -name 'dist' -print0 | xargs -r0 -- rm -r && find . -path ./node_modules -prune -o -name 'node_modules' | xargs rm -rf "
},
"license": "ISC",
"dependencies": {
"@google/genai": "^1.25.0",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"helmet": "^8.1.0",
"lucide-react": "^0.546.0",
"pg": "^8.16.3",
"react": "^19.2.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0",
"react-router": "^7.9.4",
"react-router-dom": "^7.9.4",
"vite-jsconfig-paths": "^2.0.1"
},
"packageManager": ">=npm@10.9.0",
"devDependencies": {
"eslint": "^9.38.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.24",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.14",
"vite": "^7.1.10"
}
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,34 +0,0 @@
import React, { useState } from "react";
import ChatHeader from "src/components/ui/chat/chat-header";
import ChatWindow from "src/components/ui/chat/chat-window";
import MessageInput from "src/components/ui/chat/message-input";
export default function ChatLayout() {
const [messages, setMessages] = useState([
{
role: "assistant",
content: "Hello — I can help you with code, explanations, and more.",
},
]);
function handleSend(text) {
const userMsg = { role: "user", content: text };
setMessages((s) => [...s, userMsg]);
// fake assistant reply after short delay
setTimeout(() => {
setMessages((s) => [
...s,
{ role: "assistant", content: `You said: ${text}` },
]);
}, 600);
}
return (
<div className="flex flex-col h-[80vh] w-full max-w-3xl mx-auto rounded-lg overflow-hidden shadow-lg border border-slate-700">
<ChatHeader />
<ChatWindow messages={messages} />
<MessageInput onSend={handleSend} />
</div>
);
}

View file

@ -1,19 +0,0 @@
import Button from 'react-bootstrap/Button';
export default function DeleteButton({ onClick, variant = "outline-danger", children, ...props }) {
return (
<Button onClick={onClick} variant={variant} {...props}>
{children || "Delete"}{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-trash3"
viewBox="0 0 16 16"
>
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/>
</svg>
</Button>
);
}

View file

@ -1,13 +0,0 @@
.custom-btn {
background-color: white !important;
border: 2px solid #0F2862 !important;
color: #0F2862 !important;
transition: all 0.25s ease;
}
.custom-btn:hover,
.custom-btn:focus {
background-color: #0F2862 !important;
color: white !important;
border-color: #0F2862 !important;
}

View file

@ -1,19 +0,0 @@
import Button from 'react-bootstrap/Button';
import './NewChatButton.css'
export default function NewChatButton({ onClick, variant = "outline-light", children, ...props }) {
return (
<Button onClick={onClick} className="custom-btn" {...props}>
{children || "New Chat"}{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-plus-lg"
viewBox="0 0 16 16"
>
<path fillRule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2"/>
</svg>
</Button>
);
}

View file

@ -1,19 +0,0 @@
import React from "react";
export default function ChatHeader({ title = "AI Assistant" }) {
return (
<header className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-slate-800 to-slate-900 text-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-500 rounded flex items-center justify-center font-bold">
AI
</div>
<div>
<h1 className="text-lg font-semibold">{title}</h1>
<p className="text-sm text-slate-300">
Ask anything AI is listening
</p>
</div>
</div>
</header>
);
}

View file

@ -1,37 +0,0 @@
import React from "react";
import { useRef } from "react";
function MessageBubble({ message }) {
const isUser = message.role === "user";
return (
<div
className={`flex ${isUser ? "justify-end" : "justify-start"} px-4 py-2`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${isUser ? "bg-indigo-600 text-white" : "bg-slate-700 text-slate-100"}`}
>
<div className="text-sm">{message.content}</div>
</div>
</div>
);
}
export default function ChatWindow({ messages }) {
const chatRef = useRef(null);
// Auto-scroll to bottom when new messages appear
useEffect(() => {
chatRef.current?.scrollTo({
top: chatRef.current.scrollHeight,
behavior: "smooth",
});
}, [messages]);
return (
<main className="flex-1 overflow-auto p-2 bg-gradient-to-b from-slate-900 to-slate-800">
<div className="space-y-2">
{messages.map((m, i) => (
<MessageBubble key={i} message={m} />
))}
</div>
</main>
);
}

View file

@ -1,31 +0,0 @@
import React, { useState } from "react";
export default function MessageInput({ onSend }) {
const [text, setText] = useState("");
function handleSubmit(e) {
e.preventDefault();
if (!text.trim()) return;
onSend(text.trim());
setText("");
}
return (
<form onSubmit={handleSubmit} className="p-3 bg-slate-900">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type a message..."
className="flex-1 rounded-md bg-slate-800 border border-slate-700 px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
type="submit"
className="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md"
>
Send
</button>
</div>
</form>
);
}

View file

@ -1,24 +0,0 @@
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
:root {
--color-primary: 15 40 98;
--color-secondary: 79 95 118;
--color-accent: 158 54 58;
--color-paragraph: 255 255 255;
--color-background: 9 31 54;
}
body {
margin: 0;
background-color: rgb(var(--color-background));
color: rgb(var(--color-paragraph));
font-family:
ui-sans-serif,
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial;
}

View file

@ -1,7 +0,0 @@
@theme {
--color-primary: rgba(15, 40, 98);
--color-secondary: rgba(79, 95, 118);
--color-accent: rgba(158, 54, 58);
--color-paragraph: rgba(255, 255, 255);
--color-background: rgba(9, 31, 54);
}

View file

@ -1,14 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import jsconfigPaths from "vite-jsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [tailwindcss(), react(), jsconfigPaths()],
resolve: {
alias: {
src: "/src",
},
},
});