Upload 18 files
Browse files- .env.example +47 -0
- .gitattributes +57 -0
- .gitignore +72 -0
- Dockerfile +53 -0
- README.md +108 -0
- package-lock.json +1515 -0
- package.json +53 -0
- private/admin/admin.css +1198 -0
- private/admin/admin.js +1107 -0
- private/admin/index.html +246 -0
- private/admin/routes.js +360 -0
- public/app.js +0 -0
- public/index.html +544 -0
- public/manifest.json +75 -0
- public/styles.css +0 -0
- public/sw.js +496 -0
- server.js +0 -0
- uploads/gitkeep +2 -0
.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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
| 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(/<strong>/g, '<strong>').replace(/<\/strong>/g, '</strong>')
|
| 710 |
+
.replace(/<em>/g, '<em>').replace(/<\/em>/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
|