| | <!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; |
| | } |
| | |
| | |
| | .author-box { |
| | |
| | width: 100%; |
| | height: 60px; |
| | position: relative; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | overflow: hidden; |
| | } |
| | |
| | |
| | .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); |
| | } |
| | |
| | |
| | .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); |
| | } |
| | |
| | |
| | .nav-item.token-btn { |
| | height: 36px !important; |
| | width: 36px; |
| | border-radius: 9999px !important; |
| | background: #ffffff !important; |
| | border: 1px solid #e5e5e5 !important; |
| | color: #000000 !important; |
| | padding-left: 0 !important; |
| | justify-content: center; |
| | box-shadow: none; |
| | |
| | |
| | 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; |
| | 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> |
| |
|
| | |
| | <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'; |
| | |
| | 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') { |
| | |
| | const iframe = document.querySelector('iframe.active'); |
| | if (iframe && iframe.contentWindow) { |
| | iframe.contentWindow.postMessage(data, '*'); |
| | } |
| | } |
| | }; |
| | setInterval(syncStatus, 2000); |
| | } |
| | |
| | |
| | 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'); |
| | |
| | |
| | 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') { |
| | |
| | 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 { |
| | |
| | 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() { |
| | |
| | const localToken = localStorage.getItem('modelscope_api_token'); |
| | personalInput.value = localToken || ''; |
| | |
| | |
| | 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); |
| | } |
| | } |
| | |
| | |
| | window.addEventListener('load', async () => { |
| | |
| | const localToken = localStorage.getItem('modelscope_api_token'); |
| | if (localToken) return; |
| | |
| | |
| | try { |
| | const res = await fetch('/api/config/token'); |
| | const data = await res.json(); |
| | if (data.token) return; |
| | } catch (e) {} |
| | |
| | |
| | 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> |