diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e5aa3c3..d609f17 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -69,7 +69,7 @@ jobs: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USERNAME }} key: ${{ secrets.SSH_PRIVATE_KEY }} - source: "docker-compose.yml,docker-compose.prod.yml" + source: "docker-compose.yml,docker-compose.prod.yml,rust-engine/demo-data" target: "/home/github-actions/codered-astra/" - name: Deploy to server via SSH ☁️ @@ -86,6 +86,7 @@ jobs: 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. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index b61318a..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,268 +0,0 @@ -# CodeRED-Astra Architecture - -## Overview - -CodeRED-Astra is a Retrieval-Augmented Generation (RAG) system for querying ISS technical documentation using vector search, MySQL metadata storage, and Gemini AI for analysis and response generation. - -## System Components - -### 1. **Rust Backend** (`rust-engine/`) -High-performance Rust backend using Warp for HTTP, SQLx for MySQL, and Reqwest for external API calls. - -#### Modules - -**`main.rs`** - Entry point -- Initializes tracing, database, storage -- Spawns FileWorker and QueryWorker background tasks -- Serves API routes on port 8000 - -**`db.rs`** - Database initialization -- Connects to MySQL -- Creates `files` table (id, filename, path, description, pending_analysis, analysis_status) -- Creates `queries` table (id, status, payload, result, timestamps) - -**`api.rs`** - HTTP endpoints -- `POST /api/files` - Upload file (multipart/form-data) -- `POST /api/files/import-demo` - Bulk import from demo-data directory -- `GET /api/files/list` - List all files with status -- `GET /api/files/delete?id=` - Delete file and remove from Qdrant -- `POST /api/query/create` - Create new query (returns query ID) -- `GET /api/query/status?id=` - Check query status -- `GET /api/query/result?id=` - Get query result -- `GET /api/query/cancel?id=` - Cancel in-progress query - -**`file_worker.rs`** - File analysis pipeline -- **Background worker** that processes files with `pending_analysis = TRUE` -- Claims stale/queued files (requeues if stuck >10 min) -- **Stage 1**: Call Gemini 1.5 Flash for initial description -- **Stage 2**: Call Gemini 1.5 Pro for deep vector graph data (keywords, relationships) -- **Stage 3**: Generate embedding and upsert to Qdrant -- **Stage 4**: Mark file as ready (`pending_analysis = FALSE`, `analysis_status = 'Completed'`) -- Resumable: Can recover from crashes/restarts - -**`worker.rs`** - Query processing pipeline -- **Background worker** that processes queries with `status = 'Queued'` -- Requeues stale InProgress jobs (>10 min) -- **Stage 1**: Embed query text -- **Stage 2**: Search top-K similar vectors in Qdrant -- **Stage 3**: Fetch file metadata from MySQL (only completed files) -- **Stage 4**: Call Gemini to analyze relationships between files -- **Stage 5**: Call Gemini for final answer synthesis (strict: no speculation) -- **Stage 6**: Save results to database -- Supports cancellation checks between stages - -**`gemini_client.rs`** - Gemini API integration -- `generate_text(prompt)` - Text generation with model switching via GEMINI_MODEL env var -- `demo_text_embedding(text)` - Demo 64-dim embeddings (replace with real Gemini embeddings) -- Falls back to demo responses if GEMINI_API_KEY not set - -**`vector_db.rs`** - Qdrant client -- `ensure_files_collection(dim)` - Create 'files' collection with Cosine distance -- `upsert_point(id, vector)` - Store file embedding -- `search_top_k(vector, k)` - Find k nearest neighbors -- `delete_point(id)` - Remove file from index - -**`storage.rs`** - File storage utilities -- `storage_dir()` - Get storage path from ASTRA_STORAGE env or default `/app/storage` -- `ensure_storage_dir()` - Create storage directory if missing -- `save_file(filename, contents)` - Save file to storage -- `delete_file(path)` - Remove file from storage - -**`models.rs`** - Data structures -- `FileRecord` - File metadata (mirrors files table) -- `QueryRecord` - Query metadata (mirrors queries table) -- `QueryStatus` enum - Queued, InProgress, Completed, Cancelled, Failed - -### 2. **Web App** (`web-app/`) -React + Vite frontend with Express backend for API proxying. - -#### Backend (`server.mjs`) -- Express server that proxies API calls to rust-engine:8000 -- Serves React static build from `/dist` -- **Why needed**: Docker networking - React can't call rust-engine directly from browser - -#### Frontend (`src/`) -- `App.jsx` - Main chat interface component -- `components/ui/chat/chat-header.jsx` - Header with debug-only "Seed Demo Data" button (visible with `?debug=1`) -- Calls `/api/files/import-demo` endpoint to bulk-load ISS PDFs - -### 3. **MySQL Database** -Two tables for metadata storage: - -**`files` table** -```sql -id VARCHAR(36) PRIMARY KEY -filename TEXT NOT NULL -path TEXT NOT NULL -description TEXT -created_at DATETIME DEFAULT CURRENT_TIMESTAMP -pending_analysis BOOLEAN DEFAULT TRUE -analysis_status VARCHAR(32) DEFAULT 'Queued' -``` - -**`queries` table** -```sql -id VARCHAR(36) PRIMARY KEY -status VARCHAR(32) NOT NULL -payload JSON -result JSON -created_at DATETIME DEFAULT CURRENT_TIMESTAMP -updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -``` - -### 4. **Qdrant Vector Database** -- Collection: `files` -- Dimension: 64 (demo) - replace with real Gemini embedding dimension -- Distance: Cosine similarity -- Stores file embeddings for semantic search - -### 5. **Demo Data** (`rust-engine/demo-data/`) -~20 ISS technical PDFs organized by subsystem: -- Electrical Power System (EPS) -- Environmental Control & Life Support (ECLSS) -- Command & Data Handling (C&DH) -- Structures & Mechanisms - -## Data Flow - -### File Upload & Analysis -``` -1. User uploads PDF → POST /api/files -2. API saves file to storage, inserts DB record (pending_analysis=true) -3. FileWorker claims pending file -4. Gemini 1.5 Flash generates description -5. Gemini 1.5 Pro generates vector graph data -6. Embed text → upsert to Qdrant -7. Mark file as ready (pending_analysis=false) -``` - -### Query Processing -``` -1. User submits query → POST /api/query/create -2. API inserts query record (status='Queued') -3. QueryWorker claims queued query -4. Embed query text -5. Search Qdrant for top-K similar files -6. Fetch file metadata from MySQL -7. Gemini analyzes relationships between files -8. Gemini synthesizes final answer (no speculation) -9. Save results to database -``` - -## Deployment - -### Development (`docker-compose.yml`) -- Local testing with hot-reload -- Bind mounts for code - -### Production (`docker-compose.prod.yml`) -- Used by GitHub Actions for deployment -- Runs rust-engine as user "1004" (github-actions) -- Docker volume: `rust-storage` → `/app/storage` -- Bind mount: `/var/www/codered-astra/rust-engine/demo-data` → `/app/demo-data:ro` -- Environment variables: - - `ASTRA_STORAGE=/app/storage` - - `DEMO_DATA_DIR=/app/demo-data` - - `QDRANT_URL=http://qdrant:6333` - - `GEMINI_API_KEY=` - - `DATABASE_URL=mysql://astraadmin:password@mysql:3306/astra` - -## Key Design Decisions - -### 1. **Two-Stage Analysis (Flash → Pro)** -- Flash is faster/cheaper for initial description -- Pro is better for deep analysis and relationship extraction -- Enables cost-effective scaling - -### 2. **Resumable Workers** -- Workers requeue stale jobs (>10 min in InProgress) -- Survives container restarts without data loss -- Atomic state transitions via SQL - -### 3. **Separation of Concerns** -- FileWorker: Makes files searchable -- QueryWorker: Answers user queries -- Independent scaling and failure isolation - -### 4. **Strict Answer Generation** -- Gemini prompted to not speculate -- Must state uncertainty when info is insufficient -- Prevents hallucination in critical ISS documentation - -### 5. **Demo Embeddings** -- Current: 64-dim deterministic embeddings from text hash -- Production: Replace with real Gemini text embeddings API -- Allows development/testing without embedding API credits - -## API Usage Examples - -### Upload File -```bash -curl -F "file=@document.pdf" http://localhost:3001/api/files -``` - -### Import Demo Data -```bash -curl -X POST http://localhost:3001/api/files/import-demo -``` - -### Create Query -```bash -curl -X POST http://localhost:3001/api/query/create \ - -H "Content-Type: application/json" \ - -d '{"q": "What is the voltage of the ISS main bus?", "top_k": 5}' -``` - -### Check Status -```bash -curl http://localhost:3001/api/query/status?id= -``` - -### Get Result -```bash -curl http://localhost:3001/api/query/result?id= -``` - -## Future Enhancements - -### High Priority -1. Real Gemini text embeddings (replace demo embeddings) -2. File status UI panel (show processing progress) -3. Health check endpoint (`/health`) -4. Data purge endpoint (clear all files/queries) - -### Medium Priority -1. Streaming query responses (SSE/WebSocket) -2. Query result caching -3. File chunking for large PDFs -4. User authentication - -### Low Priority -1. Multi-collection support (different document types) -2. Query history UI -3. File preview in chat -4. Export results to PDF - -## Troubleshooting - -### Storage Permission Errors -- Ensure `/app/storage` is owned by container user -- Docker volume must be writable by user 1004 in production - -### SQL Syntax Errors -- MySQL requires separate `CREATE TABLE` statements -- Cannot combine multiple DDL statements in one `sqlx::query()` - -### Qdrant Connection Issues -- Check QDRANT_URL environment variable -- Ensure qdrant service is running and healthy -- Verify network connectivity between containers - -### Worker Not Processing -- Check logs: `docker logs rust-engine` -- Verify database connectivity -- Look for stale InProgress jobs in queries/files tables - -## Demo Presentation (3 minutes) - -See `rust-engine/DEMODETAILS.md` for curated demo script with example queries. diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md deleted file mode 100644 index c694cfa..0000000 --- a/QUICK_REFERENCE.md +++ /dev/null @@ -1,219 +0,0 @@ -# CodeRED-Astra Quick Reference - -## System Overview - -**Two-worker architecture for ISS document RAG:** - -1. **FileWorker**: Analyzes uploaded files (Flash → Pro → Embed → Qdrant) -2. **QueryWorker**: Answers queries (Embed → Search → Relationships → Answer) - -Both workers are **resumable** and automatically recover from crashes. - -## Core Data Flow - -``` -Upload PDF → Storage → MySQL (pending) → FileWorker → Qdrant → MySQL (ready) - ↓ -User Query → MySQL (queued) → QueryWorker → Search Qdrant → Gemini → Result -``` - -## Module Map - -| Module | Purpose | Key Functions | -|--------|---------|---------------| -| `main.rs` | Entry point | Spawns workers, serves API | -| `db.rs` | Database init | Creates files/queries tables | -| `api.rs` | HTTP endpoints | Upload, list, delete, query CRUD | -| `file_worker.rs` | File analysis | Flash→Pro→embed→upsert | -| `worker.rs` | Query processing | Search→relationships→answer | -| `gemini_client.rs` | AI integration | Text generation, embeddings | -| `vector_db.rs` | Qdrant client | Upsert, search, delete | -| `storage.rs` | File management | Save/delete files | -| `models.rs` | Data structures | FileRecord, QueryRecord | - -## API Endpoints - -### Files -- `POST /api/files` - Upload file -- `POST /api/files/import-demo?force=1` - Bulk import demo PDFs -- `GET /api/files/list` - List all files with status -- `GET /api/files/delete?id=` - Delete file - -### Queries -- `POST /api/query/create` - Create query -- `GET /api/query/status?id=` - Check status -- `GET /api/query/result?id=` - Get result -- `GET /api/query/cancel?id=` - Cancel query - -## Database Schema - -### files -- `id` - UUID primary key -- `filename` - Original filename -- `path` - Storage path -- `description` - Gemini Flash description -- `pending_analysis` - FALSE when ready for search -- `analysis_status` - Queued/InProgress/Completed/Failed - -### queries -- `id` - UUID primary key -- `status` - Queued/InProgress/Completed/Cancelled/Failed -- `payload` - JSON query params `{"q": "...", "top_k": 5}` -- `result` - JSON result `{"summary": "...", "related_files": [...], "relationships": "...", "final_answer": "..."}` - -## Environment Variables - -### Required -- `GEMINI_API_KEY` - Gemini API key -- `DATABASE_URL` - MySQL connection string -- `QDRANT_URL` - Qdrant URL (default: http://qdrant:6333) - -### Optional -- `ASTRA_STORAGE` - Storage directory (default: /app/storage) -- `DEMO_DATA_DIR` - Demo data directory (default: /app/demo-data) -- `GEMINI_MODEL` - Override Gemini model (default: gemini-1.5-pro) - -## Worker States - -### FileWorker -1. **Queued** - File uploaded, awaiting processing -2. **InProgress** - Currently being analyzed -3. **Completed** - Ready for search (pending_analysis=FALSE) -4. **Failed** - Error during processing - -### QueryWorker -1. **Queued** - Query created, awaiting processing -2. **InProgress** - Currently searching/analyzing -3. **Completed** - Result available -4. **Cancelled** - User cancelled -5. **Failed** - Error during processing - -## Gemini Prompts - -### FileWorker Stage 1 (Flash) -``` -Describe the file '{filename}' and extract all key components, keywords, -and details for later vectorization. Be comprehensive and factual. -``` - -### FileWorker Stage 2 (Pro) -``` -Given the file '{filename}' and its description: {desc} -Generate a set of vector graph data (keywords, use cases, relationships) -that can be used for broad and precise search. Only include what is -directly supported by the file. -``` - -### QueryWorker Stage 4 (Relationships) -``` -You are an assistant analyzing relationships STRICTLY within the provided files. -Query: {query} -Files: {file_list} -Tasks: -1) Summarize key details from the files relevant to the query. -2) Describe relationships and linkages strictly supported by these files. -3) List important follow-up questions that could be answered only using the provided files. -Rules: Do NOT guess or invent. If information is insufficient in the files, explicitly state that. -``` - -### QueryWorker Stage 5 (Final Answer) -``` -You are to compose a final answer to the user query using only the information from the files. -Query: {query} -Files considered: {file_list} -Relationship analysis: {relationships} -Requirements: -- Use only information present in the files and analysis above. -- If the answer is uncertain or cannot be determined from the files, clearly state that limitation. -- Avoid speculation or assumptions. -Provide a concise, structured answer. -``` - -## Docker Architecture - -### Services -- **rust-engine** - Warp API + workers (port 8000) -- **web-app** - Express + React (port 3001) -- **mysql** - MySQL 9.1 (port 3306) -- **qdrant** - Qdrant vector DB (port 6333) -- **phpmyadmin** - DB admin UI (port 8080) - -### Volumes (Production) -- `rust-storage:/app/storage` - File storage (writable) -- `/var/www/codered-astra/rust-engine/demo-data:/app/demo-data:ro` - Demo PDFs (read-only) -- `~/astra-logs:/var/log` - Log files - -## Common Issues - -### 1. SQL Syntax Error -**Problem**: `error near 'CREATE TABLE'` -**Cause**: Multiple CREATE TABLE in one query -**Fix**: Split into separate `sqlx::query()` calls - -### 2. Permission Denied -**Problem**: `Permission denied (os error 13)` -**Cause**: Container user can't write to storage -**Fix**: Use Docker volume, ensure ownership matches container user - -### 3. Worker Not Processing -**Problem**: Files/queries stuck in Queued -**Cause**: Worker crashed or not started -**Fix**: Check logs, ensure workers spawned in main.rs - -### 4. Qdrant Connection Failed -**Problem**: `qdrant upsert/search failed` -**Cause**: Qdrant not running or wrong URL -**Fix**: Verify QDRANT_URL, check qdrant container health - -## Development Commands - -```bash -# Build and run locally -cd rust-engine -cargo build -cargo run - -# Check code -cargo check - -# Run with logs -RUST_LOG=info cargo run - -# Docker compose (dev) -docker-compose up --build - -# Docker compose (production) -docker-compose -f docker-compose.prod.yml up -d - -# View logs -docker logs rust-engine -f - -# Rebuild single service -docker-compose build rust-engine -docker-compose up -d rust-engine -``` - -## Testing Flow - -1. Start services: `docker-compose up -d` -2. Import demo data: `curl -X POST http://localhost:3001/api/files/import-demo` -3. Wait for FileWorker to complete (~30 seconds for 20 files) -4. Check file status: `curl http://localhost:3001/api/files/list` -5. Create query: `curl -X POST http://localhost:3001/api/query/create -H "Content-Type: application/json" -d '{"q": "ISS main bus voltage", "top_k": 5}'` -6. Check status: `curl http://localhost:3001/api/query/status?id=` -7. Get result: `curl http://localhost:3001/api/query/result?id=` - -## Performance Notes - -- FileWorker: ~1-2 sec per file (demo embeddings) -- QueryWorker: ~3-5 sec per query (search + 2 Gemini calls) -- Qdrant search: <100ms for 1000s of vectors -- MySQL queries: <10ms for simple selects - -## Security Considerations - -- Store GEMINI_API_KEY in GitHub Secrets (production) -- Use environment variables for all credentials -- Don't commit `.env` files -- Restrict phpmyadmin to internal network only -- Use HTTPS in production deployment diff --git a/README.md b/README.md deleted file mode 100644 index b0b6190..0000000 --- a/README.md +++ /dev/null @@ -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. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index b055e2d..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,70 +0,0 @@ -services: - web-app: - image: ghcr.io/${REPO_NAME_LOWER}/web-app:${IMAGE_TAG} - restart: always - ports: - - "127.0.0.1:3033:3000" - environment: - - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE} - - RUST_ENGINE_URL=http://rust-engine:8000 - - GEMINI_API_KEY=${GEMINI_API_KEY} - depends_on: - - mysql - - rust-engine - - rust-engine: - image: ghcr.io/${REPO_NAME_LOWER}/rust-engine:${IMAGE_TAG} - 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} - depends_on: - - mysql - - qdrant - user: "1004" - volumes: - - ~/astra-logs:/var/log - - rust-storage:/app/storage - - /var/www/codered-astra/rust-engine/demo-data:/app/demo-data:ro - - mysql: - image: mysql:8.0 - restart: always - ports: - - "45.43.2.25:3306:3306" - environment: - - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - - MYSQL_DATABASE=${MYSQL_DATABASE} - - MYSQL_USER=${MYSQL_USER} - - MYSQL_PASSWORD=${MYSQL_PASSWORD} - volumes: - - mysql-data:/var/lib/mysql - - phpmyadmin: - image: phpmyadmin/phpmyadmin - restart: always - ports: - - "127.0.0.1:8080:80" - environment: - - PMA_HOST=mysql - 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: - qdrant-data: - rust-storage: diff --git a/docker-compose.yml b/docker-compose.yml index 78e4c61..393e5d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/web-app/Dockerfile b/frontend/Dockerfile similarity index 92% rename from web-app/Dockerfile rename to frontend/Dockerfile index 9552c3f..5a98079 100644 --- a/web-app/Dockerfile +++ b/frontend/Dockerfile @@ -2,7 +2,7 @@ FROM node:23-alpine WORKDIR /app COPY package*.json ./ -RUN npm ci +RUN npm i COPY . . RUN npm run format && npm run build EXPOSE 3000 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1 @@ + diff --git a/web-app/eslint.config.js b/frontend/eslint.config.js similarity index 51% rename from web-app/eslint.config.js rename to frontend/eslint.config.js index 53a87c7..050e89d 100644 --- a/web-app/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,18 +1,15 @@ import js from "@eslint/js"; import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; // Or import { configs as reactHooks } from "eslint-plugin-react-hooks"; +import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; -import { defineConfig, globalIgnores } from "eslint/config"; -export default defineConfig([ - globalIgnores(["dist"]), +export default [ { - files: ["**/*{js,jsx}"], - extends: [ - js.configs.recommended, - reactHooks.configs["recommended-latest"], - reactRefresh.configs.vite, - ], + ignores: ["dist/**"], + }, + js.configs.recommended, + { + files: ["**/*.{js,jsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, @@ -22,8 +19,17 @@ export default defineConfig([ 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_]" }], }, }, -]); +]; diff --git a/web-app/index.html b/frontend/index.html similarity index 100% rename from web-app/index.html rename to frontend/index.html diff --git a/web-app/package-lock.json b/frontend/package-lock.json similarity index 96% rename from web-app/package-lock.json rename to frontend/package-lock.json index a0bc9b4..7dca15a 100644 --- a/web-app/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,7 @@ "@google/genai": "^1.25.0", "@tailwindcss/postcss": "^4.1.14", "@tailwindcss/vite": "^4.1.14", - "@vitejs/plugin-react": "^5.0.4", + "@vitejs/plugin-react-swc": "^3.7.0", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "class-variance-authority": "^0.7.1", @@ -20,6 +20,7 @@ "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", @@ -32,7 +33,7 @@ }, "devDependencies": { "@eslint/js": "^9.38.0", - "daisyui": "^5.3.7", + "daisyui": "^5.4.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -61,6 +62,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -75,6 +77,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -84,6 +87,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -114,6 +118,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -126,6 +131,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -142,6 +148,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -158,6 +165,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -167,6 +175,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -180,6 +189,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -193,19 +203,11 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -215,6 +217,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -224,6 +227,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -233,6 +237,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -246,6 +251,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -257,36 +263,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -300,6 +276,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -314,6 +291,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -332,6 +310,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1156,12 +1135,6 @@ "react": ">=16.14.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", - "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", - "license": "MIT" - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", @@ -1455,6 +1428,210 @@ "dev": true, "license": "MIT" }, + "node_modules/@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -1470,6 +1647,15 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", @@ -1745,47 +1931,6 @@ "vite": "^5.2.0 || ^6 || ^7" } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1889,26 +2034,25 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@vitejs/plugin-react": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", - "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.38", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4 || ^5 || ^6 || ^7" } }, + "node_modules/@vitejs/plugin-react-swc/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2264,6 +2408,7 @@ "version": "2.8.18", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -2374,6 +2519,7 @@ "version": "4.26.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2480,6 +2626,7 @@ "version": "1.0.30001751", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2706,6 +2853,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -2760,15 +2908,24 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.7.tgz", - "integrity": "sha512-0+8PaSGift0HlIQABCeZzWOBV5Nx/vsI2TihB9hbaEyZENPlZZz+se2JnAH5rz9gBYTyDLB7NJup8hkREr6WBw==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.4.7.tgz", + "integrity": "sha512-2wYO61vTPCXk7xEBgnzLZAYoE0xS5IRLu/GSq0vORpB+cTrtubdx69NnA0loc0exvCY1s2fYL4lGZtFHe2ohNQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/saadeghi/daisyui?sponsor=1" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3010,6 +3167,7 @@ "version": "1.5.237", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -3259,6 +3417,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3726,6 +3885,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3839,6 +4021,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3960,6 +4154,26 @@ "node": ">=14" } }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/gcp-metadata": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", @@ -3988,6 +4202,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5076,6 +5291,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5470,6 +5686,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -6280,30 +6497,49 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-releases": { "version": "2.0.25", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "dev": true, "license": "MIT" }, "node_modules/nodemon": { @@ -7131,15 +7367,6 @@ "react": ">=18" } }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-router": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", @@ -7489,6 +7716,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8512,6 +8740,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8696,6 +8925,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8954,6 +9192,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { diff --git a/web-app/package.json b/frontend/package.json similarity index 94% rename from web-app/package.json rename to frontend/package.json index 5b7412b..b1476ae 100644 --- a/web-app/package.json +++ b/frontend/package.json @@ -15,8 +15,8 @@ "dependencies": { "@google/genai": "^1.25.0", "@tailwindcss/postcss": "^4.1.14", - "@tailwindcss/vite": "^4.1.14", - "@vitejs/plugin-react-swc": "^3.7.0", + "@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", @@ -40,7 +40,7 @@ "packageManager": ">=npm@10.9.0", "devDependencies": { "@eslint/js": "^9.38.0", - "daisyui": "^5.3.7", + "daisyui": "^5.4.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", diff --git a/web-app/src/app/index.jsx b/frontend/src/app/index.jsx similarity index 100% rename from web-app/src/app/index.jsx rename to frontend/src/app/index.jsx diff --git a/frontend/src/components/layouts/chat-layout.jsx b/frontend/src/components/layouts/chat-layout.jsx new file mode 100644 index 0000000..951d54c --- /dev/null +++ b/frontend/src/components/layouts/chat-layout.jsx @@ -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 ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/ui/button/delete-button.jsx b/frontend/src/components/ui/button/delete-button.jsx new file mode 100644 index 0000000..37e008e --- /dev/null +++ b/frontend/src/components/ui/button/delete-button.jsx @@ -0,0 +1,19 @@ +import { Flame } from "lucide-react"; +import { motion } from "motion/react"; + +export default function FlameButton({ onClick, disabled = false }) { + return ( + + + + ); +} diff --git a/web-app/src/components/ui/button/down-button.jsx b/frontend/src/components/ui/button/down-button.jsx similarity index 52% rename from web-app/src/components/ui/button/down-button.jsx rename to frontend/src/components/ui/button/down-button.jsx index 2995d20..b55558b 100644 --- a/web-app/src/components/ui/button/down-button.jsx +++ b/frontend/src/components/ui/button/down-button.jsx @@ -3,10 +3,18 @@ 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 ( diff --git a/web-app/src/components/ui/button/schematic-button.jsx b/frontend/src/components/ui/button/schematic-button.jsx similarity index 100% rename from web-app/src/components/ui/button/schematic-button.jsx rename to frontend/src/components/ui/button/schematic-button.jsx diff --git a/frontend/src/components/ui/chat/chat-header.jsx b/frontend/src/components/ui/chat/chat-header.jsx new file mode 100644 index 0000000..b5d9a63 --- /dev/null +++ b/frontend/src/components/ui/chat/chat-header.jsx @@ -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 ( +
+
+
+ +
+

