AnuragShirke commited on
Commit
3b5d2e9
·
1 Parent(s): 1d9404d

Full Project

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +25 -0
  2. .gitignore +285 -0
  3. DOCKER.md +284 -0
  4. Dockerfile +17 -2
  5. alembic.ini +115 -0
  6. alembic/env.py +96 -0
  7. alembic/script.py.mako +24 -0
  8. alembic/versions/001_create_users_table.py +39 -0
  9. alembic/versions/002_create_documents_table.py +38 -0
  10. docker-compose.postgres.yml +33 -0
  11. docker-compose.prod.yml +23 -0
  12. docker-compose.yml +51 -12
  13. pytest.ini +17 -0
  14. rag-quest-hub/.gitignore +24 -0
  15. rag-quest-hub/Dockerfile +32 -0
  16. rag-quest-hub/Dockerfile.dev +20 -0
  17. rag-quest-hub/README.md +73 -0
  18. rag-quest-hub/components.json +20 -0
  19. rag-quest-hub/eslint.config.js +29 -0
  20. rag-quest-hub/index.html +24 -0
  21. rag-quest-hub/nginx.conf +32 -0
  22. rag-quest-hub/package-lock.json +0 -0
  23. rag-quest-hub/package.json +94 -0
  24. rag-quest-hub/postcss.config.js +6 -0
  25. rag-quest-hub/src/App.css +42 -0
  26. rag-quest-hub/src/App.tsx +56 -0
  27. rag-quest-hub/src/assets/hero-bg.jpg +0 -0
  28. rag-quest-hub/src/components/ChatInterface.tsx +389 -0
  29. rag-quest-hub/src/components/ConnectionStatus.tsx +283 -0
  30. rag-quest-hub/src/components/DocumentUpload.tsx +364 -0
  31. rag-quest-hub/src/components/ErrorBoundary.tsx +195 -0
  32. rag-quest-hub/src/components/Header.tsx +45 -0
  33. rag-quest-hub/src/components/ProtectedRoute.tsx +35 -0
  34. rag-quest-hub/src/components/ThemeToggle.tsx +31 -0
  35. rag-quest-hub/src/components/ui/accordion.tsx +56 -0
  36. rag-quest-hub/src/components/ui/alert-dialog.tsx +139 -0
  37. rag-quest-hub/src/components/ui/alert.tsx +59 -0
  38. rag-quest-hub/src/components/ui/aspect-ratio.tsx +5 -0
  39. rag-quest-hub/src/components/ui/avatar.tsx +48 -0
  40. rag-quest-hub/src/components/ui/badge.tsx +36 -0
  41. rag-quest-hub/src/components/ui/breadcrumb.tsx +115 -0
  42. rag-quest-hub/src/components/ui/button.tsx +56 -0
  43. rag-quest-hub/src/components/ui/calendar.tsx +64 -0
  44. rag-quest-hub/src/components/ui/card.tsx +79 -0
  45. rag-quest-hub/src/components/ui/carousel.tsx +260 -0
  46. rag-quest-hub/src/components/ui/chart.tsx +363 -0
  47. rag-quest-hub/src/components/ui/checkbox.tsx +28 -0
  48. rag-quest-hub/src/components/ui/collapsible.tsx +9 -0
  49. rag-quest-hub/src/components/ui/command.tsx +153 -0
  50. rag-quest-hub/src/components/ui/context-menu.tsx +198 -0
.env.example ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Knowledge Assistant RAG Environment Configuration
2
+
3
+ # Database Configuration
4
+ DATABASE_URL=sqlite:///./data/knowledge_assistant.db
5
+
6
+ # JWT Authentication Configuration
7
+ JWT_SECRET=your-super-secret-jwt-key-change-in-production
8
+ JWT_LIFETIME_SECONDS=3600
9
+
10
+ # User Registration Settings
11
+ USER_REGISTRATION_ENABLED=true
12
+ EMAIL_VERIFICATION_REQUIRED=false
13
+
14
+ # External Services
15
+ QDRANT_HOST=qdrant
16
+ OLLAMA_HOST=ollama
17
+ OLLAMA_MODEL=llama3.2:1b
18
+
19
+ # CORS Configuration
20
+ CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://frontend:8080
21
+
22
+ # Frontend Configuration (for rag-quest-hub)
23
+ VITE_API_BASE_URL=http://localhost:8000
24
+ VITE_API_TIMEOUT=30000
25
+ VITE_ENABLE_REGISTRATION=true
.gitignore ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # Database files
132
+ *.db
133
+ *.sqlite
134
+ *.sqlite3
135
+ knowledge_assistant.db
136
+ data/
137
+
138
+ # Docker volumes and data
139
+ docker-data/
140
+ postgres-data/
141
+ qdrant-data/
142
+ ollama-data/
143
+
144
+ # IDE and Editor files
145
+ .vscode/
146
+ .idea/
147
+ *.swp
148
+ *.swo
149
+ *~
150
+ .DS_Store
151
+ Thumbs.db
152
+
153
+ # OS generated files
154
+ .DS_Store
155
+ .DS_Store?
156
+ ._*
157
+ .Spotlight-V100
158
+ .Trashes
159
+ ehthumbs.db
160
+ Thumbs.db
161
+
162
+ # Logs
163
+ logs/
164
+ *.log
165
+ npm-debug.log*
166
+ yarn-debug.log*
167
+ yarn-error.log*
168
+ pnpm-debug.log*
169
+ lerna-debug.log*
170
+
171
+ # Runtime data
172
+ pids
173
+ *.pid
174
+ *.seed
175
+ *.pid.lock
176
+
177
+ # Coverage directory used by tools like istanbul
178
+ coverage/
179
+ *.lcov
180
+
181
+ # nyc test coverage
182
+ .nyc_output
183
+
184
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
185
+ .grunt
186
+
187
+ # Bower dependency directory (https://bower.io/)
188
+ bower_components
189
+
190
+ # node-waf configuration
191
+ .lock-wscript
192
+
193
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
194
+ build/Release
195
+
196
+ # Dependency directories
197
+ node_modules/
198
+ jspm_packages/
199
+
200
+ # Snowpack dependency directory (https://snowpack.dev/)
201
+ web_modules/
202
+
203
+ # TypeScript cache
204
+ *.tsbuildinfo
205
+
206
+ # Optional npm cache directory
207
+ .npm
208
+
209
+ # Optional eslint cache
210
+ .eslintcache
211
+
212
+ # Microbundle cache
213
+ .rpt2_cache/
214
+ .rts2_cache_cjs/
215
+ .rts2_cache_es/
216
+ .rts2_cache_umd/
217
+
218
+ # Optional REPL history
219
+ .node_repl_history
220
+
221
+ # Output of 'npm pack'
222
+ *.tgz
223
+
224
+ # Yarn Integrity file
225
+ .yarn-integrity
226
+
227
+ # dotenv environment variables file
228
+ .env.local
229
+ .env.development.local
230
+ .env.test.local
231
+ .env.production.local
232
+
233
+ # parcel-bundler cache (https://parceljs.org/)
234
+ .cache
235
+ .parcel-cache
236
+
237
+ # Next.js build output
238
+ .next
239
+
240
+ # Nuxt.js build / generate output
241
+ .nuxt
242
+ dist
243
+
244
+ # Gatsby files
245
+ .cache/
246
+ public
247
+
248
+ # Storybook build outputs
249
+ .out
250
+ .storybook-out
251
+
252
+ # Temporary folders
253
+ tmp/
254
+ temp/
255
+
256
+ # Application specific
257
+ # Vector store data
258
+ vector_store/
259
+ embeddings/
260
+ documents/
261
+
262
+ # Model files (if storing locally)
263
+ models/
264
+ *.bin
265
+ *.safetensors
266
+
267
+ # Configuration files with secrets (keep .example versions)
268
+ .env.production
269
+ .env.development
270
+ docker-compose.override.yml
271
+
272
+ # Test artifacts
273
+ test-results/
274
+ test-reports/
275
+
276
+ # Backup files
277
+ *.bak
278
+ *.backup
279
+ *.old
280
+
281
+ # Kiro specs (if you want to keep them private)
282
+ # .kiro/
283
+
284
+ # Docker build context files that shouldn't be included
285
+ .dockerignore
DOCKER.md ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Configuration for Knowledge Assistant RAG
2
+
3
+ This document describes the Docker setup for the Knowledge Assistant RAG application with authentication support.
4
+
5
+ ## Overview
6
+
7
+ The application consists of multiple services:
8
+ - **Frontend**: React application (rag-quest-hub)
9
+ - **Backend**: FastAPI application with authentication
10
+ - **Database**: SQLite (development) or PostgreSQL (production)
11
+ - **Qdrant**: Vector database for document embeddings
12
+ - **Ollama**: Local LLM service
13
+
14
+ ## Environment Variables
15
+
16
+ ### Backend Configuration
17
+
18
+ | Variable | Description | Default | Required |
19
+ |----------|-------------|---------|----------|
20
+ | `DATABASE_URL` | Database connection string | `sqlite:///./data/knowledge_assistant.db` | No |
21
+ | `JWT_SECRET` | Secret key for JWT tokens | - | **Yes** |
22
+ | `JWT_LIFETIME_SECONDS` | JWT token lifetime in seconds | `3600` | No |
23
+ | `USER_REGISTRATION_ENABLED` | Enable user registration | `true` | No |
24
+ | `EMAIL_VERIFICATION_REQUIRED` | Require email verification | `false` | No |
25
+ | `QDRANT_HOST` | Qdrant service hostname | `qdrant` | No |
26
+ | `OLLAMA_HOST` | Ollama service hostname | `ollama` | No |
27
+ | `OLLAMA_MODEL` | Ollama model to use | `llama3.2:1b` | No |
28
+ | `CORS_ORIGINS` | Allowed CORS origins | `http://localhost:3000,...` | No |
29
+
30
+ ### Frontend Configuration
31
+
32
+ | Variable | Description | Default | Required |
33
+ |----------|-------------|---------|----------|
34
+ | `VITE_API_BASE_URL` | Backend API URL | `http://localhost:8000` | No |
35
+ | `VITE_API_TIMEOUT` | API request timeout (ms) | `30000` | No |
36
+ | `VITE_ENABLE_REGISTRATION` | Show registration form | `true` | No |
37
+
38
+ ## Development Setup
39
+
40
+ ### Prerequisites
41
+ - Docker and Docker Compose
42
+ - At least 4GB RAM available for containers
43
+
44
+ ### Quick Start
45
+
46
+ 1. **Clone and navigate to the project:**
47
+ ```bash
48
+ cd Knowledge_Assistant_RAG
49
+ ```
50
+
51
+ 2. **Create environment file (optional):**
52
+ ```bash
53
+ cp .env.example .env
54
+ # Edit .env with your preferred settings
55
+ ```
56
+
57
+ 3. **Start all services:**
58
+ ```bash
59
+ docker-compose up --build
60
+ ```
61
+
62
+ 4. **Access the application:**
63
+ - Frontend: http://localhost:3000
64
+ - Backend API: http://localhost:8000
65
+ - API Documentation: http://localhost:8000/docs
66
+ - Qdrant Dashboard: http://localhost:6333/dashboard
67
+
68
+ ### Development with Hot Reload
69
+
70
+ The development setup includes volume mounts for hot reload:
71
+ - Frontend source code changes are reflected immediately
72
+ - Backend source code changes require container restart
73
+
74
+ ## Production Setup
75
+
76
+ ### SQLite (Simple Production)
77
+
78
+ For simple production deployments with SQLite:
79
+
80
+ ```bash
81
+ docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
82
+ ```
83
+
84
+ **Important**: Set the `JWT_SECRET` environment variable:
85
+ ```bash
86
+ export JWT_SECRET="your-super-secure-secret-key-here"
87
+ docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
88
+ ```
89
+
90
+ ### PostgreSQL (Scalable Production)
91
+
92
+ For production deployments with PostgreSQL:
93
+
94
+ ```bash
95
+ # Set database credentials
96
+ export POSTGRES_PASSWORD="your-secure-password"
97
+ export JWT_SECRET="your-super-secure-secret-key-here"
98
+
99
+ # Start with PostgreSQL
100
+ docker-compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
101
+ ```
102
+
103
+ ## Database Management
104
+
105
+ ### Migrations
106
+
107
+ Database migrations are automatically run on container startup via the `init-db.sh` script.
108
+
109
+ ### Manual Migration Commands
110
+
111
+ To run migrations manually:
112
+
113
+ ```bash
114
+ # Enter the backend container
115
+ docker-compose exec backend bash
116
+
117
+ # Run migrations
118
+ alembic upgrade head
119
+
120
+ # Create new migration
121
+ alembic revision --autogenerate -m "Description of changes"
122
+ ```
123
+
124
+ ### Database Health Check
125
+
126
+ Check database connectivity:
127
+
128
+ ```bash
129
+ # Run health check script
130
+ docker-compose exec backend /app/scripts/check-db-health.sh
131
+
132
+ # Or check via API
133
+ curl http://localhost:8000/health
134
+ ```
135
+
136
+ ### Backup and Restore (SQLite)
137
+
138
+ **Backup:**
139
+ ```bash
140
+ # Copy database file from container
141
+ docker-compose exec backend cp /app/data/knowledge_assistant.db /tmp/
142
+ docker cp $(docker-compose ps -q backend):/tmp/knowledge_assistant.db ./backup.db
143
+ ```
144
+
145
+ **Restore:**
146
+ ```bash
147
+ # Copy backup to container
148
+ docker cp ./backup.db $(docker-compose ps -q backend):/app/data/knowledge_assistant.db
149
+ docker-compose restart backend
150
+ ```
151
+
152
+ ## Troubleshooting
153
+
154
+ ### Common Issues
155
+
156
+ 1. **Database migration failures:**
157
+ ```bash
158
+ # Check logs
159
+ docker-compose logs backend
160
+
161
+ # Reset database (development only)
162
+ docker-compose down -v
163
+ docker-compose up --build
164
+ ```
165
+
166
+ 2. **JWT secret not set:**
167
+ ```
168
+ Error: JWT_SECRET environment variable is required
169
+ ```
170
+ Solution: Set the JWT_SECRET environment variable before starting containers.
171
+
172
+ 3. **Permission issues with database:**
173
+ ```bash
174
+ # Fix permissions
175
+ docker-compose exec backend chmod 755 /app/data
176
+ docker-compose exec backend chmod 644 /app/data/knowledge_assistant.db
177
+ ```
178
+
179
+ 4. **Frontend can't connect to backend:**
180
+ - Check that `VITE_API_BASE_URL` points to the correct backend URL
181
+ - Verify CORS settings in backend configuration
182
+
183
+ ### Health Checks
184
+
185
+ The application includes comprehensive health checks:
186
+
187
+ - **Container health**: Docker health checks for all services
188
+ - **API health**: `/health` endpoint with service status
189
+ - **Database health**: Automatic connectivity verification
190
+
191
+ ### Logs
192
+
193
+ View logs for specific services:
194
+
195
+ ```bash
196
+ # All services
197
+ docker-compose logs -f
198
+
199
+ # Specific service
200
+ docker-compose logs -f backend
201
+ docker-compose logs -f frontend
202
+ docker-compose logs -f postgres
203
+ ```
204
+
205
+ ## Security Considerations
206
+
207
+ ### Production Security
208
+
209
+ 1. **Change default secrets:**
210
+ - Set a strong `JWT_SECRET` (256-bit recommended)
211
+ - Use secure database passwords
212
+
213
+ 2. **Network security:**
214
+ - Use reverse proxy (nginx) for HTTPS
215
+ - Restrict database access to backend only
216
+ - Configure firewall rules
217
+
218
+ 3. **Data persistence:**
219
+ - Use named volumes for data persistence
220
+ - Regular database backups
221
+ - Monitor disk usage
222
+
223
+ ### Environment Variables Security
224
+
225
+ Never commit sensitive environment variables to version control. Use:
226
+ - `.env` files (gitignored)
227
+ - Docker secrets
228
+ - External secret management systems
229
+
230
+ ## Monitoring
231
+
232
+ ### Health Monitoring
233
+
234
+ The `/health` endpoint provides detailed service status:
235
+
236
+ ```json
237
+ {
238
+ "status": "ok",
239
+ "timestamp": "2024-01-01T12:00:00Z",
240
+ "services": {
241
+ "database": {"status": "healthy", "type": "sqlite"},
242
+ "qdrant": {"status": "healthy", "collections_count": 5},
243
+ "ollama": {"status": "healthy", "model": "llama3.2:1b"},
244
+ "embedding_model": {"status": "healthy", "embedding_dimension": 384}
245
+ }
246
+ }
247
+ ```
248
+
249
+ ### Performance Monitoring
250
+
251
+ Monitor resource usage:
252
+
253
+ ```bash
254
+ # Container resource usage
255
+ docker stats
256
+
257
+ # Disk usage
258
+ docker system df
259
+ ```
260
+
261
+ ## Scaling
262
+
263
+ ### Horizontal Scaling
264
+
265
+ For high-traffic deployments:
266
+
267
+ 1. **Load balancer**: Use nginx or similar for load balancing
268
+ 2. **Database**: Use PostgreSQL with connection pooling
269
+ 3. **Caching**: Add Redis for session/query caching
270
+ 4. **Storage**: Use external storage for document uploads
271
+
272
+ ### Vertical Scaling
273
+
274
+ Adjust resource limits in docker-compose.yml:
275
+
276
+ ```yaml
277
+ services:
278
+ backend:
279
+ deploy:
280
+ resources:
281
+ limits:
282
+ memory: 2G
283
+ cpus: '1.0'
284
+ ```
Dockerfile CHANGED
@@ -17,13 +17,28 @@ ENV PIP_DEFAULT_TIMEOUT=1000
17
  # Install any needed packages specified in requirements.txt
