Rox-Turbo commited on
Commit
31ca64a
·
verified ·
1 Parent(s): ed80e94

Upload 18 files

Browse files
.env.example ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rox AI Environment Configuration
2
+ # ================================
3
+ # Copy this file to .env and fill in your values
4
+ # DO NOT commit .env to version control
5
+ #
6
+ # Required variables are marked with [REQUIRED]
7
+ # Optional variables have default values shown
8
+
9
+ # ================================
10
+ # API Configuration [REQUIRED]
11
+ # ================================
12
+
13
+ # NVIDIA API Key for AI model access
14
+ # Get your key from: https://build.nvidia.com/
15
+ # This is required for the AI chat functionality to work
16
+ NVIDIA_API_KEY=your-nvidia-api-key-here
17
+
18
+ # ================================
19
+ # Server Configuration [OPTIONAL]
20
+ # ================================
21
+
22
+ # Port to run the server on (default: 7860)
23
+ PORT=7860
24
+
25
+ # Host to bind to (default: 0.0.0.0 for all interfaces)
26
+ HOST=0.0.0.0
27
+
28
+ # Environment mode: 'production' or 'development'
29
+ # Production mode: reduces logging, enables caching
30
+ # Development mode: verbose logging, no caching
31
+ NODE_ENV=production
32
+
33
+ # ================================
34
+ # Security Configuration [OPTIONAL]
35
+ # ================================
36
+
37
+ # Admin Panel Password (default: rox-admin-2024)
38
+ # Change this to a secure password in production!
39
+ ADMIN_PASSWORD=your-secure-admin-password
40
+
41
+ # JWT Secret for admin sessions (auto-generated if not set)
42
+ # JWT_SECRET=your-jwt-secret-key
43
+
44
+ # CORS allowed origins (comma-separated)
45
+ # Leave empty to allow all origins (not recommended for production)
46
+ # Example: https://yourdomain.com,https://app.yourdomain.com
47
+ # ALLOWED_ORIGINS=
.gitattributes ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+
4
+ # JavaScript and JSON
5
+ *.js text eol=lf
6
+ *.json text eol=lf
7
+
8
+ # CSS and HTML
9
+ *.css text eol=lf
10
+ *.html text eol=lf
11
+
12
+ # Markdown and documentation
13
+ *.md text eol=lf
14
+ *.txt text eol=lf
15
+
16
+ # Shell scripts
17
+ *.sh text eol=lf
18
+
19
+ # Docker
20
+ Dockerfile text eol=lf
21
+
22
+ # Git LFS for large files
23
+ *.7z filter=lfs diff=lfs merge=lfs -text
24
+ *.arrow filter=lfs diff=lfs merge=lfs -text
25
+ *.bin filter=lfs diff=lfs merge=lfs -text
26
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
27
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
28
+ *.ftz filter=lfs diff=lfs merge=lfs -text
29
+ *.gz filter=lfs diff=lfs merge=lfs -text
30
+ *.h5 filter=lfs diff=lfs merge=lfs -text
31
+ *.joblib filter=lfs diff=lfs merge=lfs -text
32
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
33
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
34
+ *.model filter=lfs diff=lfs merge=lfs -text
35
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
36
+ *.npy filter=lfs diff=lfs merge=lfs -text
37
+ *.npz filter=lfs diff=lfs merge=lfs -text
38
+ *.onnx filter=lfs diff=lfs merge=lfs -text
39
+ *.ot filter=lfs diff=lfs merge=lfs -text
40
+ *.parquet filter=lfs diff=lfs merge=lfs -text
41
+ *.pb filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.pt filter=lfs diff=lfs merge=lfs -text
45
+ *.pth filter=lfs diff=lfs merge=lfs -text
46
+ *.rar filter=lfs diff=lfs merge=lfs -text
47
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
48
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
49
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
50
+ *.tar filter=lfs diff=lfs merge=lfs -text
51
+ *.tflite filter=lfs diff=lfs merge=lfs -text
52
+ *.tgz filter=lfs diff=lfs merge=lfs -text
53
+ *.wasm filter=lfs diff=lfs merge=lfs -text
54
+ *.xz filter=lfs diff=lfs merge=lfs -text
55
+ *.zip filter=lfs diff=lfs merge=lfs -text
56
+ *.zst filter=lfs diff=lfs merge=lfs -text
57
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Environment files (contain secrets)
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+ .env.production
9
+ .env.development
10
+
11
+ # Uploaded files
12
+ uploads/*
13
+ !uploads/.gitkeep
14
+ !uploads/gitkeep
15
+
16
+ # Logs
17
+ *.log
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+ lerna-debug.log*
22
+
23
+ # OS files
24
+ .DS_Store
25
+ .DS_Store?
26
+ ._*
27
+ Thumbs.db
28
+ ehthumbs.db
29
+ Desktop.ini
30
+
31
+ # IDE and editors
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+ *.swn
37
+ *~
38
+ *.sublime-workspace
39
+ *.sublime-project
40
+
41
+ # Build artifacts
42
+ dist/
43
+ build/
44
+ out/
45
+ .next/
46
+ .nuxt/
47
+
48
+ # Coverage and testing
49
+ coverage/
50
+ tests/
51
+ .nyc_output/
52
+ *.lcov
53
+
54
+ # Temporary files
55
+ tmp/
56
+ temp/
57
+ *.tmp
58
+ *.temp
59
+
60
+ # Package manager locks (keep package-lock.json)
61
+ yarn.lock
62
+ pnpm-lock.yaml
63
+
64
+ # Debug
65
+ *.pid
66
+ *.seed
67
+ *.pid.lock
68
+
69
+ # Dev files
70
+ jsconfig.json
71
+ *.md
72
+ !README.md
Dockerfile ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rox AI - Hugging Face Spaces Docker Deployment
2
+ # Multi-stage build for smaller image size
3
+ FROM node:20-slim AS builder
4
+
5
+ # Install dependencies only (for caching)
6
+ WORKDIR /app
7
+ COPY package.json package-lock.json* ./
8
+ RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev && npm cache clean --force
9
+
10
+ # Production image
11
+ FROM node:20-slim
12
+
13
+ # Container metadata
14
+ LABEL maintainer="Rox AI Technologies"
15
+ LABEL version="3.9.2"
16
+ LABEL description="Rox AI - Production-Ready Professional AI Chat Interface"
17
+
18
+ # Environment configuration
19
+ ENV NODE_ENV=production
20
+ ENV PORT=7860
21
+ ENV HOST=0.0.0.0
22
+ ENV NODE_OPTIONS="--max-old-space-size=512"
23
+
24
+ # Create non-root user for security
25
+ RUN groupadd -r roxai && useradd -r -g roxai -s /bin/false roxai
26
+
27
+ # Create app directory
28
+ WORKDIR /app
29
+
30
+ # Copy dependencies from builder
31
+ COPY --from=builder /app/node_modules ./node_modules
32
+
33
+ # Copy application files
34
+ COPY package.json ./
35
+ COPY server.js ./
36
+ COPY public ./public
37
+
38
+ # Create uploads directory with proper permissions
39
+ RUN mkdir -p uploads && \
40
+ chown -R roxai:roxai /app && \
41
+ chmod -R 755 /app && \
42
+ chmod 700 uploads
43
+
44
+ # Switch to non-root user
45
+ USER roxai
46
+
47
+ EXPOSE 7860
48
+
49
+ # Health check for container orchestration
50
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
51
+ CMD node -e "require('http').get('http://localhost:7860/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
52
+
53
+ CMD ["node", "server.js"]
README.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Rox AI
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ app_port: 7860
10
+ ---
11
+
12
+ # 🚀 Rox AI
13
+
14
+ A production-ready AI chat interface with multi-model support, file processing, real-time internet search, and seamless conversations.
15
+
16
+ ## ✨ Features
17
+
18
+ - **Multi-Model Support**: Choose from 5 powerful standalone AI models
19
+ - Rox Core (405B) - Fast & efficient for everyday tasks
20
+ - Rox 2.1 Turbo (671B) - Deep thinking & reasoning
21
+ - Rox 3.5 Coder (480B) - Optimized for coding & development
22
+ - Rox 4.5 Turbo (685B) - Advanced reasoning & analysis
23
+ - Rox 5 Ultra (14.8T Data Sets) - Most powerful flagship model
24
+
25
+ All Rox AI models are standalone, proprietary models developed from scratch by Rox AI.
26
+
27
+ - **🆕 Rox Vision** - Advanced Vision-Language Model
28
+ - Dedicated vision-language model for image understanding and analysis
29
+ - Powers image processing across all Rox LLM models
30
+ - Supports JPG, PNG, GIF, WebP, BMP formats
31
+ - Advanced scene understanding, object detection, and visual reasoning
32
+ - Seamlessly integrated with all Rox models for multimodal conversations
33
+
34
+ - **🌐 Live Internet Search**
35
+ - Real-time web search for latest news, events, and information
36
+ - Multiple search sources with intelligent fallback
37
+ - Automatic detection of queries requiring live data
38
+ - Visual indicator shows when responses use internet data
39
+
40
+ - **File Processing**: Upload and analyze documents
41
+ - PDF parsing with text extraction (pdf-parse)
42
+ - Word documents (.docx) with full text extraction (mammoth)
43
+ - Excel spreadsheets (.xlsx)
44
+ - PowerPoint presentations (.pptx)
45
+ - RTF documents
46
+ - Code files (60+ languages: JS, Python, Java, C++, Go, Rust, Vue, Svelte, R, Lua, Dart, etc.)
47
+ - Text and data files (CSV, JSON, YAML, XML, etc.)
48
+ - Images with Rox Vision AI analysis (JPG, PNG, GIF, WebP, BMP)
49
+
50
+ - **Modern UI/UX**
51
+ - Dark/Light theme toggle
52
+ - Smooth animations
53
+ - Mobile-responsive design
54
+ - Code syntax highlighting
55
+ - Math rendering with KaTeX
56
+
57
+ - **Advanced Features**
58
+ - Conversation history
59
+ - Message editing & regeneration
60
+ - Text-to-speech (Listen Aloud)
61
+ - PDF export
62
+ - Keyboard shortcuts
63
+ - PWA support (installable)
64
+
65
+ ## 🚀 Deployment
66
+
67
+ Simply deploy to Hugging Face Spaces using Docker. The API key is pre-configured.
68
+
69
+ ```bash
70
+ # Local development
71
+ npm install
72
+ cp .env.example .env # Configure your NVIDIA API key
73
+ npm start
74
+ ```
75
+
76
+ ### Environment Variables
77
+
78
+ | Variable | Required | Description |
79
+ |----------|----------|-------------|
80
+ | `NVIDIA_API_KEY` | Yes | Your NVIDIA API key from build.nvidia.com |
81
+ | `PORT` | No | Server port (default: 7860) |
82
+ | `HOST` | No | Server host (default: 0.0.0.0) |
83
+ | `NODE_ENV` | No | Environment mode (production/development) |
84
+
85
+ ## 📝 API Endpoints
86
+
87
+ - `POST /api/chat` - Send messages to AI
88
+ - `GET /api/health` - Health check with system info
89
+ - `GET /api/models` - List available models
90
+ - `GET /api/version` - Get server version
91
+
92
+ ## 🛡️ Security
93
+
94
+ - Input validation and sanitization
95
+ - XSS protection
96
+ - CORS configuration
97
+ - Rate limiting (1000 req/min)
98
+ - Security headers (CSP, HSTS, etc.)
99
+ - No sensitive data logging
100
+ - Non-root Docker user
101
+
102
+ ## 📄 License
103
+
104
+ MIT License
105
+
106
+ ---
107
+
108
+ Built with ❤️ by Mohammad Faiz, CEO & Founder of Rox AI
package-lock.json ADDED
@@ -0,0 +1,1515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rox-ai",
3
+ "version": "3.9.2",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "rox-ai",
9
+ "version": "3.9.2",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "compression": "^1.7.4",
13
+ "express": "^4.18.2",
14
+ "mammoth": "^1.11.0",
15
+ "multer": "^1.4.5-lts.1",
16
+ "openai": "^4.104.0",
17
+ "pdf-parse": "^1.1.1"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0",
21
+ "npm": ">=9.0.0"
22
+ }
23
+ },
24
+ "node_modules/@types/node": {
25
+ "version": "18.19.130",
26
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
27
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "undici-types": "~5.26.4"
31
+ }
32
+ },
33
+ "node_modules/@types/node-fetch": {
34
+ "version": "2.6.13",
35
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
36
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@types/node": "*",
40
+ "form-data": "^4.0.4"
41
+ }
42
+ },
43
+ "node_modules/@xmldom/xmldom": {
44
+ "version": "0.8.11",
45
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
46
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
47
+ "license": "MIT",
48
+ "engines": {
49
+ "node": ">=10.0.0"
50
+ }
51
+ },
52
+ "node_modules/abort-controller": {
53
+ "version": "3.0.0",
54
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
55
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
56
+ "license": "MIT",
57
+ "dependencies": {
58
+ "event-target-shim": "^5.0.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=6.5"
62
+ }
63
+ },
64
+ "node_modules/accepts": {
65
+ "version": "1.3.8",
66
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
67
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "mime-types": "~2.1.34",
71
+ "negotiator": "0.6.3"
72
+ },
73
+ "engines": {
74
+ "node": ">= 0.6"
75
+ }
76
+ },
77
+ "node_modules/accepts/node_modules/negotiator": {
78
+ "version": "0.6.3",
79
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
80
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
81
+ "license": "MIT",
82
+ "engines": {
83
+ "node": ">= 0.6"
84
+ }
85
+ },
86
+ "node_modules/agentkeepalive": {
87
+ "version": "4.6.0",
88
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
89
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
90
+ "license": "MIT",
91
+ "dependencies": {
92
+ "humanize-ms": "^1.2.1"
93
+ },
94
+ "engines": {
95
+ "node": ">= 8.0.0"
96
+ }
97
+ },
98
+ "node_modules/append-field": {
99
+ "version": "1.0.0",
100
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
101
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
102
+ "license": "MIT"
103
+ },
104
+ "node_modules/argparse": {
105
+ "version": "1.0.10",
106
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
107
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
108
+ "license": "MIT",
109
+ "dependencies": {
110
+ "sprintf-js": "~1.0.2"
111
+ }
112
+ },
113
+ "node_modules/array-flatten": {
114
+ "version": "1.1.1",
115
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
116
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
117
+ "license": "MIT"
118
+ },
119
+ "node_modules/asynckit": {
120
+ "version": "0.4.0",
121
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
122
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
123
+ "license": "MIT"
124
+ },
125
+ "node_modules/base64-js": {
126
+ "version": "1.5.1",
127
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
128
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
129
+ "funding": [
130
+ {
131
+ "type": "github",
132
+ "url": "https://github.com/sponsors/feross"
133
+ },
134
+ {
135
+ "type": "patreon",
136
+ "url": "https://www.patreon.com/feross"
137
+ },
138
+ {
139
+ "type": "consulting",
140
+ "url": "https://feross.org/support"
141
+ }
142
+ ],
143
+ "license": "MIT"
144
+ },
145
+ "node_modules/bluebird": {
146
+ "version": "3.4.7",
147
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
148
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
149
+ "license": "MIT"
150
+ },
151
+ "node_modules/body-parser": {
152
+ "version": "1.20.4",
153
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
154
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
155
+ "license": "MIT",
156
+ "dependencies": {
157
+ "bytes": "~3.1.2",
158
+ "content-type": "~1.0.5",
159
+ "debug": "2.6.9",
160
+ "depd": "2.0.0",
161
+ "destroy": "~1.2.0",
162
+ "http-errors": "~2.0.1",
163
+ "iconv-lite": "~0.4.24",
164
+ "on-finished": "~2.4.1",
165
+ "qs": "~6.14.0",
166
+ "raw-body": "~2.5.3",
167
+ "type-is": "~1.6.18",
168
+ "unpipe": "~1.0.0"
169
+ },
170
+ "engines": {
171
+ "node": ">= 0.8",
172
+ "npm": "1.2.8000 || >= 1.4.16"
173
+ }
174
+ },
175
+ "node_modules/buffer-from": {
176
+ "version": "1.1.2",
177
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
178
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
179
+ "license": "MIT"
180
+ },
181
+ "node_modules/busboy": {
182
+ "version": "1.6.0",
183
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
184
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
185
+ "dependencies": {
186
+ "streamsearch": "^1.1.0"
187
+ },
188
+ "engines": {
189
+ "node": ">=10.16.0"
190
+ }
191
+ },
192
+ "node_modules/bytes": {
193
+ "version": "3.1.2",
194
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
195
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
196
+ "license": "MIT",
197
+ "engines": {
198
+ "node": ">= 0.8"
199
+ }
200
+ },
201
+ "node_modules/call-bind-apply-helpers": {
202
+ "version": "1.0.2",
203
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
204
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
205
+ "license": "MIT",
206
+ "dependencies": {
207
+ "es-errors": "^1.3.0",
208
+ "function-bind": "^1.1.2"
209
+ },
210
+ "engines": {
211
+ "node": ">= 0.4"
212
+ }
213
+ },
214
+ "node_modules/call-bound": {
215
+ "version": "1.0.4",
216
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
217
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
218
+ "license": "MIT",
219
+ "dependencies": {
220
+ "call-bind-apply-helpers": "^1.0.2",
221
+ "get-intrinsic": "^1.3.0"
222
+ },
223
+ "engines": {
224
+ "node": ">= 0.4"
225
+ },
226
+ "funding": {
227
+ "url": "https://github.com/sponsors/ljharb"
228
+ }
229
+ },
230
+ "node_modules/combined-stream": {
231
+ "version": "1.0.8",
232
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
233
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
234
+ "license": "MIT",
235
+ "dependencies": {
236
+ "delayed-stream": "~1.0.0"
237
+ },
238
+ "engines": {
239
+ "node": ">= 0.8"
240
+ }
241
+ },
242
+ "node_modules/compressible": {
243
+ "version": "2.0.18",
244
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
245
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
246
+ "license": "MIT",
247
+ "dependencies": {
248
+ "mime-db": ">= 1.43.0 < 2"
249
+ },
250
+ "engines": {
251
+ "node": ">= 0.6"
252
+ }
253
+ },
254
+ "node_modules/compression": {
255
+ "version": "1.8.1",
256
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
257
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
258
+ "license": "MIT",
259
+ "dependencies": {
260
+ "bytes": "3.1.2",
261
+ "compressible": "~2.0.18",
262
+ "debug": "2.6.9",
263
+ "negotiator": "~0.6.4",
264
+ "on-headers": "~1.1.0",
265
+ "safe-buffer": "5.2.1",
266
+ "vary": "~1.1.2"
267
+ },
268
+ "engines": {
269
+ "node": ">= 0.8.0"
270
+ }
271
+ },
272
+ "node_modules/concat-stream": {
273
+ "version": "1.6.2",
274
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
275
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
276
+ "engines": [
277
+ "node >= 0.8"
278
+ ],
279
+ "license": "MIT",
280
+ "dependencies": {
281
+ "buffer-from": "^1.0.0",
282
+ "inherits": "^2.0.3",
283
+ "readable-stream": "^2.2.2",
284
+ "typedarray": "^0.0.6"
285
+ }
286
+ },
287
+ "node_modules/content-disposition": {
288
+ "version": "0.5.4",
289
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
290
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
291
+ "license": "MIT",
292
+ "dependencies": {
293
+ "safe-buffer": "5.2.1"
294
+ },
295
+ "engines": {
296
+ "node": ">= 0.6"
297
+ }
298
+ },
299
+ "node_modules/content-type": {
300
+ "version": "1.0.5",
301
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
302
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
303
+ "license": "MIT",
304
+ "engines": {
305
+ "node": ">= 0.6"
306
+ }
307
+ },
308
+ "node_modules/cookie": {
309
+ "version": "0.7.2",
310
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
311
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
312
+ "license": "MIT",
313
+ "engines": {
314
+ "node": ">= 0.6"
315
+ }
316
+ },
317
+ "node_modules/cookie-signature": {
318
+ "version": "1.0.7",
319
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
320
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
321
+ "license": "MIT"
322
+ },
323
+ "node_modules/core-util-is": {
324
+ "version": "1.0.3",
325
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
326
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
327
+ "license": "MIT"
328
+ },
329
+ "node_modules/debug": {
330
+ "version": "2.6.9",
331
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
332
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
333
+ "license": "MIT",
334
+ "dependencies": {
335
+ "ms": "2.0.0"
336
+ }
337
+ },
338
+ "node_modules/delayed-stream": {
339
+ "version": "1.0.0",
340
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
341
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
342
+ "license": "MIT",
343
+ "engines": {
344
+ "node": ">=0.4.0"
345
+ }
346
+ },
347
+ "node_modules/depd": {
348
+ "version": "2.0.0",
349
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
350
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
351
+ "license": "MIT",
352
+ "engines": {
353
+ "node": ">= 0.8"
354
+ }
355
+ },
356
+ "node_modules/destroy": {
357
+ "version": "1.2.0",
358
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
359
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
360
+ "license": "MIT",
361
+ "engines": {
362
+ "node": ">= 0.8",
363
+ "npm": "1.2.8000 || >= 1.4.16"
364
+ }
365
+ },
366
+ "node_modules/dingbat-to-unicode": {
367
+ "version": "1.0.1",
368
+ "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
369
+ "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
370
+ "license": "BSD-2-Clause"
371
+ },
372
+ "node_modules/duck": {
373
+ "version": "0.1.12",
374
+ "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
375
+ "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
376
+ "license": "BSD",
377
+ "dependencies": {
378
+ "underscore": "^1.13.1"
379
+ }
380
+ },
381
+ "node_modules/dunder-proto": {
382
+ "version": "1.0.1",
383
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
384
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
385
+ "license": "MIT",
386
+ "dependencies": {
387
+ "call-bind-apply-helpers": "^1.0.1",
388
+ "es-errors": "^1.3.0",
389
+ "gopd": "^1.2.0"
390
+ },
391
+ "engines": {
392
+ "node": ">= 0.4"
393
+ }
394
+ },
395
+ "node_modules/ee-first": {
396
+ "version": "1.1.1",
397
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
398
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
399
+ "license": "MIT"
400
+ },
401
+ "node_modules/encodeurl": {
402
+ "version": "2.0.0",
403
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
404
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
405
+ "license": "MIT",
406
+ "engines": {
407
+ "node": ">= 0.8"
408
+ }
409
+ },
410
+ "node_modules/es-define-property": {
411
+ "version": "1.0.1",
412
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
413
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
414
+ "license": "MIT",
415
+ "engines": {
416
+ "node": ">= 0.4"
417
+ }
418
+ },
419
+ "node_modules/es-errors": {
420
+ "version": "1.3.0",
421
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
422
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
423
+ "license": "MIT",
424
+ "engines": {
425
+ "node": ">= 0.4"
426
+ }
427
+ },
428
+ "node_modules/es-object-atoms": {
429
+ "version": "1.1.1",
430
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
431
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
432
+ "license": "MIT",
433
+ "dependencies": {
434
+ "es-errors": "^1.3.0"
435
+ },
436
+ "engines": {
437
+ "node": ">= 0.4"
438
+ }
439
+ },
440
+ "node_modules/es-set-tostringtag": {
441
+ "version": "2.1.0",
442
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
443
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
444
+ "license": "MIT",
445
+ "dependencies": {
446
+ "es-errors": "^1.3.0",
447
+ "get-intrinsic": "^1.2.6",
448
+ "has-tostringtag": "^1.0.2",
449
+ "hasown": "^2.0.2"
450
+ },
451
+ "engines": {
452
+ "node": ">= 0.4"
453
+ }
454
+ },
455
+ "node_modules/escape-html": {
456
+ "version": "1.0.3",
457
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
458
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
459
+ "license": "MIT"
460
+ },
461
+ "node_modules/etag": {
462
+ "version": "1.8.1",
463
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
464
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
465
+ "license": "MIT",
466
+ "engines": {
467
+ "node": ">= 0.6"
468
+ }
469
+ },
470
+ "node_modules/event-target-shim": {
471
+ "version": "5.0.1",
472
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
473
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
474
+ "license": "MIT",
475
+ "engines": {
476
+ "node": ">=6"
477
+ }
478
+ },
479
+ "node_modules/express": {
480
+ "version": "4.22.1",
481
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
482
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
483
+ "license": "MIT",
484
+ "dependencies": {
485
+ "accepts": "~1.3.8",
486
+ "array-flatten": "1.1.1",
487
+ "body-parser": "~1.20.3",
488
+ "content-disposition": "~0.5.4",
489
+ "content-type": "~1.0.4",
490
+ "cookie": "~0.7.1",
491
+ "cookie-signature": "~1.0.6",
492
+ "debug": "2.6.9",
493
+ "depd": "2.0.0",
494
+ "encodeurl": "~2.0.0",
495
+ "escape-html": "~1.0.3",
496
+ "etag": "~1.8.1",
497
+ "finalhandler": "~1.3.1",
498
+ "fresh": "~0.5.2",
499
+ "http-errors": "~2.0.0",
500
+ "merge-descriptors": "1.0.3",
501
+ "methods": "~1.1.2",
502
+ "on-finished": "~2.4.1",
503
+ "parseurl": "~1.3.3",
504
+ "path-to-regexp": "~0.1.12",
505
+ "proxy-addr": "~2.0.7",
506
+ "qs": "~6.14.0",
507
+ "range-parser": "~1.2.1",
508
+ "safe-buffer": "5.2.1",
509
+ "send": "~0.19.0",
510
+ "serve-static": "~1.16.2",
511
+ "setprototypeof": "1.2.0",
512
+ "statuses": "~2.0.1",
513
+ "type-is": "~1.6.18",
514
+ "utils-merge": "1.0.1",
515
+ "vary": "~1.1.2"
516
+ },
517
+ "engines": {
518
+ "node": ">= 0.10.0"
519
+ },
520
+ "funding": {
521
+ "type": "opencollective",
522
+ "url": "https://opencollective.com/express"
523
+ }
524
+ },
525
+ "node_modules/finalhandler": {
526
+ "version": "1.3.2",
527
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
528
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
529
+ "license": "MIT",
530
+ "dependencies": {
531
+ "debug": "2.6.9",
532
+ "encodeurl": "~2.0.0",
533
+ "escape-html": "~1.0.3",
534
+ "on-finished": "~2.4.1",
535
+ "parseurl": "~1.3.3",
536
+ "statuses": "~2.0.2",
537
+ "unpipe": "~1.0.0"
538
+ },
539
+ "engines": {
540
+ "node": ">= 0.8"
541
+ }
542
+ },
543
+ "node_modules/form-data": {
544
+ "version": "4.0.5",
545
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
546
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
547
+ "license": "MIT",
548
+ "dependencies": {
549
+ "asynckit": "^0.4.0",
550
+ "combined-stream": "^1.0.8",
551
+ "es-set-tostringtag": "^2.1.0",
552
+ "hasown": "^2.0.2",
553
+ "mime-types": "^2.1.12"
554
+ },
555
+ "engines": {
556
+ "node": ">= 6"
557
+ }
558
+ },
559
+ "node_modules/form-data-encoder": {
560
+ "version": "1.7.2",
561
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
562
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
563
+ "license": "MIT"
564
+ },
565
+ "node_modules/formdata-node": {
566
+ "version": "4.4.1",
567
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
568
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
569
+ "license": "MIT",
570
+ "dependencies": {
571
+ "node-domexception": "1.0.0",
572
+ "web-streams-polyfill": "4.0.0-beta.3"
573
+ },
574
+ "engines": {
575
+ "node": ">= 12.20"
576
+ }
577
+ },
578
+ "node_modules/forwarded": {
579
+ "version": "0.2.0",
580
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
581
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
582
+ "license": "MIT",
583
+ "engines": {
584
+ "node": ">= 0.6"
585
+ }
586
+ },
587
+ "node_modules/fresh": {
588
+ "version": "0.5.2",
589
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
590
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
591
+ "license": "MIT",
592
+ "engines": {
593
+ "node": ">= 0.6"
594
+ }
595
+ },
596
+ "node_modules/function-bind": {
597
+ "version": "1.1.2",
598
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
599
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
600
+ "license": "MIT",
601
+ "funding": {
602
+ "url": "https://github.com/sponsors/ljharb"
603
+ }
604
+ },
605
+ "node_modules/get-intrinsic": {
606
+ "version": "1.3.0",
607
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
608
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
609
+ "license": "MIT",
610
+ "dependencies": {
611
+ "call-bind-apply-helpers": "^1.0.2",
612
+ "es-define-property": "^1.0.1",
613
+ "es-errors": "^1.3.0",
614
+ "es-object-atoms": "^1.1.1",
615
+ "function-bind": "^1.1.2",
616
+ "get-proto": "^1.0.1",
617
+ "gopd": "^1.2.0",
618
+ "has-symbols": "^1.1.0",
619
+ "hasown": "^2.0.2",
620
+ "math-intrinsics": "^1.1.0"
621
+ },
622
+ "engines": {
623
+ "node": ">= 0.4"
624
+ },
625
+ "funding": {
626
+ "url": "https://github.com/sponsors/ljharb"
627
+ }
628
+ },
629
+ "node_modules/get-proto": {
630
+ "version": "1.0.1",
631
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
632
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
633
+ "license": "MIT",
634
+ "dependencies": {
635
+ "dunder-proto": "^1.0.1",
636
+ "es-object-atoms": "^1.0.0"
637
+ },
638
+ "engines": {
639
+ "node": ">= 0.4"
640
+ }
641
+ },
642
+ "node_modules/gopd": {
643
+ "version": "1.2.0",
644
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
645
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
646
+ "license": "MIT",
647
+ "engines": {
648
+ "node": ">= 0.4"
649
+ },
650
+ "funding": {
651
+ "url": "https://github.com/sponsors/ljharb"
652
+ }
653
+ },
654
+ "node_modules/has-symbols": {
655
+ "version": "1.1.0",
656
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
657
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
658
+ "license": "MIT",
659
+ "engines": {
660
+ "node": ">= 0.4"
661
+ },
662
+ "funding": {
663
+ "url": "https://github.com/sponsors/ljharb"
664
+ }
665
+ },
666
+ "node_modules/has-tostringtag": {
667
+ "version": "1.0.2",
668
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
669
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
670
+ "license": "MIT",
671
+ "dependencies": {
672
+ "has-symbols": "^1.0.3"
673
+ },
674
+ "engines": {
675
+ "node": ">= 0.4"
676
+ },
677
+ "funding": {
678
+ "url": "https://github.com/sponsors/ljharb"
679
+ }
680
+ },
681
+ "node_modules/hasown": {
682
+ "version": "2.0.2",
683
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
684
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
685
+ "license": "MIT",
686
+ "dependencies": {
687
+ "function-bind": "^1.1.2"
688
+ },
689
+ "engines": {
690
+ "node": ">= 0.4"
691
+ }
692
+ },
693
+ "node_modules/http-errors": {
694
+ "version": "2.0.1",
695
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
696
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
697
+ "license": "MIT",
698
+ "dependencies": {
699
+ "depd": "~2.0.0",
700
+ "inherits": "~2.0.4",
701
+ "setprototypeof": "~1.2.0",
702
+ "statuses": "~2.0.2",
703
+ "toidentifier": "~1.0.1"
704
+ },
705
+ "engines": {
706
+ "node": ">= 0.8"
707
+ },
708
+ "funding": {
709
+ "type": "opencollective",
710
+ "url": "https://opencollective.com/express"
711
+ }
712
+ },
713
+ "node_modules/humanize-ms": {
714
+ "version": "1.2.1",
715
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
716
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
717
+ "license": "MIT",
718
+ "dependencies": {
719
+ "ms": "^2.0.0"
720
+ }
721
+ },
722
+ "node_modules/iconv-lite": {
723
+ "version": "0.4.24",
724
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
725
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
726
+ "license": "MIT",
727
+ "dependencies": {
728
+ "safer-buffer": ">= 2.1.2 < 3"
729
+ },
730
+ "engines": {
731
+ "node": ">=0.10.0"
732
+ }
733
+ },
734
+ "node_modules/immediate": {
735
+ "version": "3.0.6",
736
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
737
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
738
+ "license": "MIT"
739
+ },
740
+ "node_modules/inherits": {
741
+ "version": "2.0.4",
742
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
743
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
744
+ "license": "ISC"
745
+ },
746
+ "node_modules/ipaddr.js": {
747
+ "version": "1.9.1",
748
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
749
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
750
+ "license": "MIT",
751
+ "engines": {
752
+ "node": ">= 0.10"
753
+ }
754
+ },
755
+ "node_modules/isarray": {
756
+ "version": "1.0.0",
757
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
758
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
759
+ "license": "MIT"
760
+ },
761
+ "node_modules/jszip": {
762
+ "version": "3.10.1",
763
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
764
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
765
+ "license": "(MIT OR GPL-3.0-or-later)",
766
+ "dependencies": {
767
+ "lie": "~3.3.0",
768
+ "pako": "~1.0.2",
769
+ "readable-stream": "~2.3.6",
770
+ "setimmediate": "^1.0.5"
771
+ }
772
+ },
773
+ "node_modules/lie": {
774
+ "version": "3.3.0",
775
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
776
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
777
+ "license": "MIT",
778
+ "dependencies": {
779
+ "immediate": "~3.0.5"
780
+ }
781
+ },
782
+ "node_modules/lop": {
783
+ "version": "0.4.2",
784
+ "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
785
+ "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
786
+ "license": "BSD-2-Clause",
787
+ "dependencies": {
788
+ "duck": "^0.1.12",
789
+ "option": "~0.2.1",
790
+ "underscore": "^1.13.1"
791
+ }
792
+ },
793
+ "node_modules/mammoth": {
794
+ "version": "1.11.0",
795
+ "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz",
796
+ "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==",
797
+ "license": "BSD-2-Clause",
798
+ "dependencies": {
799
+ "@xmldom/xmldom": "^0.8.6",
800
+ "argparse": "~1.0.3",
801
+ "base64-js": "^1.5.1",
802
+ "bluebird": "~3.4.0",
803
+ "dingbat-to-unicode": "^1.0.1",
804
+ "jszip": "^3.7.1",
805
+ "lop": "^0.4.2",
806
+ "path-is-absolute": "^1.0.0",
807
+ "underscore": "^1.13.1",
808
+ "xmlbuilder": "^10.0.0"
809
+ },
810
+ "bin": {
811
+ "mammoth": "bin/mammoth"
812
+ },
813
+ "engines": {
814
+ "node": ">=12.0.0"
815
+ }
816
+ },
817
+ "node_modules/math-intrinsics": {
818
+ "version": "1.1.0",
819
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
820
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
821
+ "license": "MIT",
822
+ "engines": {
823
+ "node": ">= 0.4"
824
+ }
825
+ },
826
+ "node_modules/media-typer": {
827
+ "version": "0.3.0",
828
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
829
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
830
+ "license": "MIT",
831
+ "engines": {
832
+ "node": ">= 0.6"
833
+ }
834
+ },
835
+ "node_modules/merge-descriptors": {
836
+ "version": "1.0.3",
837
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
838
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
839
+ "license": "MIT",
840
+ "funding": {
841
+ "url": "https://github.com/sponsors/sindresorhus"
842
+ }
843
+ },
844
+ "node_modules/methods": {
845
+ "version": "1.1.2",
846
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
847
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
848
+ "license": "MIT",
849
+ "engines": {
850
+ "node": ">= 0.6"
851
+ }
852
+ },
853
+ "node_modules/mime": {
854
+ "version": "1.6.0",
855
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
856
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
857
+ "license": "MIT",
858
+ "bin": {
859
+ "mime": "cli.js"
860
+ },
861
+ "engines": {
862
+ "node": ">=4"
863
+ }
864
+ },
865
+ "node_modules/mime-db": {
866
+ "version": "1.54.0",
867
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
868
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
869
+ "license": "MIT",
870
+ "engines": {
871
+ "node": ">= 0.6"
872
+ }
873
+ },
874
+ "node_modules/mime-types": {
875
+ "version": "2.1.35",
876
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
877
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
878
+ "license": "MIT",
879
+ "dependencies": {
880
+ "mime-db": "1.52.0"
881
+ },
882
+ "engines": {
883
+ "node": ">= 0.6"
884
+ }
885
+ },
886
+ "node_modules/mime-types/node_modules/mime-db": {
887
+ "version": "1.52.0",
888
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
889
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
890
+ "license": "MIT",
891
+ "engines": {
892
+ "node": ">= 0.6"
893
+ }
894
+ },
895
+ "node_modules/minimist": {
896
+ "version": "1.2.8",
897
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
898
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
899
+ "license": "MIT",
900
+ "funding": {
901
+ "url": "https://github.com/sponsors/ljharb"
902
+ }
903
+ },
904
+ "node_modules/mkdirp": {
905
+ "version": "0.5.6",
906
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
907
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
908
+ "license": "MIT",
909
+ "dependencies": {
910
+ "minimist": "^1.2.6"
911
+ },
912
+ "bin": {
913
+ "mkdirp": "bin/cmd.js"
914
+ }
915
+ },
916
+ "node_modules/ms": {
917
+ "version": "2.0.0",
918
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
919
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
920
+ "license": "MIT"
921
+ },
922
+ "node_modules/multer": {
923
+ "version": "1.4.5-lts.2",
924
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
925
+ "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
926
+ "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
927
+ "license": "MIT",
928
+ "dependencies": {
929
+ "append-field": "^1.0.0",
930
+ "busboy": "^1.0.0",
931
+ "concat-stream": "^1.5.2",
932
+ "mkdirp": "^0.5.4",
933
+ "object-assign": "^4.1.1",
934
+ "type-is": "^1.6.4",
935
+ "xtend": "^4.0.0"
936
+ },
937
+ "engines": {
938
+ "node": ">= 6.0.0"
939
+ }
940
+ },
941
+ "node_modules/negotiator": {
942
+ "version": "0.6.4",
943
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
944
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
945
+ "license": "MIT",
946
+ "engines": {
947
+ "node": ">= 0.6"
948
+ }
949
+ },
950
+ "node_modules/node-domexception": {
951
+ "version": "1.0.0",
952
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
953
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
954
+ "deprecated": "Use your platform's native DOMException instead",
955
+ "funding": [
956
+ {
957
+ "type": "github",
958
+ "url": "https://github.com/sponsors/jimmywarting"
959
+ },
960
+ {
961
+ "type": "github",
962
+ "url": "https://paypal.me/jimmywarting"
963
+ }
964
+ ],
965
+ "license": "MIT",
966
+ "engines": {
967
+ "node": ">=10.5.0"
968
+ }
969
+ },
970
+ "node_modules/node-ensure": {
971
+ "version": "0.0.0",
972
+ "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
973
+ "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
974
+ "license": "MIT"
975
+ },
976
+ "node_modules/node-fetch": {
977
+ "version": "2.7.0",
978
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
979
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
980
+ "license": "MIT",
981
+ "dependencies": {
982
+ "whatwg-url": "^5.0.0"
983
+ },
984
+ "engines": {
985
+ "node": "4.x || >=6.0.0"
986
+ },
987
+ "peerDependencies": {
988
+ "encoding": "^0.1.0"
989
+ },
990
+ "peerDependenciesMeta": {
991
+ "encoding": {
992
+ "optional": true
993
+ }
994
+ }
995
+ },
996
+ "node_modules/object-assign": {
997
+ "version": "4.1.1",
998
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
999
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1000
+ "license": "MIT",
1001
+ "engines": {
1002
+ "node": ">=0.10.0"
1003
+ }
1004
+ },
1005
+ "node_modules/object-inspect": {
1006
+ "version": "1.13.4",
1007
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1008
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1009
+ "license": "MIT",
1010
+ "engines": {
1011
+ "node": ">= 0.4"
1012
+ },
1013
+ "funding": {
1014
+ "url": "https://github.com/sponsors/ljharb"
1015
+ }
1016
+ },
1017
+ "node_modules/on-finished": {
1018
+ "version": "2.4.1",
1019
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1020
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1021
+ "license": "MIT",
1022
+ "dependencies": {
1023
+ "ee-first": "1.1.1"
1024
+ },
1025
+ "engines": {
1026
+ "node": ">= 0.8"
1027
+ }
1028
+ },
1029
+ "node_modules/on-headers": {
1030
+ "version": "1.1.0",
1031
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
1032
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
1033
+ "license": "MIT",
1034
+ "engines": {
1035
+ "node": ">= 0.8"
1036
+ }
1037
+ },
1038
+ "node_modules/openai": {
1039
+ "version": "4.104.0",
1040
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
1041
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
1042
+ "license": "Apache-2.0",
1043
+ "dependencies": {
1044
+ "@types/node": "^18.11.18",
1045
+ "@types/node-fetch": "^2.6.4",
1046
+ "abort-controller": "^3.0.0",
1047
+ "agentkeepalive": "^4.2.1",
1048
+ "form-data-encoder": "1.7.2",
1049
+ "formdata-node": "^4.3.2",
1050
+ "node-fetch": "^2.6.7"
1051
+ },
1052
+ "bin": {
1053
+ "openai": "bin/cli"
1054
+ },
1055
+ "peerDependencies": {
1056
+ "ws": "^8.18.0",
1057
+ "zod": "^3.23.8"
1058
+ },
1059
+ "peerDependenciesMeta": {
1060
+ "ws": {
1061
+ "optional": true
1062
+ },
1063
+ "zod": {
1064
+ "optional": true
1065
+ }
1066
+ }
1067
+ },
1068
+ "node_modules/option": {
1069
+ "version": "0.2.4",
1070
+ "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
1071
+ "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
1072
+ "license": "BSD-2-Clause"
1073
+ },
1074
+ "node_modules/pako": {
1075
+ "version": "1.0.11",
1076
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
1077
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
1078
+ "license": "(MIT AND Zlib)"
1079
+ },
1080
+ "node_modules/parseurl": {
1081
+ "version": "1.3.3",
1082
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1083
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1084
+ "license": "MIT",
1085
+ "engines": {
1086
+ "node": ">= 0.8"
1087
+ }
1088
+ },
1089
+ "node_modules/path-is-absolute": {
1090
+ "version": "1.0.1",
1091
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1092
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
1093
+ "license": "MIT",
1094
+ "engines": {
1095
+ "node": ">=0.10.0"
1096
+ }
1097
+ },
1098
+ "node_modules/path-to-regexp": {
1099
+ "version": "0.1.12",
1100
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1101
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1102
+ "license": "MIT"
1103
+ },
1104
+ "node_modules/pdf-parse": {
1105
+ "version": "1.1.4",
1106
+ "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz",
1107
+ "integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==",
1108
+ "license": "MIT",
1109
+ "dependencies": {
1110
+ "node-ensure": "^0.0.0"
1111
+ },
1112
+ "engines": {
1113
+ "node": ">=6.8.1"
1114
+ },
1115
+ "funding": {
1116
+ "type": "github",
1117
+ "url": "https://github.com/sponsors/mehmet-kozan"
1118
+ }
1119
+ },
1120
+ "node_modules/process-nextick-args": {
1121
+ "version": "2.0.1",
1122
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
1123
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
1124
+ "license": "MIT"
1125
+ },
1126
+ "node_modules/proxy-addr": {
1127
+ "version": "2.0.7",
1128
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1129
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1130
+ "license": "MIT",
1131
+ "dependencies": {
1132
+ "forwarded": "0.2.0",
1133
+ "ipaddr.js": "1.9.1"
1134
+ },
1135
+ "engines": {
1136
+ "node": ">= 0.10"
1137
+ }
1138
+ },
1139
+ "node_modules/qs": {
1140
+ "version": "6.14.1",
1141
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
1142
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
1143
+ "license": "BSD-3-Clause",
1144
+ "dependencies": {
1145
+ "side-channel": "^1.1.0"
1146
+ },
1147
+ "engines": {
1148
+ "node": ">=0.6"
1149
+ },
1150
+ "funding": {
1151
+ "url": "https://github.com/sponsors/ljharb"
1152
+ }
1153
+ },
1154
+ "node_modules/range-parser": {
1155
+ "version": "1.2.1",
1156
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1157
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1158
+ "license": "MIT",
1159
+ "engines": {
1160
+ "node": ">= 0.6"
1161
+ }
1162
+ },
1163
+ "node_modules/raw-body": {
1164
+ "version": "2.5.3",
1165
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1166
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1167
+ "license": "MIT",
1168
+ "dependencies": {
1169
+ "bytes": "~3.1.2",
1170
+ "http-errors": "~2.0.1",
1171
+ "iconv-lite": "~0.4.24",
1172
+ "unpipe": "~1.0.0"
1173
+ },
1174
+ "engines": {
1175
+ "node": ">= 0.8"
1176
+ }
1177
+ },
1178
+ "node_modules/readable-stream": {
1179
+ "version": "2.3.8",
1180
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
1181
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
1182
+ "license": "MIT",
1183
+ "dependencies": {
1184
+ "core-util-is": "~1.0.0",
1185
+ "inherits": "~2.0.3",
1186
+ "isarray": "~1.0.0",
1187
+ "process-nextick-args": "~2.0.0",
1188
+ "safe-buffer": "~5.1.1",
1189
+ "string_decoder": "~1.1.1",
1190
+ "util-deprecate": "~1.0.1"
1191
+ }
1192
+ },
1193
+ "node_modules/readable-stream/node_modules/safe-buffer": {
1194
+ "version": "5.1.2",
1195
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
1196
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
1197
+ "license": "MIT"
1198
+ },
1199
+ "node_modules/safe-buffer": {
1200
+ "version": "5.2.1",
1201
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1202
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1203
+ "funding": [
1204
+ {
1205
+ "type": "github",
1206
+ "url": "https://github.com/sponsors/feross"
1207
+ },
1208
+ {
1209
+ "type": "patreon",
1210
+ "url": "https://www.patreon.com/feross"
1211
+ },
1212
+ {
1213
+ "type": "consulting",
1214
+ "url": "https://feross.org/support"
1215
+ }
1216
+ ],
1217
+ "license": "MIT"
1218
+ },
1219
+ "node_modules/safer-buffer": {
1220
+ "version": "2.1.2",
1221
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1222
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1223
+ "license": "MIT"
1224
+ },
1225
+ "node_modules/send": {
1226
+ "version": "0.19.2",
1227
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
1228
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
1229
+ "license": "MIT",
1230
+ "dependencies": {
1231
+ "debug": "2.6.9",
1232
+ "depd": "2.0.0",
1233
+ "destroy": "1.2.0",
1234
+ "encodeurl": "~2.0.0",
1235
+ "escape-html": "~1.0.3",
1236
+ "etag": "~1.8.1",
1237
+ "fresh": "~0.5.2",
1238
+ "http-errors": "~2.0.1",
1239
+ "mime": "1.6.0",
1240
+ "ms": "2.1.3",
1241
+ "on-finished": "~2.4.1",
1242
+ "range-parser": "~1.2.1",
1243
+ "statuses": "~2.0.2"
1244
+ },
1245
+ "engines": {
1246
+ "node": ">= 0.8.0"
1247
+ }
1248
+ },
1249
+ "node_modules/send/node_modules/ms": {
1250
+ "version": "2.1.3",
1251
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1252
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1253
+ "license": "MIT"
1254
+ },
1255
+ "node_modules/serve-static": {
1256
+ "version": "1.16.3",
1257
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
1258
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
1259
+ "license": "MIT",
1260
+ "dependencies": {
1261
+ "encodeurl": "~2.0.0",
1262
+ "escape-html": "~1.0.3",
1263
+ "parseurl": "~1.3.3",
1264
+ "send": "~0.19.1"
1265
+ },
1266
+ "engines": {
1267
+ "node": ">= 0.8.0"
1268
+ }
1269
+ },
1270
+ "node_modules/setimmediate": {
1271
+ "version": "1.0.5",
1272
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
1273
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
1274
+ "license": "MIT"
1275
+ },
1276
+ "node_modules/setprototypeof": {
1277
+ "version": "1.2.0",
1278
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1279
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1280
+ "license": "ISC"
1281
+ },
1282
+ "node_modules/side-channel": {
1283
+ "version": "1.1.0",
1284
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1285
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1286
+ "license": "MIT",
1287
+ "dependencies": {
1288
+ "es-errors": "^1.3.0",
1289
+ "object-inspect": "^1.13.3",
1290
+ "side-channel-list": "^1.0.0",
1291
+ "side-channel-map": "^1.0.1",
1292
+ "side-channel-weakmap": "^1.0.2"
1293
+ },
1294
+ "engines": {
1295
+ "node": ">= 0.4"
1296
+ },
1297
+ "funding": {
1298
+ "url": "https://github.com/sponsors/ljharb"
1299
+ }
1300
+ },
1301
+ "node_modules/side-channel-list": {
1302
+ "version": "1.0.0",
1303
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1304
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1305
+ "license": "MIT",
1306
+ "dependencies": {
1307
+ "es-errors": "^1.3.0",
1308
+ "object-inspect": "^1.13.3"
1309
+ },
1310
+ "engines": {
1311
+ "node": ">= 0.4"
1312
+ },
1313
+ "funding": {
1314
+ "url": "https://github.com/sponsors/ljharb"
1315
+ }
1316
+ },
1317
+ "node_modules/side-channel-map": {
1318
+ "version": "1.0.1",
1319
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1320
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1321
+ "license": "MIT",
1322
+ "dependencies": {
1323
+ "call-bound": "^1.0.2",
1324
+ "es-errors": "^1.3.0",
1325
+ "get-intrinsic": "^1.2.5",
1326
+ "object-inspect": "^1.13.3"
1327
+ },
1328
+ "engines": {
1329
+ "node": ">= 0.4"
1330
+ },
1331
+ "funding": {
1332
+ "url": "https://github.com/sponsors/ljharb"
1333
+ }
1334
+ },
1335
+ "node_modules/side-channel-weakmap": {
1336
+ "version": "1.0.2",
1337
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1338
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1339
+ "license": "MIT",
1340
+ "dependencies": {
1341
+ "call-bound": "^1.0.2",
1342
+ "es-errors": "^1.3.0",
1343
+ "get-intrinsic": "^1.2.5",
1344
+ "object-inspect": "^1.13.3",
1345
+ "side-channel-map": "^1.0.1"
1346
+ },
1347
+ "engines": {
1348
+ "node": ">= 0.4"
1349
+ },
1350
+ "funding": {
1351
+ "url": "https://github.com/sponsors/ljharb"
1352
+ }
1353
+ },
1354
+ "node_modules/sprintf-js": {
1355
+ "version": "1.0.3",
1356
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
1357
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
1358
+ "license": "BSD-3-Clause"
1359
+ },
1360
+ "node_modules/statuses": {
1361
+ "version": "2.0.2",
1362
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1363
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1364
+ "license": "MIT",
1365
+ "engines": {
1366
+ "node": ">= 0.8"
1367
+ }
1368
+ },
1369
+ "node_modules/streamsearch": {
1370
+ "version": "1.1.0",
1371
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1372
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1373
+ "engines": {
1374
+ "node": ">=10.0.0"
1375
+ }
1376
+ },
1377
+ "node_modules/string_decoder": {
1378
+ "version": "1.1.1",
1379
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
1380
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
1381
+ "license": "MIT",
1382
+ "dependencies": {
1383
+ "safe-buffer": "~5.1.0"
1384
+ }
1385
+ },
1386
+ "node_modules/string_decoder/node_modules/safe-buffer": {
1387
+ "version": "5.1.2",
1388
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
1389
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
1390
+ "license": "MIT"
1391
+ },
1392
+ "node_modules/toidentifier": {
1393
+ "version": "1.0.1",
1394
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1395
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1396
+ "license": "MIT",
1397
+ "engines": {
1398
+ "node": ">=0.6"
1399
+ }
1400
+ },
1401
+ "node_modules/tr46": {
1402
+ "version": "0.0.3",
1403
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1404
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
1405
+ "license": "MIT"
1406
+ },
1407
+ "node_modules/type-is": {
1408
+ "version": "1.6.18",
1409
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1410
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1411
+ "license": "MIT",
1412
+ "dependencies": {
1413
+ "media-typer": "0.3.0",
1414
+ "mime-types": "~2.1.24"
1415
+ },
1416
+ "engines": {
1417
+ "node": ">= 0.6"
1418
+ }
1419
+ },
1420
+ "node_modules/typedarray": {
1421
+ "version": "0.0.6",
1422
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1423
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1424
+ "license": "MIT"
1425
+ },
1426
+ "node_modules/underscore": {
1427
+ "version": "1.13.7",
1428
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
1429
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
1430
+ "license": "MIT"
1431
+ },
1432
+ "node_modules/undici-types": {
1433
+ "version": "5.26.5",
1434
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1435
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
1436
+ "license": "MIT"
1437
+ },
1438
+ "node_modules/unpipe": {
1439
+ "version": "1.0.0",
1440
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1441
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1442
+ "license": "MIT",
1443
+ "engines": {
1444
+ "node": ">= 0.8"
1445
+ }
1446
+ },
1447
+ "node_modules/util-deprecate": {
1448
+ "version": "1.0.2",
1449
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1450
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1451
+ "license": "MIT"
1452
+ },
1453
+ "node_modules/utils-merge": {
1454
+ "version": "1.0.1",
1455
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1456
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1457
+ "license": "MIT",
1458
+ "engines": {
1459
+ "node": ">= 0.4.0"
1460
+ }
1461
+ },
1462
+ "node_modules/vary": {
1463
+ "version": "1.1.2",
1464
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1465
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1466
+ "license": "MIT",
1467
+ "engines": {
1468
+ "node": ">= 0.8"
1469
+ }
1470
+ },
1471
+ "node_modules/web-streams-polyfill": {
1472
+ "version": "4.0.0-beta.3",
1473
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
1474
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
1475
+ "license": "MIT",
1476
+ "engines": {
1477
+ "node": ">= 14"
1478
+ }
1479
+ },
1480
+ "node_modules/webidl-conversions": {
1481
+ "version": "3.0.1",
1482
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1483
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
1484
+ "license": "BSD-2-Clause"
1485
+ },
1486
+ "node_modules/whatwg-url": {
1487
+ "version": "5.0.0",
1488
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1489
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1490
+ "license": "MIT",
1491
+ "dependencies": {
1492
+ "tr46": "~0.0.3",
1493
+ "webidl-conversions": "^3.0.0"
1494
+ }
1495
+ },
1496
+ "node_modules/xmlbuilder": {
1497
+ "version": "10.1.1",
1498
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
1499
+ "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
1500
+ "license": "MIT",
1501
+ "engines": {
1502
+ "node": ">=4.0"
1503
+ }
1504
+ },
1505
+ "node_modules/xtend": {
1506
+ "version": "4.0.2",
1507
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1508
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1509
+ "license": "MIT",
1510
+ "engines": {
1511
+ "node": ">=0.4"
1512
+ }
1513
+ }
1514
+ }
1515
+ }
package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rox-ai",
3
+ "version": "3.9.2",
4
+ "description": "Rox AI - Production-Ready Professional AI Chat Interface with Multi-Model Support and Live Internet Search",
5
+ "main": "server.js",
6
+ "engines": {
7
+ "node": ">=18.0.0",
8
+ "npm": ">=9.0.0"
9
+ },
10
+ "scripts": {
11
+ "start": "node server.js",
12
+ "clean": "node -e \"require('fs').rmSync('uploads', {recursive:true,force:true}); require('fs').mkdirSync('uploads'); console.log('Uploads cleaned')\"",
13
+ "health": "node -e \"require('http').get('http://localhost:7860/api/health', r => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>console.log(JSON.parse(d))); }).on('error', e => console.error('Server not running:', e.message))\"",
14
+ "version": "node -e \"console.log(require('./package.json').version)\""
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "chat",
19
+ "assistant",
20
+ "llm",
21
+ "nvidia",
22
+ "deepseek",
23
+ "qwen",
24
+ "llama",
25
+ "file-upload",
26
+ "multi-model",
27
+ "production-ready",
28
+ "pwa",
29
+ "streaming",
30
+ "web-search",
31
+ "internet-access",
32
+ "real-time-data"
33
+ ],
34
+ "author": "Mohammad Faiz - Rox AI",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/roxai/rox-ai",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/roxai/rox-ai.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/roxai/rox-ai/issues"
43
+ },
44
+ "dependencies": {
45
+ "compression": "^1.7.4",
46
+ "express": "^4.18.2",
47
+ "mammoth": "^1.11.0",
48
+ "multer": "^1.4.5-lts.1",
49
+ "openai": "^4.104.0",
50
+ "pdf-parse": "^1.1.1"
51
+ },
52
+ "private": true
53
+ }
private/admin/admin.css ADDED
@@ -0,0 +1,1198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rox AI Admin Panel Stylesheet
3
+ * Matches the exact theme and UI of the user-side chat interface
4
+ */
5
+
6
+ /* ===== CSS Variables - Exact Match with User Side ===== */
7
+ :root {
8
+ --bg-primary: #0f1a24;
9
+ --bg-secondary: #152232;
10
+ --bg-tertiary: #1a2b3c;
11
+ --bg-elevated: #1e3347;
12
+ --bg-hover: #243d52;
13
+ --text-primary: #ffffff;
14
+ --text-secondary: #a0b4c4;
15
+ --text-tertiary: #6b8599;
16
+ --border: #2a3f52;
17
+ --accent: #3eb489;
18
+ --accent-hover: #35a07a;
19
+ --accent-secondary: #d97bb3;
20
+ --user-bubble: #3d4f5f;
21
+ --user-avatar: #d97bb3;
22
+ --ai-avatar: #3eb489;
23
+ --success: #3eb489;
24
+ --error: #ef4444;
25
+ --warning: #f59e0b;
26
+ --sidebar-width: 280px;
27
+ --chats-width: 280px;
28
+ --header-height: 56px;
29
+ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
30
+ }
31
+
32
+ /* ===== Base Styles ===== */
33
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
+
35
+ html { scroll-behavior: smooth; }
36
+
37
+ body {
38
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
39
+ background: var(--bg-primary);
40
+ color: var(--text-primary);
41
+ overflow: hidden;
42
+ -webkit-font-smoothing: antialiased;
43
+ line-height: 1.5;
44
+ }
45
+
46
+ /* ===== Scrollbar ===== */
47
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
48
+ ::-webkit-scrollbar-track { background: transparent; }
49
+ ::-webkit-scrollbar-thumb { background: var(--text-tertiary); border-radius: 3px; }
50
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
51
+
52
+ /* ===== Login Overlay ===== */
53
+ .login-overlay {
54
+ position: fixed;
55
+ inset: 0;
56
+ background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ z-index: 1000;
61
+ }
62
+
63
+ .login-box {
64
+ background: var(--bg-tertiary);
65
+ border: 1px solid var(--border);
66
+ border-radius: 16px;
67
+ padding: 40px;
68
+ width: 100%;
69
+ max-width: 380px;
70
+ text-align: center;
71
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
72
+ }
73
+
74
+ .login-logo { margin-bottom: 24px; }
75
+ .login-logo svg { filter: drop-shadow(0 0 20px rgba(102, 126, 234, 0.4)); }
76
+
77
+ .login-box h2 {
78
+ font-size: 24px;
79
+ font-weight: 600;
80
+ margin-bottom: 8px;
81
+ color: var(--text-primary);
82
+ }
83
+
84
+ .login-box p {
85
+ font-size: 14px;
86
+ color: var(--text-secondary);
87
+ margin-bottom: 24px;
88
+ }
89
+
90
+ .input-group { margin-bottom: 16px; }
91
+
92
+ .input-group input {
93
+ width: 100%;
94
+ padding: 14px 16px;
95
+ background: var(--bg-secondary);
96
+ border: 1px solid var(--border);
97
+ border-radius: 8px;
98
+ color: var(--text-primary);
99
+ font-size: 14px;
100
+ transition: border-color 0.2s, box-shadow 0.2s;
101
+ }
102
+
103
+ .input-group input:focus {
104
+ outline: none;
105
+ border-color: var(--accent);
106
+ box-shadow: 0 0 0 3px rgba(62, 180, 137, 0.15);
107
+ }
108
+
109
+ .input-group input::placeholder { color: var(--text-tertiary); }
110
+
111
+ .login-error {
112
+ color: var(--error);
113
+ font-size: 13px;
114
+ margin-top: 12px;
115
+ min-height: 20px;
116
+ }
117
+
118
+ .login-attempts {
119
+ color: var(--text-tertiary);
120
+ font-size: 11px;
121
+ margin-top: 8px;
122
+ }
123
+
124
+ /* ===== Buttons ===== */
125
+ .btn {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ gap: 6px;
130
+ padding: 10px 16px;
131
+ border: none;
132
+ border-radius: 6px;
133
+ font-size: 13px;
134
+ font-weight: 500;
135
+ cursor: pointer;
136
+ transition: all 0.2s ease;
137
+ }
138
+
139
+ .btn-primary {
140
+ background: var(--accent);
141
+ color: white;
142
+ width: 100%;
143
+ padding: 14px;
144
+ }
145
+
146
+ .btn-primary:hover { background: var(--accent-hover); }
147
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
148
+
149
+ .btn-secondary {
150
+ background: var(--bg-elevated);
151
+ color: var(--text-secondary);
152
+ border: 1px solid var(--border);
153
+ }
154
+
155
+ .btn-secondary:hover {
156
+ background: var(--bg-hover);
157
+ color: var(--text-primary);
158
+ }
159
+
160
+ .btn-danger {
161
+ background: rgba(239, 68, 68, 0.15);
162
+ color: var(--error);
163
+ border: 1px solid rgba(239, 68, 68, 0.3);
164
+ }
165
+
166
+ .btn-danger:hover { background: rgba(239, 68, 68, 0.25); }
167
+
168
+ .btn-sm { padding: 6px 10px; font-size: 12px; }
169
+
170
+ .btn .spinner {
171
+ animation: spin 1s linear infinite;
172
+ }
173
+
174
+ .btn.spinning svg {
175
+ animation: spin 0.8s linear infinite;
176
+ }
177
+
178
+ @keyframes spin {
179
+ from { transform: rotate(0deg); }
180
+ to { transform: rotate(360deg); }
181
+ }
182
+
183
+ /* ===== App Layout ===== */
184
+ .app {
185
+ display: flex;
186
+ height: 100vh;
187
+ width: 100vw;
188
+ overflow: hidden;
189
+ }
190
+
191
+ /* ===== Sidebar ===== */
192
+ .sidebar {
193
+ width: var(--sidebar-width);
194
+ background: var(--bg-secondary);
195
+ border-right: 1px solid var(--border);
196
+ display: flex;
197
+ flex-direction: column;
198
+ flex-shrink: 0;
199
+ transition: transform 0.3s var(--ease-out-expo);
200
+ }
201
+
202
+ .sidebar-header {
203
+ padding: 16px;
204
+ border-bottom: 1px solid var(--border);
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: space-between;
208
+ }
209
+
210
+ .logo-section {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 10px;
214
+ }
215
+
216
+ .logo-section h1 {
217
+ font-size: 16px;
218
+ font-weight: 600;
219
+ background: linear-gradient(135deg, #667eea, #764ba2);
220
+ -webkit-background-clip: text;
221
+ -webkit-text-fill-color: transparent;
222
+ }
223
+
224
+ .btn-logout {
225
+ background: transparent;
226
+ border: none;
227
+ color: var(--text-tertiary);
228
+ cursor: pointer;
229
+ padding: 8px;
230
+ border-radius: 6px;
231
+ transition: all 0.2s;
232
+ }
233
+
234
+ .btn-logout:hover {
235
+ background: var(--bg-hover);
236
+ color: var(--error);
237
+ }
238
+
239
+ /* Stats Row */
240
+ .stats-row {
241
+ display: flex;
242
+ gap: 8px;
243
+ padding: 12px 16px;
244
+ border-bottom: 1px solid var(--border);
245
+ }
246
+
247
+ .stat-box {
248
+ flex: 1;
249
+ background: var(--bg-tertiary);
250
+ border-radius: 8px;
251
+ padding: 10px;
252
+ text-align: center;
253
+ }
254
+
255
+ .stat-val {
256
+ font-size: 18px;
257
+ font-weight: 600;
258
+ color: var(--accent);
259
+ }
260
+
261
+ .stat-lbl {
262
+ font-size: 10px;
263
+ color: var(--text-tertiary);
264
+ text-transform: uppercase;
265
+ letter-spacing: 0.5px;
266
+ }
267
+
268
+ /* Sidebar Tabs */
269
+ .sidebar-tabs {
270
+ display: flex;
271
+ padding: 8px 16px;
272
+ gap: 4px;
273
+ border-bottom: 1px solid var(--border);
274
+ }
275
+
276
+ .tab-btn {
277
+ flex: 1;
278
+ padding: 8px;
279
+ background: transparent;
280
+ border: none;
281
+ color: var(--text-tertiary);
282
+ font-size: 12px;
283
+ font-weight: 500;
284
+ cursor: pointer;
285
+ border-radius: 6px;
286
+ transition: all 0.2s;
287
+ }
288
+
289
+ .tab-btn:hover { background: var(--bg-tertiary); color: var(--text-secondary); }
290
+ .tab-btn.active { background: var(--bg-tertiary); color: var(--accent); }
291
+
292
+ .tab-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
293
+
294
+ /* Search Box */
295
+ .search-box {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 8px;
299
+ padding: 8px 16px;
300
+ margin: 8px 12px;
301
+ background: var(--bg-tertiary);
302
+ border-radius: 8px;
303
+ border: 1px solid var(--border);
304
+ }
305
+
306
+ .search-box svg { color: var(--text-tertiary); flex-shrink: 0; }
307
+
308
+ .search-box input {
309
+ flex: 1;
310
+ background: transparent;
311
+ border: none;
312
+ color: var(--text-primary);
313
+ font-size: 13px;
314
+ outline: none;
315
+ }
316
+
317
+ .search-box input::placeholder { color: var(--text-tertiary); }
318
+
319
+ /* Users Section */
320
+ .users-section { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
321
+
322
+ .section-title {
323
+ padding: 8px 16px;
324
+ font-size: 11px;
325
+ font-weight: 600;
326
+ color: var(--text-tertiary);
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.5px;
329
+ }
330
+
331
+ .user-list {
332
+ flex: 1;
333
+ overflow-y: auto;
334
+ padding: 0 8px 8px;
335
+ }
336
+
337
+ /* User Card */
338
+ .user-card {
339
+ padding: 12px;
340
+ background: var(--bg-tertiary);
341
+ border-radius: 8px;
342
+ margin-bottom: 6px;
343
+ cursor: pointer;
344
+ transition: all 0.2s;
345
+ border: 1px solid transparent;
346
+ }
347
+
348
+ .user-card:hover { background: var(--bg-elevated); }
349
+ .user-card.active { border-color: var(--accent); background: var(--bg-elevated); }
350
+
351
+ .user-card-header {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ margin-bottom: 6px;
356
+ }
357
+
358
+ .user-status {
359
+ width: 8px;
360
+ height: 8px;
361
+ border-radius: 50%;
362
+ background: var(--text-tertiary);
363
+ }
364
+
365
+ .user-status.online { background: var(--success); }
366
+
367
+ .user-ip {
368
+ font-size: 13px;
369
+ font-weight: 500;
370
+ color: var(--text-primary);
371
+ flex: 1;
372
+ }
373
+
374
+ .user-time {
375
+ font-size: 10px;
376
+ color: var(--text-tertiary);
377
+ }
378
+
379
+ .user-meta {
380
+ display: flex;
381
+ align-items: center;
382
+ gap: 8px;
383
+ font-size: 11px;
384
+ color: var(--text-tertiary);
385
+ }
386
+
387
+ .user-badge {
388
+ background: var(--bg-hover);
389
+ padding: 2px 6px;
390
+ border-radius: 4px;
391
+ font-size: 10px;
392
+ }
393
+
394
+ /* Stats Detail */
395
+ .stats-detail {
396
+ padding: 12px;
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 8px;
400
+ }
401
+
402
+ .stat-card {
403
+ background: var(--bg-tertiary);
404
+ border-radius: 8px;
405
+ padding: 12px;
406
+ }
407
+
408
+ .stat-card-title {
409
+ font-size: 11px;
410
+ color: var(--text-tertiary);
411
+ margin-bottom: 4px;
412
+ }
413
+
414
+ .stat-card-value {
415
+ font-size: 20px;
416
+ font-weight: 600;
417
+ color: var(--accent);
418
+ }
419
+
420
+ .model-usage { padding: 0 12px 12px; }
421
+
422
+ .usage-chart {
423
+ background: var(--bg-tertiary);
424
+ border-radius: 8px;
425
+ padding: 12px;
426
+ }
427
+
428
+ .usage-bar {
429
+ display: flex;
430
+ align-items: center;
431
+ gap: 8px;
432
+ margin-bottom: 8px;
433
+ }
434
+
435
+ .usage-bar-label {
436
+ font-size: 11px;
437
+ color: var(--text-secondary);
438
+ width: 100px;
439
+ flex-shrink: 0;
440
+ }
441
+
442
+ .usage-bar-track {
443
+ flex: 1;
444
+ height: 6px;
445
+ background: var(--bg-hover);
446
+ border-radius: 3px;
447
+ overflow: hidden;
448
+ }
449
+
450
+ .usage-bar-fill {
451
+ height: 100%;
452
+ background: var(--accent);
453
+ border-radius: 3px;
454
+ transition: width 0.3s ease;
455
+ }
456
+
457
+ .usage-bar-value {
458
+ font-size: 11px;
459
+ color: var(--text-tertiary);
460
+ width: 40px;
461
+ text-align: right;
462
+ }
463
+
464
+ /* ===== Main Content ===== */
465
+ .main {
466
+ flex: 1;
467
+ display: flex;
468
+ flex-direction: column;
469
+ overflow: hidden;
470
+ background: var(--bg-primary);
471
+ }
472
+
473
+ .main-header {
474
+ height: var(--header-height);
475
+ padding: 0 16px;
476
+ display: flex;
477
+ align-items: center;
478
+ justify-content: space-between;
479
+ border-bottom: 1px solid var(--border);
480
+ background: var(--bg-secondary);
481
+ flex-shrink: 0;
482
+ }
483
+
484
+ .header-left {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 12px;
488
+ }
489
+
490
+ .menu-btn {
491
+ display: none;
492
+ background: transparent;
493
+ border: none;
494
+ color: var(--text-secondary);
495
+ cursor: pointer;
496
+ padding: 8px;
497
+ border-radius: 6px;
498
+ }
499
+
500
+ .menu-btn:hover { background: var(--bg-tertiary); }
501
+
502
+ .main-header h2 {
503
+ font-size: 16px;
504
+ font-weight: 600;
505
+ color: var(--text-primary);
506
+ }
507
+
508
+ .last-updated {
509
+ font-size: 11px;
510
+ color: var(--text-muted);
511
+ margin-left: 8px;
512
+ padding: 2px 8px;
513
+ background: var(--bg-tertiary);
514
+ border-radius: 4px;
515
+ }
516
+
517
+ .header-actions {
518
+ display: flex;
519
+ gap: 8px;
520
+ }
521
+
522
+ /* Content Area */
523
+ .content-area {
524
+ flex: 1;
525
+ display: flex;
526
+ overflow: hidden;
527
+ }
528
+
529
+ /* Chats Panel */
530
+ .chats-panel {
531
+ width: var(--chats-width);
532
+ background: var(--bg-secondary);
533
+ border-right: 1px solid var(--border);
534
+ display: flex;
535
+ flex-direction: column;
536
+ flex-shrink: 0;
537
+ }
538
+
539
+ .chats-header {
540
+ padding: 16px;
541
+ border-bottom: 1px solid var(--border);
542
+ font-size: 13px;
543
+ color: var(--text-secondary);
544
+ }
545
+
546
+ .chats-header span:last-child {
547
+ color: var(--accent);
548
+ font-weight: 500;
549
+ }
550
+
551
+ .chats-list {
552
+ flex: 1;
553
+ overflow-y: auto;
554
+ padding: 8px;
555
+ }
556
+
557
+ /* Chat Card */
558
+ .chat-card {
559
+ padding: 12px;
560
+ background: var(--bg-tertiary);
561
+ border-radius: 8px;
562
+ margin-bottom: 6px;
563
+ cursor: pointer;
564
+ transition: all 0.2s;
565
+ border: 1px solid transparent;
566
+ }
567
+
568
+ .chat-card:hover { background: var(--bg-elevated); }
569
+ .chat-card.active { border-color: var(--accent); background: var(--bg-elevated); }
570
+
571
+ .chat-card-title {
572
+ font-size: 13px;
573
+ font-weight: 500;
574
+ color: var(--text-primary);
575
+ margin-bottom: 4px;
576
+ white-space: nowrap;
577
+ overflow: hidden;
578
+ text-overflow: ellipsis;
579
+ }
580
+
581
+ .chat-card-meta {
582
+ display: flex;
583
+ align-items: center;
584
+ gap: 8px;
585
+ font-size: 11px;
586
+ color: var(--text-tertiary);
587
+ }
588
+
589
+ .chat-card-status {
590
+ width: 6px;
591
+ height: 6px;
592
+ border-radius: 50%;
593
+ background: var(--text-tertiary);
594
+ }
595
+
596
+ .chat-card-status.active { background: var(--success); }
597
+
598
+ /* Chat View */
599
+ .chat-view {
600
+ flex: 1;
601
+ display: flex;
602
+ flex-direction: column;
603
+ overflow: hidden;
604
+ }
605
+
606
+ .chat-view-header {
607
+ padding: 12px 16px;
608
+ border-bottom: 1px solid var(--border);
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: space-between;
612
+ background: var(--bg-secondary);
613
+ }
614
+
615
+ .chat-info h3 {
616
+ font-size: 14px;
617
+ font-weight: 600;
618
+ color: var(--text-primary);
619
+ margin-bottom: 2px;
620
+ }
621
+
622
+ .chat-info p {
623
+ font-size: 11px;
624
+ color: var(--text-tertiary);
625
+ }
626
+
627
+ .messages-area {
628
+ flex: 1;
629
+ overflow-y: auto;
630
+ padding: 16px;
631
+ }
632
+
633
+ /* ===== Messages - EXACT MATCH with User Side ===== */
634
+ .message {
635
+ display: flex;
636
+ gap: 12px;
637
+ margin-bottom: 20px;
638
+ max-width: 100%;
639
+ }
640
+
641
+ .message.user { flex-direction: row-reverse; }
642
+
643
+ .message-avatar {
644
+ width: 32px;
645
+ height: 32px;
646
+ border-radius: 50%;
647
+ display: flex;
648
+ align-items: center;
649
+ justify-content: center;
650
+ flex-shrink: 0;
651
+ font-size: 14px;
652
+ font-weight: 600;
653
+ }
654
+
655
+ .message.user .message-avatar {
656
+ background: var(--user-avatar);
657
+ color: white;
658
+ }
659
+
660
+ .message.assistant .message-avatar {
661
+ background: var(--ai-avatar);
662
+ color: white;
663
+ }
664
+
665
+ .message-wrapper {
666
+ max-width: 85%;
667
+ min-width: 0;
668
+ }
669
+
670
+ .message.user .message-wrapper { text-align: right; }
671
+
672
+ .message-header {
673
+ display: flex;
674
+ align-items: center;
675
+ gap: 8px;
676
+ margin-bottom: 6px;
677
+ font-size: 12px;
678
+ }
679
+
680
+ .message.user .message-header { justify-content: flex-end; }
681
+
682
+ .message-role {
683
+ font-weight: 600;
684
+ color: var(--text-primary);
685
+ }
686
+
687
+ .model-badge {
688
+ background: var(--bg-elevated);
689
+ color: var(--accent);
690
+ padding: 2px 6px;
691
+ border-radius: 4px;
692
+ font-size: 10px;
693
+ font-weight: 500;
694
+ }
695
+
696
+ .internet-badge {
697
+ background: rgba(62, 180, 137, 0.15);
698
+ color: var(--accent);
699
+ padding: 2px 6px;
700
+ border-radius: 4px;
701
+ font-size: 9px;
702
+ }
703
+
704
+ .message-time {
705
+ color: var(--text-tertiary);
706
+ font-size: 10px;
707
+ }
708
+
709
+ /* Message Content */
710
+ .message-content {
711
+ padding: 12px 16px;
712
+ border-radius: 18px;
713
+ line-height: 1.6;
714
+ word-wrap: break-word;
715
+ overflow-wrap: break-word;
716
+ }
717
+
718
+ .message.user .message-content {
719
+ background: var(--user-bubble);
720
+ color: white;
721
+ border-radius: 18px 18px 4px 18px;
722
+ display: inline-block;
723
+ }
724
+
725
+ .message.assistant .message-content {
726
+ background: transparent;
727
+ color: var(--text-primary);
728
+ padding: 0;
729
+ }
730
+
731
+ /* Message Typography - Match User Side */
732
+ .message-content p { margin: 0 0 12px; }
733
+ .message-content p:last-child { margin-bottom: 0; }
734
+
735
+ .message-content h1, .message-content h2, .message-content h3,
736
+ .message-content h4, .message-content h5, .message-content h6 {
737
+ margin: 16px 0 8px;
738
+ font-weight: 600;
739
+ color: var(--text-primary);
740
+ }
741
+
742
+ .message-content h1 { font-size: 1.5em; }
743
+ .message-content h2 { font-size: 1.3em; }
744
+ .message-content h3 { font-size: 1.15em; }
745
+ .message-content h4, .message-content h5, .message-content h6 { font-size: 1em; }
746
+
747
+ .message-content ul, .message-content ol {
748
+ margin: 8px 0;
749
+ padding-left: 24px;
750
+ }
751
+
752
+ .message-content li { margin: 4px 0; }
753
+
754
+ .message-content blockquote {
755
+ border-left: 3px solid var(--accent);
756
+ padding-left: 12px;
757
+ margin: 12px 0;
758
+ color: var(--text-secondary);
759
+ font-style: italic;
760
+ }
761
+
762
+ .message-content a {
763
+ color: var(--accent);
764
+ text-decoration: none;
765
+ }
766
+
767
+ .message-content a:hover { text-decoration: underline; }
768
+
769
+ .message-content hr {
770
+ border: none;
771
+ border-top: 1px solid var(--border);
772
+ margin: 16px 0;
773
+ }
774
+
775
+ /* Inline Code */
776
+ .message-content code:not(.code-content code) {
777
+ background: var(--bg-tertiary);
778
+ color: var(--accent-secondary);
779
+ padding: 2px 6px;
780
+ border-radius: 4px;
781
+ font-family: 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
782
+ font-size: 0.9em;
783
+ }
784
+
785
+ /* Code Blocks - EXACT Match */
786
+ .code-block {
787
+ background: #1e293b;
788
+ border-radius: 8px;
789
+ overflow: hidden;
790
+ margin: 12px 0;
791
+ }
792
+
793
+ .code-header {
794
+ background: #0f172a;
795
+ padding: 8px 12px;
796
+ display: flex;
797
+ justify-content: space-between;
798
+ align-items: center;
799
+ }
800
+
801
+ .code-language {
802
+ font-size: 12px;
803
+ color: #94a3b8;
804
+ font-weight: 500;
805
+ }
806
+
807
+ .code-copy-btn {
808
+ background: transparent;
809
+ border: 1px solid #334155;
810
+ color: #94a3b8;
811
+ padding: 4px 8px;
812
+ border-radius: 4px;
813
+ cursor: pointer;
814
+ font-size: 11px;
815
+ display: flex;
816
+ align-items: center;
817
+ gap: 4px;
818
+ transition: all 0.2s;
819
+ }
820
+
821
+ .code-copy-btn:hover {
822
+ background: #334155;
823
+ color: #fff;
824
+ }
825
+
826
+ .code-copy-btn.copied {
827
+ background: var(--success);
828
+ border-color: var(--success);
829
+ color: white;
830
+ }
831
+
832
+ .code-content {
833
+ padding: 16px;
834
+ overflow-x: auto;
835
+ }
836
+
837
+ .code-content code {
838
+ font-family: 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
839
+ font-size: 13px;
840
+ line-height: 1.6;
841
+ color: #e2e8f0;
842
+ white-space: pre;
843
+ }
844
+
845
+ /* Math Blocks */
846
+ .math-block {
847
+ background: var(--bg-tertiary);
848
+ padding: 16px;
849
+ border-radius: 8px;
850
+ margin: 12px 0;
851
+ overflow-x: auto;
852
+ text-align: center;
853
+ }
854
+
855
+ .math-inline { display: inline; }
856
+
857
+ /* Tables */
858
+ .table-wrapper {
859
+ overflow-x: auto;
860
+ margin: 12px 0;
861
+ border-radius: 8px;
862
+ border: 1px solid var(--border);
863
+ }
864
+
865
+ .message-content table {
866
+ width: 100%;
867
+ border-collapse: collapse;
868
+ font-size: 13px;
869
+ }
870
+
871
+ .message-content th, .message-content td {
872
+ padding: 10px 12px;
873
+ text-align: left;
874
+ border-bottom: 1px solid var(--border);
875
+ }
876
+
877
+ .message-content th {
878
+ background: var(--bg-tertiary);
879
+ font-weight: 600;
880
+ color: var(--text-primary);
881
+ }
882
+
883
+ .message-content tr:last-child td { border-bottom: none; }
884
+ .message-content tr:hover td { background: var(--bg-tertiary); }
885
+
886
+ /* Attachments */
887
+ .message-attachments {
888
+ display: flex;
889
+ flex-wrap: wrap;
890
+ gap: 8px;
891
+ margin-bottom: 8px;
892
+ }
893
+
894
+ .attachment-chip {
895
+ display: flex;
896
+ align-items: center;
897
+ gap: 6px;
898
+ padding: 6px 10px;
899
+ background: var(--bg-tertiary);
900
+ border-radius: 6px;
901
+ font-size: 12px;
902
+ color: var(--text-secondary);
903
+ }
904
+
905
+ .attachment-chip svg { color: var(--accent); }
906
+
907
+ /* Empty State */
908
+ .empty-state {
909
+ display: flex;
910
+ flex-direction: column;
911
+ align-items: center;
912
+ justify-content: center;
913
+ height: 100%;
914
+ color: var(--text-tertiary);
915
+ text-align: center;
916
+ padding: 40px;
917
+ }
918
+
919
+ .empty-state svg {
920
+ margin-bottom: 16px;
921
+ opacity: 0.5;
922
+ }
923
+
924
+ .empty-state p {
925
+ font-size: 14px;
926
+ max-width: 240px;
927
+ }
928
+
929
+ /* No Data */
930
+ .no-data {
931
+ text-align: center;
932
+ padding: 40px 20px;
933
+ color: var(--text-tertiary);
934
+ font-size: 13px;
935
+ }
936
+
937
+ /* ===== Loading States ===== */
938
+ .loading-skeleton { padding: 8px; }
939
+
940
+ .skeleton-item {
941
+ height: 60px;
942
+ background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-elevated) 50%, var(--bg-tertiary) 75%);
943
+ background-size: 200% 100%;
944
+ animation: shimmer 1.5s infinite;
945
+ border-radius: 8px;
946
+ margin-bottom: 8px;
947
+ }
948
+
949
+ @keyframes shimmer {
950
+ 0% { background-position: 200% 0; }
951
+ 100% { background-position: -200% 0; }
952
+ }
953
+
954
+ /* ===== Toast Notifications ===== */
955
+ .toast-container {
956
+ position: fixed;
957
+ bottom: 20px;
958
+ right: 20px;
959
+ z-index: 2000;
960
+ display: flex;
961
+ flex-direction: column;
962
+ gap: 8px;
963
+ }
964
+
965
+ .toast {
966
+ display: flex;
967
+ align-items: center;
968
+ gap: 10px;
969
+ padding: 12px 16px;
970
+ background: var(--bg-tertiary);
971
+ border: 1px solid var(--border);
972
+ border-radius: 8px;
973
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
974
+ animation: slideIn 0.3s ease;
975
+ max-width: 320px;
976
+ }
977
+
978
+ .toast.success { border-color: var(--success); }
979
+ .toast.error { border-color: var(--error); }
980
+ .toast.warning { border-color: var(--warning); }
981
+
982
+ .toast-icon {
983
+ width: 20px;
984
+ height: 20px;
985
+ flex-shrink: 0;
986
+ }
987
+
988
+ .toast.success .toast-icon { color: var(--success); }
989
+ .toast.error .toast-icon { color: var(--error); }
990
+ .toast.warning .toast-icon { color: var(--warning); }
991
+
992
+ .toast-message {
993
+ flex: 1;
994
+ font-size: 13px;
995
+ color: var(--text-primary);
996
+ }
997
+
998
+ .toast-close {
999
+ background: transparent;
1000
+ border: none;
1001
+ color: var(--text-tertiary);
1002
+ cursor: pointer;
1003
+ padding: 4px;
1004
+ }
1005
+
1006
+ @keyframes slideIn {
1007
+ from { transform: translateX(100%); opacity: 0; }
1008
+ to { transform: translateX(0); opacity: 1; }
1009
+ }
1010
+
1011
+ /* ===== Dialog ===== */
1012
+ .dialog-overlay {
1013
+ position: fixed;
1014
+ inset: 0;
1015
+ background: rgba(0, 0, 0, 0.6);
1016
+ display: flex;
1017
+ align-items: center;
1018
+ justify-content: center;
1019
+ z-index: 3000;
1020
+ backdrop-filter: blur(4px);
1021
+ }
1022
+
1023
+ .dialog-box {
1024
+ background: var(--bg-tertiary);
1025
+ border: 1px solid var(--border);
1026
+ border-radius: 12px;
1027
+ padding: 24px;
1028
+ max-width: 400px;
1029
+ width: 90%;
1030
+ text-align: center;
1031
+ animation: scaleIn 0.2s ease;
1032
+ }
1033
+
1034
+ @keyframes scaleIn {
1035
+ from { transform: scale(0.9); opacity: 0; }
1036
+ to { transform: scale(1); opacity: 1; }
1037
+ }
1038
+
1039
+ .dialog-icon {
1040
+ width: 48px;
1041
+ height: 48px;
1042
+ margin: 0 auto 16px;
1043
+ border-radius: 50%;
1044
+ display: flex;
1045
+ align-items: center;
1046
+ justify-content: center;
1047
+ }
1048
+
1049
+ .dialog-icon.warning {
1050
+ background: rgba(245, 158, 11, 0.15);
1051
+ color: var(--warning);
1052
+ }
1053
+
1054
+ .dialog-icon.danger {
1055
+ background: rgba(239, 68, 68, 0.15);
1056
+ color: var(--error);
1057
+ }
1058
+
1059
+ .dialog-box h3 {
1060
+ font-size: 18px;
1061
+ margin-bottom: 8px;
1062
+ color: var(--text-primary);
1063
+ }
1064
+
1065
+ .dialog-box p {
1066
+ font-size: 14px;
1067
+ color: var(--text-secondary);
1068
+ margin-bottom: 20px;
1069
+ }
1070
+
1071
+ .dialog-actions {
1072
+ display: flex;
1073
+ gap: 12px;
1074
+ justify-content: center;
1075
+ }
1076
+
1077
+ .dialog-actions .btn { min-width: 100px; }
1078
+
1079
+ /* ===== Sidebar Overlay (Mobile) ===== */
1080
+ .sidebar-overlay {
1081
+ display: none;
1082
+ position: fixed;
1083
+ inset: 0;
1084
+ background: rgba(0, 0, 0, 0.5);
1085
+ z-index: 99;
1086
+ }
1087
+
1088
+ .sidebar-overlay.active { display: block; }
1089
+
1090
+ /* ===== Responsive Design ===== */
1091
+ @media (max-width: 1024px) {
1092
+ .chats-panel { width: 240px; }
1093
+ }
1094
+
1095
+ @media (max-width: 768px) {
1096
+ .sidebar {
1097
+ position: fixed;
1098
+ left: 0;
1099
+ top: 0;
1100
+ bottom: 0;
1101
+ z-index: 100;
1102
+ transform: translateX(-100%);
1103
+ }
1104
+
1105
+ .sidebar.open { transform: translateX(0); }
1106
+
1107
+ .menu-btn { display: block; }
1108
+
1109
+ .chats-panel {
1110
+ position: fixed;
1111
+ left: 0;
1112
+ top: 0;
1113
+ bottom: 0;
1114
+ z-index: 98;
1115
+ width: 280px;
1116
+ transform: translateX(-100%);
1117
+ transition: transform 0.3s var(--ease-out-expo);
1118
+ }
1119
+
1120
+ .chats-panel.open { transform: translateX(0); }
1121
+
1122
+ .header-actions .btn span { display: none; }
1123
+ .header-actions .btn { padding: 8px; }
1124
+
1125
+ .content-area { flex-direction: column; }
1126
+
1127
+ .chat-view { width: 100%; }
1128
+ }
1129
+
1130
+ @media (max-width: 480px) {
1131
+ .login-box { padding: 24px; margin: 16px; }
1132
+
1133
+ .stats-row { flex-wrap: wrap; }
1134
+ .stat-box { min-width: calc(50% - 4px); }
1135
+
1136
+ .message-wrapper { max-width: 95%; }
1137
+ }
1138
+
1139
+ /* ===== Selection ===== */
1140
+ ::selection {
1141
+ background: rgba(62, 180, 137, 0.3);
1142
+ color: inherit;
1143
+ }
1144
+
1145
+ /* ===== Focus States ===== */
1146
+ button:focus-visible, input:focus-visible {
1147
+ outline: 2px solid var(--accent);
1148
+ outline-offset: 2px;
1149
+ }
1150
+
1151
+ /* ===== Animations ===== */
1152
+ .fade-in {
1153
+ animation: fadeIn 0.3s ease;
1154
+ }
1155
+
1156
+ @keyframes fadeIn {
1157
+ from { opacity: 0; }
1158
+ to { opacity: 1; }
1159
+ }
1160
+
1161
+ .slide-up {
1162
+ animation: slideUp 0.3s ease;
1163
+ }
1164
+
1165
+ @keyframes slideUp {
1166
+ from { transform: translateY(10px); opacity: 0; }
1167
+ to { transform: translateY(0); opacity: 1; }
1168
+ }
1169
+
1170
+ /* ===== Additional Message Styles ===== */
1171
+ .message.assistant .message-content strong {
1172
+ color: var(--text-primary);
1173
+ font-weight: 600;
1174
+ }
1175
+
1176
+ .message.assistant .message-content em {
1177
+ font-style: italic;
1178
+ color: var(--text-secondary);
1179
+ }
1180
+
1181
+ /* KaTeX overrides */
1182
+ .katex { font-size: 1.1em; }
1183
+ .katex-display { margin: 16px 0; overflow-x: auto; }
1184
+
1185
+ /* Print styles */
1186
+ @media print {
1187
+ .sidebar, .chats-panel, .main-header { display: none !important; }
1188
+ .chat-view { width: 100% !important; }
1189
+ .messages-area { overflow: visible !important; }
1190
+ }
1191
+
1192
+ /* High contrast mode */
1193
+ @media (prefers-contrast: high) {
1194
+ :root {
1195
+ --border: #4a5568;
1196
+ --text-secondary: #cbd5e0;
1197
+ }
1198
+ }
private/admin/admin.js ADDED
@@ -0,0 +1,1107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rox AI Admin Panel - Secure Administration Interface
3
+ * @version 1.0.0
4
+ * @description Production-ready admin panel with JWT auth and exact UI matching
5
+ */
6
+ 'use strict';
7
+
8
+ // ==================== CONSTANTS ====================
9
+ const API_BASE = '/admin/api';
10
+ const TOKEN_KEY = 'rox_admin_token';
11
+ const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours
12
+
13
+ // HTML escape map for XSS prevention
14
+ const HTML_ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
15
+
16
+ // Language name mappings for code blocks
17
+ const LANGUAGE_NAMES = {
18
+ 'js': 'javascript', 'ts': 'typescript', 'py': 'python', 'rb': 'ruby',
19
+ 'sh': 'bash', 'yml': 'yaml', 'md': 'markdown', 'cs': 'csharp', 'cpp': 'c++'
20
+ };
21
+
22
+ // ==================== ADMIN PANEL CLASS ====================
23
+ class AdminPanel {
24
+ constructor() {
25
+ this.token = null;
26
+ this.currentUser = null;
27
+ this.currentChat = null;
28
+ this.users = [];
29
+ this.chats = [];
30
+ this.stats = {};
31
+ this.refreshInterval = null;
32
+
33
+ this._init();
34
+ }
35
+
36
+ _init() {
37
+ this._initElements();
38
+ this._initEventListeners();
39
+ this._checkAuth();
40
+ }
41
+
42
+ _initElements() {
43
+ // Login elements
44
+ this.loginOverlay = document.getElementById('loginOverlay');
45
+ this.loginForm = document.getElementById('loginForm');
46
+ this.passwordInput = document.getElementById('passwordInput');
47
+ this.loginError = document.getElementById('loginError');
48
+ this.loginAttempts = document.getElementById('loginAttempts');
49
+ this.loginBtn = document.getElementById('loginBtn');
50
+
51
+ // App elements
52
+ this.app = document.getElementById('app');
53
+ this.sidebar = document.getElementById('sidebar');
54
+ this.sidebarOverlay = document.getElementById('sidebarOverlay');
55
+ this.menuBtn = document.getElementById('menuBtn');
56
+
57
+ // Stats elements
58
+ this.totalUsers = document.getElementById('totalUsers');
59
+ this.totalChats = document.getElementById('totalChats');
60
+ this.todayQueries = document.getElementById('todayQueries');
61
+ this.avgResponseTime = document.getElementById('avgResponseTime');
62
+ this.totalMessages = document.getElementById('totalMessages');
63
+ this.activeSessions = document.getElementById('activeSessions');
64
+ this.modelUsageChart = document.getElementById('modelUsageChart');
65
+
66
+ // User list elements
67
+ this.userList = document.getElementById('userList');
68
+ this.userSearch = document.getElementById('userSearch');
69
+ this.selectedUserName = document.getElementById('selectedUserName');
70
+
71
+ // Chat elements
72
+ this.chatsPanel = document.getElementById('chatsPanel');
73
+ this.chatsList = document.getElementById('chatsList');
74
+ this.chatView = document.getElementById('chatView');
75
+ this.chatHeader = document.getElementById('chatHeader');
76
+ this.chatTitle = document.getElementById('chatTitle');
77
+ this.chatMeta = document.getElementById('chatMeta');
78
+ this.messagesArea = document.getElementById('messagesArea');
79
+
80
+ // Action buttons
81
+ this.refreshBtn = document.getElementById('refreshBtn');
82
+ this.exportBtn = document.getElementById('exportBtn');
83
+ this.clearBtn = document.getElementById('clearBtn');
84
+ this.logoutBtn = document.getElementById('btnLogout');
85
+ this.lastUpdated = document.getElementById('lastUpdated');
86
+ this.lastUpdateTime = null;
87
+
88
+ // Tabs
89
+ this.tabBtns = document.querySelectorAll('.tab-btn');
90
+ this.usersTab = document.getElementById('usersTab');
91
+ this.statsTab = document.getElementById('statsTab');
92
+
93
+ // Dialog
94
+ this.dialogOverlay = document.getElementById('dialogOverlay');
95
+ this.dialogTitle = document.getElementById('dialogTitle');
96
+ this.dialogMessage = document.getElementById('dialogMessage');
97
+ this.dialogConfirm = document.getElementById('dialogConfirm');
98
+ this.dialogCancel = document.getElementById('dialogCancel');
99
+
100
+ // Toast container
101
+ this.toastContainer = document.getElementById('toastContainer');
102
+ }
103
+
104
+ _initEventListeners() {
105
+ // Login form
106
+ this.loginForm?.addEventListener('submit', (e) => this._handleLogin(e));
107
+
108
+ // Logout
109
+ this.logoutBtn?.addEventListener('click', () => this._logout());
110
+
111
+ // Mobile menu
112
+ this.menuBtn?.addEventListener('click', () => this._toggleSidebar());
113
+ this.sidebarOverlay?.addEventListener('click', () => this._closeSidebar());
114
+
115
+ // Tabs
116
+ this.tabBtns.forEach(btn => {
117
+ btn.addEventListener('click', () => this._switchTab(btn.dataset.tab));
118
+ });
119
+
120
+ // User search
121
+ this.userSearch?.addEventListener('input', (e) => this._filterUsers(e.target.value));
122
+
123
+ // Action buttons
124
+ this.refreshBtn?.addEventListener('click', () => this._refreshData());
125
+ this.exportBtn?.addEventListener('click', () => this._exportLogs());
126
+ this.clearBtn?.addEventListener('click', () => this._confirmClearLogs());
127
+
128
+ // Dialog
129
+ this.dialogCancel?.addEventListener('click', () => this._hideDialog());
130
+ this.dialogOverlay?.addEventListener('click', (e) => {
131
+ if (e.target === this.dialogOverlay) this._hideDialog();
132
+ });
133
+
134
+ // Keyboard shortcuts
135
+ document.addEventListener('keydown', (e) => {
136
+ if (e.key === 'Escape') {
137
+ this._hideDialog();
138
+ this._closeSidebar();
139
+ }
140
+ });
141
+ }
142
+
143
+ // ==================== AUTHENTICATION ====================
144
+ _checkAuth() {
145
+ const token = localStorage.getItem(TOKEN_KEY);
146
+ if (token && this._isTokenValid(token)) {
147
+ this.token = token;
148
+ this._showApp();
149
+ this._loadData();
150
+ } else {
151
+ localStorage.removeItem(TOKEN_KEY);
152
+ this._showLogin();
153
+ }
154
+ }
155
+
156
+ _isTokenValid(token) {
157
+ try {
158
+ const payload = JSON.parse(atob(token.split('.')[1]));
159
+ return payload.exp * 1000 > Date.now();
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ async _handleLogin(e) {
166
+ e.preventDefault();
167
+ const password = this.passwordInput?.value?.trim();
168
+
169
+ if (!password) {
170
+ this._showLoginError('Please enter a password');
171
+ return;
172
+ }
173
+
174
+ this._setLoginLoading(true);
175
+
176
+ try {
177
+ const response = await fetch(`${API_BASE}/login`, {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify({ password })
181
+ });
182
+
183
+ const data = await response.json();
184
+
185
+ if (data.success && data.token) {
186
+ this.token = data.token;
187
+ localStorage.setItem(TOKEN_KEY, data.token);
188
+ this._showApp();
189
+ this._loadData();
190
+ this._showToast('Login successful', 'success');
191
+ } else {
192
+ this._showLoginError(data.error || 'Invalid password');
193
+ if (data.attemptsLeft !== undefined) {
194
+ this.loginAttempts.textContent = `${data.attemptsLeft} attempts remaining`;
195
+ }
196
+ if (data.lockoutTime) {
197
+ this._showLoginError(`Too many attempts. Try again in ${Math.ceil(data.lockoutTime / 60)} minutes`);
198
+ }
199
+ }
200
+ } catch (err) {
201
+ this._showLoginError('Connection error. Please try again.');
202
+ } finally {
203
+ this._setLoginLoading(false);
204
+ }
205
+ }
206
+
207
+ _logout() {
208
+ localStorage.removeItem(TOKEN_KEY);
209
+ this.token = null;
210
+ if (this.refreshInterval) clearInterval(this.refreshInterval);
211
+ if (this.lastUpdatedInterval) clearInterval(this.lastUpdatedInterval);
212
+ this._showLogin();
213
+ this._showToast('Logged out successfully', 'success');
214
+ }
215
+
216
+ _showLogin() {
217
+ this.loginOverlay.style.display = 'flex';
218
+ this.app.style.display = 'none';
219
+ this.passwordInput.value = '';
220
+ this.loginError.textContent = '';
221
+ this.loginAttempts.textContent = '';
222
+ }
223
+
224
+ _showApp() {
225
+ this.loginOverlay.style.display = 'none';
226
+ this.app.style.display = 'flex';
227
+ // Auto-refresh every 5 seconds for real-time updates
228
+ this.refreshInterval = setInterval(() => this._loadData(), 5000);
229
+ // Update "last updated" display every second
230
+ this.lastUpdatedInterval = setInterval(() => this._updateLastUpdatedDisplay(), 1000);
231
+ }
232
+
233
+ _showLoginError(msg) {
234
+ this.loginError.textContent = msg;
235
+ }
236
+
237
+ _setLoginLoading(loading) {
238
+ const btnText = this.loginBtn?.querySelector('.btn-text');
239
+ const btnLoading = this.loginBtn?.querySelector('.btn-loading');
240
+ if (loading) {
241
+ btnText.style.display = 'none';
242
+ btnLoading.style.display = 'inline-flex';
243
+ this.loginBtn.disabled = true;
244
+ } else {
245
+ btnText.style.display = 'inline';
246
+ btnLoading.style.display = 'none';
247
+ this.loginBtn.disabled = false;
248
+ }
249
+ }
250
+
251
+ // ==================== DATA LOADING ====================
252
+ async _loadData() {
253
+ try {
254
+ const [statsRes, usersRes] = await Promise.all([
255
+ this._apiGet('/stats'),
256
+ this._apiGet('/users')
257
+ ]);
258
+
259
+ if (statsRes.success) {
260
+ this.stats = statsRes.data;
261
+ this._renderStats();
262
+ }
263
+
264
+ if (usersRes.success) {
265
+ this.users = usersRes.data.users || [];
266
+ this._renderUsers();
267
+ }
268
+
269
+ // Update last updated time
270
+ this.lastUpdateTime = Date.now();
271
+ this._updateLastUpdatedDisplay();
272
+ } catch (err) {
273
+ console.error('Failed to load data:', err);
274
+ if (err.message === 'Unauthorized') {
275
+ this._logout();
276
+ }
277
+ }
278
+ }
279
+
280
+ _updateLastUpdatedDisplay() {
281
+ if (!this.lastUpdated || !this.lastUpdateTime) return;
282
+ const seconds = Math.floor((Date.now() - this.lastUpdateTime) / 1000);
283
+ if (seconds < 5) {
284
+ this.lastUpdated.textContent = 'Updated just now';
285
+ } else if (seconds < 60) {
286
+ this.lastUpdated.textContent = `Updated ${seconds}s ago`;
287
+ } else {
288
+ const mins = Math.floor(seconds / 60);
289
+ this.lastUpdated.textContent = `Updated ${mins}m ago`;
290
+ }
291
+ }
292
+
293
+ async _loadUserChats(userIp) {
294
+ try {
295
+ const res = await this._apiGet(`/users/${encodeURIComponent(userIp)}/chats`);
296
+ if (res.success) {
297
+ this.chats = res.data.chats || [];
298
+ this._renderChats();
299
+ }
300
+ } catch (err) {
301
+ console.error('Failed to load chats:', err);
302
+ this._showToast('Failed to load chats', 'error');
303
+ }
304
+ }
305
+
306
+ async _loadChatMessages(chatId) {
307
+ try {
308
+ const res = await this._apiGet(`/chats/${encodeURIComponent(chatId)}/messages`);
309
+ if (res.success) {
310
+ this._renderMessages(res.data.messages || [], res.data.chat);
311
+ }
312
+ } catch (err) {
313
+ console.error('Failed to load messages:', err);
314
+ this._showToast('Failed to load messages', 'error');
315
+ }
316
+ }
317
+
318
+ async _apiGet(endpoint) {
319
+ const response = await fetch(`${API_BASE}${endpoint}`, {
320
+ headers: { 'Authorization': `Bearer ${this.token}` }
321
+ });
322
+
323
+ if (response.status === 401) {
324
+ throw new Error('Unauthorized');
325
+ }
326
+
327
+ return response.json();
328
+ }
329
+
330
+ async _apiPost(endpoint, data) {
331
+ const response = await fetch(`${API_BASE}${endpoint}`, {
332
+ method: 'POST',
333
+ headers: {
334
+ 'Content-Type': 'application/json',
335
+ 'Authorization': `Bearer ${this.token}`
336
+ },
337
+ body: JSON.stringify(data)
338
+ });
339
+
340
+ if (response.status === 401) {
341
+ throw new Error('Unauthorized');
342
+ }
343
+
344
+ return response.json();
345
+ }
346
+
347
+ // ==================== RENDERING ====================
348
+ _renderStats() {
349
+ const s = this.stats;
350
+ this.totalUsers.textContent = s.totalUsers || 0;
351
+ this.totalChats.textContent = s.totalChats || 0;
352
+ this.todayQueries.textContent = s.todayQueries || 0;
353
+
354
+ if (this.avgResponseTime) {
355
+ this.avgResponseTime.textContent = `${s.avgResponseTime || 0}ms`;
356
+ }
357
+ if (this.totalMessages) {
358
+ this.totalMessages.textContent = s.totalMessages || 0;
359
+ }
360
+ if (this.activeSessions) {
361
+ this.activeSessions.textContent = s.activeSessions || 0;
362
+ }
363
+
364
+ // Render model usage chart
365
+ if (this.modelUsageChart && s.modelUsage) {
366
+ this._renderModelUsage(s.modelUsage);
367
+ }
368
+ }
369
+
370
+ _renderModelUsage(usage) {
371
+ const total = Object.values(usage).reduce((a, b) => a + b, 0) || 1;
372
+ let html = '';
373
+
374
+ for (const [model, count] of Object.entries(usage)) {
375
+ const percent = Math.round((count / total) * 100);
376
+ html += `
377
+ <div class="usage-bar">
378
+ <span class="usage-bar-label">${this.escapeHtml(model)}</span>
379
+ <div class="usage-bar-track">
380
+ <div class="usage-bar-fill" style="width: ${percent}%"></div>
381
+ </div>
382
+ <span class="usage-bar-value">${count}</span>
383
+ </div>
384
+ `;
385
+ }
386
+
387
+ this.modelUsageChart.innerHTML = html || '<div class="no-data">No usage data</div>';
388
+ }
389
+
390
+ _renderUsers() {
391
+ if (!this.users.length) {
392
+ this.userList.innerHTML = '<div class="no-data">No users found</div>';
393
+ return;
394
+ }
395
+
396
+ let html = '';
397
+ for (const user of this.users) {
398
+ const isActive = this.currentUser === user.ip;
399
+ const isOnline = user.isOnline ? 'online' : '';
400
+ const timeAgo = this._formatTimeAgo(user.lastActivity);
401
+
402
+ html += `
403
+ <div class="user-card ${isActive ? 'active' : ''}" data-ip="${this.escapeHtml(user.ip)}">
404
+ <div class="user-card-header">
405
+ <span class="user-status ${isOnline}"></span>
406
+ <span class="user-ip">${this._maskIp(user.ip)}</span>
407
+ <span class="user-time">${timeAgo}</span>
408
+ </div>
409
+ <div class="user-meta">
410
+ <span>${user.chatCount || 0} chats</span>
411
+ ${user.device?.browser ? `<span class="user-badge">${this.escapeHtml(user.device.browser)}</span>` : ''}
412
+ ${user.device?.os ? `<span class="user-badge">${this.escapeHtml(user.device.os)}</span>` : ''}
413
+ </div>
414
+ </div>
415
+ `;
416
+ }
417
+
418
+ this.userList.innerHTML = html;
419
+
420
+ // Add click handlers
421
+ this.userList.querySelectorAll('.user-card').forEach(card => {
422
+ card.addEventListener('click', () => this._selectUser(card.dataset.ip));
423
+ });
424
+ }
425
+
426
+ _renderChats() {
427
+ if (!this.chats.length) {
428
+ this.chatsList.innerHTML = `
429
+ <div class="empty-state">
430
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
431
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
432
+ </svg>
433
+ <p>No chats found for this user</p>
434
+ </div>
435
+ `;
436
+ return;
437
+ }
438
+
439
+ let html = '';
440
+ for (const chat of this.chats) {
441
+ const isActive = this.currentChat === chat.id;
442
+ const timeAgo = this._formatTimeAgo(chat.lastMessage);
443
+ const title = chat.title || 'Untitled Chat';
444
+
445
+ html += `
446
+ <div class="chat-card ${isActive ? 'active' : ''}" data-id="${this.escapeHtml(chat.id)}">
447
+ <div class="chat-card-title">${this.escapeHtml(title)}</div>
448
+ <div class="chat-card-meta">
449
+ <span class="chat-card-status ${chat.isActive ? 'active' : ''}"></span>
450
+ <span>${chat.messageCount || 0} messages</span>
451
+ <span>·</span>
452
+ <span>${timeAgo}</span>
453
+ </div>
454
+ </div>
455
+ `;
456
+ }
457
+
458
+ this.chatsList.innerHTML = html;
459
+
460
+ // Add click handlers
461
+ this.chatsList.querySelectorAll('.chat-card').forEach(card => {
462
+ card.addEventListener('click', () => this._selectChat(card.dataset.id));
463
+ });
464
+ }
465
+
466
+ _renderMessages(messages, chatInfo) {
467
+ if (!messages.length) {
468
+ this.messagesArea.innerHTML = `
469
+ <div class="empty-state">
470
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
471
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
472
+ </svg>
473
+ <p>No messages in this chat</p>
474
+ </div>
475
+ `;
476
+ return;
477
+ }
478
+
479
+ // Show chat header
480
+ this.chatHeader.style.display = 'flex';
481
+ this.chatTitle.textContent = chatInfo?.title || 'Chat';
482
+ this.chatMeta.textContent = `Started ${this._formatDate(chatInfo?.createdAt)} · ${messages.length} messages`;
483
+
484
+ let html = '';
485
+ for (const msg of messages) {
486
+ html += this._renderMessage(msg);
487
+ }
488
+
489
+ this.messagesArea.innerHTML = html;
490
+ this._initCodeCopyButtons();
491
+ this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
492
+ }
493
+
494
+ _renderMessage(msg) {
495
+ const isUser = msg.role === 'user';
496
+ const avatar = isUser ? 'U' : 'R';
497
+ const roleName = isUser ? 'User' : 'Rox';
498
+ const time = this._formatTime(msg.timestamp);
499
+
500
+ // Attachments
501
+ let attachmentsHtml = '';
502
+ if (msg.attachments?.length) {
503
+ attachmentsHtml = '<div class="message-attachments">';
504
+ for (const att of msg.attachments) {
505
+ attachmentsHtml += `
506
+ <div class="attachment-chip">
507
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
508
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
509
+ <polyline points="14 2 14 8 20 8"/>
510
+ </svg>
511
+ ${this.escapeHtml(att.name)}
512
+ </div>
513
+ `;
514
+ }
515
+ attachmentsHtml += '</div>';
516
+ }
517
+
518
+ // Model badge for assistant
519
+ let modelBadge = '';
520
+ let internetBadge = '';
521
+ if (!isUser) {
522
+ if (msg.model) {
523
+ modelBadge = `<span class="model-badge">${this.escapeHtml(msg.model)}</span>`;
524
+ }
525
+ if (msg.usedInternet) {
526
+ internetBadge = `<span class="internet-badge">🌐 ${this.escapeHtml(msg.internetSource || 'Web')}</span>`;
527
+ }
528
+ }
529
+
530
+ // Duration for assistant
531
+ let durationHtml = '';
532
+ if (!isUser && msg.duration) {
533
+ durationHtml = `<span class="message-time">${(msg.duration / 1000).toFixed(1)}s</span>`;
534
+ }
535
+
536
+ return `
537
+ <div class="message ${isUser ? 'user' : 'assistant'} fade-in">
538
+ <div class="message-avatar">${avatar}</div>
539
+ <div class="message-wrapper">
540
+ <div class="message-header">
541
+ <span class="message-role">${roleName}</span>
542
+ ${modelBadge}
543
+ ${internetBadge}
544
+ <span class="message-time">${time}</span>
545
+ ${durationHtml}
546
+ </div>
547
+ ${attachmentsHtml}
548
+ <div class="message-content">${this._formatContent(msg.content)}</div>
549
+ </div>
550
+ </div>
551
+ `;
552
+ }
553
+
554
+ // ==================== CONTENT FORMATTING (EXACT MATCH WITH USER SIDE) ====================
555
+ _formatContent(content) {
556
+ if (!content || typeof content !== 'string') return '';
557
+
558
+ let formatted = content;
559
+
560
+ // Remove internet search indicator lines
561
+ formatted = formatted.replace(/^🌐\s*Searching for.*?\.\.\.?\s*$/gm, '');
562
+ formatted = formatted.replace(/^🌐\s*LIVE INTERNET SEARCH RESULTS:?\s*$/gm, '');
563
+ formatted = formatted.replace(/^\s*\n+/g, '').replace(/\n{3,}/g, '\n\n');
564
+
565
+ // Fix malformed numbered lists
566
+ formatted = formatted.replace(/^(\d+)(\*\*)/gm, '$1. $2');
567
+ formatted = formatted.replace(/^(\d+)([A-Za-z])/gm, '$1. $2');
568
+
569
+ // Store math blocks temporarily
570
+ const mathBlocks = [];
571
+ const inlineMath = [];
572
+
573
+ // Protect display math blocks
574
+ formatted = formatted.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
575
+ const placeholder = `__MATH_BLOCK_${mathBlocks.length}__`;
576
+ mathBlocks.push({ math: math.trim(), display: true });
577
+ return placeholder;
578
+ });
579
+
580
+ // Protect inline math
581
+ formatted = formatted.replace(/\$([^\$\n]+?)\$/g, (_, math) => {
582
+ const placeholder = `__INLINE_MATH_${inlineMath.length}__`;
583
+ inlineMath.push({ math: math.trim(), display: false });
584
+ return placeholder;
585
+ });
586
+
587
+ // Store code blocks
588
+ const codeBlocks = [];
589
+ formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
590
+ const trimmedCode = code.trim();
591
+ const language = lang ? lang.toLowerCase() : '';
592
+ const displayLang = lang ? (LANGUAGE_NAMES[language] || language) : 'plaintext';
593
+ const escapedCode = this.escapeHtml(trimmedCode);
594
+ const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
595
+ codeBlocks.push(`<div class="code-block"><div class="code-header"><span class="code-language">${displayLang}</span><button class="code-copy-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg><span>Copy</span></button></div><div class="code-content"><code>${escapedCode}</code></div></div>`);
596
+ return placeholder;
597
+ });
598
+
599
+ // Inline code
600
+ const inlineCodes = [];
601
+ formatted = formatted.replace(/`([^`]+)`/g, (_, code) => {
602
+ const placeholder = `__INLINE_CODE_${inlineCodes.length}__`;
603
+ inlineCodes.push(`<code>${this.escapeHtml(code)}</code>`);
604
+ return placeholder;
605
+ });
606
+
607
+ // Tables
608
+ const tablePlaceholders = [];
609
+ formatted = this._parseMarkdownTables(formatted, tablePlaceholders);
610
+
611
+ // Headings
612
+ formatted = formatted.replace(/^###### (.+)$/gm, (_, text) => `<h6>${this._formatInlineContent(text)}</h6>`);
613
+ formatted = formatted.replace(/^##### (.+)$/gm, (_, text) => `<h5>${this._formatInlineContent(text)}</h5>`);
614
+ formatted = formatted.replace(/^#### (.+)$/gm, (_, text) => `<h4>${this._formatInlineContent(text)}</h4>`);
615
+ formatted = formatted.replace(/^### (.+)$/gm, (_, text) => `<h3>${this._formatInlineContent(text)}</h3>`);
616
+ formatted = formatted.replace(/^## (.+)$/gm, (_, text) => `<h2>${this._formatInlineContent(text)}</h2>`);
617
+ formatted = formatted.replace(/^# (.+)$/gm, (_, text) => `<h1>${this._formatInlineContent(text)}</h1>`);
618
+
619
+ // Bold, Italic
620
+ formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
621
+ formatted = formatted.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
622
+
623
+ // Lists
624
+ formatted = formatted.replace(/^(\d+)\. (.+)$/gm, (_, num, text) => `<li data-num="${num}">${this._formatInlineContent(text)}</li>`);
625
+ formatted = formatted.replace(/((?:<li data-num="\d+">[\s\S]*?<\/li>\n?)+)/g, '<ol>$1</ol>');
626
+ formatted = formatted.replace(/<\/ol>\n<ol>/g, '');
627
+ formatted = formatted.replace(/ data-num="\d+"/g, '');
628
+
629
+ formatted = formatted.replace(/^[-*] (.+)$/gm, (_, text) => `<uli>${this._formatInlineContent(text)}</uli>`);
630
+ formatted = formatted.replace(/((?:<uli>[\s\S]*?<\/uli>\n?)+)/g, '<ul>$1</ul>');
631
+ formatted = formatted.replace(/<\/ul>\n<ul>/g, '');
632
+ formatted = formatted.replace(/<uli>/g, '<li>');
633
+ formatted = formatted.replace(/<\/uli>/g, '</li>');
634
+
635
+ // Blockquotes
636
+ formatted = formatted.replace(/^> (.+)$/gm, (_, text) => `<blockquote>${this._formatInlineContent(text)}</blockquote>`);
637
+ formatted = formatted.replace(/<\/blockquote>\n<blockquote>/g, '<br>');
638
+
639
+ // Horizontal rule
640
+ formatted = formatted.replace(/^---$/gm, '<hr>');
641
+
642
+ // Links
643
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
644
+ const safeUrl = this._sanitizeUrl(url);
645
+ if (!safeUrl) return this.escapeHtml(text);
646
+ return `<a href="${this.escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">${this.escapeHtml(text)}</a>`;
647
+ });
648
+
649
+ // Paragraphs
650
+ const lines = formatted.split('\n');
651
+ let result = [];
652
+ let paragraphContent = [];
653
+
654
+ for (const line of lines) {
655
+ const trimmed = line.trim();
656
+ const isBlockElement = /^<(h[1-6]|ul|ol|li|blockquote|hr|div|pre|__CODE|__TABLE|__MATH)/.test(trimmed);
657
+
658
+ if (isBlockElement || trimmed === '') {
659
+ if (paragraphContent.length > 0) {
660
+ result.push('<p>' + paragraphContent.join('<br>') + '</p>');
661
+ paragraphContent = [];
662
+ }
663
+ if (trimmed !== '') result.push(line);
664
+ } else {
665
+ paragraphContent.push(trimmed);
666
+ }
667
+ }
668
+
669
+ if (paragraphContent.length > 0) {
670
+ result.push('<p>' + paragraphContent.join('<br>') + '</p>');
671
+ }
672
+
673
+ formatted = result.join('\n');
674
+
675
+ // Restore placeholders
676
+ inlineCodes.forEach((code, i) => {
677
+ formatted = formatted.replace(new RegExp(`__INLINE_CODE_${i}__`, 'g'), code);
678
+ });
679
+ codeBlocks.forEach((block, i) => {
680
+ formatted = formatted.replace(new RegExp(`__CODE_BLOCK_${i}__`, 'g'), block);
681
+ });
682
+ tablePlaceholders.forEach((table, i) => {
683
+ formatted = formatted.replace(new RegExp(`__TABLE_BLOCK_${i}__`, 'g'), table);
684
+ });
685
+ mathBlocks.forEach((item, i) => {
686
+ const rendered = this._renderMath(item.math, true);
687
+ formatted = formatted.replace(new RegExp(`__MATH_BLOCK_${i}__`, 'g'), rendered);
688
+ });
689
+ inlineMath.forEach((item, i) => {
690
+ const rendered = this._renderMath(item.math, false);
691
+ formatted = formatted.replace(new RegExp(`__INLINE_MATH_${i}__`, 'g'), rendered);
692
+ });
693
+
694
+ // Clean up
695
+ formatted = formatted.replace(/<p><br><\/p>/g, '');
696
+ formatted = formatted.replace(/<p>\s*<\/p>/g, '');
697
+
698
+ return formatted;
699
+ }
700
+
701
+ _formatInlineContent(text) {
702
+ if (!text) return '';
703
+ let result = text;
704
+
705
+ // Bold and italic
706
+ result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
707
+ result = result.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
708
+
709
+ return this.escapeHtml(result).replace(/&lt;strong&gt;/g, '<strong>').replace(/&lt;\/strong&gt;/g, '</strong>')
710
+ .replace(/&lt;em&gt;/g, '<em>').replace(/&lt;\/em&gt;/g, '</em>');
711
+ }
712
+
713
+ _renderMath(math, displayMode = false) {
714
+ if (!math) return '';
715
+
716
+ try {
717
+ if (typeof katex !== 'undefined') {
718
+ const html = katex.renderToString(math, {
719
+ displayMode: displayMode,
720
+ throwOnError: false,
721
+ errorColor: '#cc0000',
722
+ strict: false,
723
+ trust: true
724
+ });
725
+
726
+ if (displayMode) {
727
+ return `<div class="math-block">${html}</div>`;
728
+ }
729
+ return `<span class="math-inline">${html}</span>`;
730
+ }
731
+ } catch (e) {
732
+ console.warn('KaTeX rendering failed:', e);
733
+ }
734
+
735
+ // Fallback: basic Unicode conversion
736
+ let fallback = math
737
+ .replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)')
738
+ .replace(/\\sqrt\{([^}]+)\}/g, '√($1)')
739
+ .replace(/\\infty/g, '∞')
740
+ .replace(/\\alpha/g, 'α').replace(/\\beta/g, 'β').replace(/\\gamma/g, 'γ')
741
+ .replace(/\\pi/g, 'π').replace(/\\sigma/g, 'σ').replace(/\\theta/g, 'θ')
742
+ .replace(/\\times/g, '×').replace(/\\div/g, '÷').replace(/\\pm/g, '±')
743
+ .replace(/\\leq/g, '≤').replace(/\\geq/g, '≥').replace(/\\neq/g, '≠')
744
+ .replace(/\\rightarrow/g, '→').replace(/\\leftarrow/g, '←')
745
+ .replace(/[{}]/g, '');
746
+
747
+ if (displayMode) {
748
+ return `<div class="math-block math-fallback">${this.escapeHtml(fallback)}</div>`;
749
+ }
750
+ return `<span class="math-inline math-fallback">${this.escapeHtml(fallback)}</span>`;
751
+ }
752
+
753
+ _parseMarkdownTables(content, placeholders) {
754
+ if (!content) return content;
755
+
756
+ const lines = content.split('\n');
757
+ const result = [];
758
+ let i = 0;
759
+
760
+ while (i < lines.length) {
761
+ const line = lines[i];
762
+
763
+ if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
764
+ const nextLine = lines[i + 1];
765
+ if (nextLine && /^\|[\s:|-]+\|$/.test(nextLine.trim())) {
766
+ const tableLines = [line];
767
+ let j = i + 1;
768
+
769
+ while (j < lines.length && lines[j].trim().startsWith('|')) {
770
+ tableLines.push(lines[j]);
771
+ j++;
772
+ }
773
+
774
+ const tableHtml = this._convertTableToHtml(tableLines);
775
+ if (tableHtml) {
776
+ const placeholder = `__TABLE_BLOCK_${placeholders.length}__`;
777
+ placeholders.push(tableHtml);
778
+ result.push(placeholder);
779
+ i = j;
780
+ continue;
781
+ }
782
+ }
783
+ }
784
+
785
+ result.push(line);
786
+ i++;
787
+ }
788
+
789
+ return result.join('\n');
790
+ }
791
+
792
+ _convertTableToHtml(lines) {
793
+ if (lines.length < 2) return null;
794
+
795
+ const headerLine = lines[0].trim();
796
+ const headerCells = headerLine.split('|').filter(c => c.trim()).map(c => c.trim());
797
+
798
+ const separatorLine = lines[1].trim();
799
+ const aligns = separatorLine.split('|').filter(c => c.trim()).map(c => {
800
+ const cell = c.trim();
801
+ if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
802
+ if (cell.endsWith(':')) return 'right';
803
+ return 'left';
804
+ });
805
+
806
+ let html = '<div class="table-wrapper"><table><thead><tr>';
807
+ headerCells.forEach((h, i) => {
808
+ html += `<th style="text-align:${aligns[i] || 'left'}">${this._formatInlineContent(h)}</th>`;
809
+ });
810
+ html += '</tr></thead><tbody>';
811
+
812
+ for (let i = 2; i < lines.length; i++) {
813
+ const cells = lines[i].split('|').filter(c => c !== '').map(c => c.trim());
814
+ html += '<tr>';
815
+ cells.forEach((c, j) => {
816
+ if (c !== undefined) {
817
+ html += `<td style="text-align:${aligns[j] || 'left'}">${this._formatInlineContent(c)}</td>`;
818
+ }
819
+ });
820
+ html += '</tr>';
821
+ }
822
+
823
+ html += '</tbody></table></div>';
824
+ return html;
825
+ }
826
+
827
+ _initCodeCopyButtons() {
828
+ this.messagesArea?.querySelectorAll('.code-copy-btn').forEach(btn => {
829
+ btn.addEventListener('click', async () => {
830
+ const codeBlock = btn.closest('.code-block');
831
+ const code = codeBlock?.querySelector('code')?.textContent;
832
+
833
+ if (code) {
834
+ try {
835
+ await navigator.clipboard.writeText(code);
836
+ btn.classList.add('copied');
837
+ btn.querySelector('span').textContent = 'Copied!';
838
+ setTimeout(() => {
839
+ btn.classList.remove('copied');
840
+ btn.querySelector('span').textContent = 'Copy';
841
+ }, 2000);
842
+ } catch (err) {
843
+ console.error('Failed to copy:', err);
844
+ }
845
+ }
846
+ });
847
+ });
848
+ }
849
+
850
+ // ==================== USER INTERACTIONS ====================
851
+ _selectUser(ip) {
852
+ this.currentUser = ip;
853
+ this.currentChat = null;
854
+ this.selectedUserName.textContent = this._maskIp(ip);
855
+
856
+ // Update active state
857
+ this.userList.querySelectorAll('.user-card').forEach(card => {
858
+ card.classList.toggle('active', card.dataset.ip === ip);
859
+ });
860
+
861
+ // Load chats
862
+ this._loadUserChats(ip);
863
+
864
+ // Clear messages
865
+ this.chatHeader.style.display = 'none';
866
+ this.messagesArea.innerHTML = `
867
+ <div class="empty-state">
868
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
869
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
870
+ </svg>
871
+ <p>Select a chat to view messages</p>
872
+ </div>
873
+ `;
874
+
875
+ // On mobile, show chats panel
876
+ if (window.innerWidth <= 768) {
877
+ this.chatsPanel.classList.add('open');
878
+ }
879
+ }
880
+
881
+ _selectChat(chatId) {
882
+ this.currentChat = chatId;
883
+
884
+ // Update active state
885
+ this.chatsList.querySelectorAll('.chat-card').forEach(card => {
886
+ card.classList.toggle('active', card.dataset.id === chatId);
887
+ });
888
+
889
+ // Load messages
890
+ this._loadChatMessages(chatId);
891
+
892
+ // On mobile, hide chats panel
893
+ if (window.innerWidth <= 768) {
894
+ this.chatsPanel.classList.remove('open');
895
+ }
896
+ }
897
+
898
+ _filterUsers(query) {
899
+ const q = query.toLowerCase().trim();
900
+ this.userList.querySelectorAll('.user-card').forEach(card => {
901
+ const ip = card.dataset.ip.toLowerCase();
902
+ card.style.display = ip.includes(q) ? '' : 'none';
903
+ });
904
+ }
905
+
906
+ _switchTab(tab) {
907
+ this.tabBtns.forEach(btn => {
908
+ btn.classList.toggle('active', btn.dataset.tab === tab);
909
+ });
910
+
911
+ this.usersTab.style.display = tab === 'users' ? '' : 'none';
912
+ this.statsTab.style.display = tab === 'stats' ? '' : 'none';
913
+ }
914
+
915
+ _toggleSidebar() {
916
+ this.sidebar.classList.toggle('open');
917
+ this.sidebarOverlay.classList.toggle('active');
918
+ }
919
+
920
+ _closeSidebar() {
921
+ this.sidebar.classList.remove('open');
922
+ this.sidebarOverlay.classList.remove('active');
923
+ }
924
+
925
+ // ==================== ACTIONS ====================
926
+ async _refreshData() {
927
+ if (this.refreshBtn) {
928
+ this.refreshBtn.disabled = true;
929
+ this.refreshBtn.classList.add('spinning');
930
+ }
931
+ try {
932
+ await this._loadData();
933
+ if (this.currentUser) {
934
+ await this._loadUserChats(this.currentUser);
935
+ }
936
+ if (this.currentChat) {
937
+ await this._loadChatMessages(this.currentChat);
938
+ }
939
+ this._showToast('Data refreshed', 'success');
940
+ } catch (err) {
941
+ this._showToast('Refresh failed', 'error');
942
+ } finally {
943
+ if (this.refreshBtn) {
944
+ this.refreshBtn.disabled = false;
945
+ this.refreshBtn.classList.remove('spinning');
946
+ }
947
+ }
948
+ }
949
+
950
+ async _exportLogs() {
951
+ try {
952
+ const response = await fetch(`${API_BASE}/export?format=json`, {
953
+ headers: { 'Authorization': `Bearer ${this.token}` }
954
+ });
955
+
956
+ if (!response.ok) throw new Error('Export failed');
957
+
958
+ const blob = await response.blob();
959
+ const url = URL.createObjectURL(blob);
960
+ const a = document.createElement('a');
961
+ a.href = url;
962
+ a.download = `rox-admin-export-${new Date().toISOString().slice(0, 10)}.json`;
963
+ a.click();
964
+ URL.revokeObjectURL(url);
965
+
966
+ this._showToast('Export downloaded', 'success');
967
+ } catch (err) {
968
+ this._showToast('Export failed', 'error');
969
+ }
970
+ }
971
+
972
+ _confirmClearLogs() {
973
+ this._showDialog({
974
+ title: 'Clear All Logs',
975
+ message: 'This will permanently delete all chat logs and user data. This action cannot be undone.',
976
+ type: 'danger',
977
+ onConfirm: () => this._clearLogs()
978
+ });
979
+ }
980
+
981
+ async _clearLogs() {
982
+ try {
983
+ const res = await this._apiPost('/clear-logs', { confirm: true });
984
+ if (res.success) {
985
+ this._showToast(`Cleared ${res.cleared} logs`, 'success');
986
+ this._loadData();
987
+ this.currentUser = null;
988
+ this.currentChat = null;
989
+ this.selectedUserName.textContent = '-';
990
+ this.chatsList.innerHTML = '<div class="empty-state"><p>Select a user to view chats</p></div>';
991
+ this.messagesArea.innerHTML = '<div class="empty-state"><p>Select a chat to view messages</p></div>';
992
+ this.chatHeader.style.display = 'none';
993
+ } else {
994
+ this._showToast(res.error || 'Clear failed', 'error');
995
+ }
996
+ } catch (err) {
997
+ this._showToast('Clear failed', 'error');
998
+ }
999
+ }
1000
+
1001
+ // ==================== UI HELPERS ====================
1002
+ _showDialog({ title, message, type = 'warning', onConfirm }) {
1003
+ this.dialogTitle.textContent = title;
1004
+ this.dialogMessage.textContent = message;
1005
+
1006
+ const icon = document.getElementById('dialogIcon');
1007
+ icon.className = `dialog-icon ${type}`;
1008
+ icon.innerHTML = type === 'danger'
1009
+ ? '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
1010
+ : '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
1011
+
1012
+ this.dialogConfirm.onclick = () => {
1013
+ this._hideDialog();
1014
+ if (onConfirm) onConfirm();
1015
+ };
1016
+
1017
+ this.dialogOverlay.style.display = 'flex';
1018
+ }
1019
+
1020
+ _hideDialog() {
1021
+ this.dialogOverlay.style.display = 'none';
1022
+ }
1023
+
1024
+ _showToast(message, type = 'info') {
1025
+ const toast = document.createElement('div');
1026
+ toast.className = `toast ${type}`;
1027
+
1028
+ const icons = {
1029
+ success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
1030
+ error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
1031
+ warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
1032
+ info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>'
1033
+ };
1034
+
1035
+ toast.innerHTML = `
1036
+ <span class="toast-icon">${icons[type] || icons.info}</span>
1037
+ <span class="toast-message">${this.escapeHtml(message)}</span>
1038
+ <button class="toast-close">×</button>
1039
+ `;
1040
+
1041
+ toast.querySelector('.toast-close').onclick = () => toast.remove();
1042
+ this.toastContainer.appendChild(toast);
1043
+
1044
+ setTimeout(() => toast.remove(), 5000);
1045
+ }
1046
+
1047
+ // ==================== UTILITY FUNCTIONS ====================
1048
+ escapeHtml(str) {
1049
+ if (!str || typeof str !== 'string') return '';
1050
+ return str.replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c] || c);
1051
+ }
1052
+
1053
+ _sanitizeUrl(url) {
1054
+ if (!url || typeof url !== 'string') return null;
1055
+ const trimmed = url.trim();
1056
+ if (trimmed.startsWith('javascript:') || trimmed.startsWith('data:')) return null;
1057
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('/')) {
1058
+ return trimmed;
1059
+ }
1060
+ return null;
1061
+ }
1062
+
1063
+ _maskIp(ip) {
1064
+ if (!ip) return 'Unknown';
1065
+ const parts = ip.split('.');
1066
+ if (parts.length === 4) {
1067
+ return `${parts[0]}.${parts[1]}.xxx.${parts[3]}`;
1068
+ }
1069
+ return ip.slice(0, -3) + 'xxx';
1070
+ }
1071
+
1072
+ _formatTimeAgo(timestamp) {
1073
+ if (!timestamp) return 'Unknown';
1074
+ const date = new Date(timestamp);
1075
+ const now = new Date();
1076
+ const diff = now - date;
1077
+
1078
+ const minutes = Math.floor(diff / 60000);
1079
+ const hours = Math.floor(diff / 3600000);
1080
+ const days = Math.floor(diff / 86400000);
1081
+
1082
+ if (minutes < 1) return 'Just now';
1083
+ if (minutes < 60) return `${minutes}m ago`;
1084
+ if (hours < 24) return `${hours}h ago`;
1085
+ if (days < 7) return `${days}d ago`;
1086
+ return date.toLocaleDateString();
1087
+ }
1088
+
1089
+ _formatDate(timestamp) {
1090
+ if (!timestamp) return 'Unknown';
1091
+ return new Date(timestamp).toLocaleDateString('en-US', {
1092
+ month: 'short', day: 'numeric', year: 'numeric'
1093
+ });
1094
+ }
1095
+
1096
+ _formatTime(timestamp) {
1097
+ if (!timestamp) return '';
1098
+ return new Date(timestamp).toLocaleTimeString('en-US', {
1099
+ hour: 'numeric', minute: '2-digit', hour12: true
1100
+ });
1101
+ }
1102
+ }
1103
+
1104
+ // ==================== INITIALIZE ====================
1105
+ document.addEventListener('DOMContentLoaded', () => {
1106
+ window.adminPanel = new AdminPanel();
1107
+ });
private/admin/index.html ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, viewport-fit=cover">
6
+ <meta name="robots" content="noindex, nofollow">
7
+ <title>Rox Admin Panel</title>
8
+ <meta name="description" content="Rox AI Admin Panel - Secure administration interface">
9
+ <meta name="theme-color" content="#0f1a24">
10
+ <!-- KaTeX for Math Rendering -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
12
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js" integrity="sha384-XjKyOOlGwcjNTAIQHIpgOno0Ber8PWQV9qAwPrNQfByT6CMDgE8XYZynxBnMi5KQ" crossorigin="anonymous"></script>
13
+ <link rel="stylesheet" href="admin.css">
14
+ </head>
15
+ <body>
16
+ <!-- Login Overlay -->
17
+ <div class="login-overlay" id="loginOverlay">
18
+ <div class="login-box">
19
+ <div class="login-logo">
20
+ <svg width="64" height="64" viewBox="0 0 64 64">
21
+ <defs>
22
+ <linearGradient id="loginGrad" x1="0%" y1="0%" x2="100%" y2="100%">
23
+ <stop offset="0%" stop-color="#667eea"/>
24
+ <stop offset="100%" stop-color="#764ba2"/>
25
+ </linearGradient>
26
+ </defs>
27
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#loginGrad)" stroke-width="2"/>
28
+ <circle cx="32" cy="32" r="8" fill="url(#loginGrad)"/>
29
+ </svg>
30
+ </div>
31
+ <h2>Admin Access</h2>
32
+ <p>Enter credentials to continue</p>
33
+ <form id="loginForm">
34
+ <div class="input-group">
35
+ <input type="password" id="passwordInput" placeholder="Admin Password" required autocomplete="current-password">
36
+ </div>
37
+ <button type="submit" class="btn btn-primary" id="loginBtn">
38
+ <span class="btn-text">Login</span>
39
+ <span class="btn-loading" style="display:none;">
40
+ <svg class="spinner" width="16" height="16" viewBox="0 0 24 24">
41
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" fill="none" stroke-dasharray="31.4 31.4" stroke-linecap="round"/>
42
+ </svg>
43
+ </span>
44
+ </button>
45
+ </form>
46
+ <div class="login-error" id="loginError"></div>
47
+ <div class="login-attempts" id="loginAttempts"></div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Main App -->
52
+ <div class="app" id="app" style="display: none;">
53
+ <!-- Sidebar - Users List -->
54
+ <aside class="sidebar" id="sidebar">
55
+ <div class="sidebar-header">
56
+ <div class="logo-section">
57
+ <svg width="28" height="28" viewBox="0 0 64 64">
58
+ <defs>
59
+ <linearGradient id="sidebarGrad" x1="0%" y1="0%" x2="100%" y2="100%">
60
+ <stop offset="0%" stop-color="#667eea"/>
61
+ <stop offset="100%" stop-color="#764ba2"/>
62
+ </linearGradient>
63
+ </defs>
64
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#sidebarGrad)" stroke-width="2"/>
65
+ <circle cx="32" cy="32" r="6" fill="url(#sidebarGrad)"/>
66
+ </svg>
67
+ <h1>Rox Admin</h1>
68
+ </div>
69
+ <button class="btn-logout" id="btnLogout" title="Logout">
70
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
71
+ <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
72
+ <polyline points="16 17 21 12 16 7"/>
73
+ <line x1="21" y1="12" x2="9" y2="12"/>
74
+ </svg>
75
+ </button>
76
+ </div>
77
+
78
+ <div class="stats-row">
79
+ <div class="stat-box">
80
+ <div class="stat-val" id="totalUsers">0</div>
81
+ <div class="stat-lbl">Users</div>
82
+ </div>
83
+ <div class="stat-box">
84
+ <div class="stat-val" id="totalChats">0</div>
85
+ <div class="stat-lbl">Chats</div>
86
+ </div>
87
+ <div class="stat-box">
88
+ <div class="stat-val" id="todayQueries">0</div>
89
+ <div class="stat-lbl">Today</div>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="sidebar-tabs">
94
+ <button class="tab-btn active" data-tab="users">Users</button>
95
+ <button class="tab-btn" data-tab="stats">Stats</button>
96
+ </div>
97
+
98
+ <div class="tab-content" id="usersTab">
99
+ <div class="search-box">
100
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
101
+ <circle cx="11" cy="11" r="8"/>
102
+ <path d="M21 21l-4.35-4.35"/>
103
+ </svg>
104
+ <input type="text" id="userSearch" placeholder="Search users...">
105
+ </div>
106
+ <div class="users-section">
107
+ <div class="section-title">Active Users</div>
108
+ <div class="user-list" id="userList">
109
+ <div class="loading-skeleton">
110
+ <div class="skeleton-item"></div>
111
+ <div class="skeleton-item"></div>
112
+ <div class="skeleton-item"></div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="tab-content" id="statsTab" style="display:none;">
119
+ <div class="stats-detail">
120
+ <div class="stat-card">
121
+ <div class="stat-card-title">Avg Response Time</div>
122
+ <div class="stat-card-value" id="avgResponseTime">0ms</div>
123
+ </div>
124
+ <div class="stat-card">
125
+ <div class="stat-card-title">Total Messages</div>
126
+ <div class="stat-card-value" id="totalMessages">0</div>
127
+ </div>
128
+ <div class="stat-card">
129
+ <div class="stat-card-title">Active Sessions</div>
130
+ <div class="stat-card-value" id="activeSessions">0</div>
131
+ </div>
132
+ </div>
133
+ <div class="model-usage">
134
+ <div class="section-title">Model Usage</div>
135
+ <div id="modelUsageChart" class="usage-chart"></div>
136
+ </div>
137
+ </div>
138
+ </aside>
139
+
140
+ <!-- Main Content -->
141
+ <main class="main">
142
+ <header class="main-header">
143
+ <div class="header-left">
144
+ <button class="menu-btn" id="menuBtn">
145
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
146
+ <path d="M3 12h18M3 6h18M3 18h18"/>
147
+ </svg>
148
+ </button>
149
+ <h2 id="mainTitle">Dashboard</h2>
150
+ <span class="last-updated" id="lastUpdated">Updated just now</span>
151
+ </div>
152
+ <div class="header-actions">
153
+ <button class="btn btn-secondary" id="refreshBtn" title="Refresh data">
154
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
155
+ <path d="M23 4v6h-6M1 20v-6h6"/>
156
+ <path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
157
+ </svg>
158
+ <span>Refresh</span>
159
+ </button>
160
+ <button class="btn btn-secondary" id="exportBtn" title="Export logs">
161
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
162
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
163
+ <polyline points="7 10 12 15 17 10"/>
164
+ <line x1="12" y1="15" x2="12" y2="3"/>
165
+ </svg>
166
+ <span>Export</span>
167
+ </button>
168
+ <button class="btn btn-danger" id="clearBtn" title="Clear all logs">
169
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
170
+ <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
171
+ </svg>
172
+ <span>Clear</span>
173
+ </button>
174
+ </div>
175
+ </header>
176
+
177
+ <div class="content-area">
178
+ <!-- Chats Panel -->
179
+ <div class="chats-panel" id="chatsPanel">
180
+ <div class="chats-header">
181
+ <span>Chats by </span>
182
+ <span id="selectedUserName">-</span>
183
+ </div>
184
+ <div class="chats-list" id="chatsList">
185
+ <div class="empty-state">
186
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
187
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
188
+ </svg>
189
+ <p>Select a user to view chats</p>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Messages View -->
195
+ <div class="chat-view" id="chatView">
196
+ <div class="chat-view-header" id="chatHeader" style="display: none;">
197
+ <div class="chat-info">
198
+ <h3 id="chatTitle">Chat</h3>
199
+ <p id="chatMeta"></p>
200
+ </div>
201
+ <div class="chat-actions">
202
+ <button class="btn btn-sm" id="exportChatBtn" title="Export this chat">
203
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
204
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
205
+ <polyline points="7 10 12 15 17 10"/>
206
+ <line x1="12" y1="15" x2="12" y2="3"/>
207
+ </svg>
208
+ </button>
209
+ </div>
210
+ </div>
211
+ <div class="messages-area" id="messagesArea">
212
+ <div class="empty-state">
213
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
214
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
215
+ </svg>
216
+ <p>Select a user, then select a chat to view messages</p>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </main>
222
+ </div>
223
+
224
+ <!-- Sidebar Overlay for Mobile -->
225
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
226
+
227
+ <!-- Toast Container -->
228
+ <div class="toast-container" id="toastContainer"></div>
229
+
230
+ <!-- Confirm Dialog -->
231
+ <div class="dialog-overlay" id="dialogOverlay" style="display:none;">
232
+ <div class="dialog-box">
233
+ <div class="dialog-icon" id="dialogIcon"></div>
234
+ <h3 id="dialogTitle">Confirm</h3>
235
+ <p id="dialogMessage">Are you sure?</p>
236
+ <div class="dialog-actions">
237
+ <button class="btn btn-secondary" id="dialogCancel">Cancel</button>
238
+ <button class="btn btn-danger" id="dialogConfirm">Confirm</button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- Scripts -->
244
+ <script src="admin.js" type="module"></script>
245
+ </body>
246
+ </html>
private/admin/routes.js ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rox AI Admin Panel - Server Routes
3
+ * @description Secure admin API endpoints with JWT authentication
4
+ */
5
+ 'use strict';
6
+
7
+ const crypto = require('crypto');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ // ==================== CONFIGURATION ====================
12
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'rox-admin-2024';
13
+ const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
14
+ const TOKEN_EXPIRY = 24 * 60 * 60; // 24 hours in seconds
15
+ const MAX_LOGIN_ATTEMPTS = 5;
16
+ const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
17
+
18
+ // Rate limiting store
19
+ const loginAttempts = new Map();
20
+
21
+ // In-memory data store (in production, use a database)
22
+ const dataStore = {
23
+ users: new Map(),
24
+ chats: new Map(),
25
+ logs: []
26
+ };
27
+
28
+ // ==================== JWT HELPERS ====================
29
+ function createToken(payload) {
30
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
31
+ const now = Math.floor(Date.now() / 1000);
32
+ const data = { ...payload, iat: now, exp: now + TOKEN_EXPIRY };
33
+ const payloadB64 = Buffer.from(JSON.stringify(data)).toString('base64url');
34
+ const signature = crypto.createHmac('sha256', JWT_SECRET)
35
+ .update(`${header}.${payloadB64}`).digest('base64url');
36
+ return `${header}.${payloadB64}.${signature}`;
37
+ }
38
+
39
+ function verifyToken(token) {
40
+ try {
41
+ const [header, payload, signature] = token.split('.');
42
+ const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
43
+ .update(`${header}.${payload}`).digest('base64url');
44
+ if (signature !== expectedSig) return null;
45
+ const data = JSON.parse(Buffer.from(payload, 'base64url').toString());
46
+ if (data.exp < Math.floor(Date.now() / 1000)) return null;
47
+ return data;
48
+ } catch { return null; }
49
+ }
50
+
51
+ // ==================== MIDDLEWARE ====================
52
+ function authMiddleware(req, res, next) {
53
+ const authHeader = req.headers.authorization;
54
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
55
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
56
+ }
57
+ const token = authHeader.slice(7);
58
+ const payload = verifyToken(token);
59
+ if (!payload) {
60
+ return res.status(401).json({ success: false, error: 'Invalid or expired token' });
61
+ }
62
+ req.admin = payload;
63
+ next();
64
+ }
65
+
66
+ function checkLockout(ip) {
67
+ const attempts = loginAttempts.get(ip);
68
+ if (!attempts) return { locked: false };
69
+ if (attempts.lockoutUntil && Date.now() < attempts.lockoutUntil) {
70
+ return { locked: true, lockoutTime: Math.ceil((attempts.lockoutUntil - Date.now()) / 1000) };
71
+ }
72
+ if (attempts.lockoutUntil && Date.now() >= attempts.lockoutUntil) {
73
+ loginAttempts.delete(ip);
74
+ return { locked: false };
75
+ }
76
+ return { locked: false, attemptsLeft: MAX_LOGIN_ATTEMPTS - attempts.count };
77
+ }
78
+
79
+ function recordFailedAttempt(ip) {
80
+ const attempts = loginAttempts.get(ip) || { count: 0 };
81
+ attempts.count++;
82
+ if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
83
+ attempts.lockoutUntil = Date.now() + LOCKOUT_DURATION;
84
+ }
85
+ loginAttempts.set(ip, attempts);
86
+ return { attemptsLeft: Math.max(0, MAX_LOGIN_ATTEMPTS - attempts.count) };
87
+ }
88
+
89
+ // ==================== DATA HELPERS ====================
90
+ function recordUserActivity(ip, userAgent) {
91
+ const device = parseUserAgent(userAgent);
92
+ let user = dataStore.users.get(ip);
93
+ if (!user) {
94
+ user = { ip, device, chatCount: 0, lastActivity: Date.now(), isOnline: true, chats: [] };
95
+ dataStore.users.set(ip, user);
96
+ } else {
97
+ user.lastActivity = Date.now();
98
+ user.isOnline = true;
99
+ user.device = device;
100
+ }
101
+ return user;
102
+ }
103
+
104
+ function recordChat(ip, chatId, title, messages) {
105
+ const user = dataStore.users.get(ip);
106
+ if (user) {
107
+ const existingChat = user.chats.find(c => c.id === chatId);
108
+ if (existingChat) {
109
+ existingChat.messages = messages;
110
+ existingChat.messageCount = messages.length;
111
+ existingChat.lastMessage = Date.now();
112
+ existingChat.title = title || existingChat.title;
113
+ } else {
114
+ user.chats.push({
115
+ id: chatId,
116
+ title: title || 'New Chat',
117
+ messages,
118
+ messageCount: messages.length,
119
+ createdAt: Date.now(),
120
+ lastMessage: Date.now(),
121
+ isActive: true
122
+ });
123
+ user.chatCount = user.chats.length;
124
+ }
125
+ }
126
+ dataStore.chats.set(chatId, { ip, title, messages, lastMessage: Date.now() });
127
+ }
128
+
129
+ function parseUserAgent(ua) {
130
+ if (!ua) return { browser: 'Unknown', os: 'Unknown' };
131
+ let browser = 'Unknown', os = 'Unknown';
132
+ if (ua.includes('Chrome')) browser = 'Chrome';
133
+ else if (ua.includes('Firefox')) browser = 'Firefox';
134
+ else if (ua.includes('Safari')) browser = 'Safari';
135
+ else if (ua.includes('Edge')) browser = 'Edge';
136
+ if (ua.includes('Windows')) os = 'Windows';
137
+ else if (ua.includes('Mac')) os = 'macOS';
138
+ else if (ua.includes('Linux')) os = 'Linux';
139
+ else if (ua.includes('Android')) os = 'Android';
140
+ else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
141
+ return { browser, os };
142
+ }
143
+
144
+ // ==================== ROUTE SETUP ====================
145
+ function setupAdminRoutes(app, express) {
146
+ // Block direct access to private folder
147
+ app.use('/private', (req, res, next) => {
148
+ // Only allow access to admin panel through /admin route
149
+ res.status(403).json({ error: 'Access denied' });
150
+ });
151
+
152
+ // Serve admin panel
153
+ app.use('/admin', express.static(path.join(__dirname), {
154
+ index: 'index.html',
155
+ dotfiles: 'deny'
156
+ }));
157
+
158
+ // ==================== AUTH ROUTES ====================
159
+ app.post('/admin/api/login', (req, res) => {
160
+ try {
161
+ const ip = req.ip || req.connection?.remoteAddress || 'unknown';
162
+ const lockout = checkLockout(ip);
163
+
164
+ if (lockout.locked) {
165
+ return res.status(429).json({
166
+ success: false,
167
+ error: 'Too many attempts',
168
+ lockoutTime: lockout.lockoutTime
169
+ });
170
+ }
171
+
172
+ const { password } = req.body || {};
173
+
174
+ if (!password || password !== ADMIN_PASSWORD) {
175
+ const result = recordFailedAttempt(ip);
176
+ return res.status(401).json({
177
+ success: false,
178
+ error: 'Invalid password',
179
+ attemptsLeft: result.attemptsLeft
180
+ });
181
+ }
182
+
183
+ // Clear failed attempts on success
184
+ loginAttempts.delete(ip);
185
+
186
+ const token = createToken({ admin: true, ip });
187
+ res.json({ success: true, token, expiresAt: Date.now() + TOKEN_EXPIRY * 1000 });
188
+ } catch (err) {
189
+ res.status(500).json({ success: false, error: 'Login failed' });
190
+ }
191
+ });
192
+
193
+ app.get('/admin/api/verify', authMiddleware, (req, res) => {
194
+ res.json({ valid: true, admin: req.admin });
195
+ });
196
+
197
+ // ==================== DATA ROUTES ====================
198
+ app.get('/admin/api/stats', authMiddleware, (req, res) => {
199
+ try {
200
+ const users = Array.from(dataStore.users.values());
201
+ const totalChats = users.reduce((sum, u) => sum + (u.chatCount || 0), 0);
202
+ const today = new Date().setHours(0, 0, 0, 0);
203
+ const todayQueries = dataStore.logs.filter(l => l.timestamp >= today).length;
204
+
205
+ // Model usage stats
206
+ const modelUsage = {};
207
+ dataStore.logs.forEach(l => {
208
+ if (l.model) {
209
+ modelUsage[l.model] = (modelUsage[l.model] || 0) + 1;
210
+ }
211
+ });
212
+
213
+ // Calculate average response time
214
+ const responseTimes = dataStore.logs.filter(l => l.duration).map(l => l.duration);
215
+ const avgResponseTime = responseTimes.length
216
+ ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
217
+ : 0;
218
+
219
+ res.json({
220
+ success: true,
221
+ data: {
222
+ totalUsers: users.length,
223
+ totalChats,
224
+ todayQueries,
225
+ totalMessages: dataStore.logs.length,
226
+ activeSessions: users.filter(u => u.isOnline).length,
227
+ avgResponseTime,
228
+ modelUsage
229
+ }
230
+ });
231
+ } catch (err) {
232
+ res.status(500).json({ success: false, error: 'Failed to get stats' });
233
+ }
234
+ });
235
+
236
+ app.get('/admin/api/users', authMiddleware, (req, res) => {
237
+ try {
238
+ const users = Array.from(dataStore.users.values())
239
+ .sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0))
240
+ .map(u => ({
241
+ ip: u.ip,
242
+ device: u.device,
243
+ chatCount: u.chatCount || 0,
244
+ lastActivity: u.lastActivity,
245
+ isOnline: u.isOnline && (Date.now() - u.lastActivity < 300000) // 5 min
246
+ }));
247
+
248
+ res.json({ success: true, data: { users, total: users.length } });
249
+ } catch (err) {
250
+ res.status(500).json({ success: false, error: 'Failed to get users' });
251
+ }
252
+ });
253
+
254
+ app.get('/admin/api/users/:ip/chats', authMiddleware, (req, res) => {
255
+ try {
256
+ const userIp = decodeURIComponent(req.params.ip);
257
+ const user = dataStore.users.get(userIp);
258
+
259
+ if (!user) {
260
+ return res.json({ success: true, data: { chats: [] } });
261
+ }
262
+
263
+ const chats = (user.chats || [])
264
+ .sort((a, b) => (b.lastMessage || 0) - (a.lastMessage || 0))
265
+ .map(c => ({
266
+ id: c.id,
267
+ title: c.title || 'Untitled',
268
+ messageCount: c.messageCount || 0,
269
+ createdAt: c.createdAt,
270
+ lastMessage: c.lastMessage,
271
+ isActive: c.isActive
272
+ }));
273
+
274
+ res.json({ success: true, data: { chats } });
275
+ } catch (err) {
276
+ res.status(500).json({ success: false, error: 'Failed to get chats' });
277
+ }
278
+ });
279
+
280
+ app.get('/admin/api/chats/:chatId/messages', authMiddleware, (req, res) => {
281
+ try {
282
+ const chatId = decodeURIComponent(req.params.chatId);
283
+ const chatData = dataStore.chats.get(chatId);
284
+
285
+ if (!chatData) {
286
+ // Try to find in user chats
287
+ for (const user of dataStore.users.values()) {
288
+ const chat = user.chats?.find(c => c.id === chatId);
289
+ if (chat) {
290
+ return res.json({
291
+ success: true,
292
+ data: {
293
+ messages: chat.messages || [],
294
+ chat: { title: chat.title, createdAt: chat.createdAt }
295
+ }
296
+ });
297
+ }
298
+ }
299
+ return res.json({ success: true, data: { messages: [], chat: null } });
300
+ }
301
+
302
+ res.json({
303
+ success: true,
304
+ data: {
305
+ messages: chatData.messages || [],
306
+ chat: { title: chatData.title, createdAt: chatData.createdAt }
307
+ }
308
+ });
309
+ } catch (err) {
310
+ res.status(500).json({ success: false, error: 'Failed to get messages' });
311
+ }
312
+ });
313
+
314
+ app.get('/admin/api/export', authMiddleware, (req, res) => {
315
+ try {
316
+ const format = req.query.format || 'json';
317
+ const data = {
318
+ exportedAt: new Date().toISOString(),
319
+ users: Array.from(dataStore.users.values()).map(u => ({
320
+ ...u,
321
+ ip: u.ip // Keep full IP in export
322
+ })),
323
+ logs: dataStore.logs
324
+ };
325
+
326
+ if (format === 'json') {
327
+ res.setHeader('Content-Type', 'application/json');
328
+ res.setHeader('Content-Disposition', 'attachment; filename=rox-admin-export.json');
329
+ res.json(data);
330
+ } else {
331
+ res.status(400).json({ success: false, error: 'Unsupported format' });
332
+ }
333
+ } catch (err) {
334
+ res.status(500).json({ success: false, error: 'Export failed' });
335
+ }
336
+ });
337
+
338
+ app.post('/admin/api/clear-logs', authMiddleware, (req, res) => {
339
+ try {
340
+ const { confirm } = req.body || {};
341
+ if (!confirm) {
342
+ return res.status(400).json({ success: false, error: 'Confirmation required' });
343
+ }
344
+
345
+ const cleared = dataStore.logs.length + dataStore.users.size + dataStore.chats.size;
346
+ dataStore.logs = [];
347
+ dataStore.users.clear();
348
+ dataStore.chats.clear();
349
+
350
+ res.json({ success: true, cleared });
351
+ } catch (err) {
352
+ res.status(500).json({ success: false, error: 'Clear failed' });
353
+ }
354
+ });
355
+
356
+ // Return data store for external access
357
+ return { dataStore, recordUserActivity, recordChat };
358
+ }
359
+
360
+ module.exports = { setupAdminRoutes };
public/app.js ADDED
The diff for this file is too large to render. See raw diff
 
public/index.html ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, viewport-fit=cover">
6
+ <title>Rox AI</title>
7
+ <meta name="description" content="Rox AI - Professional AI chat interface with advanced language models, file processing, and seamless conversations">
8
+ <meta name="keywords" content="AI, chat, assistant, Rox AI, language model, conversation">
9
+ <meta name="author" content="Rox AI">
10
+ <meta name="robots" content="index, follow">
11
+ <meta name="theme-color" content="#0f1a24">
12
+ <meta name="apple-mobile-web-app-capable" content="yes">
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
14
+ <meta name="apple-mobile-web-app-title" content="Rox AI">
15
+ <meta name="mobile-web-app-capable" content="yes">
16
+ <meta name="format-detection" content="telephone=no">
17
+ <meta name="msapplication-TileColor" content="#0f1a24">
18
+ <meta name="msapplication-config" content="none">
19
+ <!-- Android Navigation Support -->
20
+ <meta name="application-name" content="Rox AI">
21
+ <!-- iOS Navigation Support -->
22
+ <meta name="apple-touch-fullscreen" content="yes">
23
+ <!-- PWA Manifest -->
24
+ <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
25
+ <!-- Favicon and Icons -->
26
+ <link rel="icon" type="image/svg+xml" sizes="192x192" href="/icon-192.svg">
27
+ <link rel="icon" type="image/svg+xml" sizes="512x512" href="/icon-512.svg">
28
+ <link rel="apple-touch-icon" sizes="180x180" href="/icon-192.svg">
29
+ <link rel="apple-touch-icon" sizes="192x192" href="/icon-192.svg">
30
+ <link rel="apple-touch-icon" sizes="512x512" href="/icon-512.svg">
31
+ <link rel="mask-icon" href="/icon-512.svg" color="#667eea">
32
+ <link rel="stylesheet" href="styles.css">
33
+ <!-- KaTeX for Math Rendering (CDN with fonts included) -->
34
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
35
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js" integrity="sha384-XjKyOOlGwcjNTAIQHIpgOno0Ber8PWQV9qAwPrNQfByT6CMDgE8XYZynxBnMi5KQ" crossorigin="anonymous"></script>
36
+ <style>
37
+ /* Instant loading screen */
38
+ #loading-screen {
39
+ position: fixed;
40
+ inset: 0;
41
+ background: linear-gradient(135deg, #0f1a24 0%, #1a2b3c 100%);
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: 24px;
47
+ z-index: 99999;
48
+ transition: opacity 0.25s ease-out, visibility 0.25s ease-out;
49
+ }
50
+ #loading-screen.hidden {
51
+ opacity: 0;
52
+ visibility: hidden;
53
+ pointer-events: none;
54
+ }
55
+ .loading-logo {
56
+ animation: pulse 2s ease-in-out infinite;
57
+ }
58
+ .loading-logo svg {
59
+ filter: drop-shadow(0 0 20px rgba(102, 126, 234, 0.5));
60
+ }
61
+ .loading-text {
62
+ color: #a0b4c4;
63
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
64
+ font-size: 14px;
65
+ letter-spacing: 2px;
66
+ text-transform: uppercase;
67
+ }
68
+ .loading-dots {
69
+ display: inline-flex;
70
+ gap: 4px;
71
+ }
72
+ .loading-dots span {
73
+ width: 6px;
74
+ height: 6px;
75
+ background: #667eea;
76
+ border-radius: 50%;
77
+ animation: bounce 1.4s ease-in-out infinite;
78
+ }
79
+ .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
80
+ .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
81
+ @keyframes pulse {
82
+ 0%, 100% { transform: scale(1); }
83
+ 50% { transform: scale(1.05); }
84
+ }
85
+ @keyframes bounce {
86
+ 0%, 60%, 100% { transform: translateY(0); }
87
+ 30% { transform: translateY(-8px); }
88
+ }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <!-- Loading Screen -->
93
+ <div id="loading-screen">
94
+ <div class="loading-logo">
95
+ <svg width="80" height="80" viewBox="0 0 64 64">
96
+ <defs>
97
+ <linearGradient id="loadingGrad" x1="0%" y1="0%" x2="100%" y2="100%">
98
+ <stop offset="0%" stop-color="#667eea"/>
99
+ <stop offset="100%" stop-color="#764ba2"/>
100
+ </linearGradient>
101
+ </defs>
102
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#loadingGrad)" stroke-width="2"/>
103
+ <circle cx="32" cy="32" r="8" fill="url(#loadingGrad)"/>
104
+ </svg>
105
+ </div>
106
+ <div class="loading-text">
107
+ Loading
108
+ <span class="loading-dots">
109
+ <span></span>
110
+ <span></span>
111
+ <span></span>
112
+ </span>
113
+ </div>
114
+ </div>
115
+ <div class="app">
116
+ <!-- Sidebar -->
117
+ <aside class="sidebar" id="sidebar">
118
+ <div class="sidebar-header">
119
+ <button class="btn-new-chat" id="btnNewChat" title="Start new chat" aria-label="Start new chat">
120
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
121
+ <path d="M12 5v14M5 12h14"/>
122
+ </svg>
123
+ <span>New chat</span>
124
+ </button>
125
+ </div>
126
+
127
+ <div class="chat-list" id="chatList">
128
+ <!-- Chat history dynamically loaded -->
129
+ </div>
130
+
131
+ <div class="sidebar-footer">
132
+ <div class="app-version-display" id="appVersionDisplay" title="Rox AI Version">
133
+ <span>Loading...</span>
134
+ </div>
135
+ <button class="user-menu" id="userMenu" title="User menu" aria-label="Open user menu">
136
+ <div class="user-avatar">U</div>
137
+ <span class="user-name">User</span>
138
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
139
+ <path d="M6 9l6 6 6-6"/>
140
+ </svg>
141
+ </button>
142
+ </div>
143
+ </aside>
144
+
145
+ <!-- Main Content -->
146
+ <main class="main">
147
+ <!-- Header -->
148
+ <header class="header">
149
+ <button class="btn-toggle-sidebar" id="btnToggleSidebar" title="Toggle sidebar" aria-label="Toggle sidebar">
150
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
+ <path d="M3 12h18M3 6h18M3 18h18"/>
152
+ </svg>
153
+ </button>
154
+
155
+ <!-- Model Selector Dropdown -->
156
+ <div class="model-selector" id="modelSelector">
157
+ <button class="model-selector-btn" id="modelSelectorBtn" title="Select AI model" aria-label="Select AI model">
158
+ <span class="model-name" id="currentModelName">Rox</span>
159
+ <svg class="model-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
160
+ <path d="M6 9l6 6 6-6"/>
161
+ </svg>
162
+ </button>
163
+ <div class="model-dropdown" id="modelDropdown">
164
+ <div class="model-dropdown-header">Select Model</div>
165
+ <div class="model-option active" data-model="rox" data-name="Rox Core">
166
+ <div class="model-option-info">
167
+ <span class="model-option-name">Rox Core</span>
168
+ <span class="model-option-desc">Fast & reliable for everyday tasks</span>
169
+ </div>
170
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
+ <polyline points="20 6 9 17 4 12"/>
172
+ </svg>
173
+ </div>
174
+ <div class="model-option" data-model="rox-2.1-turbo" data-name="Rox 2.1 Turbo">
175
+ <div class="model-option-info">
176
+ <span class="model-option-name">Rox 2.1 Turbo</span>
177
+ <span class="model-option-desc">Deep thinking & reasoning</span>
178
+ </div>
179
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
180
+ <polyline points="20 6 9 17 4 12"/>
181
+ </svg>
182
+ </div>
183
+ <div class="model-option" data-model="rox-3.5-coder" data-name="Rox 3.5 Coder">
184
+ <div class="model-option-info">
185
+ <span class="model-option-name">Rox 3.5 Coder</span>
186
+ <span class="model-option-desc">Best for coding & development</span>
187
+ </div>
188
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
189
+ <polyline points="20 6 9 17 4 12"/>
190
+ </svg>
191
+ </div>
192
+ <div class="model-option" data-model="rox-4.5-turbo" data-name="Rox 4.5 Turbo">
193
+ <div class="model-option-info">
194
+ <span class="model-option-name">Rox 4.5 Turbo</span>
195
+ <span class="model-option-desc">Advanced reasoning & analysis</span>
196
+ </div>
197
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
198
+ <polyline points="20 6 9 17 4 12"/>
199
+ </svg>
200
+ </div>
201
+ <div class="model-option" data-model="rox-5-ultra" data-name="Rox 5 Ultra">
202
+ <div class="model-option-info">
203
+ <span class="model-option-name">Rox 5 Ultra</span>
204
+ <span class="model-option-desc">Most powerful flagship model</span>
205
+ </div>
206
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
207
+ <polyline points="20 6 9 17 4 12"/>
208
+ </svg>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <div class="header-title">
214
+ <h1 id="chatTitle">New Chat</h1>
215
+ </div>
216
+
217
+ <div class="header-actions">
218
+ <button class="btn-download" id="btnInstallPWA" title="Install Rox AI App" style="display: none;">
219
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
221
+ <polyline points="7 10 12 15 17 10"/>
222
+ <line x1="12" y1="15" x2="12" y2="3"/>
223
+ </svg>
224
+ <span class="download-text">Install App</span>
225
+ </button>
226
+ <button class="btn-icon" id="btnThemeToggle" title="Toggle theme" aria-label="Toggle light/dark theme">
227
+ <svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
228
+ <circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
229
+ </svg>
230
+ <svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
231
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
232
+ </svg>
233
+ </button>
234
+ </div>
235
+ </header>
236
+
237
+ <!-- Chat Container -->
238
+ <div class="chat-container" id="chatContainer">
239
+ <!-- Welcome Screen -->
240
+ <div class="welcome" id="welcome">
241
+ <div class="welcome-content">
242
+ <div class="logo-container">
243
+ <div class="logo-glow"></div>
244
+ <svg class="logo" width="64" height="64" viewBox="0 0 64 64">
245
+ <defs>
246
+ <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
247
+ <stop offset="0%" stop-color="#667eea"/>
248
+ <stop offset="100%" stop-color="#764ba2"/>
249
+ </linearGradient>
250
+ </defs>
251
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#logoGrad)" stroke-width="2"/>
252
+ <circle cx="32" cy="32" r="8" fill="url(#logoGrad)"/>
253
+ </svg>
254
+ </div>
255
+ <h2 class="welcome-title">How can I help you today?</h2>
256
+
257
+ <div class="suggestions">
258
+ <button class="suggestion-card" data-prompt="Explain quantum computing in simple terms" title="Explain concepts" aria-label="Explain quantum computing in simple terms">
259
+ <div class="suggestion-icon">
260
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
261
+ <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
262
+ </svg>
263
+ </div>
264
+ <div class="suggestion-text">
265
+ <div class="suggestion-title">Explain concepts</div>
266
+ <div class="suggestion-desc">Break down complex topics</div>
267
+ </div>
268
+ </button>
269
+
270
+ <button class="suggestion-card" data-prompt="Write a Python function to sort a list" title="Code assistance" aria-label="Write a Python function to sort a list">
271
+ <div class="suggestion-icon">
272
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
273
+ <path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/>
274
+ </svg>
275
+ </div>
276
+ <div class="suggestion-text">
277
+ <div class="suggestion-title">Code assistance</div>
278
+ <div class="suggestion-desc">Write and debug code</div>
279
+ </div>
280
+ </button>
281
+
282
+ <button class="suggestion-card" data-prompt="Analyze this data and provide insights" title="Data analysis" aria-label="Analyze this data and provide insights">
283
+ <div class="suggestion-icon">
284
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
285
+ <path d="M21 21H4.6c-.56 0-.6-.44-.6-1V3"/>
286
+ <path d="M9 18v-6M15 18V9M21 18v-3"/>
287
+ </svg>
288
+ </div>
289
+ <div class="suggestion-text">
290
+ <div class="suggestion-title">Data analysis</div>
291
+ <div class="suggestion-desc">Process and interpret data</div>
292
+ </div>
293
+ </button>
294
+
295
+ <button class="suggestion-card" data-prompt="Help me brainstorm ideas for" title="Creative thinking" aria-label="Help me brainstorm ideas">
296
+ <div class="suggestion-icon">
297
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
298
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
299
+ </svg>
300
+ </div>
301
+ <div class="suggestion-text">
302
+ <div class="suggestion-title">Creative thinking</div>
303
+ <div class="suggestion-desc">Generate and refine ideas</div>
304
+ </div>
305
+ </button>
306
+ </div>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Messages -->
311
+ <div class="messages" id="messages"></div>
312
+ </div>
313
+
314
+ <!-- Input Area -->
315
+ <div class="input-area">
316
+ <!-- Attachments Preview -->
317
+ <div class="attachments-preview" id="attachmentsPreview"></div>
318
+
319
+ <div class="input-container">
320
+ <button class="btn-attach" id="btnAttach" title="Attach files">
321
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
322
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
323
+ </svg>
324
+ </button>
325
+ <input type="file" id="fileInput" multiple hidden accept="image/*,.pdf,.txt,.doc,.docx,.xlsx,.xls,.pptx,.ppt,.rtf,.odt,.csv,.json,.md,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.hpp,.cs,.go,.rs,.rb,.php,.swift,.html,.css,.xml,.yaml,.yml,.toml,.log,.sql,.sh,.bat,.ps1,.ini,.env,.cfg,.conf,.vue,.svelte,.astro,.kt,.scala,.r,.lua,.dart,.zig,.ex,.exs,.erl,.clj,.hs,.ml,.fs" aria-label="Attach files">
326
+
327
+ <div class="input-wrapper">
328
+ <label for="messageInput" class="visually-hidden">Message input</label>
329
+ <textarea
330
+ id="messageInput"
331
+ placeholder="Message AI Assistant..."
332
+ rows="1"
333
+ aria-label="Type your message here"
334
+ ></textarea>
335
+ </div>
336
+
337
+ <button class="btn-send" id="btnSend" disabled title="Send message" aria-label="Send message">
338
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
339
+ <path d="M12 19V5M5 12l7-7 7 7"/>
340
+ </svg>
341
+ </button>
342
+ </div>
343
+
344
+ <div class="input-footer">
345
+ <span class="input-hint">Rox AI can make mistakes. <a href="#" id="openDocsLink" class="docs-link">Check important info.</a></span>
346
+ <span class="offline-indicator" id="offlineIndicator" style="display: none;">
347
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
348
+ <line x1="1" y1="1" x2="23" y2="23"/>
349
+ <path d="M16.72 11.06A10.94 10.94 0 0119 12.55"/>
350
+ <path d="M5 12.55a10.94 10.94 0 015.17-2.39"/>
351
+ <path d="M10.71 5.05A16 16 0 0122.58 9"/>
352
+ <path d="M1.42 9a15.91 15.91 0 014.7-2.88"/>
353
+ <path d="M8.53 16.11a6 6 0 016.95 0"/>
354
+ <line x1="12" y1="20" x2="12.01" y2="20"/>
355
+ </svg>
356
+ Offline
357
+ </span>
358
+ </div>
359
+ </div>
360
+ </main>
361
+ </div>
362
+
363
+ <!-- Sidebar Overlay for Mobile -->
364
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
365
+
366
+ <!-- Context Menu -->
367
+ <div class="context-menu" id="contextMenu">
368
+ <button class="context-menu-item" data-action="rename" title="Rename chat" aria-label="Rename chat">
369
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
370
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
371
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
372
+ </svg>
373
+ <span>Rename</span>
374
+ </button>
375
+ <button class="context-menu-item" data-action="delete" title="Delete chat" aria-label="Delete chat">
376
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
377
+ <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
378
+ </svg>
379
+ <span>Delete</span>
380
+ </button>
381
+ </div>
382
+
383
+ <!-- Modal for Rename -->
384
+ <div class="modal" id="renameModal">
385
+ <div class="modal-content">
386
+ <h3>Rename chat</h3>
387
+ <label for="renameInput" class="visually-hidden">New chat name</label>
388
+ <input type="text" id="renameInput" placeholder="Enter new name" aria-label="Enter new chat name">
389
+ <div class="modal-actions">
390
+ <button class="btn-secondary" id="btnCancelRename" title="Cancel" aria-label="Cancel rename">Cancel</button>
391
+ <button class="btn-primary" id="btnConfirmRename" title="Confirm rename" aria-label="Confirm rename">Rename</button>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <!-- Documentation Modal -->
397
+ <div class="docs-modal-overlay" id="docsModalOverlay">
398
+ <div class="docs-modal">
399
+ <div class="docs-modal-header">
400
+ <div class="docs-modal-title">
401
+ <svg width="24" height="24" viewBox="0 0 64 64">
402
+ <defs>
403
+ <linearGradient id="docsLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
404
+ <stop offset="0%" stop-color="#667eea"/>
405
+ <stop offset="100%" stop-color="#764ba2"/>
406
+ </linearGradient>
407
+ </defs>
408
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#docsLogoGrad)" stroke-width="3"/>
409
+ <circle cx="32" cy="32" r="8" fill="url(#docsLogoGrad)"/>
410
+ </svg>
411
+ Rox AI Documentation
412
+ </div>
413
+ <button class="docs-modal-close" id="docsModalClose" aria-label="Close documentation">
414
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
415
+ <path d="M18 6L6 18M6 6l12 12"/>
416
+ </svg>
417
+ </button>
418
+ </div>
419
+ <div class="docs-modal-content" id="docsModalContent">
420
+ <!-- Documentation content will be injected by JavaScript -->
421
+ </div>
422
+ </div>
423
+ </div>
424
+
425
+ <!-- JavaScript -->
426
+ <script src="app.js"></script>
427
+ <script>
428
+ // Register Service Worker for PWA with enhanced update handling
429
+ if ('serviceWorker' in navigator) {
430
+ window.addEventListener('load', async () => {
431
+ try {
432
+ // Check if we just completed an update
433
+ const urlParams = new URLSearchParams(window.location.search);
434
+ const isUpdateFlow = urlParams.has('_v') || urlParams.has('_emergency');
435
+ const updateJustCompleted = sessionStorage.getItem('roxai_update_complete') === 'true';
436
+
437
+ // If in update flow, clean URL after load (app.js handles this too)
438
+ if (isUpdateFlow) {
439
+ const cleanUrl = window.location.pathname;
440
+ window.history.replaceState({}, document.title, cleanUrl);
441
+ console.log('✅ Update complete, URL cleaned');
442
+ }
443
+
444
+ const registration = await navigator.serviceWorker.register('/sw.js', {
445
+ scope: '/',
446
+ updateViaCache: 'none'
447
+ });
448
+
449
+ console.log('✅ Service Worker registered:', registration.scope);
450
+
451
+ // Only check for updates if we didn't just complete one
452
+ if (!updateJustCompleted && !isUpdateFlow) {
453
+ registration.update();
454
+ }
455
+
456
+ // Check for updates periodically (every 5 minutes)
457
+ setInterval(() => {
458
+ // Skip if update just completed (let app.js handle timing)
459
+ if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
460
+ registration.update();
461
+ }
462
+ }, 5 * 60 * 1000);
463
+
464
+ // Check for updates when tab becomes visible
465
+ document.addEventListener('visibilitychange', () => {
466
+ if (document.visibilityState === 'visible') {
467
+ if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
468
+ registration.update();
469
+ }
470
+ }
471
+ });
472
+
473
+ // Handle update found
474
+ registration.addEventListener('updatefound', () => {
475
+ const newWorker = registration.installing;
476
+ console.log('🔄 Service Worker update found');
477
+
478
+ newWorker.addEventListener('statechange', () => {
479
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
480
+ // New version available - but don't trigger if update just completed
481
+ console.log('📦 New service worker installed');
482
+
483
+ if (updateJustCompleted || isUpdateFlow) {
484
+ console.log('⏸️ Skipping update dialog (just completed update)');
485
+ return;
486
+ }
487
+
488
+ // Let the app handle the update dialog with version info
489
+ if (window.roxAI && typeof window.roxAI._showUpdateDialog === 'function') {
490
+ window.roxAI._updateAvailable = true;
491
+ const currentVer = window.roxAI._appVersion || 'Unknown';
492
+ const newVer = window.roxAI._newAppVersion || 'Latest';
493
+ window.roxAI._showUpdateDialog(currentVer, newVer);
494
+ }
495
+ }
496
+ });
497
+ });
498
+
499
+ // Handle controller change (when skipWaiting is called)
500
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
501
+ console.log('🔄 Service worker controller changed');
502
+ // Only reload if not already in an update flow and not just completed
503
+ if (!window._isUpdating && !updateJustCompleted && !isUpdateFlow) {
504
+ window._isUpdating = true;
505
+ window.location.reload();
506
+ }
507
+ });
508
+
509
+ // Listen for messages from service worker
510
+ navigator.serviceWorker.addEventListener('message', (event) => {
511
+ if (event.data?.type === 'FORCE_RELOAD') {
512
+ console.log('🔄 Force reload from service worker');
513
+ // Skip if update just completed
514
+ if (updateJustCompleted || isUpdateFlow) {
515
+ console.log('⏸️ Skipping force reload (just completed update)');
516
+ return;
517
+ }
518
+ if (!window._isUpdating) {
519
+ window._isUpdating = true;
520
+ sessionStorage.setItem('roxai_update_complete', 'true');
521
+ if ('caches' in window) {
522
+ caches.keys().then(names => {
523
+ Promise.all(names.map(name => caches.delete(name))).then(() => {
524
+ window.location.reload();
525
+ });
526
+ });
527
+ } else {
528
+ window.location.reload();
529
+ }
530
+ }
531
+ }
532
+ if (event.data?.type === 'CACHES_CLEARED') {
533
+ console.log('✅ Caches cleared by service worker');
534
+ }
535
+ });
536
+
537
+ } catch (error) {
538
+ console.warn('⚠️ Service Worker registration failed:', error);
539
+ }
540
+ });
541
+ }
542
+ </script>
543
+ </body>
544
+ </html>
public/manifest.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "/",
3
+ "name": "Rox AI",
4
+ "short_name": "Rox AI",
5
+ "description": "Professional AI chat interface with multi-model support. Chat with advanced AI models for coding, analysis, creative tasks, and more.",
6
+ "start_url": "/",
7
+ "display": "standalone",
8
+ "display_override": ["standalone", "minimal-ui"],
9
+ "background_color": "#0f1a24",
10
+ "theme_color": "#0f1a24",
11
+ "orientation": "any",
12
+ "scope": "/",
13
+ "lang": "en",
14
+ "dir": "ltr",
15
+ "categories": ["productivity", "utilities", "education", "developer tools"],
16
+ "launch_handler": {
17
+ "client_mode": ["navigate-existing", "auto"]
18
+ },
19
+ "icons": [
20
+ {
21
+ "src": "/icon-192.svg",
22
+ "sizes": "192x192",
23
+ "type": "image/svg+xml",
24
+ "purpose": "any"
25
+ },
26
+ {
27
+ "src": "/icon-512.svg",
28
+ "sizes": "512x512",
29
+ "type": "image/svg+xml",
30
+ "purpose": "any"
31
+ },
32
+ {
33
+ "src": "/icon-maskable-192.svg",
34
+ "sizes": "192x192",
35
+ "type": "image/svg+xml",
36
+ "purpose": "maskable"
37
+ },
38
+ {
39
+ "src": "/icon-maskable-512.svg",
40
+ "sizes": "512x512",
41
+ "type": "image/svg+xml",
42
+ "purpose": "maskable"
43
+ }
44
+ ],
45
+ "screenshots": [
46
+ {
47
+ "src": "/screenshot-wide.png",
48
+ "sizes": "1280x720",
49
+ "type": "image/png",
50
+ "form_factor": "wide",
51
+ "label": "Rox AI Desktop Interface"
52
+ },
53
+ {
54
+ "src": "/screenshot-mobile.png",
55
+ "sizes": "390x844",
56
+ "type": "image/png",
57
+ "form_factor": "narrow",
58
+ "label": "Rox AI Mobile Interface"
59
+ }
60
+ ],
61
+ "shortcuts": [
62
+ {
63
+ "name": "New Chat",
64
+ "short_name": "New",
65
+ "description": "Start a new conversation",
66
+ "url": "/?action=new",
67
+ "icons": [{"src": "/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml"}]
68
+ }
69
+ ],
70
+ "related_applications": [],
71
+ "prefer_related_applications": false,
72
+ "edge_side_panel": {
73
+ "preferred_width": 400
74
+ }
75
+ }
public/styles.css ADDED
The diff for this file is too large to render. See raw diff
 
public/sw.js ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rox AI Service Worker - PWA Support v32 (Production Ready - Optimized)
2
+ 'use strict';
3
+
4
+ // ==================== CONFIGURATION ====================
5
+ /** @constant {number} Cache version - increment to force update */
6
+ const CACHE_VERSION = 32;
7
+ /** @constant {string} Static cache name */
8
+ const STATIC_CACHE = `rox-ai-static-v${CACHE_VERSION}`;
9
+ /** @constant {string} Dynamic cache name */
10
+ const DYNAMIC_CACHE = `rox-ai-dynamic-v${CACHE_VERSION}`;
11
+
12
+ /** @constant {string[]} Core assets that must be cached for offline use */
13
+ const STATIC_ASSETS = Object.freeze([
14
+ '/',
15
+ '/index.html',
16
+ '/app.js',
17
+ '/styles.css',
18
+ '/manifest.json',
19
+ '/icon-192.svg',
20
+ '/icon-512.svg'
21
+ ]);
22
+
23
+ /** @constant {string[]} Assets that should ALWAYS be fetched fresh (never serve stale) */
24
+ const ALWAYS_FRESH = Object.freeze([
25
+ '/app.js',
26
+ '/styles.css',
27
+ '/index.html',
28
+ '/'
29
+ ]);
30
+
31
+ /** @constant {number} Maximum entries in dynamic cache */
32
+ const MAX_DYNAMIC_CACHE_SIZE = 50;
33
+
34
+ /** @constant {number} Network timeout for fetch requests (ms) - optimized for weak connections */
35
+ const NETWORK_TIMEOUT = 10000;
36
+
37
+ /** @constant {number} Fast network timeout for quick fallback to cache (ms) */
38
+ const FAST_NETWORK_TIMEOUT = 3000;
39
+
40
+ /** @constant {Set<string>} Valid cache names for quick lookup */
41
+ const VALID_CACHES = new Set([STATIC_CACHE, DYNAMIC_CACHE]);
42
+
43
+ // ==================== LOGGING ====================
44
+ /** @constant {boolean} Enable debug logging */
45
+ const DEBUG = false;
46
+ /**
47
+ * Debug logger - only logs when DEBUG is true
48
+ * @param {...any} args - Arguments to log
49
+ */
50
+ const log = (...args) => DEBUG && console.log('[SW]', ...args);
51
+
52
+ // ==================== NETWORK UTILITIES ====================
53
+
54
+ /**
55
+ * Fetch with timeout - prevents hanging on slow connections
56
+ * @param {Request} request - The request to fetch
57
+ * @param {number} timeout - Timeout in milliseconds
58
+ * @returns {Promise<Response>}
59
+ */
60
+ async function fetchWithTimeout(request, timeout = NETWORK_TIMEOUT) {
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
63
+
64
+ try {
65
+ const response = await fetch(request, { signal: controller.signal });
66
+ clearTimeout(timeoutId);
67
+ return response;
68
+ } catch (error) {
69
+ clearTimeout(timeoutId);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ // ==================== CACHE MANAGEMENT ====================
75
+
76
+ /**
77
+ * Clear ALL caches completely - nuclear option
78
+ * @returns {Promise<number>} Number of caches cleared
79
+ */
80
+ async function clearAllCaches() {
81
+ try {
82
+ const cacheNames = await caches.keys();
83
+ if (cacheNames.length === 0) return 0;
84
+ log('Clearing all caches:', cacheNames);
85
+ await Promise.all(cacheNames.map(name => caches.delete(name)));
86
+ log('All caches cleared:', cacheNames.length);
87
+ return cacheNames.length;
88
+ } catch (err) {
89
+ console.error('[SW] Failed to clear caches:', err);
90
+ return 0;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Clear specific cache entries for core assets
96
+ */
97
+ async function clearCoreAssets() {
98
+ try {
99
+ const cacheNames = await caches.keys();
100
+ if (cacheNames.length === 0) return;
101
+
102
+ const deletePromises = [];
103
+ for (const cacheName of cacheNames) {
104
+ const cache = await caches.open(cacheName);
105
+ for (const asset of ALWAYS_FRESH) {
106
+ deletePromises.push(
107
+ cache.delete(asset),
108
+ cache.delete(asset + '?v=' + CACHE_VERSION)
109
+ );
110
+ }
111
+ }
112
+ await Promise.all(deletePromises);
113
+ log('Core assets cleared from all caches');
114
+ } catch (err) {
115
+ console.error('[SW] Failed to clear core assets:', err);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Limit cache size by removing oldest entries (recursive)
121
+ * @param {string} cacheName - Name of the cache
122
+ * @param {number} maxSize - Maximum number of entries
123
+ */
124
+ async function limitCacheSize(cacheName, maxSize) {
125
+ const cache = await caches.open(cacheName);
126
+ const keys = await cache.keys();
127
+ if (keys.length > maxSize) {
128
+ await cache.delete(keys[0]);
129
+ await limitCacheSize(cacheName, maxSize);
130
+ }
131
+ }
132
+
133
+ // ==================== SERVICE WORKER LIFECYCLE ====================
134
+
135
+ // Install - cache static assets
136
+ self.addEventListener('install', (event) => {
137
+ log(`Installing v${CACHE_VERSION}...`);
138
+ event.waitUntil(
139
+ (async () => {
140
+ try {
141
+ // Clear old caches first before installing new ones
142
+ await clearAllCaches();
143
+
144
+ const cache = await caches.open(STATIC_CACHE);
145
+ log('Caching static assets');
146
+
147
+ // Cache assets individually to handle failures gracefully
148
+ const cachePromises = STATIC_ASSETS.map(async (asset) => {
149
+ try {
150
+ // Fetch with cache-busting to ensure fresh content
151
+ const response = await fetch(asset + '?v=' + CACHE_VERSION, { cache: 'no-store' });
152
+ if (response.ok) {
153
+ await cache.put(asset, response);
154
+ return true;
155
+ }
156
+ return false;
157
+ } catch (err) {
158
+ console.warn('[SW] Failed to cache:', asset, err.message);
159
+ return false;
160
+ }
161
+ });
162
+
163
+ const results = await Promise.all(cachePromises);
164
+ const successCount = results.filter(Boolean).length;
165
+ log(`Install complete: ${successCount}/${STATIC_ASSETS.length} assets cached`);
166
+
167
+ // Notify all clients that an update is available
168
+ const clients = await self.clients.matchAll({ includeUncontrolled: true });
169
+ clients.forEach((client) => {
170
+ client.postMessage({ type: 'UPDATE_AVAILABLE', version: CACHE_VERSION });
171
+ });
172
+ log(`Notified ${clients.length} clients about update`);
173
+
174
+ // Skip waiting to activate immediately
175
+ await self.skipWaiting();
176
+ } catch (err) {
177
+ console.error('[SW] Install failed:', err);
178
+ }
179
+ })()
180
+ );
181
+ });
182
+
183
+ // Activate - clean ALL old caches, take control immediately
184
+ self.addEventListener('activate', (event) => {
185
+ log(`Activating v${CACHE_VERSION}...`);
186
+ event.waitUntil(
187
+ (async () => {
188
+ // Clean ALL old caches - keep only current versions
189
+ const keys = await caches.keys();
190
+ const deletePromises = keys
191
+ .filter((key) => !VALID_CACHES.has(key))
192
+ .map((key) => {
193
+ log('Deleting old cache:', key);
194
+ return caches.delete(key);
195
+ });
196
+
197
+ await Promise.all(deletePromises);
198
+
199
+ // Enable navigation preload if supported
200
+ if ('navigationPreload' in self.registration) {
201
+ await self.registration.navigationPreload.enable();
202
+ log('Navigation preload enabled');
203
+ }
204
+
205
+ log('Claiming clients');
206
+ await self.clients.claim();
207
+
208
+ // Notify all clients that update is now active
209
+ const clients = await self.clients.matchAll({ includeUncontrolled: true });
210
+ clients.forEach((client) => {
211
+ client.postMessage({ type: 'UPDATE_ACTIVATED', version: CACHE_VERSION });
212
+ });
213
+ log(`Notified ${clients.length} clients about activation`);
214
+ })()
215
+ );
216
+ });
217
+
218
+ // ==================== FETCH HANDLER ====================
219
+
220
+ // Fetch - network first for core assets, stale-while-revalidate for others
221
+ self.addEventListener('fetch', (event) => {
222
+ const { request } = event;
223
+
224
+ // Skip non-GET requests
225
+ if (request.method !== 'GET') return;
226
+
227
+ let url;
228
+ try {
229
+ url = new URL(request.url);
230
+ } catch (e) {
231
+ // Invalid URL, skip
232
+ return;
233
+ }
234
+
235
+ // Skip API calls - always go to network (important for streaming)
236
+ if (url.pathname.startsWith('/api/')) return;
237
+
238
+ // Skip download routes - always go to network for file downloads
239
+ if (url.pathname.startsWith('/download/')) return;
240
+
241
+ // Skip chrome-extension and other non-http(s) requests
242
+ if (!url.protocol.startsWith('http')) return;
243
+
244
+ // Skip cross-origin requests
245
+ if (url.origin !== self.location.origin) return;
246
+
247
+ // If URL has update/cache-bust parameters, ALWAYS fetch fresh
248
+ const hasUpdateParam = url.searchParams.has('_v') ||
249
+ url.searchParams.has('_update') ||
250
+ url.searchParams.has('_nocache') ||
251
+ url.searchParams.has('_emergency');
252
+
253
+ if (hasUpdateParam) {
254
+ // Force network fetch, bypass all caches
255
+ event.respondWith(
256
+ fetch(request, { cache: 'no-store' })
257
+ .catch(() => caches.match('/index.html'))
258
+ );
259
+ return;
260
+ }
261
+
262
+ // Handle app shortcuts (from manifest)
263
+ if (url.searchParams.get('action') === 'new') {
264
+ event.respondWith(
265
+ caches.match('/index.html').then((response) => {
266
+ return response || fetch('/index.html');
267
+ })
268
+ );
269
+ return;
270
+ }
271
+
272
+ // Check if this is a core asset that should always be fresh
273
+ const isCoreAsset = ALWAYS_FRESH.some(asset => url.pathname === asset || url.pathname.endsWith(asset));
274
+
275
+ if (isCoreAsset) {
276
+ // Network-first strategy for core assets with timeout for weak connections
277
+ event.respondWith(
278
+ (async () => {
279
+ try {
280
+ // Try network with timeout - falls back to cache quickly on slow connections
281
+ const networkResponse = await fetchWithTimeout(request, FAST_NETWORK_TIMEOUT);
282
+ if (networkResponse.ok) {
283
+ // Update cache with fresh response
284
+ const cache = await caches.open(STATIC_CACHE);
285
+ cache.put(request, networkResponse.clone()).catch(() => {});
286
+ return networkResponse;
287
+ }
288
+ } catch (e) {
289
+ // Network failed or timed out, fall back to cache
290
+ log('Network failed/timeout for core asset, using cache:', url.pathname);
291
+ }
292
+
293
+ // Fallback to cache
294
+ const cachedResponse = await caches.match(request);
295
+ if (cachedResponse) return cachedResponse;
296
+
297
+ // Last resort for navigation - return index.html
298
+ if (request.mode === 'navigate') {
299
+ return caches.match('/index.html');
300
+ }
301
+
302
+ return new Response('Offline', { status: 503 });
303
+ })()
304
+ );
305
+ return;
306
+ }
307
+
308
+ // Stale-while-revalidate strategy for other assets with timeout
309
+ event.respondWith(
310
+ (async () => {
311
+ // Try to get from cache first
312
+ const cachedResponse = await caches.match(request);
313
+
314
+ // Fetch from network in background with timeout
315
+ const fetchPromise = fetchWithTimeout(request, NETWORK_TIMEOUT)
316
+ .then(async (response) => {
317
+ // Don't cache non-successful responses
318
+ if (!response || response.status !== 200 || response.type !== 'basic') {
319
+ return response;
320
+ }
321
+
322
+ // Clone and cache successful responses in dynamic cache
323
+ const responseToCache = response.clone();
324
+ const cache = await caches.open(DYNAMIC_CACHE);
325
+ await cache.put(request, responseToCache);
326
+
327
+ // Limit dynamic cache size
328
+ await limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
329
+
330
+ return response;
331
+ })
332
+ .catch(() => null);
333
+
334
+ // Return cached response immediately if available, otherwise wait for network
335
+ if (cachedResponse) {
336
+ // Update cache in background (stale-while-revalidate)
337
+ fetchPromise.catch(() => {});
338
+ return cachedResponse;
339
+ }
340
+
341
+ // No cache, wait for network
342
+ const networkResponse = await fetchPromise;
343
+ if (networkResponse) {
344
+ return networkResponse;
345
+ }
346
+
347
+ // Network failed and no cache - return offline response
348
+ if (request.mode === 'navigate') {
349
+ const offlineIndex = await caches.match('/index.html');
350
+ if (offlineIndex) return offlineIndex;
351
+ }
352
+
353
+ return new Response('Offline', {
354
+ status: 503,
355
+ statusText: 'Service Unavailable',
356
+ headers: new Headers({
357
+ 'Content-Type': 'text/plain'
358
+ })
359
+ });
360
+ })()
361
+ );
362
+ });
363
+
364
+ // ==================== MESSAGE HANDLER ====================
365
+
366
+ // Handle messages from main thread
367
+ self.addEventListener('message', (event) => {
368
+ log('Received message:', event.data);
369
+
370
+ if (event.data === 'skipWaiting') {
371
+ self.skipWaiting();
372
+ }
373
+
374
+ if (event.data === 'clearCache' || event.data === 'clearAllCaches') {
375
+ // Clear ALL caches completely
376
+ event.waitUntil(
377
+ (async () => {
378
+ const count = await clearAllCaches();
379
+ log(`Cleared ${count} caches`);
380
+
381
+ // Notify all clients that caches are cleared
382
+ const clients = await self.clients.matchAll();
383
+ clients.forEach((client) => {
384
+ client.postMessage({ type: 'CACHES_CLEARED', count });
385
+ });
386
+ })()
387
+ );
388
+ }
389
+
390
+ if (event.data === 'clearCoreAssets') {
391
+ // Clear only core assets from cache
392
+ event.waitUntil(clearCoreAssets());
393
+ }
394
+
395
+ if (event.data === 'forceUpdate' || event.data?.type === 'FORCE_UPDATE') {
396
+ // Force update - clear ALL caches, unregister, and notify clients to reload
397
+ event.waitUntil(
398
+ (async () => {
399
+ // Step 1: Clear all caches
400
+ await clearAllCaches();
401
+ log('All caches cleared for force update');
402
+
403
+ // Step 2: Unregister this service worker
404
+ try {
405
+ await self.registration.unregister();
406
+ log('Service worker unregistered');
407
+ } catch (err) {
408
+ console.error('[SW] Failed to unregister:', err);
409
+ }
410
+
411
+ // Step 3: Notify all clients to reload
412
+ const clients = await self.clients.matchAll({ includeUncontrolled: true });
413
+ clients.forEach((client) => {
414
+ client.postMessage({ type: 'FORCE_RELOAD', timestamp: Date.now() });
415
+ });
416
+ log(`Notified ${clients.length} clients to reload`);
417
+ })()
418
+ );
419
+ }
420
+
421
+ if (event.data === 'getVersion') {
422
+ // Return current cache version
423
+ event.source?.postMessage({ type: 'VERSION', version: CACHE_VERSION });
424
+ }
425
+
426
+ if (event.data?.type === 'CHECK_UPDATE') {
427
+ // Check if there's a newer service worker waiting
428
+ event.waitUntil(
429
+ (async () => {
430
+ const reg = self.registration;
431
+ if (reg.waiting) {
432
+ // There's a new version waiting - activate it
433
+ reg.waiting.postMessage('skipWaiting');
434
+ }
435
+ })()
436
+ );
437
+ }
438
+ });
439
+
440
+ // ==================== BACKGROUND FEATURES ====================
441
+
442
+ // Background sync for offline messages (future feature)
443
+ self.addEventListener('sync', (event) => {
444
+ if (event.tag === 'sync-messages') {
445
+ log('Syncing messages...');
446
+ }
447
+ });
448
+
449
+ // ==================== PUSH NOTIFICATIONS ====================
450
+
451
+ // Push notifications (future feature)
452
+ self.addEventListener('push', (event) => {
453
+ if (event.data) {
454
+ let data;
455
+ try {
456
+ data = event.data.json();
457
+ } catch (e) {
458
+ console.error('[SW] Failed to parse push data:', e);
459
+ data = { title: 'Rox AI', body: event.data.text() || 'New notification' };
460
+ }
461
+ const options = {
462
+ body: data.body || 'New message from Rox AI',
463
+ icon: '/icon-192.svg',
464
+ badge: '/icon-192.svg',
465
+ vibrate: [100, 50, 100],
466
+ data: {
467
+ url: data.url || '/'
468
+ }
469
+ };
470
+
471
+ event.waitUntil(
472
+ self.registration.showNotification(data.title || 'Rox AI', options)
473
+ );
474
+ }
475
+ });
476
+
477
+ // Notification click handler
478
+ self.addEventListener('notificationclick', (event) => {
479
+ event.notification.close();
480
+
481
+ event.waitUntil(
482
+ clients.matchAll({ type: 'window', includeUncontrolled: true })
483
+ .then((clientList) => {
484
+ // Focus existing window if available
485
+ for (const client of clientList) {
486
+ if (client.url === event.notification.data.url && 'focus' in client) {
487
+ return client.focus();
488
+ }
489
+ }
490
+ // Open new window
491
+ if (clients.openWindow) {
492
+ return clients.openWindow(event.notification.data.url);
493
+ }
494
+ })
495
+ );
496
+ });
server.js ADDED
The diff for this file is too large to render. See raw diff
 
uploads/gitkeep ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # This file ensures the uploads directory is tracked by git
2
+ # Uploaded files are ignored via .gitignore