dx8152 commited on
Commit
a0c812f
·
verified ·
1 Parent(s): 3fbe280

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +803 -803
static/index.html CHANGED
@@ -1,804 +1,804 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="icon" href="/static/logo.png" type="image/png">
8
- <title>AI Studio</title>
9
- <script src="https://cdn.tailwindcss.com"></script>
10
- <style>
11
- @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&display=swap');
12
-
13
- :root {
14
- --accent: #000000;
15
- --fluid-ease: cubic-bezier(0.3, 0, 0, 1);
16
- }
17
-
18
- /* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
19
- *::-webkit-scrollbar {
20
- width: 10px !important;
21
- height: 10px !important;
22
- background: transparent !important;
23
- }
24
-
25
- *::-webkit-scrollbar-track {
26
- background: transparent !important;
27
- border: none !important;
28
- }
29
-
30
- *::-webkit-scrollbar-thumb {
31
- background-color: #d8d8d8 !important;
32
- border: 3px solid transparent !important;
33
- border-right-width: 5px !important;
34
- /* 增加右侧间距,使滚动条向左位移 */
35
- background-clip: padding-box !important;
36
- border-radius: 10px !important;
37
- }
38
-
39
- *::-webkit-scrollbar-thumb:hover {
40
- background-color: #c0c0c0 !important;
41
- }
42
-
43
- *::-webkit-scrollbar-corner {
44
- background: transparent !important;
45
- }
46
-
47
- * {
48
- scrollbar-width: thin !important;
49
- scrollbar-color: #d8d8d8 transparent !important;
50
- }
51
-
52
- body {
53
- background: #ffffff;
54
- font-family: 'Space Grotesk', sans-serif;
55
- overflow: hidden;
56
- height: 100vh;
57
- color: #121212;
58
- }
59
-
60
- .app-shell {
61
- display: flex;
62
- width: 100%;
63
- height: 100vh;
64
- background: #fff;
65
- position: relative;
66
- }
67
-
68
- /* 侧边栏 */
69
- .sidebar {
70
- width: 80px;
71
- min-width: 80px;
72
- background: #fff;
73
- display: flex;
74
- flex-direction: column;
75
- align-items: center;
76
- border-right: 1px solid #f2f2f2;
77
- padding: 40px 0;
78
- transition: width 0.5s var(--fluid-ease) 0.5s;
79
- z-index: 50;
80
- }
81
-
82
- .sidebar:hover {
83
- width: 220px;
84
- transition-delay: 0s;
85
- }
86
-
87
- .logo-ring {
88
- width: 36px;
89
- height: 36px;
90
- border: 2px solid var(--accent);
91
- border-radius: 12px;
92
- display: flex;
93
- align-items: center;
94
- justify-content: center;
95
- transition: all 0.6s var(--fluid-ease) 0.5s;
96
- }
97
-
98
- .sidebar:hover .logo-ring {
99
- transform: rotate(90deg);
100
- border-radius: 50%;
101
- transition-delay: 0s;
102
- }
103
-
104
- /* 导航项 */
105
- .nav-item {
106
- position: relative;
107
- width: 48px;
108
- height: 48px;
109
- margin: 10px 0;
110
- display: flex;
111
- align-items: center;
112
- justify-content: flex-start;
113
- border-radius: 18px;
114
- cursor: pointer;
115
- transition: all 0.3s var(--fluid-ease) 0.5s;
116
- color: #999;
117
- overflow: hidden;
118
- padding-left: 14px;
119
- }
120
-
121
- .sidebar:hover .nav-item {
122
- width: 190px;
123
- transition-delay: 0s;
124
- }
125
-
126
- .nav-item:hover {
127
- background: #fafafa;
128
- color: #000;
129
- }
130
-
131
- .nav-item.active {
132
- background: var(--accent);
133
- color: #fff;
134
- }
135
-
136
- .nav-text {
137
- opacity: 0;
138
- margin-left: 16px;
139
- font-weight: 600;
140
- font-size: 14px;
141
- white-space: nowrap;
142
- transition: opacity 0.3s 0.5s;
143
- }
144
-
145
- .sidebar:hover .nav-text {
146
- opacity: 1;
147
- transition-delay: 0.1s;
148
- }
149
-
150
- /* 主舞台区 */
151
- .stage {
152
- flex: 1;
153
- background: #fcfcfc;
154
- margin: 16px;
155
- border-radius: 32px;
156
- overflow: hidden;
157
- border: 1px solid #f0f0f0;
158
- position: relative;
159
- }
160
-
161
- iframe {
162
- position: absolute;
163
- inset: 0;
164
- width: 100%;
165
- height: 100%;
166
- border: none;
167
- opacity: 0;
168
- transform: scale(1.02);
169
- filter: blur(4px);
170
- transition: all 0.5s var(--fluid-ease);
171
- pointer-events: none;
172
- }
173
-
174
- iframe.active {
175
- opacity: 1;
176
- transform: scale(1);
177
- filter: blur(0);
178
- pointer-events: auto;
179
- }
180
-
181
- /* --- 左下角微型监视器 --- */
182
- .nano-monitor {
183
- position: absolute;
184
- bottom: 24px;
185
- left: 24px;
186
- z-index: 100;
187
- display: flex;
188
- align-items: center;
189
- gap: 8px;
190
- background: rgba(255, 255, 255, 0.7);
191
- backdrop-filter: blur(12px);
192
- padding: 6px 14px;
193
- border-radius: 16px;
194
- border: 1px solid rgba(0, 0, 0, 0.05);
195
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
196
- font-family: monospace;
197
- transition: all 0.4s var(--fluid-ease);
198
- }
199
-
200
- .nano-monitor.is-busy {
201
- background: #000;
202
- color: #fff;
203
- border-color: rgba(255, 255, 255, 0.1);
204
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
205
- }
206
-
207
- .stat-group {
208
- display: flex;
209
- align-items: center;
210
- gap: 6px;
211
- font-size: 11px;
212
- font-weight: 700;
213
- }
214
-
215
- .divider {
216
- width: 1px;
217
- height: 12px;
218
- background: rgba(0, 0, 0, 0.1);
219
- }
220
-
221
- .is-busy .divider {
222
- background: rgba(255, 255, 255, 0.2);
223
- }
224
-
225
- .pulse-dot {
226
- width: 6px;
227
- height: 6px;
228
- border-radius: 50%;
229
- background: #10b981;
230
- }
231
-
232
- .spinner-nano {
233
- width: 10px;
234
- height: 10px;
235
- border: 2px solid rgba(255, 255, 255, 0.2);
236
- border-top-color: #fff;
237
- border-radius: 50%;
238
- animation: spin 0.8s linear infinite;
239
- display: none;
240
- }
241
-
242
- .is-busy .spinner-nano {
243
- display: block;
244
- }
245
-
246
- .is-busy .pulse-dot {
247
- display: none;
248
- }
249
-
250
- @keyframes spin {
251
- to {
252
- transform: rotate(360deg);
253
- }
254
- }
255
-
256
- .label-nano {
257
- text-transform: uppercase;
258
- letter-spacing: 0.5px;
259
- opacity: 0.5;
260
- font-size: 9px;
261
- }
262
-
263
- /* --- 设计感:Split-Expansion 作者组件 --- */
264
- .author-box {
265
- /* margin-top: auto; Moved to Token Button */
266
- width: 100%;
267
- height: 60px;
268
- position: relative;
269
- display: flex;
270
- align-items: center;
271
- justify-content: center;
272
- overflow: hidden;
273
- }
274
-
275
- /* 字母 DX 的基础样式 */
276
- .dx-letter {
277
- position: absolute;
278
- font-size: 14px;
279
- font-weight: 800;
280
- color: f5f5f5;
281
- transition: all 0.5s var(--fluid-ease) 0.4s;
282
- z-index: 10;
283
- }
284
-
285
- .letter-d {
286
- transform: translateX(-8px);
287
- }
288
-
289
- .letter-x {
290
- transform: translateX(8px);
291
- }
292
-
293
- /* 侧边栏展开时,DX 向两边消失 */
294
- .sidebar:hover .letter-d {
295
- transform: translateX(-120px);
296
- opacity: 0;
297
- transition-delay: 0s;
298
- }
299
-
300
- .sidebar:hover .letter-x {
301
- transform: translateX(120px);
302
- opacity: 0;
303
- transition-delay: 0s;
304
- }
305
-
306
- /* 中间内容的容器 */
307
- .author-content-wrap {
308
- display: flex;
309
- flex-direction: column;
310
- align-items: center;
311
- opacity: 0;
312
- transform: scale(0.9);
313
- transition: all 0.4s var(--fluid-ease) 0s;
314
- pointer-events: none;
315
- }
316
-
317
- /* 侧边栏展开时,中间内容显现 */
318
- .sidebar:hover .author-content-wrap {
319
- opacity: 1;
320
- transform: scale(1);
321
- transition-delay: 0.2s;
322
- pointer-events: auto;
323
- }
324
-
325
- .author-name-lite {
326
- font-size: 12px;
327
- font-weight: 700;
328
- margin-bottom: 8px;
329
- color: #000;
330
- }
331
-
332
- .social-row-lite {
333
- display: flex;
334
- gap: 12px;
335
- }
336
-
337
- .social-icon-lite {
338
- color: #ccc;
339
- transition: color 0.2s, transform 0.2s;
340
- }
341
-
342
- .social-icon-lite:hover {
343
- color: #000;
344
- transform: translateY(-1px);
345
- }
346
-
347
- /* --- Token Button Custom Styles --- */
348
- .nav-item.token-btn {
349
- height: 36px !important;
350
- width: 36px;
351
- border-radius: 9999px !important;
352
- background: #ffffff !important; /* White */
353
- border: 1px solid #e5e5e5 !important; /* Light gray border */
354
- color: #000000 !important;
355
- padding-left: 0 !important;
356
- justify-content: center;
357
- box-shadow: none;
358
-
359
- /* Hidden in collapsed state */
360
- opacity: 0;
361
- pointer-events: none;
362
- transform: scale(0.8);
363
- transition: all 0.3s var(--fluid-ease);
364
- }
365
-
366
- .sidebar:hover .nav-item.token-btn {
367
- width: 140px;
368
- opacity: 1;
369
- pointer-events: auto;
370
- transform: scale(1);
371
- transition-delay: 0.1s;
372
- }
373
-
374
- .nav-item.token-btn:hover {
375
- background: #f4f4f5 !important; /* Slightly darker on hover */
376
- transform: scale(1.05) !important;
377
- box-shadow: 0 4px 12px rgba(0,0,0,0.06);
378
- }
379
-
380
- .nav-item.token-btn .nav-text {
381
- color: #000000 !important;
382
- font-weight: 400;
383
- font-size: 13px;
384
- }
385
- </style>
386
- </head>
387
-
388
- <body>
389
-
390
- <div class="app-shell">
391
- <aside class="sidebar">
392
- <div class="logo-ring mb-12">
393
- <div class="w-1.5 h-1.5 bg-black rounded-full transition-colors" id="logo-dot"></div>
394
- </div>
395
-
396
- <nav>
397
- <div class="nav-item active" onclick="switchUI(this, 'zimage')">
398
- <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
399
- <path
400
- 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"
401
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
402
- </svg>
403
- <span class="nav-text">文生图</span>
404
- </div>
405
- <div class="nav-item" onclick="switchUI(this, 'enhance')">
406
- <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
407
- <path d="M13 10V3L4 14h7v7l9-11h-7z" stroke-width="2" stroke-linecap="round"
408
- stroke-linejoin="round"></path>
409
- </svg>
410
- <span class="nav-text">细节增强</span>
411
- </div>
412
- <div class="nav-item" onclick="switchUI(this, 'klein')">
413
- <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
414
- <path
415
- 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"
416
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
417
- </svg>
418
- <span class="nav-text">图片编辑</span>
419
- </div>
420
- <div class="nav-item" onclick="switchUI(this, 'angle')">
421
- <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"
422
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
423
- <path
424
- 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">
425
- </path>
426
- <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
427
- <line x1="12" y1="22.08" x2="12" y2="12"></line>
428
- </svg>
429
- <span class="nav-text">角度控制</span>
430
- </div>
431
- </nav>
432
-
433
- <div class="nav-item token-btn !mt-auto !mb-6" onclick="openTokenModal()" title="设置 API Token">
434
- <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
435
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
436
- 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" />
437
- </svg>
438
- <span class="nav-text">API Token</span>
439
- </div>
440
-
441
- <div class="author-box">
442
- <span class="dx-letter letter-d">D</span>
443
- <span class="dx-letter letter-x">X</span>
444
-
445
- <div class="author-content-wrap">
446
- <div class="author-name-lite">wuli大雄</div>
447
- <div class="social-row-lite">
448
- <a href="https://space.bilibili.com/78652351" target="_blank" class="social-icon-lite">
449
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
450
- <path
451
- 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" />
452
- </svg>
453
- </a>
454
- <a href="https://www.xiaohongshu.com/user/profile/6433c34c000000001a023538" target="_blank"
455
- class="social-icon-lite">
456
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
457
- <path
458
- 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" />
459
- </svg>
460
- </a>
461
- <a href="https://www.youtube.com/@%E5%A4%A7%E9%9B%84dx" target="_blank"
462
- class="social-icon-lite">
463
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
464
- <path
465
- 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" />
466
- </svg>
467
- </a>
468
- <a href="https://x.com/dx8152?s=21" target="_blank" class="social-icon-lite">
469
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
470
- <path
471
- 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" />
472
- </svg>
473
- </a>
474
- </div>
475
- </div>
476
- </div>
477
- </aside>
478
-
479
- <main class="stage">
480
- <iframe id="frame-zimage" src="/static/zimage.html?v=30" class="active"></iframe>
481
- <iframe id="frame-enhance" data-src="/static/enhance.html?v=30"></iframe>
482
- <iframe id="frame-klein" data-src="/static/klein.html?v=30"></iframe>
483
- <iframe id="frame-angle" data-src="/static/angle.html?v=30"></iframe>
484
-
485
- <div class="nano-monitor" id="nano-monitor">
486
- <div class="stat-group">
487
- <div class="pulse-dot animate-pulse"></div>
488
- <div class="spinner-nano"></div>
489
- <span class="label-nano">ONLINE</span>
490
- <span id="online-val">1</span>
491
- </div>
492
- <div class="divider"></div>
493
- <div class="stat-group">
494
- <span class="label-nano">QUEUE</span>
495
- <span id="queue-val">0</span>
496
- </div>
497
- </div>
498
- </main>
499
- </div>
500
-
501
- <!-- Token Modal -->
502
- <div id="token-modal" class="fixed inset-0 z-[100] hidden opacity-0 transition-opacity duration-300">
503
- <div class="absolute inset-0 bg-black/40 backdrop-blur-md" onclick="closeTokenModal()"></div>
504
-
505
- <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">
506
-
507
- <div class="p-8 pb-0">
508
- <h3 class="text-xl font-bold text-gray-900 mb-2 text-center tracking-tight">Access Token</h3>
509
- <div class="text-center mb-6">
510
- <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">
511
- 获取 API Key (Get Token) ->
512
- </a>
513
- </div>
514
-
515
- <div class="flex p-1 bg-gray-100 rounded-2xl mb-8 relative">
516
- <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>
517
- <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>
518
- </div>
519
- </div>
520
-
521
- <div class="px-8 pb-8 relative">
522
- <div id="panel-personal" class="space-y-5 transition-all duration-300">
523
- <div class="space-y-2">
524
- <div class="flex justify-between items-end">
525
- <label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Local Storage Only</label>
526
- <span class="text-[10px] text-green-500 font-bold bg-green-50 px-2 py-0.5 rounded-full">Secure</span>
527
- </div>
528
- <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...">
529
- </div>
530
- <div class="flex gap-3">
531
- <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>
532
- <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>
533
- </div>
534
- </div>
535
-
536
- <div id="panel-global" class="hidden space-y-5 transition-all duration-300 opacity-0 translate-y-4">
537
- <div class="space-y-2">
538
- <div class="flex justify-between items-end">
539
- <label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Server Configuration</label>
540
- <span class="text-[10px] text-orange-500 font-bold bg-orange-50 px-2 py-0.5 rounded-full">Shared</span>
541
- </div>
542
- <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...">
543
- </div>
544
- <div class="flex gap-3">
545
- <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>
546
- <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>
547
- </div>
548
- </div>
549
- </div>
550
-
551
- </div>
552
- </div>
553
- <script>
554
- function generateUUID() {
555
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
556
- try { return crypto.randomUUID(); } catch (e) { }
557
- }
558
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
559
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
560
- return v.toString(16);
561
- });
562
- }
563
- const CID = localStorage.getItem("client_id") || generateUUID();
564
- localStorage.setItem("client_id", CID);
565
-
566
- function switchUI(el, id) {
567
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
568
- el.classList.add('active');
569
- document.querySelectorAll('iframe').forEach(f => f.classList.remove('active'));
570
- const target = document.getElementById('frame-' + id);
571
- target.classList.add('active');
572
- if (!target.src) target.src = target.dataset.src;
573
- }
574
-
575
- async function syncStatus() {
576
- try {
577
- const res = await fetch(`/api/queue_status?client_id=${CID}`);
578
- const data = await res.json();
579
-
580
- const monitor = document.getElementById('nano-monitor');
581
- const queueVal = document.getElementById('queue-val');
582
- const logoDot = document.getElementById('logo-dot');
583
-
584
- const total = data.total || 0;
585
- const pos = data.position || 0;
586
-
587
- if (pos > 0) {
588
- monitor.classList.add('is-busy');
589
- queueVal.innerText = `${pos}/${total}`;
590
- logoDot.style.backgroundColor = '#3b82f6';
591
- } else {
592
- monitor.classList.remove('is-busy');
593
- queueVal.innerText = total > 0 ? total : '0';
594
- logoDot.style.backgroundColor = '#000';
595
- }
596
- } catch (e) { }
597
- }
598
-
599
- const host = window.location.host;
600
- if (host) {
601
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
602
- // WebSocket for queue status
603
- const ws = new WebSocket(`${protocol}://${host}/ws/stats?client_id=${CID}`);
604
- ws.onmessage = (event) => {
605
- const data = JSON.parse(event.data);
606
- if (data.type === 'stats') {
607
- document.getElementById('online-val').innerText = data.data.active_users;
608
- } else if (data.type === 'cloud_status') {
609
- // Forward cloud status to active iframe
610
- const iframe = document.querySelector('iframe.active');
611
- if (iframe && iframe.contentWindow) {
612
- iframe.contentWindow.postMessage(data, '*');
613
- }
614
- }
615
- };
616
- setInterval(syncStatus, 2000);
617
- }
618
-
619
- // --- Token Modal Logic ---
620
- const modal = document.getElementById('token-modal');
621
- const modalContent = document.getElementById('token-modal-content');
622
- const personalInput = document.getElementById('personal-token-input');
623
- const globalInput = document.getElementById('global-token-input');
624
-
625
- // 修改原有的 openTokenModal,确保每次打开默认显示个人面板
626
- window.openTokenModal = function() {
627
- modal.classList.remove('hidden');
628
- setTimeout(() => {
629
- modal.classList.remove('opacity-0');
630
- modalContent.classList.remove('scale-95');
631
- modalContent.classList.add('scale-100');
632
- }, 10);
633
- toggleTokenPanel('personal'); // 默认显示个人
634
- loadCurrentToken();
635
- }
636
-
637
- function toggleTokenPanel(type) {
638
- const pPanel = document.getElementById('panel-personal');
639
- const gPanel = document.getElementById('panel-global');
640
- const pBtn = document.getElementById('btn-personal');
641
- const gBtn = document.getElementById('btn-global');
642
-
643
- if (type === 'personal') {
644
- // Switch to Personal
645
- gPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
646
- pPanel.classList.remove('hidden');
647
- setTimeout(() => pPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
648
-
649
- pBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
650
- pBtn.classList.remove('text-gray-500');
651
- gBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
652
- gBtn.classList.add('text-gray-500');
653
- } else {
654
- // Switch to Global
655
- pPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
656
- gPanel.classList.remove('hidden');
657
- setTimeout(() => gPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
658
-
659
- gBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
660
- gBtn.classList.remove('text-gray-500');
661
- pBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
662
- pBtn.classList.add('text-gray-500');
663
- }
664
- }
665
-
666
-
667
- function closeTokenModal() {
668
- modal.classList.add('opacity-0');
669
- modalContent.classList.remove('scale-100');
670
- modalContent.classList.add('scale-95');
671
- setTimeout(() => {
672
- modal.classList.add('hidden');
673
- }, 300);
674
- }
675
-
676
- async function loadCurrentToken() {
677
- // 1. Load Personal
678
- const localToken = localStorage.getItem('modelscope_api_token');
679
- personalInput.value = localToken || '';
680
-
681
- // 2. Load Global
682
- try {
683
- const res = await fetch('/api/config/token');
684
- const data = await res.json();
685
- globalInput.value = data.token || '';
686
- } catch (e) {
687
- console.error("Failed to load global token", e);
688
- globalInput.value = '';
689
- }
690
- }
691
-
692
- function savePersonalToken() {
693
- const token = personalInput.value.trim();
694
- if (!token) {
695
- alert('请输入 Token');
696
- return;
697
- }
698
- localStorage.setItem('modelscope_api_token', token);
699
- alert('个人 Token 已保存');
700
- }
701
-
702
- function deletePersonalToken() {
703
- if (confirm('确定要删除个人 Token 吗?')) {
704
- localStorage.removeItem('modelscope_api_token');
705
- personalInput.value = '';
706
- }
707
- }
708
-
709
- async function saveGlobalToken() {
710
- const token = globalInput.value.trim();
711
- if (!token) {
712
- alert('请输入 Token');
713
- return;
714
- }
715
- if (!confirm('⚠️ 警告:全局 Token 将对所有用户可见。确定要保存吗?')) return;
716
-
717
- try {
718
- const res = await fetch('/api/config/token', {
719
- method: 'POST',
720
- headers: { 'Content-Type': 'application/json' },
721
- body: JSON.stringify({ token })
722
- });
723
- if (res.ok) {
724
- alert('全局 Token 已保存');
725
- } else {
726
- throw new Error('Save failed');
727
- }
728
- } catch (e) {
729
- alert('保存失败: ' + e.message);
730
- }
731
- }
732
-
733
- async function deleteGlobalToken() {
734
- if (!confirm('确定要删除全局 Token 吗?此操作将影响所有使用默认配置的用户。')) return;
735
-
736
- try {
737
- const res = await fetch('/api/config/token', {
738
- method: 'DELETE'
739
- });
740
- if (res.ok) {
741
- globalInput.value = '';
742
- alert('全局 Token 已删除');
743
- } else {
744
- throw new Error('Delete failed');
745
- }
746
- } catch (e) {
747
- alert('删除失败: ' + e.message);
748
- }
749
- }
750
-
751
- // Auto-open Token Modal if not set
752
- window.addEventListener('load', async () => {
753
- // Check Local
754
- const localToken = localStorage.getItem('modelscope_api_token');
755
- if (localToken) return;
756
-
757
- // Check Global
758
- try {
759
- const res = await fetch('/api/config/token');
760
- const data = await res.json();
761
- if (data.token) return;
762
- } catch (e) {}
763
-
764
- // If neither, open modal
765
- console.log("No token found, auto-opening modal");
766
- openTokenModal();
767
- });
768
- </script>
769
- </body>
770
-
771
- </html> const queueVal = document.getElementById('queue-val');
772
- const logoDot = document.getElementById('logo-dot');
773
-
774
- const total = data.total || 0;
775
- const pos = data.position || 0;
776
-
777
- if (pos > 0) {
778
- monitor.classList.add('is-busy');
779
- queueVal.innerText = `${pos}/${total}`;
780
- logoDot.style.backgroundColor = '#3b82f6';
781
- } else {
782
- monitor.classList.remove('is-busy');
783
- queueVal.innerText = total > 0 ? total : '0';
784
- logoDot.style.backgroundColor = '#000';
785
- }
786
- } catch (e) { }
787
- }
788
-
789
- const host = window.location.host;
790
- if (host) {
791
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
792
- const ws = new WebSocket(`${protocol}://${host}/ws/stats`);
793
- ws.onmessage = (e) => {
794
- const d = JSON.parse(e.data);
795
- if (d.online_count) {
796
- document.getElementById('online-val').innerText = d.online_count;
797
- }
798
- };
799
- setInterval(syncStatus, 2000);
800
- }
801
- </script>
802
- </body>
803
-
804
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="icon" href="/static/logo.png" type="image/png">
8
+ <title>AI Studio</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&display=swap');
12
+
13
+ :root {
14
+ --accent: #000000;
15
+ --fluid-ease: cubic-bezier(0.3, 0, 0, 1);
16
+ }
17
+
18
+ /* --- 极简悬浮浅灰滚动条 (无底色/左移) --- */
19
+ *::-webkit-scrollbar {
20
+ width: 10px !important;
21
+ height: 10px !important;
22
+ background: transparent !important;
23
+ }
24
+
25
+ *::-webkit-scrollbar-track {
26
+ background: transparent !important;
27
+ border: none !important;
28
+ }
29
+
30
+ *::-webkit-scrollbar-thumb {
31
+ background-color: #d8d8d8 !important;
32
+ border: 3px solid transparent !important;
33
+ border-right-width: 5px !important;
34
+ /* 增加右侧间距,使滚动条向左位移 */
35
+ background-clip: padding-box !important;
36
+ border-radius: 10px !important;
37
+ }
38
+
39
+ *::-webkit-scrollbar-thumb:hover {
40
+ background-color: #c0c0c0 !important;
41
+ }
42
+
43
+ *::-webkit-scrollbar-corner {
44
+ background: transparent !important;
45
+ }
46
+
47
+ * {
48
+ scrollbar-width: thin !important;
49
+ scrollbar-color: #d8d8d8 transparent !important;
50
+ }
51
+
52
+ body {
53
+ background: #ffffff;
54
+ font-family: 'Space Grotesk', sans-serif;
55
+ overflow: hidden;
56
+ height: 100vh;
57
+ color: #121212;
58
+ }
59
+
60
+ .app-shell {
61
+ display: flex;
62
+ width: 100%;
63
+ height: 100vh;
64
+ background: #fff;
65
+ position: relative;
66
+ }
67
+
68
+ /* 侧边栏 */
69
+ .sidebar {
70
+ width: 80px;
71
+ min-width: 80px;
72
+ background: #fff;
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: center;
76
+ border-right: 1px solid #f2f2f2;
77
+ padding: 40px 0;
78
+ transition: width 0.5s var(--fluid-ease) 0.5s;
79
+ z-index: 50;
80
+ }
81
+
82
+ .sidebar:hover {
83
+ width: 220px;
84
+ transition-delay: 0s;
85
+ }
86
+
87
+ .logo-ring {
88
+ width: 36px;
89
+ height: 36px;
90
+ border: 2px solid var(--accent);
91
+ border-radius: 12px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ transition: all 0.6s var(--fluid-ease) 0.5s;
96
+ }
97
+
98
+ .sidebar:hover .logo-ring {
99
+ transform: rotate(90deg);
100
+ border-radius: 50%;
101
+ transition-delay: 0s;
102
+ }
103
+
104
+ /* 导航项 */
105
+ .nav-item {
106
+ position: relative;
107
+ width: 48px;
108
+ height: 48px;
109
+ margin: 10px 0;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: flex-start;
113
+ border-radius: 18px;
114
+ cursor: pointer;
115
+ transition: all 0.3s var(--fluid-ease) 0.5s;
116
+ color: #999;
117
+ overflow: hidden;
118
+ padding-left: 14px;
119
+ }
120
+
121
+ .sidebar:hover .nav-item {
122
+ width: 190px;
123
+ transition-delay: 0s;
124
+ }
125
+
126
+ .nav-item:hover {
127
+ background: #fafafa;
128
+ color: #000;
129
+ }
130
+
131
+ .nav-item.active {
132
+ background: var(--accent);
133
+ color: #fff;
134
+ }
135
+
136
+ .nav-text {
137
+ opacity: 0;
138
+ margin-left: 16px;
139
+ font-weight: 600;
140
+ font-size: 14px;
141
+ white-space: nowrap;
142
+ transition: opacity 0.3s 0.5s;
143
+ }
144
+
145
+ .sidebar:hover .nav-text {
146
+ opacity: 1;
147
+ transition-delay: 0.1s;
148
+ }
149
+
150
+ /* 主舞台区 */
151
+ .stage {
152
+ flex: 1;
153
+ background: #fcfcfc;
154
+ margin: 16px;
155
+ border-radius: 32px;
156
+ overflow: hidden;
157
+ border: 1px solid #f0f0f0;
158
+ position: relative;
159
+ }
160
+
161
+ iframe {
162
+ position: absolute;
163
+ inset: 0;
164
+ width: 100%;
165
+ height: 100%;
166
+ border: none;
167
+ opacity: 0;
168
+ transform: scale(1.02);
169
+ filter: blur(4px);
170
+ transition: all 0.5s var(--fluid-ease);
171
+ pointer-events: none;
172
+ }
173
+
174
+ iframe.active {
175
+ opacity: 1;
176
+ transform: scale(1);
177
+ filter: blur(0);
178
+ pointer-events: auto;
179
+ }
180
+
181
+ /* --- 左下角微型监视器 --- */
182
+ .nano-monitor {
183
+ position: absolute;
184
+ bottom: 24px;
185
+ left: 24px;
186
+ z-index: 100;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ background: rgba(255, 255, 255, 0.7);
191
+ backdrop-filter: blur(12px);
192
+ padding: 6px 14px;
193
+ border-radius: 16px;
194
+ border: 1px solid rgba(0, 0, 0, 0.05);
195
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
196
+ font-family: monospace;
197
+ transition: all 0.4s var(--fluid-ease);
198
+ }
199
+
200
+ .nano-monitor.is-busy {
201
+ background: #000;
202
+ color: #fff;
203
+ border-color: rgba(255, 255, 255, 0.1);
204
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
205
+ }
206
+
207
+ .stat-group {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 6px;
211
+ font-size: 11px;
212
+ font-weight: 700;
213
+ }
214
+
215
+ .divider {
216
+ width: 1px;
217
+ height: 12px;
218
+ background: rgba(0, 0, 0, 0.1);
219
+ }
220
+
221
+ .is-busy .divider {
222
+ background: rgba(255, 255, 255, 0.2);
223
+ }
224
+
225
+ .pulse-dot {
226
+ width: 6px;
227
+ height: 6px;
228
+ border-radius: 50%;
229
+ background: #10b981;
230
+ }
231
+
232
+ .spinner-nano {
233
+ width: 10px;
234
+ height: 10px;
235
+ border: 2px solid rgba(255, 255, 255, 0.2);
236
+ border-top-color: #fff;
237
+ border-radius: 50%;
238
+ animation: spin 0.8s linear infinite;
239
+ display: none;
240
+ }
241
+
242
+ .is-busy .spinner-nano {
243
+ display: block;
244
+ }
245
+
246
+ .is-busy .pulse-dot {
247
+ display: none;
248
+ }
249
+
250
+ @keyframes spin {
251
+ to {
252
+ transform: rotate(360deg);
253
+ }
254
+ }
255
+
256
+ .label-nano {
257
+ text-transform: uppercase;
258
+ letter-spacing: 0.5px;
259
+ opacity: 0.5;
260
+ font-size: 9px;
261
+ }
262
+
263
+ /* --- 设计感:Split-Expansion 作者组件 --- */
264
+ .author-box {
265
+ /* margin-top: auto; Moved to Token Button */
266
+ width: 100%;
267
+ height: 60px;
268
+ position: relative;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ overflow: hidden;
273
+ }
274
+
275
+ /* 字母 DX 的基础样式 */
276
+ .dx-letter {
277
+ position: absolute;
278
+ font-size: 14px;
279
+ font-weight: 800;
280
+ color: f5f5f5;
281
+ transition: all 0.5s var(--fluid-ease) 0.4s;
282
+ z-index: 10;
283
+ }
284
+
285
+ .letter-d {
286
+ transform: translateX(-8px);
287
+ }
288
+
289
+ .letter-x {
290
+ transform: translateX(8px);
291
+ }
292
+
293
+ /* 侧边栏展开时,DX 向两边消失 */
294
+ .sidebar:hover .letter-d {
295
+ transform: translateX(-120px);
296
+ opacity: 0;
297
+ transition-delay: 0s;
298
+ }
299
+
300
+ .sidebar:hover .letter-x {
301
+ transform: translateX(120px);
302
+ opacity: 0;
303
+ transition-delay: 0s;
304
+ }
305
+
306
+ /* 中间内容的容器 */
307
+ .author-content-wrap {
308
+ display: flex;
309
+ flex-direction: column;
310
+ align-items: center;
311
+ opacity: 0;
312
+ transform: scale(0.9);
313
+ transition: all 0.4s var(--fluid-ease) 0s;
314
+ pointer-events: none;
315
+ }
316
+
317
+ /* 侧边栏展开时,中间内容显现 */
318
+ .sidebar:hover .author-content-wrap {
319
+ opacity: 1;
320
+ transform: scale(1);
321
+ transition-delay: 0.2s;
322
+ pointer-events: auto;
323
+ }
324
+
325
+ .author-name-lite {
326
+ font-size: 12px;
327
+ font-weight: 700;
328
+ margin-bottom: 8px;
329
+ color: #000;
330
+ }
331
+
332
+ .social-row-lite {
333
+ display: flex;
334
+ gap: 12px;
335
+ }
336
+
337
+ .social-icon-lite {
338
+ color: #ccc;
339
+ transition: color 0.2s, transform 0.2s;
340
+ }
341
+
342
+ .social-icon-lite:hover {
343
+ color: #000;
344
+ transform: translateY(-1px);
345
+ }
346
+
347
+ /* --- Token Button Custom Styles --- */
348
+ .nav-item.token-btn {
349
+ height: 36px !important;
350
+ width: 36px;
351
+ border-radius: 9999px !important;
352
+ background: #ffffff !important; /* White */
353
+ border: 1px solid #e5e5e5 !important; /* Light gray border */
354
+ color: #000000 !important;
355
+ padding-left: 0 !important;
356
+ justify-content: center;
357
+ box-shadow: none;
358
+
359
+ /* Hidden in collapsed state */
360
+ opacity: 0;
361
+ pointer-events: none;
362
+ transform: scale(0.8);
363
+ transition: all 0.3s var(--fluid-ease);
364
+ }
365
+
366
+ .sidebar:hover .nav-item.token-btn {
367
+ width: 140px;
368
+ opacity: 1;
369
+ pointer-events: auto;
370
+ transform: scale(1);
371
+ transition-delay: 0.1s;
372
+ }
373
+
374
+ .nav-item.token-btn:hover {
375
+ background: #f4f4f5 !important; /* Slightly darker on hover */
376
+ transform: scale(1.05) !important;
377
+ box-shadow: 0 4px 12px rgba(0,0,0,0.06);
378
+ }
379
+
380
+ .nav-item.token-btn .nav-text {
381
+ color: #000000 !important;
382
+ font-weight: 400;
383
+ font-size: 13px;
384
+ }
385
+ </style>
386
+ </head>
387
+
388
+ <body>
389
+
390
+ <div class="app-shell">
391
+ <aside class="sidebar">
392
+ <div class="logo-ring mb-12">
393
+ <div class="w-1.5 h-1.5 bg-black rounded-full transition-colors" id="logo-dot"></div>
394
+ </div>
395
+
396
+ <nav>
397
+ <div class="nav-item active" onclick="switchUI(this, 'zimage')">
398
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
399
+ <path
400
+ 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"
401
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
402
+ </svg>
403
+ <span class="nav-text">文生图</span>
404
+ </div>
405
+ <div class="nav-item" onclick="switchUI(this, 'enhance')">
406
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
407
+ <path d="M13 10V3L4 14h7v7l9-11h-7z" stroke-width="2" stroke-linecap="round"
408
+ stroke-linejoin="round"></path>
409
+ </svg>
410
+ <span class="nav-text">细节增强</span>
411
+ </div>
412
+ <div class="nav-item" onclick="switchUI(this, 'klein')">
413
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
414
+ <path
415
+ 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"
416
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
417
+ </svg>
418
+ <span class="nav-text">图片编辑</span>
419
+ </div>
420
+ <div class="nav-item" onclick="switchUI(this, 'angle')">
421
+ <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"
422
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
423
+ <path
424
+ 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">
425
+ </path>
426
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
427
+ <line x1="12" y1="22.08" x2="12" y2="12"></line>
428
+ </svg>
429
+ <span class="nav-text">角度控制</span>
430
+ </div>
431
+ </nav>
432
+
433
+ <div class="nav-item token-btn !mt-auto !mb-6" onclick="openTokenModal()" title="设置 API Token">
434
+ <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
435
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
436
+ 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" />
437
+ </svg>
438
+ <span class="nav-text">API Token</span>
439
+ </div>
440
+
441
+ <div class="author-box">
442
+ <span class="dx-letter letter-d">D</span>
443
+ <span class="dx-letter letter-x">X</span>
444
+
445
+ <div class="author-content-wrap">
446
+ <div class="author-name-lite">wuli大雄</div>
447
+ <div class="social-row-lite">
448
+ <a href="https://space.bilibili.com/78652351" target="_blank" class="social-icon-lite">
449
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
450
+ <path
451
+ 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" />
452
+ </svg>
453
+ </a>
454
+ <a href="https://www.xiaohongshu.com/user/profile/6433c34c000000001a023538" target="_blank"
455
+ class="social-icon-lite">
456
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
457
+ <path
458
+ 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" />
459
+ </svg>
460
+ </a>
461
+ <a href="https://www.youtube.com/@%E5%A4%A7%E9%9B%84dx" target="_blank"
462
+ class="social-icon-lite">
463
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
464
+ <path
465
+ 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" />
466
+ </svg>
467
+ </a>
468
+ <a href="https://x.com/dx8152?s=21" target="_blank" class="social-icon-lite">
469
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
470
+ <path
471
+ 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" />
472
+ </svg>
473
+ </a>
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </aside>
478
+
479
+ <main class="stage">
480
+ <iframe id="frame-zimage" src="/static/zimage.html?v=30" class="active"></iframe>
481
+ <iframe id="frame-enhance" data-src="/static/enhance.html?v=30"></iframe>
482
+ <iframe id="frame-klein" data-src="/static/klein.html?v=30"></iframe>
483
+ <iframe id="frame-angle" data-src="/static/angle.html?v=30"></iframe>
484
+
485
+ <div class="nano-monitor" id="nano-monitor">
486
+ <div class="stat-group">
487
+ <div class="pulse-dot animate-pulse"></div>
488
+ <div class="spinner-nano"></div>
489
+ <span class="label-nano">ONLINE</span>
490
+ <span id="online-val">1</span>
491
+ </div>
492
+ <div class="divider"></div>
493
+ <div class="stat-group">
494
+ <span class="label-nano">QUEUE</span>
495
+ <span id="queue-val">0</span>
496
+ </div>
497
+ </div>
498
+ </main>
499
+ </div>
500
+
501
+ <!-- Token Modal -->
502
+ <div id="token-modal" class="fixed inset-0 z-[100] hidden opacity-0 transition-opacity duration-300">
503
+ <div class="absolute inset-0 bg-black/40 backdrop-blur-md" onclick="closeTokenModal()"></div>
504
+
505
+ <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">
506
+
507
+ <div class="p-8 pb-0">
508
+ <h3 class="text-xl font-bold text-gray-900 mb-2 text-center tracking-tight">Access Token</h3>
509
+ <div class="text-center mb-6">
510
+ <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">
511
+ 获取 API Key (Get Token) ->
512
+ </a>
513
+ </div>
514
+
515
+ <div class="flex p-1 bg-gray-100 rounded-2xl mb-8 relative">
516
+ <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>
517
+ <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>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="px-8 pb-8 relative">
522
+ <div id="panel-personal" class="space-y-5 transition-all duration-300">
523
+ <div class="space-y-2">
524
+ <div class="flex justify-between items-end">
525
+ <label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Local Storage Only</label>
526
+ <span class="text-[10px] text-green-500 font-bold bg-green-50 px-2 py-0.5 rounded-full">Secure</span>
527
+ </div>
528
+ <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...">
529
+ </div>
530
+ <div class="flex gap-3">
531
+ <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>
532
+ <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>
533
+ </div>
534
+ </div>
535
+
536
+ <div id="panel-global" class="hidden space-y-5 transition-all duration-300 opacity-0 translate-y-4">
537
+ <div class="space-y-2">
538
+ <div class="flex justify-between items-end">
539
+ <label class="text-[10px] font-bold text-gray-400 uppercase tracking-[0.2em]">Server Configuration</label>
540
+ <span class="text-[10px] text-orange-500 font-bold bg-orange-50 px-2 py-0.5 rounded-full">Shared</span>
541
+ </div>
542
+ <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...">
543
+ </div>
544
+ <div class="flex gap-3">
545
+ <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>
546
+ <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>
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ </div>
552
+ </div>
553
+ <script>
554
+ function generateUUID() {
555
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
556
+ try { return crypto.randomUUID(); } catch (e) { }
557
+ }
558
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
559
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
560
+ return v.toString(16);
561
+ });
562
+ }
563
+ const CID = localStorage.getItem("client_id") || generateUUID();
564
+ localStorage.setItem("client_id", CID);
565
+
566
+ function switchUI(el, id) {
567
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
568
+ el.classList.add('active');
569
+ document.querySelectorAll('iframe').forEach(f => f.classList.remove('active'));
570
+ const target = document.getElementById('frame-' + id);
571
+ target.classList.add('active');
572
+ if (!target.src) target.src = target.dataset.src;
573
+ }
574
+
575
+ async function syncStatus() {
576
+ try {
577
+ const res = await fetch(`/api/queue_status?client_id=${CID}`);
578
+ const data = await res.json();
579
+
580
+ const monitor = document.getElementById('nano-monitor');
581
+ const queueVal = document.getElementById('queue-val');
582
+ const logoDot = document.getElementById('logo-dot');
583
+
584
+ const total = data.total || 0;
585
+ const pos = data.position || 0;
586
+
587
+ if (pos > 0) {
588
+ monitor.classList.add('is-busy');
589
+ queueVal.innerText = `${pos}/${total}`;
590
+ logoDot.style.backgroundColor = '#3b82f6';
591
+ } else {
592
+ monitor.classList.remove('is-busy');
593
+ queueVal.innerText = total > 0 ? total : '0';
594
+ logoDot.style.backgroundColor = '#000';
595
+ }
596
+ } catch (e) { }
597
+ }
598
+
599
+ const host = window.location.host;
600
+ if (host) {
601
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
602
+ // WebSocket for queue status
603
+ const ws = new WebSocket(`${protocol}://${host}/ws/stats?client_id=${CID}`);
604
+ ws.onmessage = (event) => {
605
+ const data = JSON.parse(event.data);
606
+ if (data.type === 'stats') {
607
+ document.getElementById('online-val').innerText = data.online_count;
608
+ } else if (data.type === 'cloud_status') {
609
+ // Forward cloud status to active iframe
610
+ const iframe = document.querySelector('iframe.active');
611
+ if (iframe && iframe.contentWindow) {
612
+ iframe.contentWindow.postMessage(data, '*');
613
+ }
614
+ }
615
+ };
616
+ setInterval(syncStatus, 2000);
617
+ }
618
+
619
+ // --- Token Modal Logic ---
620
+ const modal = document.getElementById('token-modal');
621
+ const modalContent = document.getElementById('token-modal-content');
622
+ const personalInput = document.getElementById('personal-token-input');
623
+ const globalInput = document.getElementById('global-token-input');
624
+
625
+ // 修改原有的 openTokenModal,确保每次打开默认显示个人面板
626
+ window.openTokenModal = function() {
627
+ modal.classList.remove('hidden');
628
+ setTimeout(() => {
629
+ modal.classList.remove('opacity-0');
630
+ modalContent.classList.remove('scale-95');
631
+ modalContent.classList.add('scale-100');
632
+ }, 10);
633
+ toggleTokenPanel('personal'); // 默认显示个人
634
+ loadCurrentToken();
635
+ }
636
+
637
+ function toggleTokenPanel(type) {
638
+ const pPanel = document.getElementById('panel-personal');
639
+ const gPanel = document.getElementById('panel-global');
640
+ const pBtn = document.getElementById('btn-personal');
641
+ const gBtn = document.getElementById('btn-global');
642
+
643
+ if (type === 'personal') {
644
+ // Switch to Personal
645
+ gPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
646
+ pPanel.classList.remove('hidden');
647
+ setTimeout(() => pPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
648
+
649
+ pBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
650
+ pBtn.classList.remove('text-gray-500');
651
+ gBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
652
+ gBtn.classList.add('text-gray-500');
653
+ } else {
654
+ // Switch to Global
655
+ pPanel.classList.add('hidden', 'opacity-0', 'translate-y-4');
656
+ gPanel.classList.remove('hidden');
657
+ setTimeout(() => gPanel.classList.remove('opacity-0', 'translate-y-4'), 10);
658
+
659
+ gBtn.classList.add('bg-white', 'text-black', 'shadow-sm');
660
+ gBtn.classList.remove('text-gray-500');
661
+ pBtn.classList.remove('bg-white', 'text-black', 'shadow-sm');
662
+ pBtn.classList.add('text-gray-500');
663
+ }
664
+ }
665
+
666
+
667
+ function closeTokenModal() {
668
+ modal.classList.add('opacity-0');
669
+ modalContent.classList.remove('scale-100');
670
+ modalContent.classList.add('scale-95');
671
+ setTimeout(() => {
672
+ modal.classList.add('hidden');
673
+ }, 300);
674
+ }
675
+
676
+ async function loadCurrentToken() {
677
+ // 1. Load Personal
678
+ const localToken = localStorage.getItem('modelscope_api_token');
679
+ personalInput.value = localToken || '';
680
+
681
+ // 2. Load Global
682
+ try {
683
+ const res = await fetch('/api/config/token');
684
+ const data = await res.json();
685
+ globalInput.value = data.token || '';
686
+ } catch (e) {
687
+ console.error("Failed to load global token", e);
688
+ globalInput.value = '';
689
+ }
690
+ }
691
+
692
+ function savePersonalToken() {
693
+ const token = personalInput.value.trim();
694
+ if (!token) {
695
+ alert('请输入 Token');
696
+ return;
697
+ }
698
+ localStorage.setItem('modelscope_api_token', token);
699
+ alert('个人 Token 已保存');
700
+ }
701
+
702
+ function deletePersonalToken() {
703
+ if (confirm('确定要删除个人 Token 吗?')) {
704
+ localStorage.removeItem('modelscope_api_token');
705
+ personalInput.value = '';
706
+ }
707
+ }
708
+
709
+ async function saveGlobalToken() {
710
+ const token = globalInput.value.trim();
711
+ if (!token) {
712
+ alert('请输入 Token');
713
+ return;
714
+ }
715
+ if (!confirm('⚠️ 警告:全局 Token 将对所有用户可见。确定要保存吗?')) return;
716
+
717
+ try {
718
+ const res = await fetch('/api/config/token', {
719
+ method: 'POST',
720
+ headers: { 'Content-Type': 'application/json' },
721
+ body: JSON.stringify({ token })
722
+ });
723
+ if (res.ok) {
724
+ alert('全局 Token 已保存');
725
+ } else {
726
+ throw new Error('Save failed');
727
+ }
728
+ } catch (e) {
729
+ alert('保存失败: ' + e.message);
730
+ }
731
+ }
732
+
733
+ async function deleteGlobalToken() {
734
+ if (!confirm('确定要删除全局 Token 吗?此操作将影响所有使用默认配置的用户。')) return;
735
+
736
+ try {
737
+ const res = await fetch('/api/config/token', {
738
+ method: 'DELETE'
739
+ });
740
+ if (res.ok) {
741
+ globalInput.value = '';
742
+ alert('全局 Token 已删除');
743
+ } else {
744
+ throw new Error('Delete failed');
745
+ }
746
+ } catch (e) {
747
+ alert('删除失败: ' + e.message);
748
+ }
749
+ }
750
+
751
+ // Auto-open Token Modal if not set
752
+ window.addEventListener('load', async () => {
753
+ // Check Local
754
+ const localToken = localStorage.getItem('modelscope_api_token');
755
+ if (localToken) return;
756
+
757
+ // Check Global
758
+ try {
759
+ const res = await fetch('/api/config/token');
760
+ const data = await res.json();
761
+ if (data.token) return;
762
+ } catch (e) {}
763
+
764
+ // If neither, open modal
765
+ console.log("No token found, auto-opening modal");
766
+ openTokenModal();
767
+ });
768
+ </script>
769
+ </body>
770
+
771
+ </html> const queueVal = document.getElementById('queue-val');
772
+ const logoDot = document.getElementById('logo-dot');
773
+
774
+ const total = data.total || 0;
775
+ const pos = data.position || 0;
776
+
777
+ if (pos > 0) {
778
+ monitor.classList.add('is-busy');
779
+ queueVal.innerText = `${pos}/${total}`;
780
+ logoDot.style.backgroundColor = '#3b82f6';
781
+ } else {
782
+ monitor.classList.remove('is-busy');
783
+ queueVal.innerText = total > 0 ? total : '0';
784
+ logoDot.style.backgroundColor = '#000';
785
+ }
786
+ } catch (e) { }
787
+ }
788
+
789
+ const host = window.location.host;
790
+ if (host) {
791
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
792
+ const ws = new WebSocket(`${protocol}://${host}/ws/stats`);
793
+ ws.onmessage = (e) => {
794
+ const d = JSON.parse(e.data);
795
+ if (d.online_count) {
796
+ document.getElementById('online-val').innerText = d.online_count;
797
+ }
798
+ };
799
+ setInterval(syncStatus, 2000);
800
+ }
801
+ </script>
802
+ </body>
803
+
804
  </html>