18
  RUN pip install --no-cache-dir -r requirements.txt
19
 
 
 
 
20
  # Copy the application code into the container
21
  COPY ./src /app/src
22
  COPY ./scripts /app/scripts
 
 
 
 
 
 
 
 
23
 
24
  # Expose port 8000 to allow communication to the Uvicorn server
25
  EXPOSE 8000
26
 
 
 
 
 
27
  # Define the command to run the application
28
- # --host 0.0.0.0 makes the server accessible from outside the container
29
- CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
17
  # Install any needed packages specified in requirements.txt
18
  RUN pip install --no-cache-dir -r requirements.txt
19
 
20
+ # Ensure Python scripts are in PATH
21
+ ENV PATH="/usr/local/bin:${PATH}"
22
+
23
  # Copy the application code into the container
24
  COPY ./src /app/src
25
  COPY ./scripts /app/scripts
26
+ COPY ./alembic /app/alembic
27
+ COPY ./alembic.ini /app/alembic.ini
28
+
29
+ # Create data directory for SQLite database
30
+ RUN mkdir -p /app/data
31
+
32
+ # Make scripts executable
33
+ RUN chmod +x /app/scripts/*.sh
34
 
35
  # Expose port 8000 to allow communication to the Uvicorn server
36
  EXPOSE 8000
37
 
38
+ # Add health check for database connectivity
39
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
40
+ CMD curl -f http://localhost:8000/health || exit 1
41
+
42
  # Define the command to run the application
43
+ # The init-db.sh script will handle database migrations and server startup
44
+ CMD ["/app/scripts/init-db.sh"]
alembic.ini ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = .
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python-dateutil library that can be
18
+ # installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to dateutil.tz.gettz()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version number format. This value is passed to the
37
+ # "strftime" function, and is used to generate the
38
+ # version number for new migration files.
39
+ # version_num_format = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d
40
+
41
+ # version path separator; As mentioned above, this is the character used to split
42
+ # version_locations. The default within new alembic.ini files is "os", which uses
43
+ # os.pathsep. If this key is omitted entirely, it falls back to the legacy
44
+ # behavior of splitting on spaces and/or commas.
45
+ # Valid values for version_path_separator are:
46
+ #
47
+ # version_path_separator = :
48
+ # version_path_separator = ;
49
+ # version_path_separator = space
50
+ version_path_separator = os
51
+
52
+ # set to 'true' to search source files recursively
53
+ # in each "version_locations" directory
54
+ # new in Alembic version 1.10
55
+ # recursive_version_locations = false
56
+
57
+ # the output encoding used when revision files
58
+ # are written from script.py.mako
59
+ # output_encoding = utf-8
60
+
61
+ # sqlalchemy.url = sqlite+aiosqlite:///./knowledge_assistant.db
62
+ # Database URL is set via environment variable in env.py
63
+
64
+
65
+ [post_write_hooks]
66
+ # post_write_hooks defines scripts or Python functions that are run
67
+ # on newly generated revision scripts. See the documentation for further
68
+ # detail and examples
69
+
70
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
71
+ # hooks = black
72
+ # black.type = console_scripts
73
+ # black.entrypoint = black
74
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
75
+
76
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
77
+ # hooks = ruff
78
+ # ruff.type = exec
79
+ # ruff.executable = %(here)s/.venv/bin/ruff
80
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
81
+
82
+ # Logging configuration
83
+ [loggers]
84
+ keys = root,sqlalchemy,alembic
85
+
86
+ [handlers]
87
+ keys = console
88
+
89
+ [formatters]
90
+ keys = generic
91
+
92
+ [logger_root]
93
+ level = WARN
94
+ handlers = console
95
+ qualname =
96
+
97
+ [logger_sqlalchemy]
98
+ level = WARN
99
+ handlers =
100
+ qualname = sqlalchemy.engine
101
+
102
+ [logger_alembic]
103
+ level = INFO
104
+ handlers =
105
+ qualname = alembic
106
+
107
+ [handler_console]
108
+ class = StreamHandler
109
+ args = (sys.stderr,)
110
+ level = NOTSET
111
+ formatter = generic
112
+
113
+ [formatter_generic]
114
+ format = %(levelname)-5.5s [%(name)s] %(message)s
115
+ datefmt = %H:%M:%S
alembic/env.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ from logging.config import fileConfig
4
+
5
+ from sqlalchemy import pool
6
+ from sqlalchemy.engine import Connection
7
+ from sqlalchemy.ext.asyncio import async_engine_from_config
8
+
9
+ from alembic import context
10
+
11
+ # Import your models here
12
+ from src.core.database import Base
13
+
14
+ # this is the Alembic Config object, which provides
15
+ # access to the values within the .ini file in use.
16
+ config = context.config
17
+
18
+ # Override the sqlalchemy.url with environment variable if available
19
+ database_url = os.getenv("DATABASE_URL")
20
+ if database_url:
21
+ config.set_main_option("sqlalchemy.url", database_url)
22
+
23
+ # Interpret the config file for Python logging.
24
+ # This line sets up loggers basically.
25
+ if config.config_file_name is not None:
26
+ fileConfig(config.config_file_name)
27
+
28
+ # add your model's MetaData object here
29
+ # for 'autogenerate' support
30
+ target_metadata = Base.metadata
31
+
32
+ # other values from the config, defined by the needs of env.py,
33
+ # can be acquired:
34
+ # my_important_option = config.get_main_option("my_important_option")
35
+ # ... etc.
36
+
37
+
38
+ def run_migrations_offline() -> None:
39
+ """Run migrations in 'offline' mode.
40
+
41
+ This configures the context with just a URL
42
+ and not an Engine, though an Engine is acceptable
43
+ here as well. By skipping the Engine creation
44
+ we don't even need a DBAPI to be available.
45
+
46
+ Calls to context.execute() here emit the given string to the
47
+ script output.
48
+
49
+ """
50
+ url = config.get_main_option("sqlalchemy.url")
51
+ context.configure(
52
+ url=url,
53
+ target_metadata=target_metadata,
54
+ literal_binds=True,
55
+ dialect_opts={"paramstyle": "named"},
56
+ )
57
+
58
+ with context.begin_transaction():
59
+ context.run_migrations()
60
+
61
+
62
+ def do_run_migrations(connection: Connection) -> None:
63
+ context.configure(connection=connection, target_metadata=target_metadata)
64
+
65
+ with context.begin_transaction():
66
+ context.run_migrations()
67
+
68
+
69
+ async def run_async_migrations() -> None:
70
+ """In this scenario we need to create an Engine
71
+ and associate a connection with the context.
72
+
73
+ """
74
+
75
+ connectable = async_engine_from_config(
76
+ config.get_section(config.config_ini_section, {}),
77
+ prefix="sqlalchemy.",
78
+ poolclass=pool.NullPool,
79
+ )
80
+
81
+ async with connectable.connect() as connection:
82
+ await connection.run_sync(do_run_migrations)
83
+
84
+ await connectable.dispose()
85
+
86
+
87
+ def run_migrations_online() -> None:
88
+ """Run migrations in 'online' mode."""
89
+
90
+ asyncio.run(run_async_migrations())
91
+
92
+
93
+ if context.is_offline_mode():
94
+ run_migrations_offline()
95
+ else:
96
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade() -> None:
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade() -> None:
24
+ ${downgrades if downgrades else "pass"}
alembic/versions/001_create_users_table.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Create users table
2
+
3
+ Revision ID: 001
4
+ Revises:
5
+ Create Date: 2025-01-19 12:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = '001'
13
+ down_revision = None
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ # ### commands auto generated by Alembic - please adjust! ###
20
+ op.create_table('users',
21
+ sa.Column('id', sa.CHAR(36), nullable=False),
22
+ sa.Column('email', sa.String(length=320), nullable=False),
23
+ sa.Column('hashed_password', sa.String(length=1024), nullable=False),
24
+ sa.Column('is_active', sa.Boolean(), nullable=False),
25
+ sa.Column('is_superuser', sa.Boolean(), nullable=False),
26
+ sa.Column('is_verified', sa.Boolean(), nullable=False),
27
+ sa.Column('created_at', sa.DateTime(), nullable=True),
28
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
29
+ sa.PrimaryKeyConstraint('id')
30
+ )
31
+ op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ op.drop_index(op.f('ix_users_email'), table_name='users')
38
+ op.drop_table('users')
39
+ # ### end Alembic commands ###
alembic/versions/002_create_documents_table.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Create documents table
2
+
3
+ Revision ID: 002
4
+ Revises: 001
5
+ Create Date: 2025-01-19 12:01:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = '002'
13
+ down_revision = '001'
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ # ### commands auto generated by Alembic - please adjust! ###
20
+ op.create_table('documents',
21
+ sa.Column('id', sa.CHAR(36), nullable=False),
22
+ sa.Column('user_id', sa.CHAR(36), nullable=False),
23
+ sa.Column('filename', sa.String(length=255), nullable=False),
24
+ sa.Column('original_size', sa.Integer(), nullable=True),
25
+ sa.Column('chunks_count', sa.Integer(), nullable=True),
26
+ sa.Column('upload_date', sa.DateTime(), nullable=True),
27
+ sa.Column('file_hash', sa.String(length=64), nullable=True),
28
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
29
+ sa.PrimaryKeyConstraint('id'),
30
+ sa.UniqueConstraint('file_hash')
31
+ )
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ op.drop_table('documents')
38
+ # ### end Alembic commands ###
docker-compose.postgres.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Compose override for PostgreSQL database
2
+ # Use this for production deployments with PostgreSQL
3
+ # Usage: docker-compose -f docker-compose.yml -f docker-compose.postgres.yml up
4
+
5
+ services:
6
+ backend:
7
+ environment:
8
+ - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-password}@postgres:5432/${POSTGRES_DB:-knowledge_assistant}
9
+ depends_on:
10
+ - postgres
11
+ - qdrant
12
+ - ollama
13
+
14
+ postgres:
15
+ image: postgres:15-alpine
16
+ environment:
17
+ POSTGRES_DB: ${POSTGRES_DB:-knowledge_assistant}
18
+ POSTGRES_USER: ${POSTGRES_USER:-postgres}
19
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
20
+ volumes:
21
+ - postgres_data:/var/lib/postgresql/data
22
+ ports:
23
+ - "5432:5432"
24
+ networks:
25
+ - app-network
26
+ healthcheck:
27
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
28
+ interval: 30s
29
+ timeout: 10s
30
+ retries: 3
31
+
32
+ volumes:
33
+ postgres_data:
docker-compose.prod.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ frontend:
3
+ build:
4
+ context: ./rag-quest-hub
5
+ dockerfile: Dockerfile
6
+ volumes: [] # Remove development volume mounts for production
7
+ environment:
8
+ VITE_API_BASE_URL: http://backend:8000
9
+ VITE_API_TIMEOUT: "30000"
10
+ VITE_ENABLE_REGISTRATION: "${VITE_ENABLE_REGISTRATION:-true}"
11
+
12
+ backend:
13
+ volumes:
14
+ - db_data:/app/data # SQLite database volume for production
15
+ environment:
16
+ - DATABASE_URL=sqlite:///./data/knowledge_assistant.db
17
+ - JWT_SECRET=${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
18
+ - JWT_LIFETIME_SECONDS=${JWT_LIFETIME_SECONDS:-3600}
19
+ - USER_REGISTRATION_ENABLED=${USER_REGISTRATION_ENABLED:-true}
20
+ - EMAIL_VERIFICATION_REQUIRED=${EMAIL_VERIFICATION_REQUIRED:-false}
21
+
22
+ volumes:
23
+ db_data:
docker-compose.yml CHANGED
@@ -1,6 +1,32 @@
1
- version: '3.8'
2
-
3
  services:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  backend:
5
  build: .
6
  ports:
@@ -8,13 +34,25 @@ services:
8
  volumes:
9
  - ./src:/app/src
10
  - ./scripts:/app/scripts
 
 
 
11
  depends_on:
12
  - qdrant
13
  - ollama
14
  environment:
15
  - QDRANT_HOST=qdrant
16
  - OLLAMA_HOST=ollama
17
- entrypoint: ["/app/scripts/wait-for-qdrant.sh", "qdrant:6333", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
 
 
 
 
 
 
 
 
18
 
19
  qdrant:
20
  image: qdrant/qdrant:latest
@@ -23,6 +61,8 @@ services:
23
  - "6334:6334"
24
  volumes:
25
  - qdrant_data:/qdrant/storage
 
 
26
 
27
  ollama:
28
  image: ollama/ollama:latest
@@ -32,15 +72,14 @@ services:
32
  volumes:
33
  - ./scripts:/app
34
  - ollama_data:/root/.ollama
35
- mem_limit: 6.5g
36
- deploy:
37
- resources:
38
- reservations:
39
- devices:
40
- - driver: nvidia
41
- count: 1
42
- capabilities: [gpu]
43
 
44
  volumes:
45
  qdrant_data:
46
- ollama_data:
 
 
 
 
 
 
 
 
1
  services:
2
+ frontend:
3
+ build:
4
+ context: ./rag-quest-hub
5
+ dockerfile: Dockerfile.dev
6
+ ports:
7
+ - "3000:8080"
8
+ depends_on:
9
+ - backend
10
+ environment:
11
+ - VITE_API_BASE_URL=http://backend:8000
12
+ - VITE_API_TIMEOUT=30000
13
+ - VITE_ENABLE_REGISTRATION=true
14
+ volumes:
15
+ # Development volume mounts for hot reload
16
+ - ./rag-quest-hub/src:/app/src
17
+ - ./rag-quest-hub/public:/app/public
18
+ - ./rag-quest-hub/package.json:/app/package.json
19
+ - ./rag-quest-hub/vite.config.ts:/app/vite.config.ts
20
+ - ./rag-quest-hub/tailwind.config.ts:/app/tailwind.config.ts
21
+ - ./rag-quest-hub/tsconfig.json:/app/tsconfig.json
22
+ - ./rag-quest-hub/postcss.config.js:/app/postcss.config.js
23
+ - ./rag-quest-hub/components.json:/app/components.json
24
+ - ./rag-quest-hub/.env.production:/app/.env.production
25
+ # Exclude node_modules from volume mount to avoid conflicts
26
+ - /app/node_modules
27
+ networks:
28
+ - app-network
29
+
30
  backend:
31
  build: .
32
  ports:
 
34
  volumes:
35
  - ./src:/app/src
36
  - ./scripts:/app/scripts
37
+ - ./alembic:/app/alembic
38
+ - ./alembic.ini:/app/alembic.ini
39
+ - db_data:/app/data # SQLite database volume
40
  depends_on:
41
  - qdrant
42
  - ollama
43
  environment:
44
  - QDRANT_HOST=qdrant
45
  - OLLAMA_HOST=ollama
46
+ - OLLAMA_MODEL=llama3.2:1b
47
+ - CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://frontend:8080
48
+ - DATABASE_URL=sqlite+aiosqlite:///./data/knowledge_assistant.db
49
+ - JWT_SECRET=your-super-secret-jwt-key-for-development-only
50
+ - JWT_LIFETIME_SECONDS=3600
51
+ - USER_REGISTRATION_ENABLED=true
52
+ - EMAIL_VERIFICATION_REQUIRED=false
53
+ entrypoint: ["/app/scripts/wait-for-qdrant.sh", "qdrant:6333", "/app/scripts/init-db.sh"]
54
+ networks:
55
+ - app-network
56
 
57
  qdrant:
58
  image: qdrant/qdrant:latest
 
61
  - "6334:6334"
62
  volumes:
63
  - qdrant_data:/qdrant/storage
64
+ networks:
65
+ - app-network
66
 
67
  ollama:
68
  image: ollama/ollama:latest
 
72
  volumes:
73
  - ./scripts:/app
74
  - ollama_data:/root/.ollama
75
+ networks:
76
+ - app-network
 
 
 
 
 
 
77
 
78
  volumes:
79
  qdrant_data:
80
+ ollama_data:
81
+ db_data:
82
+
83
+ networks:
84
+ app-network:
85
+ driver: bridge
pytest.ini ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool:pytest]
2
+ testpaths = tests
3
+ python_files = test_*.py
4
+ python_classes = Test*
5
+ python_functions = test_*
6
+ addopts =
7
+ -v
8
+ --tb=short
9
+ --strict-markers
10
+ --disable-warnings
11
+ --asyncio-mode=auto
12
+ markers =
13
+ auth: Authentication related tests
14
+ integration: Integration tests
15
+ unit: Unit tests
16
+ slow: Slow running tests
17
+ asyncio_mode = auto
rag-quest-hub/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
rag-quest-hub/Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for React frontend
2
+ FROM node:18-alpine as builder
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm ci --only=production
12
+
13
+ # Copy source code
14
+ COPY . .
15
+
16
+ # Build the application
17
+ RUN npm run build
18
+
19
+ # Production stage
20
+ FROM nginx:alpine
21
+
22
+ # Copy built assets from builder stage
23
+ COPY --from=builder /app/dist /usr/share/nginx/html
24
+
25
+ # Copy custom nginx configuration
26
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
27
+
28
+ # Expose port 8080
29
+ EXPOSE 8080
30
+
31
+ # Start nginx
32
+ CMD ["nginx", "-g", "daemon off;"]
rag-quest-hub/Dockerfile.dev ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Development Dockerfile for React frontend with hot reload
2
+ FROM node:18-alpine
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install all dependencies (including dev dependencies)
11
+ RUN npm install
12
+
13
+ # Copy source code
14
+ COPY . .
15
+
16
+ # Expose port 8080
17
+ EXPOSE 8080
18
+
19
+ # Start development server
20
+ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080"]
rag-quest-hub/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Welcome to your Lovable project
2
+
3
+ ## Project info
4
+
5
+ **URL**: https://lovable.dev/projects/f368a457-b56d-49a1-a578-a9a2215f4361
6
+
7
+ ## How can I edit this code?
8
+
9
+ There are several ways of editing your application.
10
+
11
+ **Use Lovable**
12
+
13
+ Simply visit the [Lovable Project](https://lovable.dev/projects/f368a457-b56d-49a1-a578-a9a2215f4361) and start prompting.
14
+
15
+ Changes made via Lovable will be committed automatically to this repo.
16
+
17
+ **Use your preferred IDE**
18
+
19
+ If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
20
+
21
+ The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
22
+
23
+ Follow these steps:
24
+
25
+ ```sh
26
+ # Step 1: Clone the repository using the project's Git URL.
27
+ git clone <YOUR_GIT_URL>
28
+
29
+ # Step 2: Navigate to the project directory.
30
+ cd <YOUR_PROJECT_NAME>
31
+
32
+ # Step 3: Install the necessary dependencies.
33
+ npm i
34
+
35
+ # Step 4: Start the development server with auto-reloading and an instant preview.
36
+ npm run dev
37
+ ```
38
+
39
+ **Edit a file directly in GitHub**
40
+
41
+ - Navigate to the desired file(s).
42
+ - Click the "Edit" button (pencil icon) at the top right of the file view.
43
+ - Make your changes and commit the changes.
44
+
45
+ **Use GitHub Codespaces**
46
+
47
+ - Navigate to the main page of your repository.
48
+ - Click on the "Code" button (green button) near the top right.
49
+ - Select the "Codespaces" tab.
50
+ - Click on "New codespace" to launch a new Codespace environment.
51
+ - Edit files directly within the Codespace and commit and push your changes once you're done.
52
+
53
+ ## What technologies are used for this project?
54
+
55
+ This project is built with:
56
+
57
+ - Vite
58
+ - TypeScript
59
+ - React
60
+ - shadcn-ui
61
+ - Tailwind CSS
62
+
63
+ ## How can I deploy this project?
64
+
65
+ Simply open [Lovable](https://lovable.dev/projects/f368a457-b56d-49a1-a578-a9a2215f4361) and click on Share -> Publish.
66
+
67
+ ## Can I connect a custom domain to my Lovable project?
68
+
69
+ Yes, you can!
70
+
71
+ To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
72
+
73
+ Read more here: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)
rag-quest-hub/components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/index.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
rag-quest-hub/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ "@typescript-eslint/no-unused-vars": "off",
27
+ },
28
+ }
29
+ );
rag-quest-hub/index.html ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>rag-quest-hub</title>
7
+ <meta name="description" content="Lovable Generated Project" />
8
+ <meta name="author" content="Lovable" />
9
+
10
+ <meta property="og:title" content="rag-quest-hub" />
11
+ <meta property="og:description" content="Lovable Generated Project" />
12
+ <meta property="og:type" content="website" />
13
+ <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
14
+
15
+ <meta name="twitter:card" content="summary_large_image" />
16
+ <meta name="twitter:site" content="@lovable_dev" />
17
+ <meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
18
+ </head>
19
+
20
+ <body>
21
+ <div id="root"></div>
22
+ <script type="module" src="/src/main.tsx"></script>
23
+ </body>
24
+ </html>
rag-quest-hub/nginx.conf ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 8080;
3
+ server_name localhost;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ # Handle client-side routing
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ # API proxy to backend
13
+ location /api/ {
14
+ proxy_pass http://backend:8000/;
15
+ proxy_set_header Host $host;
16
+ proxy_set_header X-Real-IP $remote_addr;
17
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18
+ proxy_set_header X-Forwarded-Proto $scheme;
19
+ }
20
+
21
+ # Enable gzip compression
22
+ gzip on;
23
+ gzip_vary on;
24
+ gzip_min_length 1024;
25
+ gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
26
+
27
+ # Cache static assets
28
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
29
+ expires 1y;
30
+ add_header Cache-Control "public, immutable";
31
+ }
32
+ }
rag-quest-hub/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
rag-quest-hub/package.json ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite_react_shadcn_ts",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "test:ui": "vitest --ui"
15
+ },
16
+ "dependencies": {
17
+ "@hookform/resolvers": "^3.9.0",
18
+ "@radix-ui/react-accordion": "^1.2.0",
19
+ "@radix-ui/react-alert-dialog": "^1.1.1",
20
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
21
+ "@radix-ui/react-avatar": "^1.1.0",
22
+ "@radix-ui/react-checkbox": "^1.1.1",
23
+ "@radix-ui/react-collapsible": "^1.1.0",
24
+ "@radix-ui/react-context-menu": "^2.2.1",
25
+ "@radix-ui/react-dialog": "^1.1.2",
26
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
27
+ "@radix-ui/react-hover-card": "^1.1.1",
28
+ "@radix-ui/react-label": "^2.1.0",
29
+ "@radix-ui/react-menubar": "^1.1.1",
30
+ "@radix-ui/react-navigation-menu": "^1.2.0",
31
+ "@radix-ui/react-popover": "^1.1.1",
32
+ "@radix-ui/react-progress": "^1.1.0",
33
+ "@radix-ui/react-radio-group": "^1.2.0",
34
+ "@radix-ui/react-scroll-area": "^1.1.0",
35
+ "@radix-ui/react-select": "^2.1.1",
36
+ "@radix-ui/react-separator": "^1.1.0",
37
+ "@radix-ui/react-slider": "^1.2.0",
38
+ "@radix-ui/react-slot": "^1.1.0",
39
+ "@radix-ui/react-switch": "^1.1.0",
40
+ "@radix-ui/react-tabs": "^1.1.0",
41
+ "@radix-ui/react-toast": "^1.2.1",
42
+ "@radix-ui/react-toggle": "^1.1.0",
43
+ "@radix-ui/react-toggle-group": "^1.1.0",
44
+ "@radix-ui/react-tooltip": "^1.1.4",
45
+ "@tanstack/react-query": "^5.56.2",
46
+ "axios": "^1.11.0",
47
+ "class-variance-authority": "^0.7.1",
48
+ "clsx": "^2.1.1",
49
+ "cmdk": "^1.0.0",
50
+ "date-fns": "^3.6.0",
51
+ "embla-carousel-react": "^8.3.0",
52
+ "input-otp": "^1.2.4",
53
+ "lucide-react": "^0.462.0",
54
+ "next-themes": "^0.3.0",
55
+ "react": "^18.3.1",
56
+ "react-day-picker": "^8.10.1",
57
+ "react-dom": "^18.3.1",
58
+ "react-hook-form": "^7.53.0",
59
+ "react-resizable-panels": "^2.1.3",
60
+ "react-router-dom": "^6.26.2",
61
+ "recharts": "^2.12.7",
62
+ "sonner": "^1.5.0",
63
+ "tailwind-merge": "^2.5.2",
64
+ "tailwindcss-animate": "^1.0.7",
65
+ "vaul": "^0.9.3",
66
+ "zod": "^3.23.8"
67
+ },
68
+ "devDependencies": {
69
+ "@eslint/js": "^9.9.0",
70
+ "@tailwindcss/typography": "^0.5.15",
71
+ "@types/node": "^22.5.5",
72
+ "@types/react": "^18.3.3",
73
+ "@types/react-dom": "^18.3.0",
74
+ "@vitejs/plugin-react-swc": "^3.5.0",
75
+ "@testing-library/jest-dom": "^6.4.6",
76
+ "@testing-library/react": "^16.0.0",
77
+ "@testing-library/user-event": "^14.5.2",
78
+ "@vitest/ui": "^1.6.0",
79
+ "autoprefixer": "^10.4.20",
80
+ "eslint": "^9.9.0",
81
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
82
+ "eslint-plugin-react-refresh": "^0.4.9",
83
+ "globals": "^15.9.0",
84
+ "jsdom": "^24.1.0",
85
+ "lovable-tagger": "^1.1.7",
86
+ "msw": "^2.3.1",
87
+ "postcss": "^8.4.47",
88
+ "tailwindcss": "^3.4.11",
89
+ "typescript": "^5.5.3",
90
+ "typescript-eslint": "^8.0.1",
91
+ "vite": "^5.4.1",
92
+ "vitest": "^1.6.0"
93
+ }
94
+ }
rag-quest-hub/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
rag-quest-hub/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
rag-quest-hub/src/App.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from "@/components/ui/toaster";
2
+ import { Toaster as Sonner } from "@/components/ui/sonner";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
6
+ import { AuthProvider } from "@/contexts/AuthContext";
7
+ import { ThemeProvider } from "@/contexts/ThemeContext";
8
+ import ErrorBoundary from "@/components/ErrorBoundary";
9
+ import ProtectedRoute from "@/components/ProtectedRoute";
10
+ import Login from "./pages/Login";
11
+ import Register from "./pages/Register";
12
+ import Dashboard from "./pages/Dashboard";
13
+ import NotFound from "./pages/NotFound";
14
+
15
+ const queryClient = new QueryClient();
16
+
17
+ const App = () => (
18
+ <ErrorBoundary
19
+ onError={(error, errorInfo) => {
20
+ // Log to external service in production
21
+ console.error('Application Error:', error, errorInfo);
22
+ }}
23
+ >
24
+ <QueryClientProvider client={queryClient}>
25
+ <ThemeProvider defaultTheme="dark">
26
+ <AuthProvider>
27
+ <TooltipProvider>
28
+ <Toaster />
29
+ <Sonner />
30
+ <BrowserRouter>
31
+ <ErrorBoundary>
32
+ <Routes>
33
+ <Route path="/" element={<Login />} />
34
+ <Route path="/login" element={<Login />} />
35
+ <Route path="/register" element={<Register />} />
36
+ <Route
37
+ path="/dashboard"
38
+ element={
39
+ <ProtectedRoute>
40
+ <Dashboard />
41
+ </ProtectedRoute>
42
+ }
43
+ />
44
+ {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
45
+ <Route path="*" element={<NotFound />} />
46
+ </Routes>
47
+ </ErrorBoundary>
48
+ </BrowserRouter>
49
+ </TooltipProvider>
50
+ </AuthProvider>
51
+ </ThemeProvider>
52
+ </QueryClientProvider>
53
+ </ErrorBoundary>
54
+ );
55
+
56
+ export default App;
rag-quest-hub/src/assets/hero-bg.jpg ADDED
rag-quest-hub/src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Input } from '@/components/ui/input';
4
+ import { Card } from '@/components/ui/card';
5
+ import { Send, Bot, User, Loader2, FileText, ExternalLink, RefreshCw, AlertTriangle, WifiOff } from 'lucide-react';
6
+ import { queryAPI, QueryResponse } from '@/lib/api';
7
+ import { useToast } from '@/hooks/use-toast';
8
+ import { analyzeError, createRetryFunction, showErrorToast, ConnectionMonitor } from '@/lib/errorHandling';
9
+
10
+ interface Message {
11
+ id: string;
12
+ type: 'user' | 'assistant' | 'error';
13
+ content: string;
14
+ timestamp: Date;
15
+ sources?: Array<{
16
+ source: string;
17
+ text: string;
18
+ score: number;
19
+ }>;
20
+ isRetryable?: boolean;
21
+ originalQuery?: string;
22
+ }
23
+
24
+ type QueryStatus = 'idle' | 'typing' | 'processing' | 'timeout' | 'error';
25
+
26
+ const ChatInterface: React.FC = () => {
27
+ const [messages, setMessages] = useState<Message[]>([]);
28
+ const [input, setInput] = useState('');
29
+ const [queryStatus, setQueryStatus] = useState<QueryStatus>('idle');
30
+ const [typingDots, setTypingDots] = useState('');
31
+ const [queryStartTime, setQueryStartTime] = useState<number | null>(null);
32
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
33
+ const [retryAttempts, setRetryAttempts] = useState<Map<string, number>>(new Map());
34
+ const messagesEndRef = useRef<HTMLDivElement>(null);
35
+ const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
36
+ const { toast } = useToast();
37
+
38
+ const scrollToBottom = () => {
39
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
40
+ };
41
+
42
+ useEffect(() => {
43
+ scrollToBottom();
44
+ }, [messages, queryStatus]);
45
+
46
+ useEffect(() => {
47
+ // Add welcome message
48
+ const welcomeMessage: Message = {
49
+ id: 'welcome',
50
+ type: 'assistant',
51
+ content: 'Hello! I\'m your Knowledge Assistant. Upload some documents and ask me questions about their content. I\'ll help you find the information you need.',
52
+ timestamp: new Date(),
53
+ };
54
+ setMessages([welcomeMessage]);
55
+
56
+ // Set up connection monitoring
57
+ const monitor = ConnectionMonitor.getInstance();
58
+ const unsubscribe = monitor.addListener(setIsOnline);
59
+
60
+ return unsubscribe;
61
+ }, []);
62
+
63
+ // Typing animation effect
64
+ useEffect(() => {
65
+ if (queryStatus === 'typing' || queryStatus === 'processing') {
66
+ typingIntervalRef.current = setInterval(() => {
67
+ setTypingDots(prev => {
68
+ if (prev === '...') return '';
69
+ return prev + '.';
70
+ });
71
+ }, 500);
72
+ } else {
73
+ if (typingIntervalRef.current) {
74
+ clearInterval(typingIntervalRef.current);
75
+ typingIntervalRef.current = null;
76
+ }
77
+ setTypingDots('');
78
+ }
79
+
80
+ return () => {
81
+ if (typingIntervalRef.current) {
82
+ clearInterval(typingIntervalRef.current);
83
+ }
84
+ };
85
+ }, [queryStatus]);
86
+
87
+ const executeQuery = async (queryText: string): Promise<QueryResponse> => {
88
+ return await queryAPI.ask(queryText, 60000); // 60 second timeout
89
+ };
90
+
91
+ const handleSubmit = async (e: React.FormEvent, retryQuery?: string) => {
92
+ e.preventDefault();
93
+ const queryText = retryQuery || input.trim();
94
+ if (!queryText || queryStatus !== 'idle') return;
95
+
96
+ // Check if we're online
97
+ if (!isOnline) {
98
+ toast({
99
+ title: "No internet connection",
100
+ description: "Please check your connection and try again.",
101
+ variant: "destructive",
102
+ });
103
+ return;
104
+ }
105
+
106
+ const messageId = Date.now().toString();
107
+ const userMessage: Message = {
108
+ id: messageId,
109
+ type: 'user',
110
+ content: queryText,
111
+ timestamp: new Date(),
112
+ };
113
+
114
+ // Only add user message if it's not a retry
115
+ if (!retryQuery) {
116
+ setMessages(prev => [...prev, userMessage]);
117
+ setInput('');
118
+ }
119
+
120
+ setQueryStatus('typing');
121
+ setQueryStartTime(Date.now());
122
+
123
+ // Track retry attempts
124
+ const currentAttempts = retryAttempts.get(queryText) || 0;
125
+ setRetryAttempts(prev => new Map(prev).set(queryText, currentAttempts + 1));
126
+
127
+ // Simulate brief typing delay for better UX
128
+ setTimeout(async () => {
129
+ setQueryStatus('processing');
130
+
131
+ // Create retry function with exponential backoff
132
+ const retryQuery = createRetryFunction(() => executeQuery(queryText), 2, 3000);
133
+
134
+ try {
135
+ const response = await retryQuery();
136
+
137
+ const assistantMessage: Message = {
138
+ id: (Date.now() + 1).toString(),
139
+ type: 'assistant',
140
+ content: response.answer,
141
+ timestamp: new Date(),
142
+ sources: response.source_documents,
143
+ };
144
+
145
+ setMessages(prev => [...prev, assistantMessage]);
146
+ setQueryStatus('idle');
147
+
148
+ // Reset retry count on success
149
+ setRetryAttempts(prev => {
150
+ const newMap = new Map(prev);
151
+ newMap.delete(queryText);
152
+ return newMap;
153
+ });
154
+
155
+ } catch (error: unknown) {
156
+ console.error('Query failed:', error);
157
+ setQueryStatus('error');
158
+
159
+ const errorInfo = analyzeError(error);
160
+
161
+ const errorMessage: Message = {
162
+ id: (Date.now() + 1).toString(),
163
+ type: 'error',
164
+ content: errorInfo.userMessage,
165
+ timestamp: new Date(),
166
+ isRetryable: errorInfo.canRetry && currentAttempts < 3,
167
+ originalQuery: queryText,
168
+ };
169
+
170
+ setMessages(prev => [...prev, errorMessage]);
171
+
172
+ // Show toast with specific error information
173
+ showErrorToast(error, `Query failed: ${errorInfo.userMessage}`);
174
+
175
+ // Reset status after a delay
176
+ setTimeout(() => {
177
+ setQueryStatus('idle');
178
+ }, 1000);
179
+ }
180
+ }, 800); // Brief typing simulation
181
+ };
182
+
183
+ const handleKeyPress = (e: React.KeyboardEvent) => {
184
+ if (e.key === 'Enter' && !e.shiftKey) {
185
+ e.preventDefault();
186
+ handleSubmit(e);
187
+ }
188
+ };
189
+
190
+ const handleRetry = (originalQuery: string) => {
191
+ const syntheticEvent = { preventDefault: () => {} } as React.FormEvent;
192
+ handleSubmit(syntheticEvent, originalQuery);
193
+ };
194
+
195
+ const getLoadingMessage = () => {
196
+ if (queryStatus === 'typing') {
197
+ return `Thinking${typingDots}`;
198
+ } else if (queryStatus === 'processing') {
199
+ const elapsed = queryStartTime ? Math.floor((Date.now() - queryStartTime) / 1000) : 0;
200
+ if (elapsed < 10) {
201
+ return `Processing${typingDots}`;
202
+ } else if (elapsed < 30) {
203
+ return `Still working${typingDots} This might take a moment for complex queries.`;
204
+ } else {
205
+ return `Taking longer than usual${typingDots} Please be patient.`;
206
+ }
207
+ }
208
+ return 'Thinking...';
209
+ };
210
+
211
+ return (
212
+ <div className="flex flex-col h-full">
213
+ {/* Messages Container */}
214
+ <div className="flex-1 overflow-y-auto p-4 space-y-4 min-h-0 scroll-smooth">
215
+ {messages.map((message) => (
216
+ <div
217
+ key={message.id}
218
+ className={`flex gap-3 ${
219
+ message.type === 'user' ? 'justify-end' : 'justify-start'
220
+ }`}
221
+ >
222
+ {message.type === 'assistant' && (
223
+ <div className="flex-shrink-0">
224
+ <div className="w-8 h-8 bg-gradient-primary rounded-full flex items-center justify-center shadow-glow">
225
+ <Bot className="h-4 w-4 text-primary-foreground" />
226
+ </div>
227
+ </div>
228
+ )}
229
+
230
+ <Card
231
+ className={`max-w-[80%] p-4 ${
232
+ message.type === 'user'
233
+ ? 'bg-primary text-primary-foreground shadow-glow'
234
+ : message.type === 'error'
235
+ ? 'bg-destructive/10 border-destructive/20 backdrop-blur-sm'
236
+ : 'bg-card/50 border-border/50 backdrop-blur-sm'
237
+ }`}
238
+ >
239
+ {message.type === 'error' && (
240
+ <div className="flex items-center gap-2 mb-2">
241
+ <AlertTriangle className="h-4 w-4 text-destructive" />
242
+ <span className="text-sm font-medium text-destructive">
243
+ Query Failed
244
+ </span>
245
+ </div>
246
+ )}
247
+
248
+ <p className="text-sm leading-relaxed whitespace-pre-wrap">
249
+ {message.content}
250
+ </p>
251
+
252
+ {message.type === 'error' && message.isRetryable && message.originalQuery && (
253
+ <div className="mt-3 pt-3 border-t border-border/30">
254
+ <div className="flex items-center justify-between">
255
+ <Button
256
+ onClick={() => handleRetry(message.originalQuery!)}
257
+ disabled={queryStatus !== 'idle' || !isOnline}
258
+ variant="outline"
259
+ size="sm"
260
+ className="text-xs"
261
+ >
262
+ <RefreshCw className="h-3 w-3 mr-1" />
263
+ Retry Query
264
+ {retryAttempts.get(message.originalQuery!) &&
265
+ ` (${retryAttempts.get(message.originalQuery!)})`}
266
+ </Button>
267
+ {!isOnline && (
268
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
269
+ <WifiOff className="h-3 w-3" />
270
+ Offline
271
+ </div>
272
+ )}
273
+ </div>
274
+ </div>
275
+ )}
276
+
277
+ {/* Source Citations */}
278
+ {message.type === 'assistant' && message.sources && message.sources.length > 0 && (
279
+ <div className="mt-3 pt-3 border-t border-border/30">
280
+ <div className="flex items-center gap-2 mb-2">
281
+ <FileText className="h-3 w-3 text-muted-foreground" />
282
+ <span className="text-xs font-medium text-muted-foreground">
283
+ Sources ({message.sources.length})
284
+ </span>
285
+ </div>
286
+ <div className="space-y-2">
287
+ {message.sources.map((source, index) => (
288
+ <div
289
+ key={index}
290
+ className="text-xs p-2 bg-muted/30 rounded border border-border/20"
291
+ >
292
+ <div className="flex items-center justify-between mb-1">
293
+ <span className="font-medium text-foreground truncate">
294
+ {source.source}
295
+ </span>
296
+ <span className="text-muted-foreground ml-2">
297
+ {Math.round(source.score * 100)}% match
298
+ </span>
299
+ </div>
300
+ <p className="text-muted-foreground overflow-hidden" style={{
301
+ display: '-webkit-box',
302
+ WebkitLineClamp: 2,
303
+ WebkitBoxOrient: 'vertical'
304
+ }}>
305
+ {source.text}
306
+ </p>
307
+ </div>
308
+ ))}
309
+ </div>
310
+ </div>
311
+ )}
312
+
313
+ <p
314
+ className={`text-xs mt-2 ${
315
+ message.type === 'user'
316
+ ? 'text-primary-foreground/70'
317
+ : 'text-muted-foreground'
318
+ }`}
319
+ >
320
+ {message.timestamp.toLocaleTimeString()}
321
+ </p>
322
+ </Card>
323
+
324
+ {message.type === 'user' && (
325
+ <div className="flex-shrink-0">
326
+ <div className="w-8 h-8 bg-secondary rounded-full flex items-center justify-center">
327
+ <User className="h-4 w-4 text-secondary-foreground" />
328
+ </div>
329
+ </div>
330
+ )}
331
+ </div>
332
+ ))}
333
+
334
+ {(queryStatus === 'typing' || queryStatus === 'processing') && (
335
+ <div className="flex gap-3 justify-start">
336
+ <div className="flex-shrink-0">
337
+ <div className="w-8 h-8 bg-gradient-primary rounded-full flex items-center justify-center shadow-glow">
338
+ <Bot className="h-4 w-4 text-primary-foreground" />
339
+ </div>
340
+ </div>
341
+ <Card className="p-4 bg-card/50 border-border/50 backdrop-blur-sm">
342
+ <div className="flex items-center gap-2">
343
+ <Loader2 className="h-4 w-4 animate-spin text-primary" />
344
+ <span className="text-sm text-muted-foreground">
345
+ {getLoadingMessage()}
346
+ </span>
347
+ </div>
348
+ </Card>
349
+ </div>
350
+ )}
351
+
352
+ <div ref={messagesEndRef} />
353
+ </div>
354
+
355
+ {/* Input Form */}
356
+ <div className="border-t border-border/50 p-4">
357
+ <form onSubmit={handleSubmit} className="flex gap-2">
358
+ <Input
359
+ value={input}
360
+ onChange={(e) => setInput(e.target.value)}
361
+ onKeyPress={handleKeyPress}
362
+ placeholder={
363
+ !isOnline ? "You're offline. Please check your connection." :
364
+ queryStatus === 'idle' ? "Ask a question about your documents..." :
365
+ "Processing query..."
366
+ }
367
+ disabled={queryStatus !== 'idle' || !isOnline}
368
+ className="flex-1 bg-input/50 border-border/50"
369
+ />
370
+ <Button
371
+ type="submit"
372
+ disabled={!input.trim() || queryStatus !== 'idle' || !isOnline}
373
+ className="bg-gradient-primary hover:opacity-90 text-primary-foreground shadow-glow transition-smooth"
374
+ >
375
+ {!isOnline ? (
376
+ <WifiOff className="h-4 w-4" />
377
+ ) : queryStatus === 'typing' || queryStatus === 'processing' ? (
378
+ <Loader2 className="h-4 w-4 animate-spin" />
379
+ ) : (
380
+ <Send className="h-4 w-4" />
381
+ )}
382
+ </Button>
383
+ </form>
384
+ </div>
385
+ </div>
386
+ );
387
+ };
388
+
389
+ export default ChatInterface;
rag-quest-hub/src/components/ConnectionStatus.tsx ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Wifi, WifiOff, AlertCircle, CheckCircle, Server, Database, Brain, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
3
+ import { Alert, AlertDescription } from '@/components/ui/alert';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
7
+ import { ConnectionMonitor, HealthCheckResponse } from '@/lib/errorHandling';
8
+
9
+ interface ConnectionStatusProps {
10
+ showWhenOnline?: boolean;
11
+ className?: string;
12
+ showServiceDetails?: boolean;
13
+ }
14
+
15
+ const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
16
+ showWhenOnline = false,
17
+ className = "",
18
+ showServiceDetails = true
19
+ }) => {
20
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
21
+ const [serverStatus, setServerStatus] = useState<'checking' | 'online' | 'offline'>('checking');
22
+ const [serviceHealth, setServiceHealth] = useState<HealthCheckResponse | null>(null);
23
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
24
+ const [isRetrying, setIsRetrying] = useState(false);
25
+
26
+ useEffect(() => {
27
+ const monitor = ConnectionMonitor.getInstance();
28
+
29
+ // Listen for connection changes
30
+ const unsubscribeConnection = monitor.addListener((online) => {
31
+ setIsOnline(online);
32
+ if (online) {
33
+ setServerStatus(monitor.getServerStatus());
34
+ } else {
35
+ setServerStatus('offline');
36
+ setServiceHealth(null);
37
+ }
38
+ });
39
+
40
+ // Listen for health status changes
41
+ const unsubscribeHealth = monitor.addHealthListener((health) => {
42
+ setServiceHealth(health);
43
+ if (health) {
44
+ setServerStatus(health.status === 'ok' ? 'online' : 'offline');
45
+ }
46
+ });
47
+
48
+ // Set initial state
49
+ setIsOnline(monitor.getStatus());
50
+ setServerStatus(monitor.getServerStatus());
51
+ setServiceHealth(monitor.getServiceHealth());
52
+
53
+ return () => {
54
+ unsubscribeConnection();
55
+ unsubscribeHealth();
56
+ };
57
+ }, []);
58
+
59
+ const handleRetryConnection = async () => {
60
+ setIsRetrying(true);
61
+ const monitor = ConnectionMonitor.getInstance();
62
+ await monitor.forceHealthCheck();
63
+ setIsRetrying(false);
64
+ };
65
+
66
+ const getServiceIcon = (serviceName: string) => {
67
+ switch (serviceName) {
68
+ case 'qdrant':
69
+ return <Database className="h-3 w-3" />;
70
+ case 'ollama':
71
+ return <Brain className="h-3 w-3" />;
72
+ case 'embedding_model':
73
+ return <Server className="h-3 w-3" />;
74
+ default:
75
+ return <Server className="h-3 w-3" />;
76
+ }
77
+ };
78
+
79
+ const getServiceDisplayName = (serviceName: string) => {
80
+ switch (serviceName) {
81
+ case 'qdrant':
82
+ return 'Vector Database';
83
+ case 'ollama':
84
+ return 'Language Model';
85
+ case 'embedding_model':
86
+ return 'Embedding Model';
87
+ default:
88
+ return serviceName;
89
+ }
90
+ };
91
+
92
+ const getServiceStatusBadge = (status: string) => {
93
+ switch (status) {
94
+ case 'healthy':
95
+ return <Badge variant="default" className="bg-green-500/10 text-green-600 border-green-500/20">Healthy</Badge>;
96
+ case 'unhealthy':
97
+ return <Badge variant="destructive">Unhealthy</Badge>;
98
+ default:
99
+ return <Badge variant="secondary">Unknown</Badge>;
100
+ }
101
+ };
102
+
103
+ // Don't show anything if online and showWhenOnline is false
104
+ if (isOnline && serverStatus === 'online' && !showWhenOnline) {
105
+ return null;
106
+ }
107
+
108
+ // Compact corner indicator mode when showServiceDetails is false
109
+ if (!showServiceDetails) {
110
+ const getCompactStatus = () => {
111
+ if (!isOnline) {
112
+ return { icon: <WifiOff className="h-3 w-3" />, text: 'Offline', color: 'bg-red-500' };
113
+ }
114
+ if (serverStatus === 'offline') {
115
+ return { icon: <AlertCircle className="h-3 w-3" />, text: 'Server Down', color: 'bg-red-500' };
116
+ }
117
+ if (serverStatus === 'checking') {
118
+ return { icon: <RefreshCw className="h-3 w-3 animate-spin" />, text: 'Checking...', color: 'bg-yellow-500' };
119
+ }
120
+
121
+ const hasUnhealthyServices = serviceHealth?.services &&
122
+ Object.values(serviceHealth.services).some(service => service?.status === 'unhealthy');
123
+
124
+ if (hasUnhealthyServices) {
125
+ return { icon: <AlertCircle className="h-3 w-3" />, text: 'Issues', color: 'bg-yellow-500' };
126
+ }
127
+
128
+ return { icon: <CheckCircle className="h-3 w-3" />, text: 'Online', color: 'bg-green-500' };
129
+ };
130
+
131
+ const compactStatus = getCompactStatus();
132
+
133
+ return (
134
+ <div className={`${className} flex items-center gap-2 px-3 py-2 bg-card/90 backdrop-blur-sm border border-border/50 rounded-full shadow-lg text-xs`}>
135
+ <div className={`w-2 h-2 rounded-full ${compactStatus.color}`} />
136
+ {compactStatus.icon}
137
+ <span className="font-medium">{compactStatus.text}</span>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ const getStatusInfo = () => {
143
+ if (!isOnline) {
144
+ return {
145
+ icon: <WifiOff className="h-4 w-4" />,
146
+ variant: 'destructive' as const,
147
+ title: 'No Internet Connection',
148
+ description: 'You are currently offline. Please check your internet connection.',
149
+ showRetry: false,
150
+ };
151
+ }
152
+
153
+ if (serverStatus === 'offline') {
154
+ return {
155
+ icon: <AlertCircle className="h-4 w-4" />,
156
+ variant: 'destructive' as const,
157
+ title: 'Server Unavailable',
158
+ description: 'Cannot connect to the server. Some features may not work properly.',
159
+ showRetry: true,
160
+ };
161
+ }
162
+
163
+ if (serverStatus === 'checking') {
164
+ return {
165
+ icon: <Wifi className="h-4 w-4 animate-pulse" />,
166
+ variant: 'default' as const,
167
+ title: 'Checking Connection',
168
+ description: 'Verifying server connection...',
169
+ showRetry: false,
170
+ };
171
+ }
172
+
173
+ // Check if any services are unhealthy
174
+ const hasUnhealthyServices = serviceHealth?.services &&
175
+ Object.values(serviceHealth.services).some(service => service?.status === 'unhealthy');
176
+
177
+ if (hasUnhealthyServices) {
178
+ return {
179
+ icon: <AlertCircle className="h-4 w-4" />,
180
+ variant: 'destructive' as const,
181
+ title: 'Service Issues Detected',
182
+ description: 'Some services are experiencing issues. Check details below.',
183
+ showRetry: true,
184
+ };
185
+ }
186
+
187
+ return {
188
+ icon: <CheckCircle className="h-4 w-4" />,
189
+ variant: 'default' as const,
190
+ title: 'All Systems Operational',
191
+ description: serviceHealth ? `Response time: ${serviceHealth.services.qdrant?.responseTime || 0}ms` : 'Connected to server.',
192
+ showRetry: false,
193
+ };
194
+ };
195
+
196
+ const statusInfo = getStatusInfo();
197
+
198
+ return (
199
+ <Alert variant={statusInfo.variant} className={className}>
200
+ {statusInfo.icon}
201
+ <AlertDescription>
202
+ <div className="space-y-3">
203
+ <div className="flex items-center justify-between">
204
+ <div>
205
+ <div className="font-medium">{statusInfo.title}</div>
206
+ <div className="text-sm">{statusInfo.description}</div>
207
+ {serviceHealth && (
208
+ <div className="text-xs text-muted-foreground mt-1">
209
+ Last checked: {new Date(serviceHealth.timestamp).toLocaleTimeString()}
210
+ </div>
211
+ )}
212
+ </div>
213
+ <div className="flex items-center gap-2">
214
+ {statusInfo.showRetry && (
215
+ <Button
216
+ variant="outline"
217
+ size="sm"
218
+ onClick={handleRetryConnection}
219
+ disabled={serverStatus === 'checking' || isRetrying}
220
+ >
221
+ <RefreshCw className={`h-3 w-3 mr-1 ${isRetrying ? 'animate-spin' : ''}`} />
222
+ {isRetrying ? 'Retrying...' : 'Retry'}
223
+ </Button>
224
+ )}
225
+ {showServiceDetails && serviceHealth && (
226
+ <Collapsible open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
227
+ <CollapsibleTrigger asChild>
228
+ <Button variant="ghost" size="sm">
229
+ {isDetailsOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
230
+ </Button>
231
+ </CollapsibleTrigger>
232
+ </Collapsible>
233
+ )}
234
+ </div>
235
+ </div>
236
+
237
+ {/* Service Details */}
238
+ {showServiceDetails && serviceHealth && (
239
+ <Collapsible open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
240
+ <CollapsibleContent className="space-y-2">
241
+ <div className="border-t border-border/50 pt-3">
242
+ <div className="text-xs font-medium text-muted-foreground mb-2">Service Status</div>
243
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
244
+ {Object.entries(serviceHealth.services).map(([serviceName, service]) => (
245
+ <div key={serviceName} className="flex items-center justify-between p-2 bg-muted/30 rounded-md">
246
+ <div className="flex items-center gap-2">
247
+ {getServiceIcon(serviceName)}
248
+ <span className="text-xs font-medium">{getServiceDisplayName(serviceName)}</span>
249
+ </div>
250
+ <div className="flex flex-col items-end gap-1">
251
+ {getServiceStatusBadge(service?.status || 'unknown')}
252
+ {service?.responseTime && (
253
+ <span className="text-xs text-muted-foreground">{service.responseTime}ms</span>
254
+ )}
255
+ </div>
256
+ </div>
257
+ ))}
258
+ </div>
259
+
260
+ {/* Show errors if any */}
261
+ {Object.entries(serviceHealth.services).some(([, service]) => service?.error) && (
262
+ <div className="mt-3">
263
+ <div className="text-xs font-medium text-muted-foreground mb-1">Service Errors</div>
264
+ {Object.entries(serviceHealth.services).map(([serviceName, service]) =>
265
+ service?.error && (
266
+ <div key={serviceName} className="text-xs text-destructive bg-destructive/10 p-2 rounded-md">
267
+ <span className="font-medium">{getServiceDisplayName(serviceName)}:</span> {service.error}
268
+ </div>
269
+ )
270
+ )}
271
+ </div>
272
+ )}
273
+ </div>
274
+ </CollapsibleContent>
275
+ </Collapsible>
276
+ )}
277
+ </div>
278
+ </AlertDescription>
279
+ </Alert>
280
+ );
281
+ };
282
+
283
+ export default ConnectionStatus;
rag-quest-hub/src/components/DocumentUpload.tsx ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { Progress } from '@/components/ui/progress';
5
+ import { Upload, FileText, CheckCircle, AlertCircle, X, Loader2, RefreshCw } from 'lucide-react';
6
+ import { documentAPI, UploadResponse } from '@/lib/api';
7
+ import { useToast } from '@/hooks/use-toast';
8
+ import { analyzeError, createRetryFunction, showErrorToast } from '@/lib/errorHandling';
9
+
10
+ interface UploadedDocument {
11
+ id: string;
12
+ name: string;
13
+ size: string;
14
+ uploadedAt: Date;
15
+ chunksProcessed?: number;
16
+ }
17
+
18
+ type UploadStatus = 'idle' | 'uploading' | 'processing' | 'completed' | 'error';
19
+
20
+ const DocumentUpload: React.FC = () => {
21
+ const [uploadStatus, setUploadStatus] = useState<UploadStatus>('idle');
22
+ const [uploadProgress, setUploadProgress] = useState(0);
23
+ const [currentFileName, setCurrentFileName] = useState<string>('');
24
+ const [uploadedDocs, setUploadedDocs] = useState<UploadedDocument[]>([]);
25
+ const [lastError, setLastError] = useState<unknown>(null);
26
+ const [retryCount, setRetryCount] = useState(0);
27
+ const fileInputRef = useRef<HTMLInputElement>(null);
28
+ const { toast } = useToast();
29
+
30
+ const formatFileSize = (bytes: number): string => {
31
+ if (bytes === 0) return '0 Bytes';
32
+ const k = 1024;
33
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
34
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
35
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
36
+ };
37
+
38
+ const handleFileSelect = () => {
39
+ fileInputRef.current?.click();
40
+ };
41
+
42
+ const uploadFile = async (file: File): Promise<UploadResponse> => {
43
+ return await documentAPI.upload(file, (progressEvent) => {
44
+ if (progressEvent.lengthComputable) {
45
+ const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
46
+ setUploadProgress(percentCompleted);
47
+
48
+ // Switch to processing status when upload is complete
49
+ if (percentCompleted === 100) {
50
+ setUploadStatus('processing');
51
+ }
52
+ }
53
+ });
54
+ };
55
+
56
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
57
+ const file = e.target.files?.[0];
58
+ if (!file) return;
59
+
60
+ // Validate file type
61
+ const allowedTypes = ['application/pdf', 'text/plain', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
62
+ const allowedExtensions = ['.pdf', '.txt', '.docx'];
63
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
64
+
65
+ if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
66
+ toast({
67
+ title: "Invalid file type",
68
+ description: "Please upload a PDF, TXT, or DOCX file.",
69
+ variant: "destructive",
70
+ });
71
+ return;
72
+ }
73
+
74
+ // Validate file size (max 10MB)
75
+ const maxSize = 10 * 1024 * 1024;
76
+ if (file.size > maxSize) {
77
+ toast({
78
+ title: "File too large",
79
+ description: "Please upload a file smaller than 10MB.",
80
+ variant: "destructive",
81
+ });
82
+ return;
83
+ }
84
+
85
+ setUploadStatus('uploading');
86
+ setUploadProgress(0);
87
+ setCurrentFileName(file.name);
88
+ setLastError(null);
89
+ setRetryCount(0);
90
+
91
+ // Create retry function with exponential backoff
92
+ const retryUpload = createRetryFunction(() => uploadFile(file), 3, 2000);
93
+
94
+ try {
95
+ const uploadResponse = await retryUpload();
96
+
97
+ setUploadStatus('completed');
98
+
99
+ // Add to uploaded documents list
100
+ const newDoc: UploadedDocument = {
101
+ id: Date.now().toString(),
102
+ name: file.name,
103
+ size: formatFileSize(file.size),
104
+ uploadedAt: new Date(),
105
+ chunksProcessed: uploadResponse.num_chunks_stored,
106
+ };
107
+ setUploadedDocs(prev => [newDoc, ...prev]);
108
+
109
+ toast({
110
+ title: "Upload successful",
111
+ description: `${uploadResponse.filename} has been processed into ${uploadResponse.num_chunks_stored} chunks.`,
112
+ });
113
+
114
+ // Reset after a brief delay to show completion
115
+ setTimeout(() => {
116
+ setUploadStatus('idle');
117
+ setUploadProgress(0);
118
+ setCurrentFileName('');
119
+ setLastError(null);
120
+ }, 2000);
121
+
122
+ } catch (error: unknown) {
123
+ console.error('Upload failed:', error);
124
+ setUploadStatus('error');
125
+ setLastError(error);
126
+
127
+ const errorInfo = analyzeError(error);
128
+ showErrorToast(error, `Upload failed: ${errorInfo.userMessage}`);
129
+
130
+ // Don't auto-reset on error - let user decide to retry or cancel
131
+ } finally {
132
+ if (fileInputRef.current) {
133
+ fileInputRef.current.value = '';
134
+ }
135
+ }
136
+ };
137
+
138
+ const handleRetry = async () => {
139
+ if (!lastError) return;
140
+
141
+ setRetryCount(prev => prev + 1);
142
+ setUploadStatus('uploading');
143
+ setUploadProgress(0);
144
+
145
+ // Get the file from the input (if still available) or ask user to select again
146
+ const file = fileInputRef.current?.files?.[0];
147
+ if (!file) {
148
+ toast({
149
+ title: "File not found",
150
+ description: "Please select the file again to retry upload.",
151
+ variant: "destructive",
152
+ });
153
+ setUploadStatus('idle');
154
+ return;
155
+ }
156
+
157
+ try {
158
+ const uploadResponse = await uploadFile(file);
159
+
160
+ setUploadStatus('completed');
161
+
162
+ // Add to uploaded documents list
163
+ const newDoc: UploadedDocument = {
164
+ id: Date.now().toString(),
165
+ name: file.name,
166
+ size: formatFileSize(file.size),
167
+ uploadedAt: new Date(),
168
+ chunksProcessed: uploadResponse.num_chunks_stored,
169
+ };
170
+ setUploadedDocs(prev => [newDoc, ...prev]);
171
+
172
+ toast({
173
+ title: "Upload successful",
174
+ description: `${uploadResponse.filename} has been processed into ${uploadResponse.num_chunks_stored} chunks.`,
175
+ });
176
+
177
+ // Reset after success
178
+ setTimeout(() => {
179
+ setUploadStatus('idle');
180
+ setUploadProgress(0);
181
+ setCurrentFileName('');
182
+ setLastError(null);
183
+ setRetryCount(0);
184
+ }, 2000);
185
+
186
+ } catch (error: unknown) {
187
+ console.error('Retry upload failed:', error);
188
+ setUploadStatus('error');
189
+ setLastError(error);
190
+ showErrorToast(error, 'Retry failed. Please try again.');
191
+ }
192
+ };
193
+
194
+ const handleCancel = () => {
195
+ setUploadStatus('idle');
196
+ setUploadProgress(0);
197
+ setCurrentFileName('');
198
+ setLastError(null);
199
+ setRetryCount(0);
200
+ if (fileInputRef.current) {
201
+ fileInputRef.current.value = '';
202
+ }
203
+ };
204
+
205
+ const removeDocument = (id: string) => {
206
+ setUploadedDocs(prev => prev.filter(doc => doc.id !== id));
207
+ };
208
+
209
+ return (
210
+ <div className="space-y-6">
211
+ {/* Upload Section */}
212
+ <Card className="border-border/50 bg-card/50 backdrop-blur-sm">
213
+ <CardHeader>
214
+ <CardTitle className="flex items-center gap-2">
215
+ <Upload className="h-5 w-5" />
216
+ Document Upload
217
+ </CardTitle>
218
+ <CardDescription>
219
+ Upload PDF or TXT files to ask questions about their content
220
+ </CardDescription>
221
+ </CardHeader>
222
+ <CardContent className="space-y-4">
223
+ <input
224
+ ref={fileInputRef}
225
+ type="file"
226
+ accept=".pdf,.txt"
227
+ onChange={handleFileChange}
228
+ className="hidden"
229
+ />
230
+
231
+ {uploadStatus === 'error' ? (
232
+ <div className="space-y-2">
233
+ <div className="flex gap-2">
234
+ <Button
235
+ onClick={handleRetry}
236
+ variant="default"
237
+ className="flex-1"
238
+ >
239
+ <RefreshCw className="mr-2 h-4 w-4" />
240
+ Retry Upload {retryCount > 0 && `(${retryCount})`}
241
+ </Button>
242
+ <Button
243
+ onClick={handleCancel}
244
+ variant="outline"
245
+ className="flex-1"
246
+ >
247
+ <X className="mr-2 h-4 w-4" />
248
+ Cancel
249
+ </Button>
250
+ </div>
251
+ {lastError && (
252
+ <div className="text-xs text-destructive text-center">
253
+ {analyzeError(lastError).userMessage}
254
+ </div>
255
+ )}
256
+ </div>
257
+ ) : (
258
+ <Button
259
+ onClick={handleFileSelect}
260
+ disabled={uploadStatus !== 'idle'}
261
+ className="w-full bg-gradient-primary hover:opacity-90 text-primary-foreground shadow-glow transition-smooth"
262
+ >
263
+ {uploadStatus === 'uploading' || uploadStatus === 'processing' ? (
264
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
265
+ ) : uploadStatus === 'completed' ? (
266
+ <CheckCircle className="mr-2 h-4 w-4" />
267
+ ) : (
268
+ <Upload className="mr-2 h-4 w-4" />
269
+ )}
270
+ {uploadStatus === 'uploading' ? 'Uploading...' :
271
+ uploadStatus === 'processing' ? 'Processing...' :
272
+ uploadStatus === 'completed' ? 'Upload Complete!' :
273
+ 'Select Document'}
274
+ </Button>
275
+ )}
276
+
277
+ {uploadStatus !== 'idle' && (
278
+ <div className="space-y-3">
279
+ {currentFileName && (
280
+ <div className="text-sm text-muted-foreground text-center">
281
+ {uploadStatus === 'uploading' ? 'Uploading' :
282
+ uploadStatus === 'processing' ? 'Processing' :
283
+ uploadStatus === 'completed' ? 'Completed' : 'Failed'}: {currentFileName}
284
+ </div>
285
+ )}
286
+
287
+ <div className="space-y-2">
288
+ <div className="flex justify-between text-sm">
289
+ <span>
290
+ {uploadStatus === 'uploading' ? 'Upload Progress' :
291
+ uploadStatus === 'processing' ? 'Processing Document...' :
292
+ uploadStatus === 'completed' ? 'Processing Complete' :
293
+ 'Upload Failed'}
294
+ </span>
295
+ {uploadStatus === 'uploading' && (
296
+ <span>{Math.round(uploadProgress)}%</span>
297
+ )}
298
+ </div>
299
+ <Progress
300
+ value={uploadStatus === 'processing' ? 100 : uploadProgress}
301
+ className="h-2"
302
+ />
303
+ {uploadStatus === 'processing' && (
304
+ <div className="text-xs text-muted-foreground text-center">
305
+ Chunking document and creating embeddings...
306
+ </div>
307
+ )}
308
+ </div>
309
+ </div>
310
+ )}
311
+
312
+ <p className="text-xs text-muted-foreground text-center">
313
+ Supported formats: PDF, TXT, DOCX • Max size: 10MB
314
+ </p>
315
+ </CardContent>
316
+ </Card>
317
+
318
+ {/* Uploaded Documents */}
319
+ {uploadedDocs.length > 0 && (
320
+ <Card className="border-border/50 bg-card/50 backdrop-blur-sm">
321
+ <CardHeader>
322
+ <CardTitle className="flex items-center gap-2">
323
+ <FileText className="h-5 w-5" />
324
+ Uploaded Documents
325
+ </CardTitle>
326
+ </CardHeader>
327
+ <CardContent>
328
+ <div className="space-y-2">
329
+ {uploadedDocs.map((doc) => (
330
+ <div
331
+ key={doc.id}
332
+ className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border border-border/50"
333
+ >
334
+ <div className="flex items-center gap-3">
335
+ <CheckCircle className="h-4 w-4 text-green-500" />
336
+ <div>
337
+ <p className="text-sm font-medium truncate max-w-[200px]">
338
+ {doc.name}
339
+ </p>
340
+ <p className="text-xs text-muted-foreground">
341
+ {doc.size} • {doc.uploadedAt.toLocaleDateString()}
342
+ {doc.chunksProcessed && ` • ${doc.chunksProcessed} chunks`}
343
+ </p>
344
+ </div>
345
+ </div>
346
+ <Button
347
+ variant="ghost"
348
+ size="sm"
349
+ onClick={() => removeDocument(doc.id)}
350
+ className="h-8 w-8 p-0 hover:bg-destructive/20"
351
+ >
352
+ <X className="h-4 w-4" />
353
+ </Button>
354
+ </div>
355
+ ))}
356
+ </div>
357
+ </CardContent>
358
+ </Card>
359
+ )}
360
+ </div>
361
+ );
362
+ };
363
+
364
+ export default DocumentUpload;
rag-quest-hub/src/components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+ import { AlertTriangle, RefreshCw, Home, Bug } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Alert, AlertDescription } from '@/components/ui/alert';
6
+
7
+ interface Props {
8
+ children: ReactNode;
9
+ fallback?: ReactNode;
10
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
11
+ }
12
+
13
+ interface State {
14
+ hasError: boolean;
15
+ error: Error | null;
16
+ errorInfo: ErrorInfo | null;
17
+ errorId: string;
18
+ }
19
+
20
+ class ErrorBoundary extends Component<Props, State> {
21
+ constructor(props: Props) {
22
+ super(props);
23
+ this.state = {
24
+ hasError: false,
25
+ error: null,
26
+ errorInfo: null,
27
+ errorId: '',
28
+ };
29
+ }
30
+
31
+ static getDerivedStateFromError(error: Error): Partial<State> {
32
+ // Update state so the next render will show the fallback UI
33
+ return {
34
+ hasError: true,
35
+ error,
36
+ errorId: `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
37
+ };
38
+ }
39
+
40
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
41
+ // Log error details
42
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
43
+
44
+ // Update state with error info
45
+ this.setState({
46
+ errorInfo,
47
+ });
48
+
49
+ // Call custom error handler if provided
50
+ if (this.props.onError) {
51
+ this.props.onError(error, errorInfo);
52
+ }
53
+
54
+ // Log to external service in production
55
+ if (import.meta.env.PROD) {
56
+ this.logErrorToService(error, errorInfo);
57
+ }
58
+ }
59
+
60
+ private logErrorToService = (error: Error, errorInfo: ErrorInfo) => {
61
+ // In a real application, you would send this to your error tracking service
62
+ // like Sentry, LogRocket, or Bugsnag
63
+ const errorData = {
64
+ message: error.message,
65
+ stack: error.stack,
66
+ componentStack: errorInfo.componentStack,
67
+ errorId: this.state.errorId,
68
+ timestamp: new Date().toISOString(),
69
+ userAgent: navigator.userAgent,
70
+ url: window.location.href,
71
+ };
72
+
73
+ console.log('Error logged:', errorData);
74
+ // Example: sendToErrorService(errorData);
75
+ };
76
+
77
+ private handleRetry = () => {
78
+ this.setState({
79
+ hasError: false,
80
+ error: null,
81
+ errorInfo: null,
82
+ errorId: '',
83
+ });
84
+ };
85
+
86
+ private handleReload = () => {
87
+ window.location.reload();
88
+ };
89
+
90
+ private handleGoHome = () => {
91
+ window.location.href = '/';
92
+ };
93
+
94
+ private copyErrorDetails = () => {
95
+ const errorDetails = {
96
+ errorId: this.state.errorId,
97
+ message: this.state.error?.message,
98
+ stack: this.state.error?.stack,
99
+ componentStack: this.state.errorInfo?.componentStack,
100
+ timestamp: new Date().toISOString(),
101
+ };
102
+
103
+ navigator.clipboard.writeText(JSON.stringify(errorDetails, null, 2))
104
+ .then(() => {
105
+ alert('Error details copied to clipboard');
106
+ })
107
+ .catch(() => {
108
+ console.log('Failed to copy error details');
109
+ });
110
+ };
111
+
112
+ render() {
113
+ if (this.state.hasError) {
114
+ // Custom fallback UI
115
+ if (this.props.fallback) {
116
+ return this.props.fallback;
117
+ }
118
+
119
+ // Default error UI
120
+ return (
121
+ <div className="min-h-screen bg-background flex items-center justify-center p-4">
122
+ <Card className="w-full max-w-2xl">
123
+ <CardHeader className="text-center">
124
+ <div className="mx-auto mb-4 w-16 h-16 bg-destructive/10 rounded-full flex items-center justify-center">
125
+ <AlertTriangle className="w-8 h-8 text-destructive" />
126
+ </div>
127
+ <CardTitle className="text-2xl">Something went wrong</CardTitle>
128
+ <CardDescription>
129
+ We're sorry, but something unexpected happened. Our team has been notified.
130
+ </CardDescription>
131
+ </CardHeader>
132
+ <CardContent className="space-y-4">
133
+ <Alert>
134
+ <Bug className="h-4 w-4" />
135
+ <AlertDescription>
136
+ <strong>Error ID:</strong> {this.state.errorId}
137
+ <br />
138
+ <strong>Time:</strong> {new Date().toLocaleString()}
139
+ </AlertDescription>
140
+ </Alert>
141
+
142
+ {import.meta.env.DEV && this.state.error && (
143
+ <Alert variant="destructive">
144
+ <AlertDescription>
145
+ <strong>Error:</strong> {this.state.error.message}
146
+ <details className="mt-2">
147
+ <summary className="cursor-pointer">Stack trace</summary>
148
+ <pre className="mt-2 text-xs overflow-auto max-h-40 bg-muted p-2 rounded">
149
+ {this.state.error.stack}
150
+ </pre>
151
+ </details>
152
+ </AlertDescription>
153
+ </Alert>
154
+ )}
155
+
156
+ <div className="flex flex-col sm:flex-row gap-3 justify-center">
157
+ <Button onClick={this.handleRetry} variant="default">
158
+ <RefreshCw className="w-4 h-4 mr-2" />
159
+ Try Again
160
+ </Button>
161
+ <Button onClick={this.handleReload} variant="outline">
162
+ <RefreshCw className="w-4 h-4 mr-2" />
163
+ Reload Page
164
+ </Button>
165
+ <Button onClick={this.handleGoHome} variant="outline">
166
+ <Home className="w-4 h-4 mr-2" />
167
+ Go Home
168
+ </Button>
169
+ </div>
170
+
171
+ <div className="text-center">
172
+ <Button
173
+ onClick={this.copyErrorDetails}
174
+ variant="ghost"
175
+ size="sm"
176
+ className="text-muted-foreground"
177
+ >
178
+ Copy Error Details
179
+ </Button>
180
+ </div>
181
+
182
+ <div className="text-center text-sm text-muted-foreground">
183
+ If this problem persists, please contact support with the error ID above.
184
+ </div>
185
+ </CardContent>
186
+ </Card>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ return this.props.children;
192
+ }
193
+ }
194
+
195
+ export default ErrorBoundary;
rag-quest-hub/src/components/Header.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Brain, LogOut } from 'lucide-react';
4
+ import { useAuth } from '@/contexts/AuthContext';
5
+ import ThemeToggle from '@/components/ThemeToggle';
6
+
7
+ const Header: React.FC = () => {
8
+ const { logout } = useAuth();
9
+
10
+ const handleLogout = async () => {
11
+ await logout();
12
+ window.location.href = '/login';
13
+ };
14
+
15
+ return (
16
+ <header className="border-b border-border/50 bg-card/50 backdrop-blur-sm">
17
+ <div className="container mx-auto px-4 h-16 flex items-center justify-between">
18
+ {/* Logo and Title */}
19
+ <div className="flex items-center gap-3">
20
+ <div className="p-2 bg-gradient-primary rounded-lg shadow-glow">
21
+ <Brain className="h-6 w-6 text-primary-foreground" />
22
+ </div>
23
+ <h1 className="text-xl font-bold bg-gradient-primary bg-clip-text text-transparent">
24
+ Knowledge Assistant
25
+ </h1>
26
+ </div>
27
+
28
+ {/* Theme Toggle and Logout Button */}
29
+ <div className="flex items-center gap-3">
30
+ <ThemeToggle />
31
+ <Button
32
+ onClick={handleLogout}
33
+ variant="outline"
34
+ className="border-border/50 hover:bg-muted/50 transition-smooth"
35
+ >
36
+ <LogOut className="mr-2 h-4 w-4" />
37
+ Logout
38
+ </Button>
39
+ </div>
40
+ </div>
41
+ </header>
42
+ );
43
+ };
44
+
45
+ export default Header;
rag-quest-hub/src/components/ProtectedRoute.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Navigate, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '@/contexts/AuthContext';
4
+ import { Loader2 } from 'lucide-react';
5
+
6
+ interface ProtectedRouteProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
11
+ const { isAuthenticated, loading } = useAuth();
12
+ const location = useLocation();
13
+
14
+ // Show loading spinner while checking authentication
15
+ if (loading) {
16
+ return (
17
+ <div className="min-h-screen flex items-center justify-center bg-gradient-surface">
18
+ <div className="text-center">
19
+ <Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
20
+ <p className="text-muted-foreground">Checking authentication...</p>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ // Redirect to login if not authenticated, preserving the intended destination
27
+ if (!isAuthenticated) {
28
+ return <Navigate to="/login" state={{ from: location }} replace />;
29
+ }
30
+
31
+ // Render the protected content
32
+ return <>{children}</>;
33
+ };
34
+
35
+ export default ProtectedRoute;
rag-quest-hub/src/components/ThemeToggle.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Moon, Sun } from 'lucide-react';
4
+ import { useTheme } from '@/contexts/ThemeContext';
5
+
6
+ const ThemeToggle: React.FC = () => {
7
+ const { theme, toggleTheme } = useTheme();
8
+
9
+ return (
10
+ <Button
11
+ variant="outline"
12
+ size="sm"
13
+ onClick={toggleTheme}
14
+ className="border-border/50 hover:bg-muted/50 transition-smooth"
15
+ >
16
+ {theme === 'dark' ? (
17
+ <>
18
+ <Sun className="h-4 w-4 mr-2" />
19
+ Light
20
+ </>
21
+ ) : (
22
+ <>
23
+ <Moon className="h-4 w-4 mr-2" />
24
+ Dark
25
+ </>
26
+ )}
27
+ </Button>
28
+ );
29
+ };
30
+
31
+ export default ThemeToggle;
rag-quest-hub/src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDown } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Accordion = AccordionPrimitive.Root
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item
14
+ ref={ref}
15
+ className={cn("border-b", className)}
16
+ {...props}
17
+ />
18
+ ))
19
+ AccordionItem.displayName = "AccordionItem"
20
+
21
+ const AccordionTrigger = React.forwardRef<
22
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
23
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
24
+ >(({ className, children, ...props }, ref) => (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
36
+ </AccordionPrimitive.Trigger>
37
+ </AccordionPrimitive.Header>
38
+ ))
39
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40
+
41
+ const AccordionContent = React.forwardRef<
42
+ React.ElementRef<typeof AccordionPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <AccordionPrimitive.Content
46
+ ref={ref}
47
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ ))
53
+
54
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
55
+
56
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
rag-quest-hub/src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ))
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45
+
46
+ const AlertDialogHeader = ({
47
+ className,
48
+ ...props
49
+ }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div
51
+ className={cn(
52
+ "flex flex-col space-y-2 text-center sm:text-left",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ AlertDialogHeader.displayName = "AlertDialogHeader"
59
+
60
+ const AlertDialogFooter = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ AlertDialogFooter.displayName = "AlertDialogFooter"
73
+
74
+ const AlertDialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Title
79
+ ref={ref}
80
+ className={cn("text-lg font-semibold", className)}
81
+ {...props}
82
+ />
83
+ ))
84
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85
+
86
+ const AlertDialogDescription = React.forwardRef<
87
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
88
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89
+ >(({ className, ...props }, ref) => (
90
+ <AlertDialogPrimitive.Description
91
+ ref={ref}
92
+ className={cn("text-sm text-muted-foreground", className)}
93
+ {...props}
94
+ />
95
+ ))
96
+ AlertDialogDescription.displayName =
97
+ AlertDialogPrimitive.Description.displayName
98
+
99
+ const AlertDialogAction = React.forwardRef<
100
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
101
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102
+ >(({ className, ...props }, ref) => (
103
+ <AlertDialogPrimitive.Action
104
+ ref={ref}
105
+ className={cn(buttonVariants(), className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110
+
111
+ const AlertDialogCancel = React.forwardRef<
112
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
+ >(({ className, ...props }, ref) => (
115
+ <AlertDialogPrimitive.Cancel
116
+ ref={ref}
117
+ className={cn(
118
+ buttonVariants({ variant: "outline" }),
119
+ "mt-2 sm:mt-0",
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ ))
125
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogPortal,
130
+ AlertDialogOverlay,
131
+ AlertDialogTrigger,
132
+ AlertDialogContent,
133
+ AlertDialogHeader,
134
+ AlertDialogFooter,
135
+ AlertDialogTitle,
136
+ AlertDialogDescription,
137
+ AlertDialogAction,
138
+ AlertDialogCancel,
139
+ }
rag-quest-hub/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
rag-quest-hub/src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root
4
+
5
+ export { AspectRatio }
rag-quest-hub/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Avatar = React.forwardRef<
7
+ React.ElementRef<typeof AvatarPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <AvatarPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ))
19
+ Avatar.displayName = AvatarPrimitive.Root.displayName
20
+
21
+ const AvatarImage = React.forwardRef<
22
+ React.ElementRef<typeof AvatarPrimitive.Image>,
23
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
24
+ >(({ className, ...props }, ref) => (
25
+ <AvatarPrimitive.Image
26
+ ref={ref}
27
+ className={cn("aspect-square h-full w-full", className)}
28
+ {...props}
29
+ />
30
+ ))
31
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
32
+
33
+ const AvatarFallback = React.forwardRef<
34
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
35
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
36
+ >(({ className, ...props }, ref) => (
37
+ <AvatarPrimitive.Fallback
38
+ ref={ref}
39
+ className={cn(
40
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ ))
46
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47
+
48
+ export { Avatar, AvatarImage, AvatarFallback }
rag-quest-hub/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
rag-quest-hub/src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
13
+ Breadcrumb.displayName = "Breadcrumb"
14
+
15
+ const BreadcrumbList = React.forwardRef<
16
+ HTMLOListElement,
17
+ React.ComponentPropsWithoutRef<"ol">
18
+ >(({ className, ...props }, ref) => (
19
+ <ol
20
+ ref={ref}
21
+ className={cn(
22
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ BreadcrumbList.displayName = "BreadcrumbList"
29
+
30
+ const BreadcrumbItem = React.forwardRef<
31
+ HTMLLIElement,
32
+ React.ComponentPropsWithoutRef<"li">
33
+ >(({ className, ...props }, ref) => (
34
+ <li
35
+ ref={ref}
36
+ className={cn("inline-flex items-center gap-1.5", className)}
37
+ {...props}
38
+ />
39
+ ))
40
+ BreadcrumbItem.displayName = "BreadcrumbItem"
41
+
42
+ const BreadcrumbLink = React.forwardRef<
43
+ HTMLAnchorElement,
44
+ React.ComponentPropsWithoutRef<"a"> & {
45
+ asChild?: boolean
46
+ }
47
+ >(({ asChild, className, ...props }, ref) => {
48
+ const Comp = asChild ? Slot : "a"
49
+
50
+ return (
51
+ <Comp
52
+ ref={ref}
53
+ className={cn("transition-colors hover:text-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ })
58
+ BreadcrumbLink.displayName = "BreadcrumbLink"
59
+
60
+ const BreadcrumbPage = React.forwardRef<
61
+ HTMLSpanElement,
62
+ React.ComponentPropsWithoutRef<"span">
63
+ >(({ className, ...props }, ref) => (
64
+ <span
65
+ ref={ref}
66
+ role="link"
67
+ aria-disabled="true"
68
+ aria-current="page"
69
+ className={cn("font-normal text-foreground", className)}
70
+ {...props}
71
+ />
72
+ ))
73
+ BreadcrumbPage.displayName = "BreadcrumbPage"
74
+
75
+ const BreadcrumbSeparator = ({
76
+ children,
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<"li">) => (
80
+ <li
81
+ role="presentation"
82
+ aria-hidden="true"
83
+ className={cn("[&>svg]:size-3.5", className)}
84
+ {...props}
85
+ >
86
+ {children ?? <ChevronRight />}
87
+ </li>
88
+ )
89
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90
+
91
+ const BreadcrumbEllipsis = ({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<"span">) => (
95
+ <span
96
+ role="presentation"
97
+ aria-hidden="true"
98
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
99
+ {...props}
100
+ >
101
+ <MoreHorizontal className="h-4 w-4" />
102
+ <span className="sr-only">More</span>
103
+ </span>
104
+ )
105
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106
+
107
+ export {
108
+ Breadcrumb,
109
+ BreadcrumbList,
110
+ BreadcrumbItem,
111
+ BreadcrumbLink,
112
+ BreadcrumbPage,
113
+ BreadcrumbSeparator,
114
+ BreadcrumbEllipsis,
115
+ }
rag-quest-hub/src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
rag-quest-hub/src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { DayPicker } from "react-day-picker";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { buttonVariants } from "@/components/ui/button";
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
+ month: "space-y-4",
23
+ caption: "flex justify-center pt-1 relative items-center",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "space-x-1 flex items-center",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-y-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
38
+ day: cn(
39
+ buttonVariants({ variant: "ghost" }),
40
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
41
+ ),
42
+ day_range_end: "day-range-end",
43
+ day_selected:
44
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
45
+ day_today: "bg-accent text-accent-foreground",
46
+ day_outside:
47
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
48
+ day_disabled: "text-muted-foreground opacity-50",
49
+ day_range_middle:
50
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
56
+ IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
57
+ }}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+ Calendar.displayName = "Calendar";
63
+
64
+ export { Calendar };
rag-quest-hub/src/components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <p
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
rag-quest-hub/src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import useEmblaCarousel, {
3
+ type UseEmblaCarouselType,
4
+ } from "embla-carousel-react"
5
+ import { ArrowLeft, ArrowRight } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+
10
+ type CarouselApi = UseEmblaCarouselType[1]
11
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
12
+ type CarouselOptions = UseCarouselParameters[0]
13
+ type CarouselPlugin = UseCarouselParameters[1]
14
+
15
+ type CarouselProps = {
16
+ opts?: CarouselOptions
17
+ plugins?: CarouselPlugin
18
+ orientation?: "horizontal" | "vertical"
19
+ setApi?: (api: CarouselApi) => void
20
+ }
21
+
22
+ type CarouselContextProps = {
23
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
24
+ api: ReturnType<typeof useEmblaCarousel>[1]
25
+ scrollPrev: () => void
26
+ scrollNext: () => void
27
+ canScrollPrev: boolean
28
+ canScrollNext: boolean
29
+ } & CarouselProps
30
+
31
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
+
33
+ function useCarousel() {
34
+ const context = React.useContext(CarouselContext)
35
+
36
+ if (!context) {
37
+ throw new Error("useCarousel must be used within a <Carousel />")
38
+ }
39
+
40
+ return context
41
+ }
42
+
43
+ const Carousel = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
46
+ >(
47
+ (
48
+ {
49
+ orientation = "horizontal",
50
+ opts,
51
+ setApi,
52
+ plugins,
53
+ className,
54
+ children,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [carouselRef, api] = useEmblaCarousel(
60
+ {
61
+ ...opts,
62
+ axis: orientation === "horizontal" ? "x" : "y",
63
+ },
64
+ plugins
65
+ )
66
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
68
+
69
+ const onSelect = React.useCallback((api: CarouselApi) => {
70
+ if (!api) {
71
+ return
72
+ }
73
+
74
+ setCanScrollPrev(api.canScrollPrev())
75
+ setCanScrollNext(api.canScrollNext())
76
+ }, [])
77
+
78
+ const scrollPrev = React.useCallback(() => {
79
+ api?.scrollPrev()
80
+ }, [api])
81
+
82
+ const scrollNext = React.useCallback(() => {
83
+ api?.scrollNext()
84
+ }, [api])
85
+
86
+ const handleKeyDown = React.useCallback(
87
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (event.key === "ArrowLeft") {
89
+ event.preventDefault()
90
+ scrollPrev()
91
+ } else if (event.key === "ArrowRight") {
92
+ event.preventDefault()
93
+ scrollNext()
94
+ }
95
+ },
96
+ [scrollPrev, scrollNext]
97
+ )
98
+
99
+ React.useEffect(() => {
100
+ if (!api || !setApi) {
101
+ return
102
+ }
103
+
104
+ setApi(api)
105
+ }, [api, setApi])
106
+
107
+ React.useEffect(() => {
108
+ if (!api) {
109
+ return
110
+ }
111
+
112
+ onSelect(api)
113
+ api.on("reInit", onSelect)
114
+ api.on("select", onSelect)
115
+
116
+ return () => {
117
+ api?.off("select", onSelect)
118
+ }
119
+ }, [api, onSelect])
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api: api,
126
+ opts,
127
+ orientation:
128
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129
+ scrollPrev,
130
+ scrollNext,
131
+ canScrollPrev,
132
+ canScrollNext,
133
+ }}
134
+ >
135
+ <div
136
+ ref={ref}
137
+ onKeyDownCapture={handleKeyDown}
138
+ className={cn("relative", className)}
139
+ role="region"
140
+ aria-roledescription="carousel"
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ </CarouselContext.Provider>
146
+ )
147
+ }
148
+ )
149
+ Carousel.displayName = "Carousel"
150
+
151
+ const CarouselContent = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.HTMLAttributes<HTMLDivElement>
154
+ >(({ className, ...props }, ref) => {
155
+ const { carouselRef, orientation } = useCarousel()
156
+
157
+ return (
158
+ <div ref={carouselRef} className="overflow-hidden">
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "flex",
163
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164
+ className
165
+ )}
166
+ {...props}
167
+ />
168
+ </div>
169
+ )
170
+ })
171
+ CarouselContent.displayName = "CarouselContent"
172
+
173
+ const CarouselItem = React.forwardRef<
174
+ HTMLDivElement,
175
+ React.HTMLAttributes<HTMLDivElement>
176
+ >(({ className, ...props }, ref) => {
177
+ const { orientation } = useCarousel()
178
+
179
+ return (
180
+ <div
181
+ ref={ref}
182
+ role="group"
183
+ aria-roledescription="slide"
184
+ className={cn(
185
+ "min-w-0 shrink-0 grow-0 basis-full",
186
+ orientation === "horizontal" ? "pl-4" : "pt-4",
187
+ className
188
+ )}
189
+ {...props}
190
+ />
191
+ )
192
+ })
193
+ CarouselItem.displayName = "CarouselItem"
194
+
195
+ const CarouselPrevious = React.forwardRef<
196
+ HTMLButtonElement,
197
+ React.ComponentProps<typeof Button>
198
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200
+
201
+ return (
202
+ <Button
203
+ ref={ref}
204
+ variant={variant}
205
+ size={size}
206
+ className={cn(
207
+ "absolute h-8 w-8 rounded-full",
208
+ orientation === "horizontal"
209
+ ? "-left-12 top-1/2 -translate-y-1/2"
210
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211
+ className
212
+ )}
213
+ disabled={!canScrollPrev}
214
+ onClick={scrollPrev}
215
+ {...props}
216
+ >
217
+ <ArrowLeft className="h-4 w-4" />
218
+ <span className="sr-only">Previous slide</span>
219
+ </Button>
220
+ )
221
+ })
222
+ CarouselPrevious.displayName = "CarouselPrevious"
223
+
224
+ const CarouselNext = React.forwardRef<
225
+ HTMLButtonElement,
226
+ React.ComponentProps<typeof Button>
227
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
229
+
230
+ return (
231
+ <Button
232
+ ref={ref}
233
+ variant={variant}
234
+ size={size}
235
+ className={cn(
236
+ "absolute h-8 w-8 rounded-full",
237
+ orientation === "horizontal"
238
+ ? "-right-12 top-1/2 -translate-y-1/2"
239
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240
+ className
241
+ )}
242
+ disabled={!canScrollNext}
243
+ onClick={scrollNext}
244
+ {...props}
245
+ >
246
+ <ArrowRight className="h-4 w-4" />
247
+ <span className="sr-only">Next slide</span>
248
+ </Button>
249
+ )
250
+ })
251
+ CarouselNext.displayName = "CarouselNext"
252
+
253
+ export {
254
+ type CarouselApi,
255
+ Carousel,
256
+ CarouselContent,
257
+ CarouselItem,
258
+ CarouselPrevious,
259
+ CarouselNext,
260
+ }
rag-quest-hub/src/components/ui/chart.tsx ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as RechartsPrimitive from "recharts"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode
12
+ icon?: React.ComponentType
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ )
17
+ }
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig
21
+ }
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext)
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />")
30
+ }
31
+
32
+ return context
33
+ }
34
+
35
+ const ChartContainer = React.forwardRef<
36
+ HTMLDivElement,
37
+ React.ComponentProps<"div"> & {
38
+ config: ChartConfig
39
+ children: React.ComponentProps<
40
+ typeof RechartsPrimitive.ResponsiveContainer
41
+ >["children"]
42
+ }
43
+ >(({ id, className, children, config, ...props }, ref) => {
44
+ const uniqueId = React.useId()
45
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
46
+
47
+ return (
48
+ <ChartContext.Provider value={{ config }}>
49
+ <div
50
+ data-chart={chartId}
51
+ ref={ref}
52
+ className={cn(
53
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ <ChartStyle id={chartId} config={config} />
59
+ <RechartsPrimitive.ResponsiveContainer>
60
+ {children}
61
+ </RechartsPrimitive.ResponsiveContainer>
62
+ </div>
63
+ </ChartContext.Provider>
64
+ )
65
+ })
66
+ ChartContainer.displayName = "Chart"
67
+
68
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
69
+ const colorConfig = Object.entries(config).filter(
70
+ ([_, config]) => config.theme || config.color
71
+ )
72
+
73
+ if (!colorConfig.length) {
74
+ return null
75
+ }
76
+
77
+ return (
78
+ <style
79
+ dangerouslySetInnerHTML={{
80
+ __html: Object.entries(THEMES)
81
+ .map(
82
+ ([theme, prefix]) => `
83
+ ${prefix} [data-chart=${id}] {
84
+ ${colorConfig
85
+ .map(([key, itemConfig]) => {
86
+ const color =
87
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
88
+ itemConfig.color
89
+ return color ? ` --color-${key}: ${color};` : null
90
+ })
91
+ .join("\n")}
92
+ }
93
+ `
94
+ )
95
+ .join("\n"),
96
+ }}
97
+ />
98
+ )
99
+ }
100
+
101
+ const ChartTooltip = RechartsPrimitive.Tooltip
102
+
103
+ const ChartTooltipContent = React.forwardRef<
104
+ HTMLDivElement,
105
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
106
+ React.ComponentProps<"div"> & {
107
+ hideLabel?: boolean
108
+ hideIndicator?: boolean
109
+ indicator?: "line" | "dot" | "dashed"
110
+ nameKey?: string
111
+ labelKey?: string
112
+ }
113
+ >(
114
+ (
115
+ {
116
+ active,
117
+ payload,
118
+ className,
119
+ indicator = "dot",
120
+ hideLabel = false,
121
+ hideIndicator = false,
122
+ label,
123
+ labelFormatter,
124
+ labelClassName,
125
+ formatter,
126
+ color,
127
+ nameKey,
128
+ labelKey,
129
+ },
130
+ ref
131
+ ) => {
132
+ const { config } = useChart()
133
+
134
+ const tooltipLabel = React.useMemo(() => {
135
+ if (hideLabel || !payload?.length) {
136
+ return null
137
+ }
138
+
139
+ const [item] = payload
140
+ const key = `${labelKey || item.dataKey || item.name || "value"}`
141
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
142
+ const value =
143
+ !labelKey && typeof label === "string"
144
+ ? config[label as keyof typeof config]?.label || label
145
+ : itemConfig?.label
146
+
147
+ if (labelFormatter) {
148
+ return (
149
+ <div className={cn("font-medium", labelClassName)}>
150
+ {labelFormatter(value, payload)}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ if (!value) {
156
+ return null
157
+ }
158
+
159
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
160
+ }, [
161
+ label,
162
+ labelFormatter,
163
+ payload,
164
+ hideLabel,
165
+ labelClassName,
166
+ config,
167
+ labelKey,
168
+ ])
169
+
170
+ if (!active || !payload?.length) {
171
+ return null
172
+ }
173
+
174
+ const nestLabel = payload.length === 1 && indicator !== "dot"
175
+
176
+ return (
177
+ <div
178
+ ref={ref}
179
+ className={cn(
180
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
181
+ className
182
+ )}
183
+ >
184
+ {!nestLabel ? tooltipLabel : null}
185
+ <div className="grid gap-1.5">
186
+ {payload.map((item, index) => {
187
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
188
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
189
+ const indicatorColor = color || item.payload.fill || item.color
190
+
191
+ return (
192
+ <div
193
+ key={item.dataKey}
194
+ className={cn(
195
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
196
+ indicator === "dot" && "items-center"
197
+ )}
198
+ >
199
+ {formatter && item?.value !== undefined && item.name ? (
200
+ formatter(item.value, item.name, item, index, item.payload)
201
+ ) : (
202
+ <>
203
+ {itemConfig?.icon ? (
204
+ <itemConfig.icon />
205
+ ) : (
206
+ !hideIndicator && (
207
+ <div
208
+ className={cn(
209
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
210
+ {
211
+ "h-2.5 w-2.5": indicator === "dot",
212
+ "w-1": indicator === "line",
213
+ "w-0 border-[1.5px] border-dashed bg-transparent":
214
+ indicator === "dashed",
215
+ "my-0.5": nestLabel && indicator === "dashed",
216
+ }
217
+ )}
218
+ style={
219
+ {
220
+ "--color-bg": indicatorColor,
221
+ "--color-border": indicatorColor,
222
+ } as React.CSSProperties
223
+ }
224
+ />
225
+ )
226
+ )}
227
+ <div
228
+ className={cn(
229
+ "flex flex-1 justify-between leading-none",
230
+ nestLabel ? "items-end" : "items-center"
231
+ )}
232
+ >
233
+ <div className="grid gap-1.5">
234
+ {nestLabel ? tooltipLabel : null}
235
+ <span className="text-muted-foreground">
236
+ {itemConfig?.label || item.name}
237
+ </span>
238
+ </div>
239
+ {item.value && (
240
+ <span className="font-mono font-medium tabular-nums text-foreground">
241
+ {item.value.toLocaleString()}
242
+ </span>
243
+ )}
244
+ </div>
245
+ </>
246
+ )}
247
+ </div>
248
+ )
249
+ })}
250
+ </div>
251
+ </div>
252
+ )
253
+ }
254
+ )
255
+ ChartTooltipContent.displayName = "ChartTooltip"
256
+
257
+ const ChartLegend = RechartsPrimitive.Legend
258
+
259
+ const ChartLegendContent = React.forwardRef<
260
+ HTMLDivElement,
261
+ React.ComponentProps<"div"> &
262
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
263
+ hideIcon?: boolean
264
+ nameKey?: string
265
+ }
266
+ >(
267
+ (
268
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
269
+ ref
270
+ ) => {
271
+ const { config } = useChart()
272
+
273
+ if (!payload?.length) {
274
+ return null
275
+ }
276
+
277
+ return (
278
+ <div
279
+ ref={ref}
280
+ className={cn(
281
+ "flex items-center justify-center gap-4",
282
+ verticalAlign === "top" ? "pb-3" : "pt-3",
283
+ className
284
+ )}
285
+ >
286
+ {payload.map((item) => {
287
+ const key = `${nameKey || item.dataKey || "value"}`
288
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
289
+
290
+ return (
291
+ <div
292
+ key={item.value}
293
+ className={cn(
294
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
295
+ )}
296
+ >
297
+ {itemConfig?.icon && !hideIcon ? (
298
+ <itemConfig.icon />
299
+ ) : (
300
+ <div
301
+ className="h-2 w-2 shrink-0 rounded-[2px]"
302
+ style={{
303
+ backgroundColor: item.color,
304
+ }}
305
+ />
306
+ )}
307
+ {itemConfig?.label}
308
+ </div>
309
+ )
310
+ })}
311
+ </div>
312
+ )
313
+ }
314
+ )
315
+ ChartLegendContent.displayName = "ChartLegend"
316
+
317
+ // Helper to extract item config from a payload.
318
+ function getPayloadConfigFromPayload(
319
+ config: ChartConfig,
320
+ payload: unknown,
321
+ key: string
322
+ ) {
323
+ if (typeof payload !== "object" || payload === null) {
324
+ return undefined
325
+ }
326
+
327
+ const payloadPayload =
328
+ "payload" in payload &&
329
+ typeof payload.payload === "object" &&
330
+ payload.payload !== null
331
+ ? payload.payload
332
+ : undefined
333
+
334
+ let configLabelKey: string = key
335
+
336
+ if (
337
+ key in payload &&
338
+ typeof payload[key as keyof typeof payload] === "string"
339
+ ) {
340
+ configLabelKey = payload[key as keyof typeof payload] as string
341
+ } else if (
342
+ payloadPayload &&
343
+ key in payloadPayload &&
344
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
345
+ ) {
346
+ configLabelKey = payloadPayload[
347
+ key as keyof typeof payloadPayload
348
+ ] as string
349
+ }
350
+
351
+ return configLabelKey in config
352
+ ? config[configLabelKey]
353
+ : config[key as keyof typeof config]
354
+ }
355
+
356
+ export {
357
+ ChartContainer,
358
+ ChartTooltip,
359
+ ChartTooltipContent,
360
+ ChartLegend,
361
+ ChartLegendContent,
362
+ ChartStyle,
363
+ }
rag-quest-hub/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
rag-quest-hub/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
rag-quest-hub/src/components/ui/command.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { type DialogProps } from "@radix-ui/react-dialog"
3
+ import { Command as CommandPrimitive } from "cmdk"
4
+ import { Search } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ ))
22
+ Command.displayName = CommandPrimitive.displayName
23
+
24
+ interface CommandDialogProps extends DialogProps {}
25
+
26
+ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
30
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ )
36
+ }
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ))
54
+
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName
56
+
57
+ const CommandList = React.forwardRef<
58
+ React.ElementRef<typeof CommandPrimitive.List>,
59
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
+ >(({ className, ...props }, ref) => (
61
+ <CommandPrimitive.List
62
+ ref={ref}
63
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
+ {...props}
65
+ />
66
+ ))
67
+
68
+ CommandList.displayName = CommandPrimitive.List.displayName
69
+
70
+ const CommandEmpty = React.forwardRef<
71
+ React.ElementRef<typeof CommandPrimitive.Empty>,
72
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
+ >((props, ref) => (
74
+ <CommandPrimitive.Empty
75
+ ref={ref}
76
+ className="py-6 text-center text-sm"
77
+ {...props}
78
+ />
79
+ ))
80
+
81
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82
+
83
+ const CommandGroup = React.forwardRef<
84
+ React.ElementRef<typeof CommandPrimitive.Group>,
85
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
86
+ >(({ className, ...props }, ref) => (
87
+ <CommandPrimitive.Group
88
+ ref={ref}
89
+ className={cn(
90
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ ))
96
+
97
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
98
+
99
+ const CommandSeparator = React.forwardRef<
100
+ React.ElementRef<typeof CommandPrimitive.Separator>,
101
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
102
+ >(({ className, ...props }, ref) => (
103
+ <CommandPrimitive.Separator
104
+ ref={ref}
105
+ className={cn("-mx-1 h-px bg-border", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110
+
111
+ const CommandItem = React.forwardRef<
112
+ React.ElementRef<typeof CommandPrimitive.Item>,
113
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
114
+ >(({ className, ...props }, ref) => (
115
+ <CommandPrimitive.Item
116
+ ref={ref}
117
+ className={cn(
118
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ ))
124
+
125
+ CommandItem.displayName = CommandPrimitive.Item.displayName
126
+
127
+ const CommandShortcut = ({
128
+ className,
129
+ ...props
130
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
131
+ return (
132
+ <span
133
+ className={cn(
134
+ "ml-auto text-xs tracking-widest text-muted-foreground",
135
+ className
136
+ )}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+ CommandShortcut.displayName = "CommandShortcut"
142
+
143
+ export {
144
+ Command,
145
+ CommandDialog,
146
+ CommandInput,
147
+ CommandList,
148
+ CommandEmpty,
149
+ CommandGroup,
150
+ CommandItem,
151
+ CommandShortcut,
152
+ CommandSeparator,
153
+ }
rag-quest-hub/src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ))
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ))
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ))
111
+ ContextMenuCheckboxItem.displayName =
112
+ ContextMenuPrimitive.CheckboxItem.displayName
113
+
114
+ const ContextMenuRadioItem = React.forwardRef<
115
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <ContextMenuPrimitive.RadioItem
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <ContextMenuPrimitive.ItemIndicator>
128
+ <Circle className="h-2 w-2 fill-current" />
129
+ </ContextMenuPrimitive.ItemIndicator>
130
+ </span>
131
+ {children}
132
+ </ContextMenuPrimitive.RadioItem>
133
+ ))
134
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
135
+
136
+ const ContextMenuLabel = React.forwardRef<
137
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
138
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139
+ inset?: boolean
140
+ }
141
+ >(({ className, inset, ...props }, ref) => (
142
+ <ContextMenuPrimitive.Label
143
+ ref={ref}
144
+ className={cn(
145
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
146
+ inset && "pl-8",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ ))
152
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
153
+
154
+ const ContextMenuSeparator = React.forwardRef<
155
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157
+ >(({ className, ...props }, ref) => (
158
+ <ContextMenuPrimitive.Separator
159
+ ref={ref}
160
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
161
+ {...props}
162
+ />
163
+ ))
164
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
165
+
166
+ const ContextMenuShortcut = ({
167
+ className,
168
+ ...props
169
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
170
+ return (
171
+ <span
172
+ className={cn(
173
+ "ml-auto text-xs tracking-widest text-muted-foreground",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
181
+
182
+ export {
183
+ ContextMenu,
184
+ ContextMenuTrigger,
185
+ ContextMenuContent,
186
+ ContextMenuItem,
187
+ ContextMenuCheckboxItem,
188
+ ContextMenuRadioItem,
189
+ ContextMenuLabel,
190
+ ContextMenuSeparator,
191
+ ContextMenuShortcut,
192
+ ContextMenuGroup,
193
+ ContextMenuPortal,
194
+ ContextMenuSub,
195
+ ContextMenuSubContent,
196
+ ContextMenuSubTrigger,
197
+ ContextMenuRadioGroup,
198
+ }