Commit
·
3b5d2e9
1
Parent(s):
1d9404d
Full Project
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +25 -0
- .gitignore +285 -0
- DOCKER.md +284 -0
- Dockerfile +17 -2
- alembic.ini +115 -0
- alembic/env.py +96 -0
- alembic/script.py.mako +24 -0
- alembic/versions/001_create_users_table.py +39 -0
- alembic/versions/002_create_documents_table.py +38 -0
- docker-compose.postgres.yml +33 -0
- docker-compose.prod.yml +23 -0
- docker-compose.yml +51 -12
- pytest.ini +17 -0
- rag-quest-hub/.gitignore +24 -0
- rag-quest-hub/Dockerfile +32 -0
- rag-quest-hub/Dockerfile.dev +20 -0
- rag-quest-hub/README.md +73 -0
- rag-quest-hub/components.json +20 -0
- rag-quest-hub/eslint.config.js +29 -0
- rag-quest-hub/index.html +24 -0
- rag-quest-hub/nginx.conf +32 -0
- rag-quest-hub/package-lock.json +0 -0
- rag-quest-hub/package.json +94 -0
- rag-quest-hub/postcss.config.js +6 -0
- rag-quest-hub/src/App.css +42 -0
- rag-quest-hub/src/App.tsx +56 -0
- rag-quest-hub/src/assets/hero-bg.jpg +0 -0
- rag-quest-hub/src/components/ChatInterface.tsx +389 -0
- rag-quest-hub/src/components/ConnectionStatus.tsx +283 -0
- rag-quest-hub/src/components/DocumentUpload.tsx +364 -0
- rag-quest-hub/src/components/ErrorBoundary.tsx +195 -0
- rag-quest-hub/src/components/Header.tsx +45 -0
- rag-quest-hub/src/components/ProtectedRoute.tsx +35 -0
- rag-quest-hub/src/components/ThemeToggle.tsx +31 -0
- rag-quest-hub/src/components/ui/accordion.tsx +56 -0
- rag-quest-hub/src/components/ui/alert-dialog.tsx +139 -0
- rag-quest-hub/src/components/ui/alert.tsx +59 -0
- rag-quest-hub/src/components/ui/aspect-ratio.tsx +5 -0
- rag-quest-hub/src/components/ui/avatar.tsx +48 -0
- rag-quest-hub/src/components/ui/badge.tsx +36 -0
- rag-quest-hub/src/components/ui/breadcrumb.tsx +115 -0
- rag-quest-hub/src/components/ui/button.tsx +56 -0
- rag-quest-hub/src/components/ui/calendar.tsx +64 -0
- rag-quest-hub/src/components/ui/card.tsx +79 -0
- rag-quest-hub/src/components/ui/carousel.tsx +260 -0
- rag-quest-hub/src/components/ui/chart.tsx +363 -0
- rag-quest-hub/src/components/ui/checkbox.tsx +28 -0
- rag-quest-hub/src/components/ui/collapsible.tsx +9 -0
- rag-quest-hub/src/components/ui/command.tsx +153 -0
- 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 |
-
#
|
| 29 |
-
CMD ["
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 36 |
-
|
| 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 |
+
}
|