| const express = require('express'); |
| const morgan = require('morgan'); |
| const { createProxyMiddleware } = require('http-proxy-middleware'); |
| const axios = require('axios'); |
| const url = require('url'); |
| const app = express(); |
|
|
| |
| app.use(morgan('dev')); |
|
|
| |
| const PORT = process.env.HF_PORT || 7860; |
| const TARGET_URL = process.env.TARGET_URL || 'http://localhost:3010'; |
| const API_PATH = process.env.API_PATH || '/v1'; |
| const TIMEOUT = parseInt(process.env.TIMEOUT) || 30000; |
|
|
| console.log(`Service configuration: |
| - Port: ${PORT} |
| - Target URL: ${TARGET_URL} |
| - API Path: ${API_PATH} |
| - Timeout: ${TIMEOUT}ms`); |
|
|
| |
| let proxyPool = []; |
| if (process.env.PROXY) { |
| proxyPool = process.env.PROXY.split(',').map(p => p.trim()).filter(p => p); |
| console.log(`Loaded ${proxyPool.length} proxies from environment`); |
| |
| if (proxyPool.length > 0) { |
| console.log('Proxy pool initialized:'); |
| proxyPool.forEach((proxy, index) => { |
| |
| const maskedProxy = proxy.replace(/(https?:\/\/)([^:]+):([^@]+)@/, '$1$2:****@'); |
| console.log(` [${index + 1}] ${maskedProxy}`); |
| }); |
| } |
| } |
|
|
| |
| function getRandomProxy() { |
| if (proxyPool.length === 0) return null; |
| const randomIndex = Math.floor(Math.random() * proxyPool.length); |
| const proxyUrl = proxyPool[randomIndex]; |
| const parsedUrl = url.parse(proxyUrl); |
|
|
| return { |
| host: parsedUrl.hostname, |
| port: parsedUrl.port || 80, |
| auth: parsedUrl.auth ? { |
| username: parsedUrl.auth.split(':')[0], |
| password: parsedUrl.auth.split(':')[1] |
| } : undefined |
| }; |
| } |
|
|
| |
| app.get('/hf/v1/models', (req, res) => { |
| const models = { |
| "object": "list", |
| "data": [ |
| { |
| "id": "claude-3.5-sonnet", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-4", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-4o", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "claude-3-opus", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-3.5-turbo", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-4-turbo-2024-04-09", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-4o-128k", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gemini-1.5-flash-500k", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "claude-3-haiku-200k", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "claude-3-5-sonnet-200k", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "claude-3-5-sonnet-20241022", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gpt-4o-mini", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "o1-mini", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "o1-preview", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "o1", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "claude-3.5-haiku", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gemini-exp-1206", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gemini-2.0-flash-thinking-exp", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "gemini-2.0-flash-exp", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "deepseek-v3", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| }, |
| { |
| "id": "deepseek-r1", |
| "object": "model", |
| "created": 1706745938, |
| "owned_by": "cursor" |
| } |
| ] |
| }; |
| res.json(models); |
| }); |
|
|
| |
| app.use('/hf/v1/chat/completions', (req, res, next) => { |
| const proxy = getRandomProxy(); |
| const targetEndpoint = `${TARGET_URL}${API_PATH}/chat/completions`; |
| |
| console.log(`Forwarding request to: ${targetEndpoint}`); |
| |
| const middleware = createProxyMiddleware({ |
| target: targetEndpoint, |
| changeOrigin: true, |
| proxy: proxy ? proxy : undefined, |
| timeout: TIMEOUT, |
| proxyTimeout: TIMEOUT, |
| onProxyReq: (proxyReq, req, res) => { |
| if (req.body) { |
| const bodyData = JSON.stringify(req.body); |
| proxyReq.setHeader('Content-Type', 'application/json'); |
| proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); |
| proxyReq.write(bodyData); |
| proxyReq.end(); |
| } |
| }, |
| onError: (err, req, res) => { |
| console.error('Proxy error:', err); |
| res.status(502).json({ |
| error: { |
| message: 'Proxy connection error - unable to reach target service', |
| type: 'proxy_error', |
| details: process.env.NODE_ENV === 'development' ? err.message : undefined |
| } |
| }); |
| }, |
| onProxyRes: (proxyRes, req, res) => { |
| console.log(`Proxy response status: ${proxyRes.statusCode}`); |
| |
| |
| if (proxyRes.statusCode >= 400) { |
| let responseBody = ''; |
| |
| proxyRes.on('data', function(chunk) { |
| responseBody += chunk; |
| }); |
| |
| proxyRes.on('end', function() { |
| try { |
| |
| JSON.parse(responseBody); |
| |
| } catch (e) { |
| |
| const originalStatusCode = proxyRes.statusCode; |
| res.writeHead(originalStatusCode, {'Content-Type': 'application/json'}); |
| res.end(JSON.stringify({ |
| error: { |
| message: `Error from target service: ${responseBody.substring(0, 200)}${responseBody.length > 200 ? '...' : ''}`, |
| type: 'target_service_error', |
| status: originalStatusCode |
| } |
| })); |
| return; |
| } |
| }); |
| } |
| } |
| }); |
| |
| if (proxy) { |
| const maskedProxy = `${proxy.host}:${proxy.port}` + (proxy.auth ? ' (with auth)' : ''); |
| console.log(`Using proxy: ${maskedProxy}`); |
| } else { |
| console.log('Direct connection (no proxy)'); |
| } |
| |
| middleware(req, res, next); |
| }); |
|
|
| |
| app.get('/', (req, res) => { |
| const htmlContent = ` |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI Models Dashboard</title> |
| <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css"> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css"> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css"> |
| <script src="https://cdn.jsdelivr.net/npm/marked@4.2.5/marked.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.umd.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.0/dist/purify.min.js"></script> |
| <style> |
| :root { |
| --primary-color: #5D5CDE; |
| --primary-light: #8687E7; |
| --primary-dark: #4945C4; |
| --secondary-color: #10b981; |
| --accent-color: #f97316; |
| --bg-dark: #111827; |
| --bg-card: #1f2937; |
| --card-light: #2a3441; |
| --text-primary: #f3f4f6; |
| --text-secondary: #d1d5db; |
| --text-muted: #9ca3af; |
| --border-color: #374151; |
| --success-bg: #065f46; |
| --success-text: #a7f3d0; |
| --error-bg: #7f1d1d; |
| --error-text: #fecaca; |
| --warning-bg: #92400e; |
| --warning-text: #fde68a; |
| --input-bg: #1e293b; |
| --hover-bg: #2d3748; |
| --shadow-color: rgba(0, 0, 0, 0.25); |
| --backdrop-blur: blur(10px); |
| --toast-bg: rgba(31, 41, 55, 0.9); |
| --theme-transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; |
| } |
| |
| .light-theme { |
| --bg-dark: #f1f5f9; |
| --bg-card: #ffffff; |
| --card-light: #f8fafc; |
| --text-primary: #0f172a; |
| --text-secondary: #1e293b; |
| --text-muted: #475569; |
| --border-color: #e2e8f0; |
| --input-bg: #f8fafc; |
| --hover-bg: #f1f5f9; |
| --shadow-color: rgba(0, 0, 0, 0.1); |
| --toast-bg: rgba(255, 255, 255, 0.9); |
| } |
| |
| body { |
| background-color: var(--bg-dark); |
| color: var(--text-primary); |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| transition: var(--theme-transition); |
| min-height: 100vh; |
| } |
| |
| .dashboard-container { |
| display: grid; |
| grid-template-columns: 260px 1fr; |
| min-height: 100vh; |
| } |
| |
| .sidebar { |
| background-color: var(--bg-card); |
| border-right: 1px solid var(--border-color); |
| overflow-y: auto; |
| transition: transform 0.3s ease, background-color 0.3s ease; |
| z-index: 30; |
| } |
| |
| .main-content { |
| overflow-y: auto; |
| padding: 1.5rem; |
| } |
| |
| .logo { |
| font-size: 1.5rem; |
| font-weight: 600; |
| color: var(--primary-light); |
| display: flex; |
| align-items: center; |
| padding: 1.5rem 1rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .logo i { |
| margin-right: 0.5rem; |
| color: var(--accent-color); |
| } |
| |
| .nav-section { |
| padding: 1rem 0.75rem 0.5rem; |
| font-size: 0.75rem; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| color: var(--text-muted); |
| } |
| |
| .nav-item { |
| padding: 0.875rem 1rem; |
| border-radius: 0.375rem; |
| margin: 0.25rem 0.5rem; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| display: flex; |
| align-items: center; |
| color: var(--text-secondary); |
| position: relative; |
| } |
| |
| .nav-item:hover { |
| background-color: var(--hover-bg); |
| color: var(--text-primary); |
| } |
| |
| .nav-item.active { |
| background-color: var(--primary-color); |
| color: white; |
| } |
| |
| .nav-item.active::before { |
| content: ''; |
| position: absolute; |
| left: -0.5rem; |
| top: 50%; |
| transform: translateY(-50%); |
| width: 0.25rem; |
| height: 1.5rem; |
| background-color: var(--accent-color); |
| border-radius: 0 0.125rem 0.125rem 0; |
| } |
| |
| .nav-item i { |
| width: 1.25rem; |
| margin-right: 0.75rem; |
| text-align: center; |
| } |
| |
| .beta-tag { |
| background-color: var(--accent-color); |
| color: white; |
| font-size: 0.7rem; |
| padding: 0.1rem 0.4rem; |
| border-radius: 0.25rem; |
| margin-left: 0.5rem; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| |
| .card { |
| background-color: var(--bg-card); |
| border-radius: 0.75rem; |
| border: 1px solid var(--border-color); |
| box-shadow: 0 4px 6px var(--shadow-color); |
| margin-bottom: 1.5rem; |
| padding: 1.5rem; |
| transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease; |
| } |
| |
| .card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 8px 15px var(--shadow-color); |
| } |
| |
| .card-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1rem; |
| padding-bottom: 0.75rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .card-title { |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: var(--text-primary); |
| display: flex; |
| align-items: center; |
| } |
| |
| .card-title i { |
| margin-right: 0.5rem; |
| color: var(--primary-light); |
| } |
| |
| .status-badge { |
| background-color: var(--success-bg); |
| color: var(--success-text); |
| border-radius: 2rem; |
| padding: 0.25rem 0.75rem; |
| font-size: 0.875rem; |
| font-weight: 500; |
| display: flex; |
| align-items: center; |
| gap: 0.375rem; |
| } |
| |
| .status-badge i { |
| font-size: 0.75rem; |
| } |
| |
| .status-badge.error { |
| background-color: var(--error-bg); |
| color: var(--error-text); |
| } |
| |
| .status-badge.warning { |
| background-color: var(--warning-bg); |
| color: var(--warning-text); |
| } |
| |
| .info-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| gap: 1rem; |
| } |
| |
| .info-item { |
| background-color: var(--card-light); |
| border-radius: 0.5rem; |
| border: 1px solid var(--border-color); |
| padding: 1rem; |
| display: flex; |
| flex-direction: column; |
| transition: background-color 0.3s ease; |
| } |
| |
| .info-label { |
| color: var(--text-muted); |
| font-size: 0.875rem; |
| margin-bottom: 0.5rem; |
| display: flex; |
| align-items: center; |
| gap: 0.375rem; |
| } |
| |
| .info-label i { |
| color: var(--text-secondary); |
| } |
| |
| .info-value { |
| color: var(--primary-light); |
| font-weight: 500; |
| word-break: break-all; |
| } |
| |
| .model-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| gap: 1rem; |
| max-height: 400px; |
| overflow-y: auto; |
| padding-right: 0.5rem; |
| scrollbar-width: thin; |
| scrollbar-color: var(--primary-color) var(--bg-dark); |
| } |
| |
| .model-grid::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .model-grid::-webkit-scrollbar-track { |
| background: var(--bg-dark); |
| border-radius: 3px; |
| } |
| |
| .model-grid::-webkit-scrollbar-thumb { |
| background-color: var(--primary-color); |
| border-radius: 3px; |
| } |
| |
| .model-item { |
| background-color: var(--card-light); |
| border-radius: 0.5rem; |
| border: 1px solid var(--border-color); |
| padding: 1rem; |
| transition: all 0.2s ease; |
| position: relative; |
| cursor: pointer; |
| } |
| |
| .model-item:hover { |
| background-color: var(--hover-bg); |
| transform: translateY(-2px); |
| box-shadow: 0 4px 8px var(--shadow-color); |
| } |
| |
| .model-item.selected { |
| border-color: var(--primary-color); |
| background-color: var(--hover-bg); |
| } |
| |
| .model-item.selected::after { |
| content: '✓'; |
| position: absolute; |
| right: 10px; |
| bottom: 10px; |
| background-color: var(--primary-color); |
| color: white; |
| width: 20px; |
| height: 20px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 12px; |
| } |
| |
| .model-name { |
| font-weight: 600; |
| color: var(--text-primary); |
| margin-bottom: 0.5rem; |
| display: block; |
| } |
| |
| .model-provider { |
| position: absolute; |
| top: 0.5rem; |
| right: 0.5rem; |
| background-color: var(--primary-color); |
| color: white; |
| border-radius: 0.25rem; |
| padding: 0.125rem 0.375rem; |
| font-size: 0.75rem; |
| } |
| |
| .endpoint-box { |
| background-color: var(--input-bg); |
| border-radius: 0.5rem; |
| padding: 1rem; |
| margin-top: 1rem; |
| border: 1px solid var(--border-color); |
| transition: background-color 0.3s ease; |
| } |
| |
| .endpoint-url { |
| font-family: monospace; |
| background-color: var(--bg-dark); |
| padding: 0.75rem; |
| border-radius: 0.25rem; |
| margin: 0.5rem 0; |
| overflow-x: auto; |
| white-space: nowrap; |
| transition: background-color 0.3s ease; |
| } |
| |
| .copy-btn { |
| background-color: var(--primary-color); |
| color: white; |
| border: none; |
| border-radius: 0.25rem; |
| padding: 0.375rem 0.75rem; |
| font-size: 0.875rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| display: inline-flex; |
| align-items: center; |
| gap: 0.375rem; |
| } |
| |
| .copy-btn:hover { |
| background-color: var(--primary-dark); |
| } |
| |
| .copy-btn:active { |
| transform: scale(0.98); |
| } |
| |
| .chat-container { |
| display: flex; |
| flex-direction: column; |
| height: calc(100vh - 3rem); |
| } |
| |
| .chat-header { |
| padding: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| flex-wrap: wrap; |
| gap: 1rem; |
| background-color: var(--bg-card); |
| transition: background-color 0.3s ease; |
| } |
| |
| .chat-body { |
| flex-grow: 1; |
| overflow-y: auto; |
| padding: 1rem; |
| scrollbar-width: thin; |
| scrollbar-color: var(--primary-color) var(--bg-dark); |
| } |
| |
| .chat-body::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .chat-body::-webkit-scrollbar-track { |
| background: var(--bg-dark); |
| } |
| |
| .chat-body::-webkit-scrollbar-thumb { |
| background-color: var(--primary-color); |
| border-radius: 3px; |
| } |
| |
| .message-list { |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| } |
| |
| .message { |
| display: flex; |
| max-width: 80%; |
| animation: fadeInUp 0.3s ease; |
| } |
| |
| .message.user { |
| align-self: flex-end; |
| } |
| |
| .message.bot { |
| align-self: flex-start; |
| } |
| |
| .message-bubble { |
| padding: 0.75rem 1rem; |
| border-radius: 1rem; |
| position: relative; |
| } |
| |
| .message.user .message-bubble { |
| background-color: var(--primary-color); |
| color: white; |
| border-bottom-right-radius: 0.25rem; |
| } |
| |
| .message.bot .message-bubble { |
| background-color: var(--card-light); |
| color: var(--text-primary); |
| border-bottom-left-radius: 0.25rem; |
| } |
| |
| .message-time { |
| font-size: 0.75rem; |
| color: var(--text-muted); |
| margin-top: 0.25rem; |
| text-align: right; |
| } |
| |
| .message-actions { |
| visibility: hidden; |
| opacity: 0; |
| position: absolute; |
| right: 10px; |
| top: -20px; |
| display: flex; |
| gap: 5px; |
| transition: visibility 0s, opacity 0.3s; |
| } |
| |
| .message:hover .message-actions { |
| visibility: visible; |
| opacity: 1; |
| } |
| |
| .message-action-btn { |
| width: 24px; |
| height: 24px; |
| border-radius: 50%; |
| background: var(--bg-card); |
| border: 1px solid var(--border-color); |
| color: var(--text-secondary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| font-size: 12px; |
| transition: all 0.2s; |
| } |
| |
| .message-action-btn:hover { |
| background: var(--primary-color); |
| color: white; |
| } |
| |
| .chat-input { |
| padding: 1rem; |
| border-top: 1px solid var(--border-color); |
| background-color: var(--bg-card); |
| transition: background-color 0.3s ease; |
| } |
| |
| .input-container { |
| position: relative; |
| display: flex; |
| align-items: center; |
| } |
| |
| .message-input { |
| background-color: var(--input-bg); |
| border: 1px solid var(--border-color); |
| color: var(--text-primary); |
| border-radius: 1.5rem; |
| padding: 0.875rem 4rem 0.875rem 1rem; |
| width: 100%; |
| resize: none; |
| max-height: 120px; |
| overflow-y: auto; |
| font-size: 1rem; |
| transition: border-color 0.3s ease, background-color 0.3s ease; |
| } |
| |
| .message-input:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| } |
| |
| .send-btn { |
| position: absolute; |
| right: 0.5rem; |
| background-color: var(--primary-color); |
| color: white; |
| border: none; |
| border-radius: 50%; |
| width: 2.5rem; |
| height: 2.5rem; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| |
| .send-btn:hover { |
| background-color: var(--primary-dark); |
| transform: scale(1.05); |
| } |
| |
| .send-btn:active { |
| transform: scale(0.95); |
| } |
| |
| .send-btn:disabled { |
| background-color: var(--border-color); |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .model-select-container { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.75rem; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .model-select { |
| background-color: var(--input-bg); |
| border: 1px solid var(--border-color); |
| color: var(--text-primary); |
| border-radius: 0.5rem; |
| padding: 0.5rem 2rem 0.5rem 0.75rem; |
| font-size: 1rem; |
| flex-grow: 1; |
| appearance: none; |
| -webkit-appearance: none; |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%239ca3af'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); |
| background-repeat: no-repeat; |
| background-position: right 0.5rem center; |
| background-size: 1.5em; |
| transition: border-color 0.3s ease, background-color 0.3s ease; |
| min-width: 200px; |
| } |
| |
| .model-select:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| } |
| |
| .model-label { |
| color: var(--text-secondary); |
| font-weight: 500; |
| white-space: nowrap; |
| } |
| |
| .chat-options { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| } |
| |
| .option-label { |
| color: var(--text-secondary); |
| font-size: 0.875rem; |
| } |
| |
| .button-group { |
| display: flex; |
| gap: 0.5rem; |
| flex-wrap: wrap; |
| } |
| |
| .btn { |
| background-color: var(--primary-color); |
| color: white; |
| border: none; |
| border-radius: 0.5rem; |
| padding: 0.5rem 1rem; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| font-weight: 500; |
| font-size: 0.875rem; |
| } |
| |
| .btn:hover { |
| background-color: var(--primary-dark); |
| } |
| |
| .btn:active { |
| transform: scale(0.98); |
| } |
| |
| .btn.outline { |
| background-color: transparent; |
| color: var(--text-secondary); |
| border: 1px solid var(--border-color); |
| } |
| |
| .btn.outline:hover { |
| background-color: var(--hover-bg); |
| color: var(--text-primary); |
| } |
| |
| .btn.small { |
| padding: 0.25rem 0.75rem; |
| font-size: 0.75rem; |
| } |
| |
| .system-message { |
| background-color: var(--card-light); |
| border-radius: 0.5rem; |
| padding: 0.75rem; |
| margin-bottom: 1rem; |
| color: var(--text-muted); |
| font-style: italic; |
| font-size: 0.875rem; |
| display: flex; |
| align-items: center; |
| border-left: 3px solid var(--text-muted); |
| animation: fadeIn 0.5s ease; |
| } |
| |
| .system-message i { |
| margin-right: 0.5rem; |
| font-size: 1rem; |
| } |
| |
| .system-message.error { |
| border-left-color: var(--error-bg); |
| background-color: rgba(127, 29, 29, 0.1); |
| } |
| |
| .system-message.warning { |
| border-left-color: var(--warning-bg); |
| background-color: rgba(146, 64, 14, 0.1); |
| } |
| |
| .system-message.info { |
| border-left-color: var(--primary-color); |
| background-color: rgba(79, 70, 229, 0.1); |
| } |
| |
| .connection-status { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| color: var(--text-secondary); |
| font-size: 0.875rem; |
| } |
| |
| .status-indicator { |
| width: 0.625rem; |
| height: 0.625rem; |
| border-radius: 50%; |
| background-color: var(--success-bg); |
| } |
| |
| .status-indicator.error { |
| background-color: var(--error-bg); |
| } |
| |
| .status-indicator.warning { |
| background-color: var(--warning-bg); |
| } |
| |
| .mobile-menu-btn { |
| display: none; |
| background: none; |
| border: none; |
| color: var(--text-primary); |
| font-size: 1.5rem; |
| cursor: pointer; |
| padding: 0.5rem; |
| z-index: 40; |
| } |
| |
| .settings-row { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 1rem; |
| padding-bottom: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .settings-row:last-child { |
| border-bottom: none; |
| margin-bottom: 0; |
| padding-bottom: 0; |
| } |
| |
| .settings-label { |
| color: var(--text-primary); |
| font-weight: 500; |
| } |
| |
| .settings-description { |
| color: var(--text-muted); |
| font-size: 0.875rem; |
| margin-top: 0.25rem; |
| } |
| |
| .settings-control { |
| min-width: 100px; |
| } |
| |
| .toggle { |
| position: relative; |
| display: inline-block; |
| width: 48px; |
| height: 24px; |
| } |
| |
| .toggle input { |
| opacity: 0; |
| width: 0; |
| height: 0; |
| } |
| |
| .toggle-slider { |
| position: absolute; |
| cursor: pointer; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: var(--border-color); |
| transition: .4s; |
| border-radius: 24px; |
| } |
| |
| .toggle-slider:before { |
| position: absolute; |
| content: ""; |
| height: 18px; |
| width: 18px; |
| left: 3px; |
| bottom: 3px; |
| background-color: white; |
| transition: .4s; |
| border-radius: 50%; |
| } |
| |
| .toggle input:checked + .toggle-slider { |
| background-color: var(--primary-color); |
| } |
| |
| .toggle input:checked + .toggle-slider:before { |
| transform: translateX(24px); |
| } |
| |
| .parameter-row { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 1rem; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .parameter-control { |
| display: flex; |
| flex-direction: column; |
| min-width: 150px; |
| flex: 1; |
| } |
| |
| .parameter-label { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 0.25rem; |
| } |
| |
| .parameter-name { |
| color: var(--text-secondary); |
| font-size: 0.875rem; |
| } |
| |
| .parameter-value { |
| color: var(--primary-light); |
| font-size: 0.875rem; |
| font-weight: 500; |
| } |
| |
| input[type="range"] { |
| -webkit-appearance: none; |
| width: 100%; |
| height: 6px; |
| background: var(--border-color); |
| border-radius: 3px; |
| outline: none; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 16px; |
| height: 16px; |
| border-radius: 50%; |
| background: var(--primary-color); |
| cursor: pointer; |
| border: 2px solid white; |
| } |
| |
| input[type="range"]::-moz-range-thumb { |
| width: 16px; |
| height: 16px; |
| border-radius: 50%; |
| background: var(--primary-color); |
| cursor: pointer; |
| border: 2px solid white; |
| } |
| |
| .number-input { |
| background-color: var(--input-bg); |
| border: 1px solid var(--border-color); |
| color: var(--text-primary); |
| border-radius: 0.375rem; |
| padding: 0.375rem 0.5rem; |
| font-size: 0.875rem; |
| width: 100%; |
| transition: border-color 0.3s ease; |
| } |
| |
| .number-input:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| } |
| |
| .toast-container { |
| position: fixed; |
| top: 1rem; |
| right: 1rem; |
| z-index: 9999; |
| display: flex; |
| flex-direction: column; |
| gap: 0.5rem; |
| max-width: 350px; |
| } |
| |
| .toast { |
| background-color: var(--toast-bg); |
| color: var(--text-primary); |
| border-radius: 0.5rem; |
| padding: 1rem; |
| box-shadow: 0 4px 6px var(--shadow-color); |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| animation: slideInRight 0.3s, fadeOut 0.3s 2.7s; |
| backdrop-filter: var(--backdrop-blur); |
| border-left: 4px solid var(--primary-color); |
| } |
| |
| .toast.success { |
| border-left-color: var(--success-bg); |
| } |
| |
| .toast.error { |
| border-left-color: var(--error-bg); |
| } |
| |
| .toast.warning { |
| border-left-color: var(--warning-bg); |
| } |
| |
| .toast-content { |
| flex: 1; |
| } |
| |
| .toast-title { |
| font-weight: 600; |
| margin-bottom: 0.125rem; |
| } |
| |
| .toast-message { |
| font-size: 0.875rem; |
| color: var(--text-secondary); |
| } |
| |
| .toast-icon { |
| font-size: 1.25rem; |
| } |
| |
| .toast-close { |
| color: var(--text-muted); |
| cursor: pointer; |
| font-size: 1.25rem; |
| padding: 0.125rem; |
| } |
| |
| .toast-close:hover { |
| color: var(--text-primary); |
| } |
| |
| .conversation-list { |
| max-height: calc(100vh - 200px); |
| overflow-y: auto; |
| } |
| |
| .conversation-item { |
| display: flex; |
| align-items: center; |
| padding: 0.75rem 1rem; |
| border-radius: 0.375rem; |
| margin-bottom: 0.5rem; |
| cursor: pointer; |
| color: var(--text-secondary); |
| transition: all 0.2s ease; |
| } |
| |
| .conversation-item:hover { |
| background-color: var(--hover-bg); |
| color: var(--text-primary); |
| } |
| |
| .conversation-item.active { |
| background-color: var(--card-light); |
| color: var(--primary-light); |
| font-weight: 500; |
| } |
| |
| .conversation-icon { |
| margin-right: 0.75rem; |
| color: var(--text-muted); |
| } |
| |
| .conversation-title { |
| flex: 1; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .conversation-time { |
| font-size: 0.75rem; |
| color: var(--text-muted); |
| } |
| |
| .modal-backdrop { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: rgba(0, 0, 0, 0.5); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| z-index: 50; |
| backdrop-filter: var(--backdrop-blur); |
| transition: all 0.3s ease; |
| } |
| |
| .modal { |
| background-color: var(--bg-card); |
| border-radius: 0.75rem; |
| width: 90%; |
| max-width: 500px; |
| max-height: 90vh; |
| overflow-y: auto; |
| box-shadow: 0 10px 25px var(--shadow-color); |
| animation: fadeInUp 0.3s ease; |
| } |
| |
| .modal-header { |
| padding: 1.25rem; |
| border-bottom: 1px solid var(--border-color); |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| } |
| |
| .modal-title { |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: var(--text-primary); |
| } |
| |
| .modal-body { |
| padding: 1.25rem; |
| } |
| |
| .modal-footer { |
| padding: 1.25rem; |
| border-top: 1px solid var(--border-color); |
| display: flex; |
| justify-content: flex-end; |
| gap: 0.75rem; |
| } |
| |
| .modal-close { |
| color: var(--text-muted); |
| cursor: pointer; |
| font-size: 1.25rem; |
| } |
| |
| .modal-close:hover { |
| color: var(--text-primary); |
| } |
| |
| /* Custom scrollbar for chat */ |
| .chat-body::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .chat-body::-webkit-scrollbar-track { |
| background: transparent; |
| } |
| |
| .chat-body::-webkit-scrollbar-thumb { |
| background-color: var(--border-color); |
| border-radius: 3px; |
| } |
| |
| .markdown-content { |
| color: inherit; |
| line-height: 1.5; |
| } |
| |
| .markdown-content h1, |
| .markdown-content h2, |
| .markdown-content h3, |
| .markdown-content h4, |
| .markdown-content h5, |
| .markdown-content h6 { |
| margin-top: 1.5em; |
| margin-bottom: 0.5em; |
| font-weight: 600; |
| line-height: 1.25; |
| color: var(--text-primary); |
| } |
| |
| .markdown-content h1 { font-size: 1.5em; } |
| .markdown-content h2 { font-size: 1.3em; } |
| .markdown-content h3 { font-size: 1.1em; } |
| .markdown-content h4 { font-size: 1em; } |
| |
| .markdown-content p { |
| margin: 0.8em 0; |
| } |
| |
| .markdown-content a { |
| color: var(--primary-light); |
| text-decoration: none; |
| } |
| |
| .markdown-content a:hover { |
| text-decoration: underline; |
| } |
| |
| .markdown-content code { |
| background-color: rgba(0, 0, 0, 0.1); |
| padding: 0.2em 0.4em; |
| border-radius: 3px; |
| font-family: monospace; |
| font-size: 0.9em; |
| } |
| |
| .markdown-content pre { |
| background-color: rgba(0, 0, 0, 0.15); |
| padding: 0.8em; |
| border-radius: 5px; |
| overflow-x: auto; |
| margin: 1em 0; |
| } |
| |
| .markdown-content pre code { |
| background-color: transparent; |
| padding: 0; |
| } |
| |
| .markdown-content ul, .markdown-content ol { |
| margin: 0.8em 0; |
| padding-left: 2em; |
| } |
| |
| .markdown-content li { |
| margin: 0.3em 0; |
| } |
| |
| .markdown-content blockquote { |
| border-left: 3px solid var(--text-muted); |
| padding-left: 1em; |
| margin: 1em 0; |
| color: var(--text-secondary); |
| } |
| |
| .markdown-content table { |
| border-collapse: collapse; |
| width: 100%; |
| margin: 1em 0; |
| } |
| |
| .markdown-content th, .markdown-content td { |
| border: 1px solid var(--border-color); |
| padding: 0.5em; |
| text-align: left; |
| } |
| |
| .markdown-content th { |
| background-color: rgba(0, 0, 0, 0.05); |
| } |
| |
| .spinner { |
| border: 2px solid rgba(255, 255, 255, 0.1); |
| border-radius: 50%; |
| border-top: 2px solid var(--primary-light); |
| width: 18px; |
| height: 18px; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| |
| @keyframes fadeInUp { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes slideInRight { |
| from { |
| transform: translateX(100%); |
| opacity: 0; |
| } |
| to { |
| transform: translateX(0); |
| opacity: 1; |
| } |
| } |
| |
| @keyframes fadeOut { |
| from { opacity: 1; } |
| to { opacity: 0; } |
| } |
| |
| /* Responsive design */ |
| @media (max-width: 1024px) { |
| .dashboard-container { |
| grid-template-columns: 220px 1fr; |
| } |
| |
| .info-grid, .model-grid { |
| grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .dashboard-container { |
| grid-template-columns: 1fr; |
| } |
| |
| .sidebar { |
| position: fixed; |
| top: 0; |
| left: 0; |
| bottom: 0; |
| width: 260px; |
| z-index: 30; |
| transform: translateX(-100%); |
| } |
| |
| .sidebar.open { |
| transform: translateX(0); |
| } |
| |
| .mobile-menu-btn { |
| display: flex; |
| position: fixed; |
| top: 1rem; |
| left: 1rem; |
| z-index: 40; |
| background-color: var(--bg-card); |
| border-radius: 0.5rem; |
| box-shadow: 0 2px 5px var(--shadow-color); |
| } |
| |
| .main-content { |
| padding-top: 4rem; |
| } |
| } |
| |
| @media (max-width: 640px) { |
| .info-grid, .model-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .message { |
| max-width: 90%; |
| } |
| |
| .toast-container { |
| left: 1rem; |
| right: 1rem; |
| max-width: unset; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="toast-container" id="toast-container"></div> |
| |
| <button class="mobile-menu-btn" id="mobile-menu-btn"> |
| <i class="fas fa-bars"></i> |
| </button> |
| |
| <div class="dashboard-container"> |
| <aside class="sidebar" id="sidebar"> |
| <div class="logo"> |
| <i class="fas fa-robot"></i> |
| <span>AI Dashboard</span> |
| </div> |
| |
| <nav class="mt-6"> |
| <div class="nav-section">Main</div> |
| <div class="nav-item active" data-section="dashboard"> |
| <i class="fas fa-chart-line"></i> |
| <span>Dashboard</span> |
| </div> |
| <div class="nav-item" data-section="chat"> |
| <i class="fas fa-comments"></i> |
| <span>Chat</span> |
| </div> |
| <div class="nav-item" data-section="models"> |
| <i class="fas fa-cube"></i> |
| <span>Models</span> |
| </div> |
| |
| <div class="nav-section mt-6">History</div> |
| <div class="nav-item" data-section="history"> |
| <i class="fas fa-history"></i> |
| <span>Conversations</span> |
| </div> |
| |
| <div class="nav-section mt-6">System</div> |
| <div class="nav-item" data-section="settings"> |
| <i class="fas fa-cog"></i> |
| <span>Settings</span> |
| </div> |
| <div class="nav-item" data-section="help"> |
| <i class="fas fa-question-circle"></i> |
| <span>Help</span> |
| <span class="beta-tag">Beta</span> |
| </div> |
| </nav> |
| |
| <div class="mt-auto p-4 text-sm flex flex-col gap-3"> |
| <button id="theme-toggle" class="btn outline small w-full"> |
| <i class="fas fa-moon"></i> |
| <span>Toggle Dark Mode</span> |
| </button> |
| |
| <div class="connection-status"> |
| <div class="status-indicator" id="connection-indicator"></div> |
| <span id="connection-status">Connected</span> |
| </div> |
| </div> |
| </aside> |
| |
| <main class="main-content"> |
| <!-- Dashboard Section --> |
| <section id="section-dashboard" class="content-section active"> |
| <h1 class="text-2xl font-bold mb-6">Dashboard Overview</h1> |
| |
| <div class="info-grid"> |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-server"></i> |
| <span>API Status</span> |
| </div> |
| <div class="status-badge" id="api-status"> |
| <i class="fas fa-circle-notch fa-spin"></i> |
| <span>Checking...</span> |
| </div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-globe"></i> |
| <span>Target Service</span> |
| </div> |
| <div class="info-value" id="target-service">${TARGET_URL}${API_PATH}</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-shield-alt"></i> |
| <span>Proxy Status</span> |
| </div> |
| <div class="info-value" id="proxy-status">${proxyPool.length > 0 ? `Enabled (${proxyPool.length} proxies)` : 'Disabled'}</div> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-tachometer-alt"></i> |
| <span>Performance</span> |
| </div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-clock"></i> |
| <span>Request Timeout</span> |
| </div> |
| <div class="info-value">${TIMEOUT}ms</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-network-wired"></i> |
| <span>Service Port</span> |
| </div> |
| <div class="info-value">${PORT}</div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-link"></i> |
| <span>API Endpoint</span> |
| </div> |
| </div> |
| <p class="text-sm text-gray-400 mb-2">Use this endpoint in your applications to connect to the AI models:</p> |
| <div class="endpoint-box"> |
| <div class="endpoint-url" id="endpoint-url"></div> |
| <div class="flex gap-2 mt-2"> |
| <button class="copy-btn" id="copy-endpoint"> |
| <i class="fas fa-copy"></i> Copy to clipboard |
| </button> |
| <button class="btn outline" id="test-connection"> |
| <i class="fas fa-plug"></i> Test Connection |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-cube"></i> |
| <span>Popular Models</span> |
| </div> |
| <a href="#" class="text-primary-light hover:underline text-sm" data-section="models">View All</a> |
| </div> |
| <div id="popular-models" class="model-grid mt-4"> |
| <div class="flex justify-center items-center p-4"> |
| <div class="spinner"></div> |
| <span class="ml-2">Loading models...</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-chart-bar"></i> |
| <span>Recent Activity</span> |
| </div> |
| </div> |
| <div id="recent-activity" class="mt-2"> |
| <div class="text-center text-gray-400 py-4"> |
| <i class="fas fa-comment-slash text-3xl mb-2"></i> |
| <p>No recent conversations.</p> |
| <p class="text-sm mt-1">Start chatting to see your activity here.</p> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- Chat Section --> |
| <section id="section-chat" class="content-section hidden"> |
| <div class="chat-container"> |
| <div class="chat-header"> |
| <div class="model-select-container"> |
| <label class="model-label">Model:</label> |
| <select id="chat-model-select" class="model-select"> |
| <option value="">Select a model</option> |
| </select> |
| </div> |
| <div class="chat-options"> |
| <div class="parameter-control"> |
| <div class="parameter-label"> |
| <span class="parameter-name">Temperature</span> |
| <span class="parameter-value" id="temp-value">0.7</span> |
| </div> |
| <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7"> |
| </div> |
| |
| <div class="button-group"> |
| <button class="btn" id="new-chat-btn"> |
| <i class="fas fa-plus"></i> |
| <span>New Chat</span> |
| </button> |
| <button class="btn outline" id="export-chat-btn"> |
| <i class="fas fa-download"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| <div class="chat-body" id="chat-body"> |
| <div class="system-message info"> |
| <i class="fas fa-info-circle"></i> |
| <span>This is a new conversation. Select a model and start chatting!</span> |
| </div> |
| <div class="message-list" id="message-list"></div> |
| </div> |
| <div class="chat-input"> |
| <div class="input-container"> |
| <textarea |
| id="message-input" |
| class="message-input" |
| placeholder="Type your message here..." |
| rows="1"></textarea> |
| <button id="send-message-btn" class="send-btn" disabled> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| <div class="text-right text-xs text-gray-400 mt-1 mr-2"> |
| Press Enter to send, Shift+Enter for new line |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- Models Section --> |
| <section id="section-models" class="content-section hidden"> |
| <h1 class="text-2xl font-bold mb-4">Available Models</h1> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-search"></i> |
| <span>Model Library</span> |
| </div> |
| <div class="flex gap-2"> |
| <input |
| type="text" |
| id="model-search" |
| placeholder="Search models..." |
| class="px-3 py-1 bg-input-bg text-text-primary border border-border-color rounded-md w-64 text-sm"> |
| <button class="btn outline small" id="refresh-models-btn"> |
| <i class="fas fa-sync-alt"></i> |
| </button> |
| </div> |
| </div> |
| <div id="all-models" class="model-grid mt-4"> |
| <div class="flex justify-center items-center p-4"> |
| <div class="spinner"></div> |
| <span class="ml-2">Loading models...</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-info-circle"></i> |
| <span>Model Information</span> |
| </div> |
| </div> |
| <div id="model-info-content" class="mt-2"> |
| <div class="text-center text-gray-400 py-4"> |
| <i class="fas fa-cube text-3xl mb-2"></i> |
| <p>Select a model to view information.</p> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- Conversation History Section --> |
| <section id="section-history" class="content-section hidden"> |
| <h1 class="text-2xl font-bold mb-4">Conversation History</h1> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-history"></i> |
| <span>Recent Conversations</span> |
| </div> |
| <button class="btn outline small" id="clear-history-btn"> |
| <i class="fas fa-trash"></i> |
| <span>Clear All</span> |
| </button> |
| </div> |
| <div id="conversations-list" class="conversation-list mt-2"> |
| <div class="text-center text-gray-400 py-4"> |
| <i class="fas fa-comment-slash text-3xl mb-2"></i> |
| <p>No conversations yet.</p> |
| <p class="text-sm mt-1">Your chat history will appear here.</p> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- Settings Section --> |
| <section id="section-settings" class="content-section hidden"> |
| <h1 class="text-2xl font-bold mb-4">Settings</h1> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-sliders-h"></i> |
| <span>Appearance</span> |
| </div> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">Dark Mode</div> |
| <div class="settings-description">Toggle between light and dark theme.</div> |
| </div> |
| <div class="settings-control"> |
| <label class="toggle"> |
| <input type="checkbox" id="dark-mode-toggle" checked> |
| <span class="toggle-slider"></span> |
| </label> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-robot"></i> |
| <span>AI Settings</span> |
| </div> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">Default Model</div> |
| <div class="settings-description">Choose your preferred model for new chats.</div> |
| </div> |
| <div class="settings-control"> |
| <select id="default-model" class="model-select w-full"> |
| <option value="">Loading models...</option> |
| </select> |
| </div> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">Default Parameters</div> |
| <div class="settings-description">Set default generation parameters.</div> |
| </div> |
| <div class="settings-control"> |
| <a href="#" class="text-primary-light" id="edit-params-btn">Edit Parameters</a> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-wrench"></i> |
| <span>Connection Settings</span> |
| </div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-globe"></i> |
| <span>Target URL</span> |
| </div> |
| <div class="info-value">${TARGET_URL}</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-sitemap"></i> |
| <span>API Path</span> |
| </div> |
| <div class="info-value">${API_PATH}</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-network-wired"></i> |
| <span>Server Port</span> |
| </div> |
| <div class="info-value">${PORT}</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-clock"></i> |
| <span>Timeout Setting</span> |
| </div> |
| <div class="info-value">${TIMEOUT}ms</div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-shield-alt"></i> |
| <span>Proxy Configuration</span> |
| </div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-toggle-on"></i> |
| <span>Proxy Status</span> |
| </div> |
| <div class="info-value">${proxyPool.length > 0 ? 'Enabled' : 'Disabled'}</div> |
| </div> |
| <div class="info-item"> |
| <div class="info-label"> |
| <i class="fas fa-server"></i> |
| <span>Active Proxies</span> |
| </div> |
| <div class="info-value">${proxyPool.length}</div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- Help Section --> |
| <section id="section-help" class="content-section hidden"> |
| <h1 class="text-2xl font-bold mb-4">Help & Documentation</h1> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-book"></i> |
| <span>Getting Started</span> |
| </div> |
| </div> |
| <div class="mt-2"> |
| <h3 class="text-xl font-semibold mb-2">Welcome to the AI Dashboard</h3> |
| <p class="mb-4">This dashboard allows you to interact with various AI models through a simple interface.</p> |
| |
| <h4 class="text-lg font-semibold mb-2">Quick Start Guide</h4> |
| <ol class="list-decimal pl-6 mb-4 space-y-2"> |
| <li>Go to the <strong>Chat</strong> section using the sidebar navigation</li> |
| <li>Select a model from the dropdown menu</li> |
| <li>Type your message in the input field</li> |
| <li>Press Enter or click the send button</li> |
| <li>View the AI's response in the chat window</li> |
| </ol> |
| |
| <h4 class="text-lg font-semibold mb-2">API Usage</h4> |
| <p class="mb-2">To use the API in your applications:</p> |
| <div class="bg-input-bg p-3 rounded-md mb-4"> |
| <code>POST https://vidbye-cursor-ai.hf.space/hf/v1/chat/completions</code> |
| <pre class="mt-2">{ |
| "model": "gpt-4o", |
| "messages": [ |
| {"role": "user", "content": "Hello, how are you?"} |
| ], |
| "temperature": 0.7 |
| }</pre> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card mt-6"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <i class="fas fa-question-circle"></i> |
| <span>FAQ</span> |
| </div> |
| </div> |
| <div class="mt-2 space-y-4"> |
| <div> |
| <h4 class="font-semibold">What models are available?</h4> |
| <p class="text-gray-400">The dashboard supports various OpenAI and Anthropic models including GPT-4o, Claude, and more.</p> |
| </div> |
| <div> |
| <h4 class="font-semibold">How do I save my conversations?</h4> |
| <p class="text-gray-400">Conversations are automatically saved in your browser's local storage. You can also export them using the download button in the chat interface.</p> |
| </div> |
| <div> |
| <h4 class="font-semibold">What if I encounter an error?</h4> |
| <p class="text-gray-400">Check your connection settings and make sure the target service is available. Most errors will be displayed with helpful messages.</p> |
| </div> |
| <div> |
| <h4 class="font-semibold">Can I customize the model parameters?</h4> |
| <p class="text-gray-400">Yes, you can adjust temperature and other parameters in the chat interface or set defaults in the Settings section.</p> |
| </div> |
| </div> |
| </div> |
| </section> |
| </main> |
| </div> |
| |
| <!-- Parameters Modal (Hidden by default) --> |
| <div id="params-modal" class="modal-backdrop hidden"> |
| <div class="modal"> |
| <div class="modal-header"> |
| <h3 class="modal-title">Default Model Parameters</h3> |
| <div class="modal-close" id="close-params-modal"> |
| <i class="fas fa-times"></i> |
| </div> |
| </div> |
| <div class="modal-body"> |
| <div class="parameter-row"> |
| <div class="parameter-control"> |
| <div class="parameter-label"> |
| <span class="parameter-name">Temperature</span> |
| <span class="parameter-value" id="modal-temp-value">0.7</span> |
| </div> |
| <input type="range" id="modal-temperature" min="0" max="2" step="0.1" value="0.7"> |
| <p class="mt-1 text-xs text-gray-400">Controls randomness: Lower is more deterministic, higher is more creative.</p> |
| </div> |
| </div> |
| |
| <div class="parameter-row"> |
| <div class="parameter-control"> |
| <div class="parameter-label"> |
| <span class="parameter-name">Top P</span> |
| <span class="parameter-value" id="modal-top-p-value">1.0</span> |
| </div> |
| <input type="range" id="modal-top-p" min="0" max="1" step="0.05" value="1.0"> |
| <p class="mt-1 text-xs text-gray-400">Alternative to temperature, controls diversity of responses.</p> |
| </div> |
| </div> |
| |
| <div class="parameter-row"> |
| <div class="parameter-control"> |
| <div class="parameter-label"> |
| <span class="parameter-name">Max Tokens</span> |
| <span class="parameter-value" id="modal-max-tokens-value">2048</span> |
| </div> |
| <input type="range" id="modal-max-tokens" min="256" max="8192" step="256" value="2048"> |
| <p class="mt-1 text-xs text-gray-400">Maximum length of response.</p> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn outline" id="reset-params-btn">Reset to Defaults</button> |
| <button class="btn" id="save-params-btn">Save Changes</button> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| // Declare global variables |
| let chatHistory = []; |
| let conversations = []; |
| let selectedModel = ''; |
| let darkMode = true; |
| let connectionState = { |
| connected: true, |
| status: 'Connected', |
| }; |
| |
| // Model parameters |
| let modelParams = { |
| temperature: 0.7, |
| top_p: 1.0, |
| max_tokens: 2048 |
| }; |
| |
| // Initialize the dashboard |
| document.addEventListener('DOMContentLoaded', function() { |
| // Load saved settings |
| loadSettings(); |
| |
| // Set up mobile menu |
| const mobileMenuBtn = document.getElementById('mobile-menu-btn'); |
| const sidebar = document.getElementById('sidebar'); |
| |
| mobileMenuBtn.addEventListener('click', () => { |
| sidebar.classList.toggle('open'); |
| }); |
| |
| // Handle navigation |
| const navItems = document.querySelectorAll('.nav-item'); |
| const sections = document.querySelectorAll('.content-section'); |
| |
| navItems.forEach(item => { |
| item.addEventListener('click', () => { |
| const sectionId = item.getAttribute('data-section'); |
| |
| // Update active nav item |
| navItems.forEach(navItem => navItem.classList.remove('active')); |
| item.classList.add('active'); |
| |
| // Show selected section |
| sections.forEach(section => { |
| section.classList.add('hidden'); |
| section.classList.remove('active'); |
| }); |
| |
| const selectedSection = document.getElementById('section-' + sectionId); |
| selectedSection.classList.remove('hidden'); |
| selectedSection.classList.add('active'); |
| |
| // Close mobile menu after selection |
| sidebar.classList.remove('open'); |
| }); |
| }); |
| |
| // Section links |
| document.querySelectorAll('[data-section]').forEach(link => { |
| if (!link.classList.contains('nav-item')) { |
| link.addEventListener('click', (e) => { |
| e.preventDefault(); |
| const sectionId = link.getAttribute('data-section'); |
| document.querySelector('.nav-item[data-section="' + sectionId + '"]').click(); |
| }); |
| } |
| }); |
| |
| // Set endpoint URL |
| const endpointUrl = "https://vidbye-cursor-ai.hf.space/hf/v1"; |
| document.getElementById('endpoint-url').textContent = endpointUrl; |
| |
| // Copy endpoint button |
| document.getElementById('copy-endpoint').addEventListener('click', () => { |
| navigator.clipboard.writeText(endpointUrl).then(() => { |
| showToast('Copied!', 'Endpoint URL copied to clipboard', 'success'); |
| }).catch(() => { |
| showToast('Error', 'Failed to copy to clipboard', 'error'); |
| }); |
| }); |
| |
| // Test connection button |
| document.getElementById('test-connection').addEventListener('click', async () => { |
| await checkConnectionStatus(true); |
| }); |
| |
| // Initialize chat |
| initChat(); |
| |
| // Initialize conversation history |
| loadConversations(); |
| |
| // Fetch and display models |
| fetchModels(); |
| |
| // Check connection status |
| checkConnectionStatus(); |
| |
| // Set up theme toggles |
| document.getElementById('theme-toggle').addEventListener('click', toggleDarkMode); |
| document.getElementById('dark-mode-toggle').addEventListener('change', function() { |
| toggleDarkMode(this.checked); |
| }); |
| |
| // Set up parameters modal |
| document.getElementById('edit-params-btn').addEventListener('click', (e) => { |
| e.preventDefault(); |
| document.getElementById('params-modal').classList.remove('hidden'); |
| updateModalValues(); |
| }); |
| |
| document.getElementById('close-params-modal').addEventListener('click', () => { |
| document.getElementById('params-modal').classList.add('hidden'); |
| }); |
| |
| document.getElementById('modal-temperature').addEventListener('input', (e) => { |
| document.getElementById('modal-temp-value').textContent = e.target.value; |
| }); |
| |
| document.getElementById('modal-top-p').addEventListener('input', (e) => { |
| document.getElementById('modal-top-p-value').textContent = e.target.value; |
| }); |
| |
| document.getElementById('modal-max-tokens').addEventListener('input', (e) => { |
| document.getElementById('modal-max-tokens-value').textContent = e.target.value; |
| }); |
| |
| document.getElementById('reset-params-btn').addEventListener('click', () => { |
| resetParameters(); |
| updateModalValues(); |
| }); |
| |
| document.getElementById('save-params-btn').addEventListener('click', () => { |
| saveParameters(); |
| document.getElementById('params-modal').classList.add('hidden'); |
| showToast('Saved', 'Default parameters updated', 'success'); |
| }); |
| |
| // Set up refresh models button |
| document.getElementById('refresh-models-btn').addEventListener('click', fetchModels); |
| |
| // Set up clear history button |
| document.getElementById('clear-history-btn').addEventListener('click', () => { |
| clearHistory(); |
| }); |
| |
| // Set up export chat button |
| document.getElementById('export-chat-btn').addEventListener('click', exportChat); |
| |
| // Initialize tooltips |
| tippy('[data-tippy-content]'); |
| }); |
| |
| // Toggle dark mode |
| function toggleDarkMode(forceDark) { |
| if (typeof forceDark === 'boolean') { |
| darkMode = forceDark; |
| } else { |
| darkMode = !darkMode; |
| } |
| |
| if (darkMode) { |
| document.body.classList.remove('light-theme'); |
| document.getElementById('theme-toggle').innerHTML = '<i class="fas fa-sun"></i><span>Toggle Light Mode</span>'; |
| } else { |
| document.body.classList.add('light-theme'); |
| document.getElementById('theme-toggle').innerHTML = '<i class="fas fa-moon"></i><span>Toggle Dark Mode</span>'; |
| } |
| |
| document.getElementById('dark-mode-toggle').checked = darkMode; |
| |
| // Save setting |
| localStorage.setItem('dark-mode', darkMode); |
| } |
| |
| // Load saved settings |
| function loadSettings() { |
| // Load dark mode setting |
| const savedDarkMode = localStorage.getItem('dark-mode'); |
| if (savedDarkMode !== null) { |
| toggleDarkMode(savedDarkMode === 'true'); |
| } else { |
| // Default to user's system preference |
| const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; |
| toggleDarkMode(prefersDark); |
| } |
| |
| // Load saved parameters |
| const savedParams = localStorage.getItem('model-params'); |
| if (savedParams) { |
| try { |
| modelParams = JSON.parse(savedParams); |
| } catch (e) { |
| console.error('Error parsing saved parameters:', e); |
| } |
| } |
| |
| // Load default model |
| const defaultModel = localStorage.getItem('default-model'); |
| if (defaultModel) { |
| selectedModel = defaultModel; |
| } |
| } |
| |
| // Update modal values |
| function updateModalValues() { |
| document.getElementById('modal-temperature').value = modelParams.temperature; |
| document.getElementById('modal-temp-value').textContent = modelParams.temperature; |
| |
| document.getElementById('modal-top-p').value = modelParams.top_p; |
| document.getElementById('modal-top-p-value').textContent = modelParams.top_p; |
| |
| document.getElementById('modal-max-tokens').value = modelParams.max_tokens; |
| document.getElementById('modal-max-tokens-value').textContent = modelParams.max_tokens; |
| } |
| |
| // Reset parameters to defaults |
| function resetParameters() { |
| modelParams = { |
| temperature: 0.7, |
| top_p: 1.0, |
| max_tokens: 2048 |
| }; |
| } |
| |
| // Save parameters |
| function saveParameters() { |
| modelParams.temperature = parseFloat(document.getElementById('modal-temperature').value); |
| modelParams.top_p = parseFloat(document.getElementById('modal-top-p').value); |
| modelParams.max_tokens = parseInt(document.getElementById('modal-max-tokens').value); |
| |
| // Save to localStorage |
| localStorage.setItem('model-params', JSON.stringify(modelParams)); |
| |
| // Update UI |
| document.getElementById('temp-value').textContent = modelParams.temperature; |
| document.getElementById('temperature').value = modelParams.temperature; |
| } |
| |
| // Initialize chat functionality |
| function initChat() { |
| const messageInput = document.getElementById('message-input'); |
| const sendBtn = document.getElementById('send-message-btn'); |
| const messageList = document.getElementById('message-list'); |
| const modelSelect = document.getElementById('chat-model-select'); |
| const newChatBtn = document.getElementById('new-chat-btn'); |
| const tempSlider = document.getElementById('temperature'); |
| const tempValue = document.getElementById('temp-value'); |
| |
| // Set initial temperature |
| tempSlider.value = modelParams.temperature; |
| tempValue.textContent = modelParams.temperature; |
| |
| // Auto-resize textarea |
| messageInput.addEventListener('input', () => { |
| messageInput.style.height = 'auto'; |
| messageInput.style.height = (messageInput.scrollHeight) + 'px'; |
| |
| // Enable/disable send button based on input and model selection |
| sendBtn.disabled = !messageInput.value.trim() || !modelSelect.value; |
| }); |
| |
| // Update temperature |
| tempSlider.addEventListener('input', () => { |
| tempValue.textContent = tempSlider.value; |
| }); |
| |
| // Enable/disable send button based on model selection |
| modelSelect.addEventListener('change', () => { |
| selectedModel = modelSelect.value; |
| sendBtn.disabled = !messageInput.value.trim() || !selectedModel; |
| |
| // Save default model |
| if (selectedModel) { |
| localStorage.setItem('default-model', selectedModel); |
| |
| // Update select in settings |
| const defaultModelSelect = document.getElementById('default-model'); |
| if (defaultModelSelect) { |
| defaultModelSelect.value = selectedModel; |
| } |
| } |
| }); |
| |
| // Send message |
| sendBtn.addEventListener('click', sendMessage); |
| |
| // Send message with Enter (but Shift+Enter for new line) |
| messageInput.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| if (!sendBtn.disabled) { |
| sendMessage(); |
| } |
| } |
| }); |
| |
| // New chat button |
| newChatBtn.addEventListener('click', () => { |
| if (chatHistory.length > 0) { |
| // Save current conversation if not empty |
| saveConversation(); |
| } |
| |
| // Clear chat history and UI |
| chatHistory = []; |
| messageList.innerHTML = ''; |
| const systemMessage = document.createElement('div'); |
| systemMessage.className = 'system-message info'; |
| systemMessage.innerHTML = '<i class="fas fa-info-circle"></i><span>This is a new conversation. Select a model and start chatting!</span>'; |
| messageList.appendChild(systemMessage); |
| |
| // Update recent activity |
| updateRecentActivity(); |
| }); |
| } |
| |
| // Send chat message |
| async function sendMessage() { |
| const messageInput = document.getElementById('message-input'); |
| const sendBtn = document.getElementById('send-message-btn'); |
| const messageList = document.getElementById('message-list'); |
| const modelSelect = document.getElementById('chat-model-select'); |
| const temperature = document.getElementById('temperature').value; |
| |
| // Get message content |
| const messageText = messageInput.value.trim(); |
| if (!messageText || !modelSelect.value) return; |
| |
| // Disable input during sending |
| messageInput.disabled = true; |
| sendBtn.disabled = true; |
| |
| // Add user message to chat |
| addMessageToChat('user', messageText); |
| |
| // Clear input |
| messageInput.value = ''; |
| messageInput.style.height = 'auto'; |
| |
| // Prepare payload |
| const messages = [ |
| ...chatHistory.map(msg => ({ |
| role: msg.role, |
| content: msg.content |
| })), |
| { role: 'user', content: messageText } |
| ]; |
| |
| // Add user message to history |
| chatHistory.push({ |
| role: 'user', |
| content: messageText, |
| timestamp: new Date().toISOString() |
| }); |
| |
| // Show loading indicator for bot response |
| const loadingMessageId = 'msg-loading-' + Date.now(); |
| const loadingHTML = \` |
| <div class="message bot" id="\${loadingMessageId}"> |
| <div class="message-bubble"> |
| <div class="flex items-center"> |
| <div class="spinner"></div> |
| <span class="ml-2">Thinking...</span> |
| </div> |
| </div> |
| </div> |
| \`; |
| messageList.insertAdjacentHTML('beforeend', loadingHTML); |
| messageList.scrollTop = messageList.scrollHeight; |
| |
| try { |
| // Send request to API |
| const endpoint = "https://vidbye-cursor-ai.hf.space/hf/v1/chat/completions"; |
| |
| const payload = { |
| model: modelSelect.value, |
| messages: messages, |
| temperature: parseFloat(temperature), |
| max_tokens: modelParams.max_tokens, |
| top_p: modelParams.top_p, |
| stream: false |
| }; |
| |
| const response = await fetch(endpoint, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(payload) |
| }); |
| |
| // Remove loading message |
| const loadingElement = document.getElementById(loadingMessageId); |
| if (loadingElement) { |
| loadingElement.remove(); |
| } |
| |
| if (response.ok) { |
| // Parse response as JSON - with error handling |
| let data; |
| try { |
| data = await response.json(); |
| } catch (error) { |
| // If response is not valid JSON |
| const text = await response.text(); |
| throw new Error(\`Invalid JSON response: \${text.substring(0, 100)}...\`); |
| } |
| |
| if (data.choices && data.choices.length > 0) { |
| const botResponse = data.choices[0].message.content; |
| |
| // Add bot message to chat |
| addMessageToChat('bot', botResponse); |
| |
| // Add to history |
| chatHistory.push({ |
| role: 'assistant', |
| content: botResponse, |
| timestamp: new Date().toISOString() |
| }); |
| |
| // Update recent activity |
| updateRecentActivity(); |
| |
| // Update connection status |
| updateConnectionStatus(true, 'Connected'); |
| } else { |
| // Handle empty response |
| addSystemMessage('Received an empty response from the model.', 'warning'); |
| } |
| } else { |
| // Handle error response |
| let errorMessage; |
| try { |
| const errorData = await response.json(); |
| errorMessage = errorData.error?.message || 'An error occurred while communicating with the API.'; |
| } catch (e) { |
| // If the error response is not valid JSON |
| const text = await response.text(); |
| errorMessage = \`Error (\${response.status}): \${text.substring(0, 100)}\`; |
| } |
| |
| addSystemMessage('Error: ' + errorMessage, 'error'); |
| |
| // Update connection status |
| updateConnectionStatus(false, 'Connection Error'); |
| } |
| } catch (error) { |
| // Remove loading message |
| const loadingElement = document.getElementById(loadingMessageId); |
| if (loadingElement) { |
| loadingElement.remove(); |
| } |
| |
| // Handle error |
| console.error('Error sending message:', error); |
| addSystemMessage('Error: ' + (error.message || 'Failed to send message'), 'error'); |
| |
| // Update connection status |
| updateConnectionStatus(false, 'Connection Error'); |
| } finally { |
| // Re-enable input |
| messageInput.disabled = false; |
| sendBtn.disabled = !modelSelect.value; |
| messageList.scrollTop = messageList.scrollHeight; |
| |
| // Focus back on input |
| messageInput.focus(); |
| } |
| } |
| |
| // Add message to chat |
| function addMessageToChat(role, content) { |
| const messageList = document.getElementById('message-list'); |
| const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
| |
| // Create message element |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = \`message \${role}\`; |
| |
| // Create message bubble |
| const bubbleDiv = document.createElement('div'); |
| bubbleDiv.className = 'message-bubble'; |
| |
| // Create actions |
| const actionsDiv = document.createElement('div'); |
| actionsDiv.className = 'message-actions'; |
| actionsDiv.innerHTML = \` |
| <button class="message-action-btn" title="Copy to clipboard"> |
| <i class="fas fa-copy"></i> |
| </button> |
| \`; |
| |
| // Process markdown for bot messages |
| if (role === 'bot') { |
| // Sanitize and render markdown |
| const sanitizedContent = DOMPurify.sanitize(marked.parse(content)); |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'markdown-content'; |
| contentDiv.innerHTML = sanitizedContent; |
| bubbleDiv.appendChild(contentDiv); |
| } else { |
| // User messages - just escape HTML |
| bubbleDiv.textContent = content; |
| } |
| |
| // Add time |
| const timeDiv = document.createElement('div'); |
| timeDiv.className = 'message-time'; |
| timeDiv.textContent = timestamp; |
| bubbleDiv.appendChild(timeDiv); |
| |
| // Add actions to bubble |
| bubbleDiv.appendChild(actionsDiv); |
| |
| // Add copy functionality |
| const copyBtn = actionsDiv.querySelector('.message-action-btn'); |
| copyBtn.addEventListener('click', () => { |
| navigator.clipboard.writeText(content).then(() => { |
| showToast('Copied!', 'Message copied to clipboard', 'success'); |
| }).catch(() => { |
| showToast('Error', 'Failed to copy to clipboard', 'error'); |
| }); |
| }); |
| |
| // Assemble message |
| messageDiv.appendChild(bubbleDiv); |
| messageList.appendChild(messageDiv); |
| |
| // Scroll to bottom |
| messageList.scrollTop = messageList.scrollHeight; |
| } |
| |
| // Add system message |
| function addSystemMessage(message, type = 'info') { |
| const messageList = document.getElementById('message-list'); |
| |
| let icon; |
| switch(type) { |
| case 'error': |
| icon = 'exclamation-circle'; |
| break; |
| case 'warning': |
| icon = 'exclamation-triangle'; |
| break; |
| case 'info': |
| default: |
| icon = 'info-circle'; |
| } |
| |
| const systemHTML = \` |
| <div class="system-message \${type}"> |
| <i class="fas fa-\${icon}"></i> |
| <span>\${message}</span> |
| </div> |
| \`; |
| |
| messageList.insertAdjacentHTML('beforeend', systemHTML); |
| messageList.scrollTop = messageList.scrollHeight; |
| } |
| |
| // Show toast notification |
| function showToast(title, message, type = 'info') { |
| const toastContainer = document.getElementById('toast-container'); |
| |
| let icon; |
| switch(type) { |
| case 'success': |
| icon = 'check-circle'; |
| break; |
| case 'error': |
| icon = 'exclamation-circle'; |
| break; |
| case 'warning': |
| icon = 'exclamation-triangle'; |
| break; |
| case 'info': |
| default: |
| icon = 'info-circle'; |
| } |
| |
| const toastId = 'toast-' + Date.now(); |
| const toastHTML = \` |
| <div id="\${toastId}" class="toast \${type}"> |
| <div class="toast-icon"> |
| <i class="fas fa-\${icon}"></i> |
| </div> |
| <div class="toast-content"> |
| <div class="toast-title">\${title}</div> |
| <div class="toast-message">\${message}</div> |
| </div> |
| <div class="toast-close"> |
| <i class="fas fa-times"></i> |
| </div> |
| </div> |
| \`; |
| |
| toastContainer.insertAdjacentHTML('beforeend', toastHTML); |
| |
| // Add close functionality |
| const toast = document.getElementById(toastId); |
| const closeBtn = toast.querySelector('.toast-close'); |
| closeBtn.addEventListener('click', () => { |
| toast.remove(); |
| }); |
| |
| // Auto remove after 3 seconds |
| setTimeout(() => { |
| if (toast && toast.parentNode) { |
| toast.classList.add('animate__fadeOut'); |
| setTimeout(() => { |
| if (toast && toast.parentNode) { |
| toast.remove(); |
| } |
| }, 300); |
| } |
| }, 3000); |
| } |
| |
| // Fetch models from the API |
| async function fetchModels() { |
| try { |
| // Show loading state |
| const containers = ['popular-models', 'all-models']; |
| containers.forEach(id => { |
| const container = document.getElementById(id); |
| if (container) { |
| container.innerHTML = '<div class="flex justify-center items-center p-4"><div class="spinner"></div><span class="ml-2">Loading models...</span></div>'; |
| } |
| }); |
| |
| document.getElementById('chat-model-select').innerHTML = '<option value="">Loading models...</option>'; |
| document.getElementById('default-model').innerHTML = '<option value="">Loading models...</option>'; |
| |
| const link = "https://vidbye-cursor-ai.hf.space/hf/v1/models"; |
| const response = await fetch(link); |
| |
| if (!response.ok) { |
| throw new Error(\`Failed to fetch models: \${response.status} \${response.statusText}\`); |
| } |
| |
| // Parse response as JSON - with error handling |
| let data; |
| try { |
| data = await response.json(); |
| } catch (error) { |
| // If response is not valid JSON |
| const text = await response.text(); |
| throw new Error(\`Invalid JSON response: \${text.substring(0, 100)}...\`); |
| } |
| |
| const popularModelsContainer = document.getElementById('popular-models'); |
| const allModelsContainer = document.getElementById('all-models'); |
| const modelSelect = document.getElementById('chat-model-select'); |
| const defaultModelSelect = document.getElementById('default-model'); |
| |
| // Clear containers |
| popularModelsContainer.innerHTML = ''; |
| allModelsContainer.innerHTML = ''; |
| modelSelect.innerHTML = '<option value="">Select a model</option>'; |
| defaultModelSelect.innerHTML = '<option value="">Select a default model</option>'; |
| |
| // Categorize models |
| const popularModels = data.data.filter(model => |
| model.id.includes('gpt-4') || |
| model.id.includes('claude-3') || |
| model.id === 'o1' || |
| model.id === 'gemini-1.5-flash-500k' |
| ); |
| |
| // Display popular models (limited to 8) |
| popularModels.slice(0, 8).forEach(model => { |
| const modelItem = createModelItem(model); |
| popularModelsContainer.appendChild(modelItem); |
| }); |
| |
| // Display all models |
| data.data.forEach(model => { |
| const modelItem = createModelItem(model); |
| allModelsContainer.appendChild(modelItem); |
| |
| // Add to select dropdowns |
| const option = document.createElement('option'); |
| option.value = model.id; |
| option.textContent = model.id; |
| modelSelect.appendChild(option.cloneNode(true)); |
| defaultModelSelect.appendChild(option); |
| }); |
| |
| // Set selected model if exists |
| if (selectedModel) { |
| modelSelect.value = selectedModel; |
| defaultModelSelect.value = selectedModel; |
| } |
| |
| // Set up model search |
| const modelSearch = document.getElementById('model-search'); |
| modelSearch.addEventListener('input', (e) => { |
| const searchTerm = e.target.value.toLowerCase().trim(); |
| |
| // Filter models |
| const modelItems = allModelsContainer.querySelectorAll('.model-item'); |
| modelItems.forEach(item => { |
| const modelName = item.querySelector('.model-name').textContent.toLowerCase(); |
| if (searchTerm === '' || modelName.includes(searchTerm)) { |
| item.style.display = 'block'; |
| } else { |
| item.style.display = 'none'; |
| } |
| }); |
| }); |
| |
| // Update connection status on successful fetch |
| updateConnectionStatus(true, 'Connected'); |
| |
| // Show success toast |
| showToast('Models Loaded', \`\${data.data.length} AI models available\`, 'success'); |
| } catch (error) { |
| console.error('Error fetching models:', error); |
| |
| // Update UI for error state |
| const containers = ['popular-models', 'all-models']; |
| containers.forEach(id => { |
| const container = document.getElementById(id); |
| if (container) { |
| container.innerHTML = '<div class="p-4 text-center text-red-400"><i class="fas fa-exclamation-circle mr-2"></i>Failed to load models</div>'; |
| } |
| }); |
| |
| document.getElementById('chat-model-select').innerHTML = '<option value="">Failed to load models</option>'; |
| document.getElementById('default-model').innerHTML = '<option value="">Failed to load models</option>'; |
| |
| // Update connection status |
| updateConnectionStatus(false, 'Connection Error'); |
| |
| // Show error toast |
| showToast('Error', 'Failed to load models: ' + error.message, 'error'); |
| } |
| } |
| |
| // Create model item element |
| function createModelItem(model) { |
| const div = document.createElement('div'); |
| div.className = 'model-item'; |
| div.dataset.model = model.id; |
| div.innerHTML = \` |
| <span class="model-name">\${model.id}</span> |
| <span class="model-provider">\${model.owned_by}</span> |
| \`; |
| |
| // Add selected class if this is the current model |
| if (model.id === selectedModel) { |
| div.classList.add('selected'); |
| } |
| |
| // Click to select model for chat |
| div.addEventListener('click', () => { |
| const chatModelSelect = document.getElementById('chat-model-select'); |
| chatModelSelect.value = model.id; |
| selectedModel = model.id; |
| |
| // Update selected styling |
| document.querySelectorAll('.model-item').forEach(item => { |
| item.classList.remove('selected'); |
| }); |
| div.classList.add('selected'); |
| |
| // Trigger change event |
| const event = new Event('change'); |
| chatModelSelect.dispatchEvent(event); |
| |
| // Update model info |
| updateModelInfo(model); |
| |
| // Navigate to chat section if we're in the models section |
| if (document.getElementById('section-models').classList.contains('active')) { |
| document.querySelector('.nav-item[data-section="chat"]').click(); |
| } |
| }); |
| |
| // Show model info on right-click |
| div.addEventListener('contextmenu', (e) => { |
| e.preventDefault(); |
| updateModelInfo(model); |
| }); |
| |
| return div; |
| } |
| |
| // Update model info panel |
| function updateModelInfo(model) { |
| const modelInfoContent = document.getElementById('model-info-content'); |
| |
| modelInfoContent.innerHTML = \` |
| <div class="p-4"> |
| <h3 class="text-xl font-semibold mb-4">\${model.id}</h3> |
| |
| <div class="info-item"> |
| <div class="info-label">Provider</div> |
| <div class="info-value">\${model.owned_by}</div> |
| </div> |
| |
| <div class="info-item"> |
| <div class="info-label">Created</div> |
| <div class="info-value">\${new Date(model.created * 1000).toLocaleDateString()}</div> |
| </div> |
| |
| <div class="mt-4"> |
| <button class="btn w-full" data-model-id="\${model.id}" id="select-model-btn"> |
| <i class="fas fa-check-circle"></i> |
| Select This Model |
| </button> |
| </div> |
| </div> |
| \`; |
| |
| // Add select button functionality |
| document.getElementById('select-model-btn').addEventListener('click', () => { |
| const chatModelSelect = document.getElementById('chat-model-select'); |
| chatModelSelect.value = model.id; |
| |
| // Trigger change event |
| const event = new Event('change'); |
| chatModelSelect.dispatchEvent(event); |
| |
| // Navigate to chat |
| document.querySelector('.nav-item[data-section="chat"]').click(); |
| }); |
| } |
| |
| // Check and update connection status |
| async function checkConnectionStatus(showFeedback = false) { |
| try { |
| if (showFeedback) { |
| // Update status to checking |
| document.getElementById('api-status').innerHTML = '<i class="fas fa-circle-notch fa-spin"></i><span>Checking...</span>'; |
| } |
| |
| // Attempt to fetch models as a connection test |
| const endpoint = "https://vidbye-cursor-ai.hf.space/hf/v1/models"; |
| |
| const response = await fetch(endpoint); |
| |
| if (response.ok) { |
| updateConnectionStatus(true, 'Connected'); |
| if (showFeedback) { |
| showToast('Connected', 'Successfully connected to the API', 'success'); |
| } |
| } else { |
| updateConnectionStatus(false, \`Error: \${response.status}\`); |
| if (showFeedback) { |
| showToast('Connection Error', \`Status code: \${response.status}\`, 'error'); |
| } |
| } |
| } catch (error) { |
| updateConnectionStatus(false, 'Connection Error'); |
| if (showFeedback) { |
| showToast('Connection Error', error.message, 'error'); |
| } |
| } |
| |
| // Check again after 30 seconds if not manually triggered |
| if (!showFeedback) { |
| setTimeout(() => checkConnectionStatus(), 30000); |
| } |
| } |
| |
| // Update connection status display |
| function updateConnectionStatus(connected, status) { |
| connectionState.connected = connected; |
| connectionState.status = status; |
| |
| const indicator = document.getElementById('connection-indicator'); |
| const statusText = document.getElementById('connection-status'); |
| const apiStatus = document.getElementById('api-status'); |
| |
| if (connected) { |
| indicator.classList.remove('error'); |
| statusText.textContent = status; |
| |
| if (apiStatus) { |
| apiStatus.classList.remove('error'); |
| apiStatus.innerHTML = '<i class="fas fa-check-circle"></i><span>Active</span>'; |
| } |
| } else { |
| indicator.classList.add('error'); |
| statusText.textContent = status; |
| |
| if (apiStatus) { |
| apiStatus.classList.add('error'); |
| apiStatus.innerHTML = '<i class="fas fa-exclamation-circle"></i><span>Error</span>'; |
| } |
| } |
| } |
| |
| // Save current conversation |
| function saveConversation() { |
| if (chatHistory.length === 0) return; |
| |
| const model = document.getElementById('chat-model-select').value; |
| const firstUserMsg = chatHistory.find(msg => msg.role === 'user'); |
| const title = firstUserMsg ? firstUserMsg.content.substring(0, 30) + (firstUserMsg.content.length > 30 ? '...' : '') : 'Conversation'; |
| |
| const conversation = { |
| id: 'conv-' + Date.now(), |
| title: title, |
| model: model, |
| messages: [...chatHistory], |
| timestamp: new Date().toISOString() |
| }; |
| |
| // Add to conversations array |
| conversations.unshift(conversation); |
| |
| // Limit to 20 conversations |
| if (conversations.length > 20) { |
| conversations = conversations.slice(0, 20); |
| } |
| |
| // Save to localStorage |
| localStorage.setItem('chat-conversations', JSON.stringify(conversations)); |
| |
| // Update UI |
| updateConversationsList(); |
| return conversation; |
| } |
| |
| // Load saved conversations |
| function loadConversations() { |
| const saved = localStorage.getItem('chat-conversations'); |
| if (saved) { |
| try { |
| conversations = JSON.parse(saved); |
| updateConversationsList(); |
| updateRecentActivity(); |
| } catch (e) { |
| console.error('Error loading conversations:', e); |
| } |
| } |
| } |
| |
| // Update conversations list in UI |
| function updateConversationsList() { |
| const container = document.getElementById('conversations-list'); |
| |
| if (conversations.length === 0) { |
| container.innerHTML = \` |
| <div class="text-center text-gray-400 py-4"> |
| <i class="fas fa-comment-slash text-3xl mb-2"></i> |
| <p>No conversations yet.</p> |
| <p class="text-sm mt-1">Your chat history will appear here.</p> |
| </div> |
| \`; |
| return; |
| } |
| |
| let html = ''; |
| |
| conversations.forEach(conv => { |
| const date = new Date(conv.timestamp); |
| const formattedDate = date.toLocaleDateString(undefined, { |
| month: 'short', |
| day: 'numeric' |
| }); |
| |
| html += \` |
| <div class="conversation-item" data-conversation-id="\${conv.id}"> |
| <i class="fas fa-comment conversation-icon"></i> |
| <div class="conversation-title">\${conv.title}</div> |
| <div class="conversation-time">\${formattedDate}</div> |
| </div> |
| \`; |
| }); |
| |
| container.innerHTML = html; |
| |
| // Add click events |
| container.querySelectorAll('.conversation-item').forEach(item => { |
| item.addEventListener('click', () => { |
| const convId = item.dataset.conversationId; |
| loadConversation(convId); |
| }); |
| }); |
| } |
| |
| // Update recent activity on dashboard |
| function updateRecentActivity() { |
| const container = document.getElementById('recent-activity'); |
| |
| if (!container) return; |
| |
| if (conversations.length === 0 && chatHistory.length === 0) { |
| container.innerHTML = \` |
| <div class="text-center text-gray-400 py-4"> |
| <i class="fas fa-comment-slash text-3xl mb-2"></i> |
| <p>No recent conversations.</p> |
| <p class="text-sm mt-1">Start chatting to see your activity here.</p> |
| </div> |
| \`; |
| return; |
| } |
| |
| let html = ''; |
| |
| // Add current conversation if not empty |
| if (chatHistory.length > 0) { |
| const firstUserMsg = chatHistory.find(msg => msg.role === 'user'); |
| const title = firstUserMsg ? firstUserMsg.content.substring(0, 30) + (firstUserMsg.content.length > 30 ? '...' : '') : 'Current conversation'; |
| const model = document.getElementById('chat-model-select').value || 'Unknown model'; |
| const msgCount = chatHistory.length; |
| |
| html += \` |
| <div class="conversation-item active"> |
| <i class="fas fa-comment-dots conversation-icon"></i> |
| <div class="conversation-title">\${title}</div> |
| <div class="text-xs text-gray-400">\${model} · \${msgCount} message\${msgCount !== 1 ? 's' : ''}</div> |
| </div> |
| \`; |
| } |
| |
| // Add recent conversations (up to 5) |
| conversations.slice(0, 5).forEach(conv => { |
| const date = new Date(conv.timestamp); |
| const formattedDate = date.toLocaleDateString(undefined, { |
| month: 'short', |
| day: 'numeric' |
| }); |
| |
| html += \` |
| <div class="conversation-item" data-conversation-id="\${conv.id}"> |
| <i class="fas fa-comment conversation-icon"></i> |
| <div class="conversation-title">\${conv.title}</div> |
| <div class="text-xs text-gray-400">\${conv.model} · \${formattedDate}</div> |
| </div> |
| \`; |
| }); |
| |
| container.innerHTML = html; |
| |
| // Add click events |
| container.querySelectorAll('.conversation-item').forEach(item => { |
| if (item.dataset.conversationId) { |
| item.addEventListener('click', () => { |
| const convId = item.dataset.conversationId; |
| loadConversation(convId); |
| |
| // Navigate to chat |
| document.querySelector('.nav-item[data-section="chat"]').click(); |
| }); |
| } |
| }); |
| } |
| |
| // Load a conversation |
| function loadConversation(conversationId) { |
| const conversation = conversations.find(c => c.id === conversationId); |
| if (!conversation) return; |
| |
| // Save current conversation if not empty |
| if (chatHistory.length > 0) { |
| saveConversation(); |
| } |
| |
| // Load conversation |
| chatHistory = [...conversation.messages]; |
| |
| // Update UI |
| const messageList = document.getElementById('message-list'); |
| messageList.innerHTML = ''; |
| |
| chatHistory.forEach(msg => { |
| if (msg.role === 'user' || msg.role === 'assistant') { |
| addMessageToChat(msg.role === 'user' ? 'user' : 'bot', msg.content); |
| } |
| }); |
| |
| // Set model |
| const modelSelect = document.getElementById('chat-model-select'); |
| if (modelSelect && conversation.model) { |
| modelSelect.value = conversation.model; |
| } |
| |
| // Highlight active conversation |
| document.querySelectorAll('.conversation-item').forEach(item => { |
| item.classList.remove('active'); |
| if (item.dataset.conversationId === conversationId) { |
| item.classList.add('active'); |
| } |
| }); |
| |
| // Show toast |
| showToast('Conversation Loaded', 'Loaded: ' + conversation.title, 'info'); |
| } |
| |
| // Clear conversation history |
| function clearHistory() { |
| // Show confirmation dialog |
| if (confirm('Are you sure you want to clear all conversation history? This cannot be undone.')) { |
| conversations = []; |
| localStorage.removeItem('chat-conversations'); |
| updateConversationsList(); |
| updateRecentActivity(); |
| showToast('History Cleared', 'All conversation history has been deleted', 'warning'); |
| } |
| } |
| |
| // Export chat as JSON |
| function exportChat() { |
| if (chatHistory.length === 0) { |
| showToast('Nothing to Export', 'Start a conversation first', 'warning'); |
| return; |
| } |
| |
| // Save current conversation |
| const conversation = saveConversation(); |
| |
| // Create file content |
| const fileContent = JSON.stringify(conversation, null, 2); |
| const blob = new Blob([fileContent], {type: 'application/json'}); |
| const url = URL.createObjectURL(blob); |
| |
| // Create temporary link and trigger download |
| const link = document.createElement('a'); |
| link.href = url; |
| link.download = \`conversation-\${new Date().toISOString().slice(0,10)}.json\`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| |
| // Cleanup |
| URL.revokeObjectURL(url); |
| |
| // Show toast |
| showToast('Exported', 'Conversation exported successfully', 'success'); |
| } |
| </script> |
| </body> |
| </html> |
| `; |
| res.send(htmlContent); |
| }); |
|
|
| |
| app.get('/health', (req, res) => { |
| res.status(200).json({ |
| status: 'ok', |
| time: new Date().toISOString(), |
| proxyCount: proxyPool.length, |
| target: `${TARGET_URL}${API_PATH}` |
| }); |
| }); |
|
|
| |
| app.listen(PORT, () => { |
| console.log(`HF Proxy server is running at PORT: ${PORT}`); |
| console.log(`Target service: ${TARGET_URL}${API_PATH}`); |
| console.log(`Proxy status: ${proxyPool.length > 0 ? 'Enabled' : 'Disabled'}`); |
| }); |