cursor-ai / hf.js
vidbye's picture
Update hf.js
4e32346 verified
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();
// Enable logging
app.use(morgan('dev'));
// Environment variables configuration
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`);
// Parse proxy settings
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) => {
// Log with sensitive information masked
const maskedProxy = proxy.replace(/(https?:\/\/)([^:]+):([^@]+)@/, '$1$2:****@');
console.log(` [${index + 1}] ${maskedProxy}`);
});
}
}
// Randomly select a proxy from the pool
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
};
}
// Models list API
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);
});
// Proxy forwarding using dynamic proxy pool
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}`);
// Handle non-JSON responses
if (proxyRes.statusCode >= 400) {
let responseBody = '';
proxyRes.on('data', function(chunk) {
responseBody += chunk;
});
proxyRes.on('end', function() {
try {
// Try to parse as JSON first
JSON.parse(responseBody);
// If successful, let the normal flow continue
} catch (e) {
// If not valid JSON, override the response with a proper JSON error
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);
});
// Modern enhanced dashboard homepage
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);
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
time: new Date().toISOString(),
proxyCount: proxyPool.length,
target: `${TARGET_URL}${API_PATH}`
});
});
// Start server
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'}`);
});