|
|
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); |
|
|
} |
|
|
|
|
|
.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> |
|
|
|
|
|
<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 in the chat interface.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</main> |
|
|
</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 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 |
|
|
if (typeof tippy !== 'undefined') { |
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
// 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') { |
|
|
try { |
|
|
// Sanitize and render markdown if libraries available |
|
|
if (typeof DOMPurify !== 'undefined' && typeof marked !== 'undefined') { |
|
|
const sanitizedContent = DOMPurify.sanitize(marked.parse(content)); |
|
|
const contentDiv = document.createElement('div'); |
|
|
contentDiv.className = 'markdown-content'; |
|
|
contentDiv.innerHTML = sanitizedContent; |
|
|
bubbleDiv.appendChild(contentDiv); |
|
|
} else { |
|
|
// Fallback to plain text if libraries not available |
|
|
bubbleDiv.textContent = content; |
|
|
} |
|
|
} catch (e) { |
|
|
console.error('Error processing markdown:', e); |
|
|
// Fallback to plain text |
|
|
bubbleDiv.textContent = content; |
|
|
} |
|
|
} 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'}`); |
|
|
}); |