Ai-Studio / static /index.html
dx8152's picture
Update static/index.html
a0c812f verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/static/logo.png" type="image/png">
<title>AI Studio</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&display=swap');
:root {
--accent: #000000;
--fluid-ease: cubic-bezier(0.3, 0, 0, 1);
}
/* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
*::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
background: transparent !important;
}
*::-webkit-scrollbar-track {
background: transparent !important;
border: none !important;
}
*::-webkit-scrollbar-thumb {
background-color: #d8d8d8 !important;
border: 3px solid transparent !important;
border-right-width: 5px !important;
/* 增加右侧间距,使滚动条向左位移 */
background-clip: padding-box !important;
border-radius: 10px !important;
}
*::-webkit-scrollbar-thumb:hover {
background-color: #c0c0c0 !important;
}
*::-webkit-scrollbar-corner {
background: transparent !important;
}
* {
scrollbar-width: thin !important;
scrollbar-color: #d8d8d8 transparent !important;
}
body {
background: #ffffff;
font-family: 'Space Grotesk', sans-serif;
overflow: hidden;
height: 100vh;
color: #121212;
}
.app-shell {
display: flex;
width: 100%;
height: 100vh;
background: #fff;
position: relative;
}
/* 侧边栏 */
.sidebar {
width: 80px;
min-width: 80px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid #f2f2f2;
padding: 40px 0;
transition: width 0.5s var(--fluid-ease) 0.5s;
z-index: 50;
}
.sidebar:hover {
width: 220px;
transition-delay: 0s;
}
.logo-ring {
width: 36px;
height: 36px;
border: 2px solid var(--accent);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.6s var(--fluid-ease) 0.5s;
}
.sidebar:hover .logo-ring {
transform: rotate(90deg);
border-radius: 50%;
transition-delay: 0s;
}
/* 导航项 */
.nav-item {
position: relative;
width: 48px;
height: 48px;
margin: 10px 0;
display: flex;
align-items: center;
justify-content: flex-start;
border-radius: 18px;
cursor: pointer;
transition: all 0.3s var(--fluid-ease) 0.5s;
color: #999;
overflow: hidden;
padding-left: 14px;
}
.sidebar:hover .nav-item {
width: 190px;
transition-delay: 0s;
}
.nav-item:hover {
background: #fafafa;
color: #000;
}
.nav-item.active {
background: var(--accent);
color: #fff;
}
.nav-text {
opacity: 0;
margin-left: 16px;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
transition: opacity 0.3s 0.5s;
}
.sidebar:hover .nav-text {
opacity: 1;
transition-delay: 0.1s;
}
/* 主舞台区 */
.stage {
flex: 1;
background: #fcfcfc;
margin: 16px;
border-radius: 32px;
overflow: hidden;
border: 1px solid #f0f0f0;
position: relative;
}
iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: none;
opacity: 0;
transform: scale(1.02);
filter: blur(4px);
transition: all 0.5s var(--fluid-ease);
pointer-events: none;
}
iframe.active {
opacity: 1;
transform: scale(1);
filter: blur(0);
pointer-events: auto;
}
/* --- 左下角微型监视器 --- */
.nano-monitor {
position: absolute;
bottom: 24px;
left: 24px;
z-index: 100;
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
padding: 6px 14px;
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
font-family: monospace;
transition: all 0.4s var(--fluid-ease);
}
.nano-monitor.is-busy {
background: #000;
color: #fff;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}
.stat-group {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
}
.divider {
width: 1px;
height: 12px;
background: rgba(0, 0, 0, 0.1);
}
.is-busy .divider {
background: rgba(255, 255, 255, 0.2);
}
.pulse-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
}
.spinner-nano {
width: 10px;
height: 10px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: none;
}
.is-busy .spinner-nano {
display: block;
}
.is-busy .pulse-dot {
display: none;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.label-nano {
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.5;
font-size: 9px;
}
/* --- 设计感:Split-Expansion 作者组件 --- */
.author-box {
/* margin-top: auto; Moved to Token Button */
width: 100%;
height: 60px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 字母 DX 的基础样式 */
.dx-letter {
position: absolute;
font-size: 14px;
font-weight: 800;
color: f5f5f5;
transition: all 0.5s var(--fluid-ease) 0.4s;
z-index: 10;
}
.letter-d {
transform: translateX(-8px);
}
.letter-x {
transform: translateX(8px);
}
/* 侧边栏展开时,DX 向两边消失 */
.sidebar:hover .letter-d {
transform: translateX(-120px);
opacity: 0;
transition-delay: 0s;
}
.sidebar:hover .letter-x {
transform: translateX(120px);
opacity: 0;
transition-delay: 0s;
}
/* 中间内容的容器 */
.author-content-wrap {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
transform: scale(0.9);
transition: all 0.4s var(--fluid-ease) 0s;
pointer-events: none;
}
/* 侧边栏展开时,中间内容显现 */
.sidebar:hover .author-content-wrap {
opacity: 1;
transform: scale(1);
transition-delay: 0.2s;
pointer-events: auto;
}
.author-name-lite {
font-size: 12px;
font-weight: 700;
margin-bottom: 8px;
color: #000;
}
.social-row-lite {
display: flex;
gap: 12px;
}
.social-icon-lite {
color: #ccc;
transition: color 0.2s, transform 0.2s;
}
.social-icon-lite:hover {
color: #000;
transform: translateY(-1px);
}
/* --- Token Button Custom Styles --- */
.nav-item.token-btn {
height: 36px !important;
width: 36px;
border-radius: 9999px !important;
background: #ffffff !important; /* White */
border: 1px solid #e5e5e5 !important; /* Light gray border */
color: #000000 !important;
padding-left: 0 !important;
justify-content: center;
box-shadow: none;
/* Hidden in collapsed state */
opacity: 0;
pointer-events: none;
transform: scale(0.8);
transition: all 0.3s var(--fluid-ease);
}
.sidebar:hover .nav-item.token-btn {
width: 140px;
opacity: 1;
pointer-events: auto;
transform: scale(1);
transition-delay: 0.1s;
}
.nav-item.token-btn:hover {
background: #f4f4f5 !important; /* Slightly darker on hover */
transform: scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.nav-item.token-btn .nav-text {
color: #000000 !important;
font-weight: 400;
font-size: 13px;
}
</style>
</head>
<body>
<div class="app-shell">
<aside class="sidebar">
<div class="logo-ring mb-12">
<div class="w-1.5 h-1.5 bg-black rounded-full transition-colors" id="logo-dot"></div>
</div>
<nav>
<div class="nav-item active" onclick="switchUI(this, 'zimage')">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.587-1.587a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<span class="nav-text">文生图</span>
</div>
<div class="nav-item" onclick="switchUI(this, 'enhance')">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M13 10V3L4 14h7v7l9-11h-7z" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
<span class="nav-text">细节增强</span>
</div>
<div class="nav-item" onclick="switchUI(this, 'klein')">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<span class="nav-text">图片编辑</span>
</div>
<div class="nav-item" onclick="switchUI(this, 'angle')">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z">
</path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
<span class="nav-text">角度控制</span>
</div>
</nav>
<div class="nav-item token-btn !mt-auto !mb-6" onclick="openTokenModal()" title="设置 API Token">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<span class="nav-text">API Token</span>
</div>
<div class="author-box">
<span class="dx-letter letter-d">D</span>
<span class="dx-letter letter-x">X</span>
<div class="author-content-wrap">
<div class="author-name-lite">wuli大雄</div>
<div class="social-row-lite">
<a href="https://space.bilibili.com/78652351" target="_blank" class="social-icon-lite">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M17.813 4.653h-.854L15.66 3.053a1.147 1.147 0 00-1.63 0l-1.3 1.6h-1.46L9.97 3.053a1.147 1.147 0 00-1.63 0L7.043 4.653h-.854a3.946 3.946 0 00-3.93 3.934v8.117a3.946 3.946 0 003.93 3.934h11.624a3.946 3.946 0 003.93-3.934V8.587a3.946 3.946 0 00-3.93-3.934zM7.152 13.9a1.465 1.465 0 111.47-1.462 1.465 1.465 0 01-1.47 1.462zm7.696 0a1.465 1.465 0 111.47-1.462 1.465 1.465 0 01-1.47 1.462z" />
</svg>
</a>
<a href="https://www.xiaohongshu.com/user/profile/6433c34c000000001a023538" target="_blank"
class="social-icon-lite">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z" />
</svg>
</a>
<a href="https://www.youtube.com/@%E5%A4%A7%E9%9B%84dx" target="_blank"
class="social-icon-lite">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
</a>
<a href="https://x.com/dx8152?s=21" target="_blank" class="social-icon-lite">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
</div>
</div>
</div>
</aside>
<main class="stage">
<iframe id="frame-zimage" src="/static/zimage.html?v=30" class="active"></iframe>
<iframe id="frame-enhance" data-src="/static/enhance.html?v=30"></iframe>
<iframe id="frame-klein" data-src="/static/klein.html?v=30"></iframe>
<iframe id="frame-angle" data-src="/static/angle.html?v=30"></iframe>
<div class="nano-monitor" id="nano-monitor">
<div class="stat-group">
<div class="pulse-dot animate-pulse"></div>
<div class="spinner-nano"></div>
<span class="label-nano">ONLINE</span>
<span id="online-val">1</span>
</div>
<div class="divider"></div>
<div class="stat-group">
<span class="label-nano">QUEUE</span>
<span id="queue-val">0</span>
</div>
</div>
</main>
</div>
<!-- Token Modal -->
<div id="token-modal" class="fixed inset-0 z-[100] hidden opacity-0 transition-opacity duration-300">
<div class="absolute inset-0 bg-black/40 backdrop-blur-md" onclick="closeTokenModal()"></div>
<div id="token-modal-content" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-3xl shadow-[0_32px_64px_-12px_rgba(0,0,0,0.2)] w-[440px] overflow-hidden scale-95 transition-all duration-300 border border-gray-100">
<div class="p-8 pb-0">
<h3 class="text-xl font-bold text-gray-900 mb-2 text-center tracking-tight">Access Token</h3>
<div class="text-center mb-6">
<a href="https://www.modelscope.cn/my/access/token" target="_blank" class="text-xs text-blue-500 hover:text-blue-600 hover:underline transition-colors">
获取 API Key (Get Token) ->
</a>
</div>
<div class="flex p-1 bg-gray-100 rounded-2xl mb-8 relative">
<button onclick="toggleTokenPanel('personal')" id="btn-personal" class="flex-1 py-2 text-sm font-bold rounded-xl transition-all duration-300 z-10 text-black bg-white shadow-sm">个人-Personal</button>
<button onclick="toggleTokenPanel('global')" id="btn-global" class="flex-1 py-2 text-sm font-bold rounded-xl transition-all duration-300 z-10 text-gray-500">全局-Global</button>
</div>
</div>
<div class="px-8 pb-8 relative">
<div id="panel-personal" class="space-y-5 transition-all duration-300">
<div class="space-y-2">
<div class="flex justify-between items-end">
<label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Local Storage Only</label>
<span class="text-[10px] text-green-500 font-bold bg-green-50 px-2 py-0.5 rounded-full">Secure</span>
</div>
<input type="password" id="personal-token-input" class="w-full px-5 py-4 bg-gray-50 border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-black/5 focus:border-black transition-all text-sm font-mono" placeholder="Enter personal token...">
</div>
<div class="flex gap-3">
<button onclick="savePersonalToken()" class="flex-[2] py-4 text-sm font-bold text-white bg-black hover:bg-gray-800 rounded-2xl transition-all shadow-lg shadow-black/10 active:scale-[0.98]">Save Token</button>
<button onclick="deletePersonalToken()" class="flex-1 py-4 text-sm font-bold text-red-500 bg-red-50 hover:bg-red-100 rounded-2xl transition-all active:scale-[0.98]">Reset</button>
</div>
</div>
<div id="panel-global" class="hidden space-y-5 transition-all duration-300 opacity-0 translate-y-4">
<div class="space-y-2">
<div class="flex justify-between items-end">
<label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Server Configuration</label>
<span class="text-[10px] text-orange-500 font-bold bg-orange-50 px-2 py-0.5 rounded-full">Shared</span>
</div>
<input type="password" id="global-token-input" class="w-full px-5 py-4 bg-blue-50/20 border border-blue-100 rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-500/10 focus:border-blue-300 transition-all text-sm font-mono" placeholder="Enter server token...">
</div>
<div class="flex gap-3">
<button onclick="saveGlobalToken()" class="flex-[2] py-4 text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 rounded-2xl transition-all shadow-lg shadow-blue-500/20 active:scale-[0.98]">Deploy Global</button>
<button onclick="deleteGlobalToken()" class="flex-1 py-4 text-sm font-bold text-red-500 bg-red-50 hover:bg-red-100 rounded-2xl transition-all active:scale-[0.98]">Remove</button>
</div>
</div>
</div>
</div>
</div>
<script>
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
try { return crypto.randomUUID(); } catch (e) { }
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const CID = localStorage.getItem("client_id") || generateUUID();
localStorage.setItem("client_id", CID);
function switchUI(el, id) {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('iframe').forEach(f => f.classList.remove('active'));
const target = document.getElementById('frame-' + id);
target.classList.add('active');
if (!target.src) target.src = target.dataset.src;
}
async function syncStatus() {
try {
const res = await fetch(`/api/queue_status?client_id=${CID}`);
const data = await res.json();
const monitor = document.getElementById('nano-monitor');
const queueVal = document.getElementById('queue-val');
const logoDot = document.getElementById('logo-dot');
const total = data.total || 0;
const pos = data.position || 0;
if (pos > 0) {
monitor.classList.add('is-busy');
queueVal.innerText = `${pos}/${total}`;
logoDot.style.backgroundColor = '#3b82f6';
} else {
monitor.classList.remove('is-busy');
queueVal.innerText = total > 0 ? total : '0';
logoDot.style.backgroundColor = '#000';
}
} catch (e) { }
}
const host = window.location.host;
if (host) {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
// WebSocket for queue status
const ws = new WebSocket(`${protocol}://${host}/ws/stats?client_id=${CID}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'stats') {
document.getElementById('online-val').innerText = data.online_count;
} else if (data.type === 'cloud_status') {
// Forward cloud status to active iframe
const iframe = document.querySelector('iframe.active');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(data, '*');
}
}
};
setInterval(syncStatus, 2000);
}
// --- Token Modal Logic ---
const modal = document.getElementById('token-modal');
const modalContent = document.getElementById('token-modal-content');
const personalInput = document.getElementById('personal-token-input');
const globalInput = document.getElementById('global-token-input');
// 修改原有的 openTokenModal,确保每次打开默认显示个人面板
window.openTokenModal = function() {
modal.classList.remove('hidden');
setTimeout(() => {
modal.classList.remove('opacity-0');
modalContent.classList.remove('scale-95');
modalContent.classList.add('scale-100');
}, 10);
toggleTokenPanel('personal'); // 默认显示个人
loadCurrentToken();
}
function toggleTokenPanel(type) {
const pPanel = document.getElementById('panel-personal');
const gPanel = document.getElementById('panel-global');
const pBtn = document.getElementById('btn-personal');
const gBtn = document.getElementById('btn-global');
if (type === 'personal') {
// Switch to Personal
gPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
pPanel.classList.remove('hidden');
setTimeout(() => pPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
pBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
pBtn.classList.remove('text-gray-500');
gBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
gBtn.classList.add('text-gray-500');
} else {
// Switch to Global
pPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
gPanel.classList.remove('hidden');
setTimeout(() => gPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
gBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
gBtn.classList.remove('text-gray-500');
pBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
pBtn.classList.add('text-gray-500');
}
}
function closeTokenModal() {
modal.classList.add('opacity-0');
modalContent.classList.remove('scale-100');
modalContent.classList.add('scale-95');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
async function loadCurrentToken() {
// 1. Load Personal
const localToken = localStorage.getItem('modelscope_api_token');
personalInput.value = localToken || '';
// 2. Load Global
try {
const res = await fetch('/api/config/token');
const data = await res.json();
globalInput.value = data.token || '';
} catch (e) {
console.error("Failed to load global token", e);
globalInput.value = '';
}
}
function savePersonalToken() {
const token = personalInput.value.trim();
if (!token) {
alert('请输入 Token');
return;
}
localStorage.setItem('modelscope_api_token', token);
alert('个人 Token 已保存');
}
function deletePersonalToken() {
if (confirm('确定要删除个人 Token 吗?')) {
localStorage.removeItem('modelscope_api_token');
personalInput.value = '';
}
}
async function saveGlobalToken() {
const token = globalInput.value.trim();
if (!token) {
alert('请输入 Token');
return;
}
if (!confirm('⚠️ 警告:全局 Token 将对所有用户可见。确定要保存吗?')) return;
try {
const res = await fetch('/api/config/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
if (res.ok) {
alert('全局 Token 已保存');
} else {
throw new Error('Save failed');
}
} catch (e) {
alert('保存失败: ' + e.message);
}
}
async function deleteGlobalToken() {
if (!confirm('确定要删除全局 Token 吗?此操作将影响所有使用默认配置的用户。')) return;
try {
const res = await fetch('/api/config/token', {
method: 'DELETE'
});
if (res.ok) {
globalInput.value = '';
alert('全局 Token 已删除');
} else {
throw new Error('Delete failed');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// Auto-open Token Modal if not set
window.addEventListener('load', async () => {
// Check Local
const localToken = localStorage.getItem('modelscope_api_token');
if (localToken) return;
// Check Global
try {
const res = await fetch('/api/config/token');
const data = await res.json();
if (data.token) return;
} catch (e) {}
// If neither, open modal
console.log("No token found, auto-opening modal");
openTokenModal();
});
</script>
</body>
</html> const queueVal = document.getElementById('queue-val');
const logoDot = document.getElementById('logo-dot');
const total = data.total || 0;
const pos = data.position || 0;
if (pos > 0) {
monitor.classList.add('is-busy');
queueVal.innerText = `${pos}/${total}`;
logoDot.style.backgroundColor = '#3b82f6';
} else {
monitor.classList.remove('is-busy');
queueVal.innerText = total > 0 ? total : '0';
logoDot.style.backgroundColor = '#000';
}
} catch (e) { }
}
const host = window.location.host;
if (host) {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${protocol}://${host}/ws/stats`);
ws.onmessage = (e) => {
const d = JSON.parse(e.data);
if (d.online_count) {
document.getElementById('online-val').innerText = d.online_count;
}
};
setInterval(syncStatus, 2000);
}
</script>
</body>
</html>