+ {title} +

+ {fileSummary && ( +
+ {fileSummary} +
+ )} + + {isDebug && ( + + + {ingesting ? "Seeding…" : "Seed Demo Data"} + + )} +
+
+ {toast && ( +
+ {toast} +
+ )} + {externalToast && ( +
+ {externalToast} +
+ )} +
+
+ ); +} diff --git a/web-app/src/components/ui/chat/chat-window.jsx b/frontend/src/components/ui/chat/chat-window.jsx similarity index 62% rename from web-app/src/components/ui/chat/chat-window.jsx rename to frontend/src/components/ui/chat/chat-window.jsx index 2b78ce2..1252d54 100644 --- a/web-app/src/components/ui/chat/chat-window.jsx +++ b/frontend/src/components/ui/chat/chat-window.jsx @@ -4,10 +4,11 @@ import { MARKDOWN_COMPONENTS } from "src/config/markdown"; function MessageBubble({ message }) { const isUser = message.role === "user"; + const isError = !!message.error; return (
{isUser ? (
{message.content}
@@ -22,12 +23,21 @@ function MessageBubble({ message }) { } export default function ChatWindow({ messages }) { + const bottomRef = useRef(null); + + useEffect(() => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); + return (
{messages.map((m, i) => ( - + ))} +
); diff --git a/frontend/src/components/ui/chat/message-input.jsx b/frontend/src/components/ui/chat/message-input.jsx new file mode 100644 index 0000000..0f49a7f --- /dev/null +++ b/frontend/src/components/ui/chat/message-input.jsx @@ -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 ( +
+