|
|
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(500).json({ |
|
|
error: { |
|
|
message: 'Proxy error occurred', |
|
|
type: 'proxy_error', |
|
|
details: process.env.NODE_ENV === 'development' ? err.message : undefined |
|
|
} |
|
|
}); |
|
|
}, |
|
|
onProxyRes: (proxyRes, req, res) => { |
|
|
console.log(`Proxy response status: ${proxyRes.statusCode}`); |
|
|
} |
|
|
}); |
|
|
|
|
|
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"> |
|
|
<style> |
|
|
:root { |
|
|
--primary-color: #5D5CDE; |
|
|
--primary-dark: #4338ca; |
|
|
--primary-light: #818cf8; |
|
|
--secondary-color: #10b981; |
|
|
--accent-color: #f97316; |
|
|
--bg-dark: #111827; |
|
|
--bg-card: #1f2937; |
|
|
--text-primary: #f3f4f6; |
|
|
--text-secondary: #d1d5db; |
|
|
--text-muted: #9ca3af; |
|
|
--border-color: #374151; |
|
|
--success-bg: #065f46; |
|
|
--success-text: #a7f3d0; |
|
|
--error-bg: #7f1d1d; |
|
|
--error-text: #fecaca; |
|
|
--input-bg: #1e293b; |
|
|
--hover-bg: #2d3748; |
|
|
--shadow-color: rgba(0, 0, 0, 0.25); |
|
|
} |
|
|
|
|
|
body { |
|
|
background-color: var(--bg-dark); |
|
|
color: var(--text-primary); |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
transition: background-color 0.3s ease, color 0.3s ease; |
|
|
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; |
|
|
} |
|
|
|
|
|
.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-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); |
|
|
} |
|
|
|
|
|
.nav-item:hover { |
|
|
background-color: var(--hover-bg); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.nav-item.active { |
|
|
background-color: var(--primary-color); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.nav-item i { |
|
|
width: 1.25rem; |
|
|
margin-right: 0.75rem; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.status-badge.error { |
|
|
background-color: var(--error-bg); |
|
|
color: var(--error-text); |
|
|
} |
|
|
|
|
|
.info-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.info-item { |
|
|
background-color: var(--hover-bg); |
|
|
border-radius: 0.5rem; |
|
|
border: 1px solid var(--border-color); |
|
|
padding: 1rem; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.info-label { |
|
|
color: var(--text-muted); |
|
|
font-size: 0.875rem; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.model-item { |
|
|
background-color: var(--hover-bg); |
|
|
border-radius: 0.5rem; |
|
|
border: 1px solid var(--border-color); |
|
|
padding: 1rem; |
|
|
transition: all 0.2s ease; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.model-item:hover { |
|
|
background-color: var(--bg-dark); |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 8px var(--shadow-color); |
|
|
} |
|
|
|
|
|
.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); |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.copy-btn:hover { |
|
|
background-color: var(--primary-dark); |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.chat-body { |
|
|
flex-grow: 1; |
|
|
overflow-y: auto; |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.message-list { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.message { |
|
|
display: flex; |
|
|
max-width: 80%; |
|
|
} |
|
|
|
|
|
.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(--hover-bg); |
|
|
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; |
|
|
} |
|
|
|
|
|
.chat-input { |
|
|
padding: 1rem; |
|
|
border-top: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.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); |
|
|
} |
|
|
|
|
|
.send-btn:disabled { |
|
|
background-color: var(--border-color); |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.model-select-container { |
|
|
display: flex; |
|
|
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; |
|
|
font-size: 1rem; |
|
|
flex-grow: 1; |
|
|
} |
|
|
|
|
|
.model-label { |
|
|
color: var(--text-secondary); |
|
|
font-weight: 500; |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.new-chat-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; |
|
|
} |
|
|
|
|
|
.new-chat-btn:hover { |
|
|
background-color: var(--primary-dark); |
|
|
} |
|
|
|
|
|
.loading-spinner { |
|
|
display: inline-block; |
|
|
width: 1.5rem; |
|
|
height: 1.5rem; |
|
|
border: 0.25rem solid rgba(255,255,255,.3); |
|
|
border-radius: 50%; |
|
|
border-top-color: white; |
|
|
animation: spin 1s ease-in-out infinite; |
|
|
margin-right: 0.5rem; |
|
|
} |
|
|
|
|
|
@keyframes spin { |
|
|
to { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
.system-message { |
|
|
background-color: var(--hover-bg); |
|
|
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; |
|
|
} |
|
|
|
|
|
.system-message i { |
|
|
margin-right: 0.5rem; |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
.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); |
|
|
} |
|
|
|
|
|
.mobile-menu-btn { |
|
|
display: none; |
|
|
background: none; |
|
|
border: none; |
|
|
color: var(--text-primary); |
|
|
font-size: 1.5rem; |
|
|
cursor: pointer; |
|
|
padding: 0.5rem; |
|
|
} |
|
|
|
|
|
/* Light mode theming */ |
|
|
@media (prefers-color-scheme: light) { |
|
|
:root { |
|
|
--bg-dark: #f8fafc; |
|
|
--bg-card: #ffffff; |
|
|
--text-primary: #1e293b; |
|
|
--text-secondary: #475569; |
|
|
--text-muted: #64748b; |
|
|
--border-color: #e2e8f0; |
|
|
--success-bg: #dcfce7; |
|
|
--success-text: #166534; |
|
|
--error-bg: #fee2e2; |
|
|
--error-text: #991b1b; |
|
|
--input-bg: #f1f5f9; |
|
|
--hover-bg: #f8fafc; |
|
|
--shadow-color: rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
} |
|
|
|
|
|
/* 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: 10; |
|
|
transform: translateX(-100%); |
|
|
} |
|
|
|
|
|
.sidebar.open { |
|
|
transform: translateX(0); |
|
|
} |
|
|
|
|
|
.mobile-menu-btn { |
|
|
display: flex; |
|
|
position: fixed; |
|
|
top: 1rem; |
|
|
left: 1rem; |
|
|
z-index: 20; |
|
|
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; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<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-4"> |
|
|
<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-item" data-section="settings"> |
|
|
<i class="fas fa-cog"></i> |
|
|
<span>Settings</span> |
|
|
</div> |
|
|
</nav> |
|
|
|
|
|
<div class="mt-auto p-4 text-sm text-gray-400"> |
|
|
<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-4">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">Active</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Target Service</div> |
|
|
<div class="info-value" id="target-service">${TARGET_URL}${API_PATH}</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Proxy Status</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-clock"></i> |
|
|
<span>Performance</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Request Timeout</div> |
|
|
<div class="info-value">${TIMEOUT}ms</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Service Port</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> |
|
|
<button class="copy-btn" id="copy-endpoint"> |
|
|
<i class="fas fa-copy mr-2"></i> Copy to clipboard |
|
|
</button> |
|
|
</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="loading-spinner"></div> |
|
|
<span class="ml-2">Loading models...</span> |
|
|
</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="">Loading models...</option> |
|
|
</select> |
|
|
</div> |
|
|
<button class="new-chat-btn" id="new-chat-btn"> |
|
|
<i class="fas fa-plus"></i> |
|
|
<span>New Chat</span> |
|
|
</button> |
|
|
</div> |
|
|
<div class="chat-body"> |
|
|
<div class="system-message"> |
|
|
<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> |
|
|
</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> |
|
|
<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"> |
|
|
</div> |
|
|
<div id="all-models" class="model-grid mt-4"> |
|
|
<div class="flex justify-center items-center p-4"> |
|
|
<div class="loading-spinner"></div> |
|
|
<span class="ml-2">Loading models...</span> |
|
|
</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-wrench"></i> |
|
|
<span>Connection Settings</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Target URL</div> |
|
|
<div class="info-value">${TARGET_URL}</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">API Path</div> |
|
|
<div class="info-value">${API_PATH}</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Server Port</div> |
|
|
<div class="info-value">${PORT}</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Timeout Setting</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">Proxy Status</div> |
|
|
<div class="info-value">${proxyPool.length > 0 ? 'Enabled' : 'Disabled'}</div> |
|
|
</div> |
|
|
<div class="info-item"> |
|
|
<div class="info-label">Active Proxies</div> |
|
|
<div class="info-value">${proxyPool.length}</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// Store chat history |
|
|
let chatHistory = []; |
|
|
let selectedModel = ''; |
|
|
let connectionState = { |
|
|
connected: true, |
|
|
status: 'Connected', |
|
|
}; |
|
|
|
|
|
// Initialize the dashboard |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
// 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 url = new URL(window.location.href); |
|
|
const endpointUrl = url.protocol + '//' + url.host + '/hf/v1'; |
|
|
document.getElementById('endpoint-url').textContent = endpointUrl; |
|
|
|
|
|
// Copy endpoint button |
|
|
document.getElementById('copy-endpoint').addEventListener('click', () => { |
|
|
navigator.clipboard.writeText(endpointUrl).then(() => { |
|
|
const btn = document.getElementById('copy-endpoint'); |
|
|
const originalText = btn.innerHTML; |
|
|
btn.innerHTML = '<i class="fas fa-check mr-2"></i> Copied!'; |
|
|
setTimeout(() => { |
|
|
btn.innerHTML = originalText; |
|
|
}, 2000); |
|
|
}); |
|
|
}); |
|
|
|
|
|
// Initialize chat |
|
|
initChat(); |
|
|
|
|
|
// Fetch and display models |
|
|
fetchModels(); |
|
|
|
|
|
// Check connection status |
|
|
checkConnectionStatus(); |
|
|
}); |
|
|
|
|
|
// 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'); |
|
|
|
|
|
// 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; |
|
|
}); |
|
|
|
|
|
// Enable/disable send button based on model selection |
|
|
modelSelect.addEventListener('change', () => { |
|
|
selectedModel = modelSelect.value; |
|
|
sendBtn.disabled = !messageInput.value.trim() || !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', () => { |
|
|
chatHistory = []; |
|
|
messageList.innerHTML = ''; |
|
|
const systemMessage = document.createElement('div'); |
|
|
systemMessage.className = 'system-message'; |
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
// 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'); |
|
|
|
|
|
// 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="loading-spinner" style="width: 1rem; height: 1rem;"></div> |
|
|
<span class="ml-2">Thinking...</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
\`; |
|
|
messageList.insertAdjacentHTML('beforeend', loadingHTML); |
|
|
messageList.scrollTop = messageList.scrollHeight; |
|
|
|
|
|
try { |
|
|
// Send request to API |
|
|
const url = new URL(window.location.href); |
|
|
const endpoint = url.protocol + '//' + url.host + '/hf/v1/chat/completions'; |
|
|
|
|
|
const payload = { |
|
|
model: modelSelect.value, |
|
|
messages: messages, |
|
|
temperature: 0.7, |
|
|
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) { |
|
|
const data = await response.json(); |
|
|
|
|
|
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() |
|
|
}); |
|
|
} else { |
|
|
// Handle empty response |
|
|
addSystemMessage('Received an empty response from the model.'); |
|
|
} |
|
|
} else { |
|
|
// Handle error response |
|
|
const errorData = await response.json(); |
|
|
const errorMessage = errorData.error?.message || 'An error occurred while communicating with the API.'; |
|
|
addSystemMessage('Error: ' + errorMessage); |
|
|
|
|
|
// 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')); |
|
|
|
|
|
// 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' }); |
|
|
|
|
|
const messageHTML = \` |
|
|
<div class="message \${role}"> |
|
|
<div class="message-bubble"> |
|
|
\${content} |
|
|
<div class="message-time">\${timestamp}</div> |
|
|
</div> |
|
|
</div> |
|
|
\`; |
|
|
|
|
|
messageList.insertAdjacentHTML('beforeend', messageHTML); |
|
|
messageList.scrollTop = messageList.scrollHeight; |
|
|
} |
|
|
|
|
|
// Add system message |
|
|
function addSystemMessage(message) { |
|
|
const messageList = document.getElementById('message-list'); |
|
|
|
|
|
const systemHTML = \` |
|
|
<div class="system-message"> |
|
|
<i class="fas fa-exclamation-circle"></i> |
|
|
<span>\${message}</span> |
|
|
</div> |
|
|
\`; |
|
|
|
|
|
messageList.insertAdjacentHTML('beforeend', systemHTML); |
|
|
messageList.scrollTop = messageList.scrollHeight; |
|
|
} |
|
|
|
|
|
// Fetch models from the API |
|
|
async function fetchModels() { |
|
|
try { |
|
|
const url = new URL(window.location.href); |
|
|
const link = url.protocol + '//' + url.host + '/hf/v1/models'; |
|
|
const response = await fetch(link); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error('Failed to fetch models'); |
|
|
} |
|
|
|
|
|
const popularModelsContainer = document.getElementById('popular-models'); |
|
|
const allModelsContainer = document.getElementById('all-models'); |
|
|
const modelSelect = document.getElementById('chat-model-select'); |
|
|
|
|
|
// Clear containers |
|
|
popularModelsContainer.innerHTML = ''; |
|
|
allModelsContainer.innerHTML = ''; |
|
|
modelSelect.innerHTML = '<option value="">Select a 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 dropdown |
|
|
const option = document.createElement('option'); |
|
|
option.value = model.id; |
|
|
option.textContent = model.id; |
|
|
modelSelect.appendChild(option); |
|
|
}); |
|
|
|
|
|
// 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'); |
|
|
} 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); |
|
|
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>'; |
|
|
|
|
|
// Update connection status |
|
|
updateConnectionStatus(false, 'Connection Error'); |
|
|
} |
|
|
} |
|
|
|
|
|
// Create model item element |
|
|
function createModelItem(model) { |
|
|
const div = document.createElement('div'); |
|
|
div.className = 'model-item'; |
|
|
div.innerHTML = \` |
|
|
<span class="model-name">\${model.id}</span> |
|
|
<span class="model-provider">\${model.owned_by}</span> |
|
|
\`; |
|
|
|
|
|
// Click to select model for chat |
|
|
div.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 section |
|
|
document.querySelector('.nav-item[data-section="chat"]').click(); |
|
|
}); |
|
|
|
|
|
return div; |
|
|
} |
|
|
|
|
|
// Check and update connection status |
|
|
async function checkConnectionStatus() { |
|
|
try { |
|
|
const url = new URL(window.location.href); |
|
|
const endpoint = url.protocol + '//' + url.host + '/health'; |
|
|
|
|
|
const response = await fetch(endpoint); |
|
|
if (response.ok) { |
|
|
updateConnectionStatus(true, 'Connected'); |
|
|
} else { |
|
|
updateConnectionStatus(false, 'Connection Error'); |
|
|
} |
|
|
} catch (error) { |
|
|
updateConnectionStatus(false, 'Connection Error'); |
|
|
} |
|
|
|
|
|
// Check again after 30 seconds |
|
|
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.textContent = 'Active'; |
|
|
} |
|
|
} else { |
|
|
indicator.classList.add('error'); |
|
|
statusText.textContent = status; |
|
|
|
|
|
if (apiStatus) { |
|
|
apiStatus.classList.add('error'); |
|
|
apiStatus.textContent = 'Error'; |
|
|
} |
|
|
} |
|
|
} |
|
|
</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'}`); |
|
|
}); |