gitinstall 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/web_ui.html ADDED
@@ -0,0 +1,2277 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>gitinstall — GitHub 开源项目一键安装</title>
7
+ <style>
8
+ @import url('https://fonts.loli.net/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
9
+
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+
12
+ :root {
13
+ --bg: #0a0a0f;
14
+ --bg-card: rgba(255,255,255,0.03);
15
+ --bg-card-hover: rgba(255,255,255,0.06);
16
+ --bg-glass: rgba(255,255,255,0.04);
17
+ --bg-elevated: rgba(255,255,255,0.06);
18
+ --border: rgba(255,255,255,0.06);
19
+ --border-hover: rgba(255,255,255,0.12);
20
+ --border-accent: rgba(139,92,246,0.3);
21
+ --text: #f0f0f5;
22
+ --text-sec: #94949e;
23
+ --text-muted: #5a5a66;
24
+ --accent: #8b5cf6;
25
+ --accent-2: #6366f1;
26
+ --accent-3: #a78bfa;
27
+ --accent-glow: rgba(139,92,246,0.15);
28
+ --accent-glow-strong: rgba(139,92,246,0.3);
29
+ --cyan: #22d3ee;
30
+ --green: #34d399;
31
+ --green-bg: rgba(52,211,153,0.08);
32
+ --red: #f87171;
33
+ --red-bg: rgba(248,113,113,0.08);
34
+ --amber: #fbbf24;
35
+ --mono: 'JetBrains Mono','SF Mono','Fira Code',Consolas,monospace;
36
+ --sans: 'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
37
+ --radius: 16px;
38
+ --radius-sm: 10px;
39
+ --radius-xs: 6px;
40
+ }
41
+
42
+ body {
43
+ font-family: var(--sans);
44
+ background: var(--bg);
45
+ color: var(--text);
46
+ line-height: 1.6;
47
+ min-height: 100vh;
48
+ overflow-x: hidden;
49
+ }
50
+
51
+ /* ── Animated Background ──────────────── */
52
+ body::before {
53
+ content: '';
54
+ position: fixed;
55
+ inset: 0;
56
+ background:
57
+ radial-gradient(ellipse 80% 60% at 10% 5%, rgba(139,92,246,0.08) 0%, transparent 60%),
58
+ radial-gradient(ellipse 60% 50% at 90% 20%, rgba(34,211,238,0.05) 0%, transparent 50%),
59
+ radial-gradient(ellipse 70% 60% at 50% 90%, rgba(99,102,241,0.06) 0%, transparent 50%);
60
+ pointer-events: none;
61
+ z-index: 0;
62
+ }
63
+
64
+ /* ── Noise texture ────────────────────── */
65
+ body::after {
66
+ content: '';
67
+ position: fixed;
68
+ inset: 0;
69
+ opacity: 0.015;
70
+ background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
71
+ pointer-events: none;
72
+ z-index: 0;
73
+ }
74
+
75
+ a { color: var(--accent-3); text-decoration: none; }
76
+ a:hover { text-decoration: underline; }
77
+
78
+ /* ── Layout ───────────────────────────── */
79
+ .app {
80
+ position: relative;
81
+ z-index: 1;
82
+ max-width: 1080px;
83
+ margin: 0 auto;
84
+ padding: 2rem 2rem 6rem;
85
+ }
86
+
87
+ /* ── Header ───────────────────────────── */
88
+ header {
89
+ text-align: center;
90
+ padding: 3.5rem 0 2rem;
91
+ }
92
+ .logo {
93
+ font-size: 2.8rem;
94
+ font-weight: 800;
95
+ letter-spacing: -0.03em;
96
+ background: linear-gradient(135deg, #a78bfa 0%, #818cf8 30%, #22d3ee 70%, #34d399 100%);
97
+ -webkit-background-clip: text;
98
+ -webkit-text-fill-color: transparent;
99
+ background-clip: text;
100
+ display: inline-block;
101
+ }
102
+ .subtitle {
103
+ color: var(--text-sec);
104
+ margin-top: 0.5rem;
105
+ font-size: 1.1rem;
106
+ font-weight: 400;
107
+ letter-spacing: 0.01em;
108
+ }
109
+ .subtitle strong {
110
+ color: var(--text);
111
+ font-weight: 600;
112
+ }
113
+
114
+ /* ── Glass Card ───────────────────────── */
115
+ .glass {
116
+ background: var(--bg-card);
117
+ backdrop-filter: blur(20px);
118
+ -webkit-backdrop-filter: blur(20px);
119
+ border: 1px solid var(--border);
120
+ border-radius: var(--radius);
121
+ padding: 1.5rem;
122
+ margin-bottom: 1.25rem;
123
+ transition: border-color 0.3s, box-shadow 0.3s;
124
+ }
125
+ .glass:hover {
126
+ border-color: var(--border-hover);
127
+ }
128
+ .section-label {
129
+ font-size: 0.7rem;
130
+ font-weight: 700;
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.12em;
133
+ color: var(--text-muted);
134
+ margin-bottom: 1rem;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.5rem;
138
+ }
139
+ .section-label::after {
140
+ content: '';
141
+ flex: 1;
142
+ height: 1px;
143
+ background: linear-gradient(90deg, var(--border), transparent);
144
+ }
145
+
146
+ /* ── Search ───────────────────────────── */
147
+ .search-wrap {
148
+ position: relative;
149
+ margin-bottom: 1rem;
150
+ }
151
+ .search-glow {
152
+ position: absolute;
153
+ inset: -1px;
154
+ border-radius: 14px;
155
+ background: linear-gradient(135deg, rgba(139,92,246,0.2), rgba(34,211,238,0.1));
156
+ opacity: 0;
157
+ transition: opacity 0.3s;
158
+ z-index: 0;
159
+ filter: blur(1px);
160
+ }
161
+ .search-wrap:focus-within .search-glow {
162
+ opacity: 1;
163
+ }
164
+ .search-inner {
165
+ position: relative;
166
+ z-index: 1;
167
+ display: flex;
168
+ gap: 0;
169
+ background: rgba(255,255,255,0.03);
170
+ border: 1px solid var(--border);
171
+ border-radius: 13px;
172
+ overflow: hidden;
173
+ transition: border-color 0.3s, box-shadow 0.3s;
174
+ }
175
+ .search-wrap:focus-within .search-inner {
176
+ border-color: var(--border-accent);
177
+ }
178
+ .search-input {
179
+ flex: 1;
180
+ padding: 1rem 1.25rem;
181
+ background: transparent;
182
+ border: none;
183
+ color: var(--text);
184
+ font-size: 1.05rem;
185
+ font-family: var(--mono);
186
+ font-weight: 500;
187
+ outline: none;
188
+ min-width: 0;
189
+ }
190
+ .search-input::placeholder {
191
+ color: var(--text-muted);
192
+ font-family: var(--sans);
193
+ font-weight: 400;
194
+ }
195
+ .btn-search {
196
+ padding: 1rem 2rem;
197
+ background: linear-gradient(135deg, var(--accent), var(--accent-2));
198
+ color: #fff;
199
+ border: none;
200
+ font-size: 0.95rem;
201
+ font-weight: 700;
202
+ font-family: var(--sans);
203
+ cursor: pointer;
204
+ transition: all 0.2s;
205
+ letter-spacing: 0.02em;
206
+ white-space: nowrap;
207
+ }
208
+ .btn-search:hover:not(:disabled) {
209
+ filter: brightness(1.15);
210
+ }
211
+ .btn-search:disabled {
212
+ opacity: 0.5;
213
+ cursor: not-allowed;
214
+ }
215
+
216
+ /* ── Path Selector ────────────────────── */
217
+ .path-bar {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 0.75rem;
221
+ padding: 0.65rem 1rem;
222
+ background: rgba(255,255,255,0.02);
223
+ border: 1px solid var(--border);
224
+ border-radius: var(--radius-sm);
225
+ font-size: 0.85rem;
226
+ transition: border-color 0.2s;
227
+ }
228
+ .path-bar:focus-within {
229
+ border-color: var(--border-accent);
230
+ }
231
+ .path-icon {
232
+ color: var(--accent-3);
233
+ flex-shrink: 0;
234
+ font-size: 0.9rem;
235
+ }
236
+ .path-label {
237
+ color: var(--text-sec);
238
+ font-size: 0.8rem;
239
+ white-space: nowrap;
240
+ flex-shrink: 0;
241
+ }
242
+ .path-sep {
243
+ width: 1px;
244
+ height: 18px;
245
+ background: var(--border);
246
+ flex-shrink: 0;
247
+ }
248
+ .path-input {
249
+ flex: 1;
250
+ background: transparent;
251
+ border: none;
252
+ color: var(--text);
253
+ font-family: var(--mono);
254
+ font-size: 0.85rem;
255
+ font-weight: 500;
256
+ outline: none;
257
+ min-width: 0;
258
+ }
259
+ .path-input::placeholder {
260
+ color: var(--text-muted);
261
+ font-family: var(--sans);
262
+ font-weight: 400;
263
+ }
264
+
265
+ /* ── Trending ─────────────────────────── */
266
+ .filter-bar {
267
+ display: flex;
268
+ gap: 0.3rem;
269
+ margin-bottom: 1rem;
270
+ }
271
+ .filter-pill {
272
+ padding: 0.35rem 0.85rem;
273
+ border-radius: 100px;
274
+ font-size: 0.78rem;
275
+ font-weight: 600;
276
+ color: var(--text-sec);
277
+ background: transparent;
278
+ border: 1px solid transparent;
279
+ cursor: pointer;
280
+ transition: all 0.2s;
281
+ white-space: nowrap;
282
+ }
283
+ .filter-pill:hover {
284
+ color: var(--text);
285
+ background: rgba(255,255,255,0.04);
286
+ }
287
+ .filter-pill.active {
288
+ color: var(--accent-3);
289
+ background: var(--accent-glow);
290
+ border-color: rgba(139,92,246,0.2);
291
+ }
292
+
293
+ .trending-grid {
294
+ display: grid;
295
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
296
+ gap: 0.75rem;
297
+ }
298
+ .t-card {
299
+ position: relative;
300
+ display: flex;
301
+ gap: 1rem;
302
+ padding: 1.1rem 1.2rem;
303
+ border-radius: var(--radius-sm);
304
+ background: rgba(255,255,255,0.02);
305
+ border: 1px solid var(--border);
306
+ cursor: pointer;
307
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
308
+ overflow: hidden;
309
+ }
310
+ .t-card::before {
311
+ content: '';
312
+ position: absolute;
313
+ inset: 0;
314
+ background: linear-gradient(135deg, rgba(139,92,246,0.06), transparent 60%);
315
+ opacity: 0;
316
+ transition: opacity 0.3s;
317
+ }
318
+ .t-card:hover {
319
+ border-color: var(--border-accent);
320
+ transform: translateY(-2px);
321
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px rgba(139,92,246,0.1);
322
+ }
323
+ .t-card:hover::before { opacity: 1; }
324
+ .t-card:active { transform: translateY(0); }
325
+
326
+ .t-icon {
327
+ font-size: 2rem;
328
+ flex-shrink: 0;
329
+ width: 44px;
330
+ height: 44px;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ border-radius: 12px;
335
+ background: rgba(255,255,255,0.04);
336
+ position: relative;
337
+ z-index: 1;
338
+ }
339
+ .t-body {
340
+ flex: 1;
341
+ min-width: 0;
342
+ position: relative;
343
+ z-index: 1;
344
+ }
345
+ .t-head {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.5rem;
349
+ margin-bottom: 0.2rem;
350
+ }
351
+ .t-name {
352
+ font-weight: 700;
353
+ font-size: 0.95rem;
354
+ color: var(--text);
355
+ }
356
+ .t-tag {
357
+ font-size: 0.6rem;
358
+ padding: 0.12rem 0.45rem;
359
+ border-radius: 100px;
360
+ font-weight: 700;
361
+ letter-spacing: 0.04em;
362
+ text-transform: uppercase;
363
+ }
364
+ .t-tag[data-tag="AI"] { background: rgba(139,92,246,0.15); color: var(--accent-3); }
365
+ .t-tag[data-tag="Web"] { background: rgba(34,211,238,0.12); color: var(--cyan); }
366
+ .t-tag[data-tag="工具"] { background: rgba(251,191,36,0.12); color: var(--amber); }
367
+ .t-tag[data-tag="IoT"] { background: rgba(52,211,153,0.12); color: var(--green); }
368
+
369
+ .t-desc {
370
+ font-size: 0.8rem;
371
+ color: var(--text-sec);
372
+ line-height: 1.45;
373
+ display: -webkit-box;
374
+ -webkit-line-clamp: 2;
375
+ -webkit-box-orient: vertical;
376
+ overflow: hidden;
377
+ }
378
+ .t-foot {
379
+ display: flex;
380
+ gap: 0.75rem;
381
+ margin-top: 0.45rem;
382
+ font-size: 0.72rem;
383
+ color: var(--text-muted);
384
+ font-weight: 500;
385
+ }
386
+ .t-foot span { display: flex; align-items: center; gap: 0.25rem; }
387
+ .t-stars { color: var(--amber); }
388
+
389
+ /* ── Env section ──────────────────────── */
390
+ .env-grid {
391
+ display: grid;
392
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
393
+ gap: 0.6rem;
394
+ }
395
+ .env-item {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 0.6rem;
399
+ padding: 0.6rem 0.75rem;
400
+ border-radius: var(--radius-xs);
401
+ background: rgba(255,255,255,0.02);
402
+ font-size: 0.82rem;
403
+ }
404
+ .env-icon {
405
+ font-size: 1rem;
406
+ width: 1.5rem;
407
+ text-align: center;
408
+ flex-shrink: 0;
409
+ }
410
+ .env-value {
411
+ color: var(--text);
412
+ font-weight: 500;
413
+ font-size: 0.8rem;
414
+ }
415
+
416
+ /* ── Two-column layout ────────────────── */
417
+ .info-grid {
418
+ display: grid;
419
+ gap: 1.25rem;
420
+ }
421
+ @media (min-width: 700px) {
422
+ .info-grid { grid-template-columns: 320px 1fr; }
423
+ }
424
+
425
+ /* ── Plan ─────────────────────────────── */
426
+ .plan-header {
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: space-between;
430
+ margin-bottom: 0.75rem;
431
+ }
432
+ .plan-project {
433
+ font-size: 1.05rem;
434
+ font-weight: 700;
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 0.4rem;
438
+ }
439
+ .plan-meta { font-size: 0.78rem; color: var(--text-muted); }
440
+ .badge {
441
+ display: inline-block;
442
+ padding: 0.15rem 0.55rem;
443
+ border-radius: 100px;
444
+ font-size: 0.65rem;
445
+ font-weight: 700;
446
+ text-transform: uppercase;
447
+ letter-spacing: 0.04em;
448
+ }
449
+ .badge-high { background: var(--green-bg); color: var(--green); }
450
+ .badge-medium { background: rgba(251,191,36,0.1); color: var(--amber); }
451
+ .badge-low { background: var(--red-bg); color: var(--red); }
452
+
453
+ .steps-list { display: flex; flex-direction: column; gap: 0.2rem; }
454
+ .step {
455
+ display: flex;
456
+ align-items: flex-start;
457
+ gap: 0.6rem;
458
+ padding: 0.65rem 0.75rem;
459
+ border-radius: var(--radius-xs);
460
+ transition: all 0.25s;
461
+ }
462
+ .step.active { background: var(--accent-glow); }
463
+ .step.success { background: var(--green-bg); }
464
+ .step.error { background: var(--red-bg); }
465
+
466
+ .step-indicator {
467
+ width: 24px; height: 24px;
468
+ border-radius: 50%;
469
+ display: flex; align-items: center; justify-content: center;
470
+ flex-shrink: 0;
471
+ font-size: 0.65rem;
472
+ font-weight: 800;
473
+ margin-top: 1px;
474
+ transition: all 0.3s;
475
+ }
476
+ .step-indicator.pending {
477
+ background: rgba(255,255,255,0.04);
478
+ color: var(--text-muted);
479
+ border: 1.5px solid rgba(255,255,255,0.1);
480
+ }
481
+ .step-indicator.active {
482
+ background: var(--accent);
483
+ color: #fff;
484
+ border: none;
485
+ animation: glow-pulse 1.5s ease-in-out infinite;
486
+ }
487
+ .step-indicator.success { background: var(--green); color: #fff; border: none; }
488
+ .step-indicator.error { background: var(--red); color: #fff; border: none; }
489
+
490
+ @keyframes glow-pulse {
491
+ 0%,100% { box-shadow: 0 0 0 0 var(--accent-glow-strong); }
492
+ 50% { box-shadow: 0 0 0 8px transparent; }
493
+ }
494
+
495
+ .step-body { flex: 1; min-width: 0; }
496
+ .step-desc { font-size: 0.85rem; font-weight: 600; color: var(--text); }
497
+ .step-cmd-row {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 0.4rem;
501
+ margin-top: 0.15rem;
502
+ }
503
+ .step-cmd {
504
+ font-family: var(--mono);
505
+ font-size: 0.75rem;
506
+ color: var(--text-muted);
507
+ word-break: break-all;
508
+ flex: 1;
509
+ min-width: 0;
510
+ }
511
+ .btn-copy-step {
512
+ flex-shrink: 0;
513
+ padding: 0.2rem 0.5rem;
514
+ font-size: 0.65rem;
515
+ font-weight: 600;
516
+ font-family: var(--sans);
517
+ color: var(--text-muted);
518
+ background: rgba(255,255,255,0.04);
519
+ border: 1px solid var(--border);
520
+ border-radius: var(--radius-xs);
521
+ cursor: pointer;
522
+ transition: all 0.2s;
523
+ white-space: nowrap;
524
+ }
525
+ .btn-copy-step:hover {
526
+ color: var(--accent-3);
527
+ border-color: var(--border-hover);
528
+ background: rgba(255,255,255,0.06);
529
+ }
530
+ .btn-copy-step.copied {
531
+ color: var(--green);
532
+ border-color: rgba(52,211,153,0.3);
533
+ }
534
+
535
+ /* ── Install Guide ────────────────────── */
536
+ .guide-section {
537
+ margin-top: 0.75rem;
538
+ padding-top: 0.75rem;
539
+ border-top: 1px solid var(--border);
540
+ }
541
+ .guide-toggle {
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 0.4rem;
545
+ cursor: pointer;
546
+ font-size: 0.82rem;
547
+ font-weight: 600;
548
+ color: var(--text-sec);
549
+ padding: 0.3rem 0;
550
+ transition: color 0.2s;
551
+ user-select: none;
552
+ }
553
+ .guide-toggle:hover { color: var(--text); }
554
+ .guide-toggle .arrow { transition: transform 0.2s; font-size: 0.7rem; }
555
+ .guide-toggle .arrow.open { transform: rotate(90deg); }
556
+ .guide-body {
557
+ overflow: hidden;
558
+ max-height: 0;
559
+ transition: max-height 0.3s ease;
560
+ }
561
+ .guide-body.open { max-height: 800px; }
562
+ .guide-content {
563
+ padding: 0.75rem 0 0.25rem;
564
+ font-size: 0.8rem;
565
+ color: var(--text-sec);
566
+ line-height: 1.7;
567
+ }
568
+ .guide-content ol {
569
+ padding-left: 1.4rem;
570
+ display: flex;
571
+ flex-direction: column;
572
+ gap: 0.4rem;
573
+ }
574
+ .guide-content li { padding-left: 0.2rem; }
575
+ .guide-content li code {
576
+ font-family: var(--mono);
577
+ font-size: 0.73rem;
578
+ background: rgba(255,255,255,0.04);
579
+ padding: 0.1rem 0.35rem;
580
+ border-radius: 3px;
581
+ color: var(--accent-3);
582
+ }
583
+ .guide-tip {
584
+ margin-top: 0.5rem;
585
+ padding: 0.55rem 0.7rem;
586
+ background: rgba(251,191,36,0.05);
587
+ border-left: 2px solid var(--amber);
588
+ border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
589
+ font-size: 0.75rem;
590
+ color: var(--text-sec);
591
+ }
592
+ .guide-copy-all {
593
+ margin-top: 0.5rem;
594
+ padding: 0.4rem 0.8rem;
595
+ font-size: 0.73rem;
596
+ font-weight: 600;
597
+ font-family: var(--sans);
598
+ color: var(--accent-3);
599
+ background: rgba(139,92,246,0.08);
600
+ border: 1px solid rgba(139,92,246,0.2);
601
+ border-radius: var(--radius-xs);
602
+ cursor: pointer;
603
+ transition: all 0.2s;
604
+ }
605
+ .guide-copy-all:hover { background: rgba(139,92,246,0.15); }
606
+
607
+ .step-dur {
608
+ font-size: 0.7rem;
609
+ font-family: var(--mono);
610
+ color: var(--text-muted);
611
+ margin-left: auto;
612
+ flex-shrink: 0;
613
+ align-self: center;
614
+ }
615
+
616
+ .plan-actions {
617
+ display: flex;
618
+ justify-content: flex-end;
619
+ gap: 0.5rem;
620
+ margin-top: 1rem;
621
+ padding-top: 0.75rem;
622
+ border-top: 1px solid var(--border);
623
+ }
624
+ .btn {
625
+ padding: 0.7rem 1.5rem;
626
+ border-radius: var(--radius-sm);
627
+ border: none;
628
+ font-size: 0.88rem;
629
+ font-weight: 700;
630
+ font-family: var(--sans);
631
+ cursor: pointer;
632
+ transition: all 0.2s;
633
+ letter-spacing: 0.01em;
634
+ white-space: nowrap;
635
+ }
636
+ .btn-outline {
637
+ background: transparent;
638
+ color: var(--text-sec);
639
+ border: 1px solid var(--border-hover);
640
+ }
641
+ .btn-outline:hover { border-color: var(--text-sec); color: var(--text); }
642
+ .btn-success {
643
+ background: linear-gradient(135deg, #059669, var(--green));
644
+ color: #0a0a0f;
645
+ }
646
+ .btn-success:hover:not(:disabled) {
647
+ filter: brightness(1.1);
648
+ box-shadow: 0 4px 20px rgba(52,211,153,0.25);
649
+ }
650
+ .btn-success:disabled { opacity: 0.5; cursor: not-allowed; }
651
+
652
+ /* ── Terminal ─────────────────────────── */
653
+ .terminal {
654
+ background: rgba(0,0,0,0.5);
655
+ border: 1px solid var(--border);
656
+ border-radius: var(--radius-sm);
657
+ padding: 1rem 1.25rem;
658
+ font-family: var(--mono);
659
+ font-size: 0.78rem;
660
+ line-height: 1.6;
661
+ max-height: 420px;
662
+ overflow-y: auto;
663
+ color: #c4c4cc;
664
+ white-space: pre-wrap;
665
+ word-break: break-all;
666
+ }
667
+ .terminal::-webkit-scrollbar { width: 4px; }
668
+ .terminal::-webkit-scrollbar-track { background: transparent; }
669
+ .terminal::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
670
+ .term-prompt { color: var(--accent-3); font-weight: 600; }
671
+ .term-success { color: var(--green); }
672
+ .term-error { color: var(--red); }
673
+
674
+ /* ── Result Banner ────────────────────── */
675
+ .result-banner {
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 1rem;
679
+ padding: 1.5rem;
680
+ border-radius: var(--radius);
681
+ animation: fadeUp .4s cubic-bezier(0.4,0,0.2,1);
682
+ }
683
+ .result-banner.success {
684
+ background: linear-gradient(135deg, rgba(52,211,153,0.08), rgba(52,211,153,0.02));
685
+ border: 1px solid rgba(52,211,153,0.15);
686
+ }
687
+ .result-banner.fail {
688
+ background: linear-gradient(135deg, rgba(248,113,113,0.08), rgba(248,113,113,0.02));
689
+ border: 1px solid rgba(248,113,113,0.15);
690
+ }
691
+ .result-icon { font-size: 2.2rem; flex-shrink: 0; }
692
+ .result-body { flex: 1; }
693
+ .result-title { font-size: 1.1rem; font-weight: 700; }
694
+ .result-sub { font-size: 0.85rem; color: var(--text-sec); margin-top: 0.2rem; }
695
+ .result-launch {
696
+ display: flex;
697
+ align-items: center;
698
+ gap: 0.5rem;
699
+ margin-top: 0.6rem;
700
+ padding: 0.55rem 0.75rem;
701
+ background: rgba(0,0,0,0.3);
702
+ border: 1px solid var(--border);
703
+ border-radius: var(--radius-xs);
704
+ font-family: var(--mono);
705
+ font-size: 0.8rem;
706
+ color: var(--text);
707
+ }
708
+ .copy-btn {
709
+ margin-left: auto;
710
+ padding: 0.2rem 0.6rem;
711
+ font-size: 0.7rem;
712
+ cursor: pointer;
713
+ border-radius: 4px;
714
+ background: rgba(255,255,255,0.06);
715
+ color: var(--text-sec);
716
+ border: 1px solid var(--border);
717
+ font-weight: 600;
718
+ font-family: var(--sans);
719
+ transition: all .15s;
720
+ }
721
+ .copy-btn:hover { color: var(--text); border-color: var(--text-sec); }
722
+
723
+ /* ── Error Toast ──────────────────────── */
724
+ .error-toast {
725
+ padding: 0.85rem 1.1rem;
726
+ border-radius: var(--radius-sm);
727
+ background: linear-gradient(135deg, rgba(248,113,113,0.08), rgba(248,113,113,0.02));
728
+ border: 1px solid rgba(248,113,113,0.15);
729
+ color: var(--red);
730
+ font-size: 0.88rem;
731
+ font-weight: 500;
732
+ margin-bottom: 1rem;
733
+ animation: fadeUp .25s;
734
+ }
735
+
736
+ /* ── Progress Bar ─────────────────────── */
737
+ .progress-wrap {
738
+ height: 3px;
739
+ background: rgba(255,255,255,0.04);
740
+ border-radius: 2px;
741
+ margin-bottom: 1.25rem;
742
+ overflow: hidden;
743
+ }
744
+ .progress-fill {
745
+ height: 100%;
746
+ background: linear-gradient(90deg, var(--accent), var(--cyan));
747
+ border-radius: 2px;
748
+ transition: width .5s cubic-bezier(0.4,0,0.2,1);
749
+ width: 0%;
750
+ }
751
+
752
+ /* ── Spinner ──────────────────────────── */
753
+ .spinner {
754
+ display: inline-block;
755
+ width: 14px; height: 14px;
756
+ border: 2px solid rgba(255,255,255,0.1);
757
+ border-top-color: var(--accent);
758
+ border-radius: 50%;
759
+ animation: spin .6s linear infinite;
760
+ vertical-align: middle;
761
+ margin-right: 0.4rem;
762
+ }
763
+ @keyframes spin { to { transform: rotate(360deg); } }
764
+
765
+ @keyframes fadeUp {
766
+ from { opacity: 0; transform: translateY(10px); }
767
+ to { opacity: 1; transform: translateY(0); }
768
+ }
769
+
770
+ .loading-text {
771
+ color: var(--text-muted);
772
+ font-size: 0.85rem;
773
+ padding: 0.5rem 0;
774
+ }
775
+
776
+ /* ── Footer ───────────────────────────── */
777
+ footer {
778
+ text-align: center;
779
+ padding: 3rem 0 1rem;
780
+ color: var(--text-muted);
781
+ font-size: 0.75rem;
782
+ letter-spacing: 0.02em;
783
+ }
784
+ footer a { color: var(--text-sec); }
785
+ .footer-line {
786
+ width: 40px;
787
+ height: 2px;
788
+ background: linear-gradient(90deg, transparent, var(--border), transparent);
789
+ margin: 0 auto 1rem;
790
+ border-radius: 1px;
791
+ }
792
+
793
+ /* ── Responsive ───────────────────────── */
794
+ @media (max-width: 699px) {
795
+ .app { padding: 1rem 1rem 4rem; }
796
+ header { padding: 2rem 0 1.5rem; }
797
+ .logo { font-size: 2rem; }
798
+ .glass { padding: 1.1rem; }
799
+ .search-input { padding: 0.85rem 1rem; font-size: 0.95rem; }
800
+ .btn-search { padding: 0.85rem 1.2rem; }
801
+ .trending-grid { grid-template-columns: 1fr; }
802
+ .env-grid { grid-template-columns: 1fr 1fr; }
803
+ .path-bar { flex-wrap: wrap; }
804
+ }
805
+
806
+ /* ── Analysis Progress ─────────────── */
807
+ .analyze-bar {
808
+ height: 3px;
809
+ background: rgba(255,255,255,0.04);
810
+ border-radius: 2px;
811
+ overflow: hidden;
812
+ margin-bottom: 1rem;
813
+ }
814
+ .analyze-bar-fill {
815
+ height: 100%;
816
+ width: 30%;
817
+ background: linear-gradient(90deg, var(--accent), var(--cyan), var(--accent));
818
+ border-radius: 2px;
819
+ animation: indeterminate 1.5s ease-in-out infinite;
820
+ }
821
+ @keyframes indeterminate {
822
+ 0% { transform: translateX(-100%); }
823
+ 100% { transform: translateX(400%); }
824
+ }
825
+ .analyze-steps { display: flex; flex-direction: column; gap: 0.35rem; }
826
+ .a-step {
827
+ font-size: 0.85rem;
828
+ color: var(--text-muted);
829
+ display: flex; align-items: center; gap: 0.6rem;
830
+ padding: 0.3rem 0;
831
+ transition: all 0.3s;
832
+ }
833
+ .a-step.active { color: var(--text); }
834
+ .a-step.done { color: var(--green); }
835
+ .a-step .a-dot {
836
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
837
+ background: rgba(255,255,255,0.1);
838
+ transition: all 0.3s;
839
+ }
840
+ .a-step.active .a-dot {
841
+ background: var(--accent);
842
+ box-shadow: 0 0 8px var(--accent-glow-strong);
843
+ animation: glow-pulse 1.5s ease-in-out infinite;
844
+ }
845
+ .a-step.done .a-dot { background: var(--green); }
846
+ .a-step .a-check { display: none; }
847
+ .a-step.done .a-check { display: inline; }
848
+ .a-step.done .a-dot { display: none; }
849
+ .analyze-timer {
850
+ margin-top: 0.75rem;
851
+ font-size: 0.78rem;
852
+ font-family: var(--mono);
853
+ color: var(--text-muted);
854
+ }
855
+
856
+ /* ── GitHub Search Button ─────────── */
857
+ .btn-gh-search {
858
+ padding: 1rem 1.2rem;
859
+ background: transparent;
860
+ color: var(--text-sec);
861
+ border: none;
862
+ border-left: 1px solid var(--border);
863
+ font-size: 0.88rem;
864
+ font-weight: 600;
865
+ font-family: var(--sans);
866
+ cursor: pointer;
867
+ transition: all 0.2s;
868
+ white-space: nowrap;
869
+ }
870
+ .btn-gh-search:hover:not(:disabled) { color: var(--text); background: rgba(255,255,255,0.03); }
871
+ .btn-gh-search:disabled { opacity: 0.5; cursor: not-allowed; }
872
+
873
+ /* ── Trending Header ─────────────── */
874
+ .trending-header {
875
+ display: flex;
876
+ align-items: center;
877
+ justify-content: space-between;
878
+ margin-bottom: 1rem;
879
+ }
880
+ .trending-meta {
881
+ font-size: 0.72rem;
882
+ color: var(--text-muted);
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 0.75rem;
886
+ }
887
+ .btn-refresh {
888
+ padding: 0.25rem 0.6rem;
889
+ border-radius: 6px;
890
+ font-size: 0.72rem;
891
+ font-weight: 600;
892
+ color: var(--text-sec);
893
+ background: rgba(255,255,255,0.03);
894
+ border: 1px solid var(--border);
895
+ cursor: pointer;
896
+ transition: all 0.2s;
897
+ font-family: var(--sans);
898
+ }
899
+ .btn-refresh:hover:not(:disabled) { color: var(--text); border-color: var(--border-hover); }
900
+ .btn-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
901
+ .btn-refresh.spinning { animation: spin 0.8s linear infinite; }
902
+
903
+ /* ── Search Results Dropdown ─────── */
904
+ .search-results {
905
+ position: absolute;
906
+ top: 100%;
907
+ left: 0; right: 0;
908
+ z-index: 100;
909
+ margin-top: 0.4rem;
910
+ background: rgba(12,12,20,0.98);
911
+ backdrop-filter: blur(24px);
912
+ -webkit-backdrop-filter: blur(24px);
913
+ border: 1px solid var(--border-hover);
914
+ border-radius: var(--radius-sm);
915
+ max-height: 420px;
916
+ overflow-y: auto;
917
+ box-shadow: 0 16px 48px rgba(0,0,0,0.5);
918
+ animation: fadeUp 0.2s;
919
+ }
920
+ .search-results::-webkit-scrollbar { width: 4px; }
921
+ .search-results::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
922
+ .sr-header {
923
+ padding: 0.6rem 1rem;
924
+ font-size: 0.7rem;
925
+ font-weight: 700;
926
+ text-transform: uppercase;
927
+ letter-spacing: 0.1em;
928
+ color: var(--text-muted);
929
+ border-bottom: 1px solid var(--border);
930
+ }
931
+ .sr-item {
932
+ display: flex;
933
+ align-items: flex-start;
934
+ gap: 0.75rem;
935
+ padding: 0.75rem 1rem;
936
+ cursor: pointer;
937
+ transition: background 0.15s;
938
+ border-bottom: 1px solid rgba(255,255,255,0.03);
939
+ }
940
+ .sr-item:last-child { border-bottom: none; }
941
+ .sr-item:hover { background: rgba(255,255,255,0.04); }
942
+ .sr-name {
943
+ font-weight: 700;
944
+ font-size: 0.88rem;
945
+ color: var(--text);
946
+ font-family: var(--mono);
947
+ }
948
+ .sr-desc {
949
+ font-size: 0.78rem;
950
+ color: var(--text-sec);
951
+ margin-top: 0.1rem;
952
+ display: -webkit-box;
953
+ -webkit-line-clamp: 1;
954
+ -webkit-box-orient: vertical;
955
+ overflow: hidden;
956
+ }
957
+ .sr-meta {
958
+ display: flex;
959
+ gap: 0.6rem;
960
+ margin-top: 0.2rem;
961
+ font-size: 0.7rem;
962
+ color: var(--text-muted);
963
+ font-weight: 500;
964
+ }
965
+ .sr-meta .sr-stars { color: var(--amber); }
966
+
967
+ [hidden] { display: none !important; }
968
+
969
+ /* ── Header layout ────────────────────── */
970
+ .header-top {
971
+ display: flex;
972
+ align-items: center;
973
+ justify-content: space-between;
974
+ }
975
+ .user-area { display: flex; gap: 0.5rem; align-items: center; }
976
+ .btn-user {
977
+ background: var(--bg-elevated);
978
+ border: 1px solid var(--border);
979
+ color: var(--text-sec);
980
+ padding: 0.35rem 0.9rem;
981
+ border-radius: 20px;
982
+ font-size: 0.8rem;
983
+ cursor: pointer;
984
+ transition: all 0.2s;
985
+ font-family: var(--sans);
986
+ }
987
+ .btn-user:hover {
988
+ border-color: var(--accent);
989
+ color: var(--text);
990
+ background: var(--accent-glow);
991
+ }
992
+ .btn-user.logged-in {
993
+ border-color: var(--green);
994
+ color: var(--green);
995
+ background: var(--green-bg);
996
+ }
997
+ .user-badge {
998
+ font-size: 0.7rem;
999
+ background: var(--accent);
1000
+ color: #fff;
1001
+ padding: 0.15rem 0.5rem;
1002
+ border-radius: 10px;
1003
+ }
1004
+
1005
+ /* ── Modal ────────────────────────────── */
1006
+ .modal-overlay {
1007
+ position: fixed;
1008
+ inset: 0;
1009
+ background: rgba(0,0,0,0.6);
1010
+ backdrop-filter: blur(8px);
1011
+ z-index: 1000;
1012
+ display: flex;
1013
+ align-items: center;
1014
+ justify-content: center;
1015
+ }
1016
+ .modal-box {
1017
+ background: #141420;
1018
+ border: 1px solid var(--border-hover);
1019
+ border-radius: var(--radius);
1020
+ padding: 2rem;
1021
+ width: 90%;
1022
+ max-width: 380px;
1023
+ position: relative;
1024
+ }
1025
+ .modal-close {
1026
+ position: absolute;
1027
+ top: 0.8rem;
1028
+ right: 1rem;
1029
+ background: none;
1030
+ border: none;
1031
+ color: var(--text-muted);
1032
+ font-size: 1.2rem;
1033
+ cursor: pointer;
1034
+ }
1035
+ .modal-close:hover { color: var(--text); }
1036
+ .modal-tabs {
1037
+ display: flex;
1038
+ gap: 1rem;
1039
+ margin-bottom: 1.2rem;
1040
+ border-bottom: 1px solid var(--border);
1041
+ padding-bottom: 0.5rem;
1042
+ }
1043
+ .modal-tab {
1044
+ font-size: 0.9rem;
1045
+ color: var(--text-muted);
1046
+ cursor: pointer;
1047
+ padding-bottom: 0.3rem;
1048
+ border-bottom: 2px solid transparent;
1049
+ transition: all 0.2s;
1050
+ }
1051
+ .modal-tab.active {
1052
+ color: var(--text);
1053
+ border-bottom-color: var(--accent);
1054
+ }
1055
+ .modal-input {
1056
+ display: block;
1057
+ width: 100%;
1058
+ padding: 0.6rem 0.8rem;
1059
+ margin-bottom: 0.6rem;
1060
+ background: var(--bg-glass);
1061
+ border: 1px solid var(--border);
1062
+ border-radius: var(--radius-xs);
1063
+ color: var(--text);
1064
+ font-size: 0.85rem;
1065
+ font-family: var(--sans);
1066
+ outline: none;
1067
+ transition: border-color 0.2s;
1068
+ }
1069
+ .modal-input:focus {
1070
+ border-color: var(--accent);
1071
+ }
1072
+ .auth-msg {
1073
+ margin-top: 0.5rem;
1074
+ font-size: 0.8rem;
1075
+ min-height: 1.2rem;
1076
+ }
1077
+ .auth-msg.ok { color: var(--green); }
1078
+ .auth-msg.err { color: var(--red); }
1079
+ .user-profile {
1080
+ text-align: center;
1081
+ padding: 1rem 0;
1082
+ }
1083
+ .user-profile .user-name {
1084
+ font-size: 1.2rem;
1085
+ font-weight: 700;
1086
+ margin-bottom: 0.3rem;
1087
+ }
1088
+ .user-profile .user-tier {
1089
+ display: inline-block;
1090
+ background: var(--accent-glow);
1091
+ border: 1px solid var(--accent);
1092
+ color: var(--accent-3);
1093
+ padding: 0.2rem 0.8rem;
1094
+ border-radius: 12px;
1095
+ font-size: 0.75rem;
1096
+ margin-bottom: 0.8rem;
1097
+ }
1098
+ .user-profile .quota-info {
1099
+ font-size: 0.8rem;
1100
+ color: var(--text-sec);
1101
+ }
1102
+ .btn-logout {
1103
+ display: block;
1104
+ width: 100%;
1105
+ margin-top: 1rem;
1106
+ padding: 0.5rem;
1107
+ background: var(--red-bg);
1108
+ border: 1px solid rgba(248,113,113,0.3);
1109
+ color: var(--red);
1110
+ border-radius: var(--radius-xs);
1111
+ cursor: pointer;
1112
+ font-size: 0.85rem;
1113
+ font-family: var(--sans);
1114
+ transition: all 0.2s;
1115
+ }
1116
+ .btn-logout:hover {
1117
+ background: rgba(248,113,113,0.15);
1118
+ }
1119
+ .link-forgot {
1120
+ font-size: 0.78rem;
1121
+ color: var(--accent-3);
1122
+ text-decoration: none;
1123
+ transition: color 0.2s;
1124
+ }
1125
+ .link-forgot:hover { color: var(--accent); }
1126
+
1127
+ /* ── Hero Section ─────────────────── */
1128
+ .hero {
1129
+ text-align: center;
1130
+ padding: 4rem 0 1rem;
1131
+ position: relative;
1132
+ }
1133
+ .hero::before {
1134
+ content: '';
1135
+ position: absolute;
1136
+ top: -20px;
1137
+ left: 50%;
1138
+ transform: translateX(-50%);
1139
+ width: 600px;
1140
+ height: 600px;
1141
+ background: radial-gradient(circle, rgba(139,92,246,0.12) 0%, rgba(34,211,238,0.06) 40%, transparent 70%);
1142
+ pointer-events: none;
1143
+ z-index: -1;
1144
+ }
1145
+ .hero .logo {
1146
+ font-size: 3.4rem;
1147
+ margin-bottom: 0.3rem;
1148
+ }
1149
+ .hero-tagline {
1150
+ font-size: 1.5rem;
1151
+ font-weight: 300;
1152
+ color: var(--text);
1153
+ letter-spacing: -0.01em;
1154
+ margin-bottom: 0.4rem;
1155
+ }
1156
+ .hero-tagline strong {
1157
+ font-weight: 700;
1158
+ background: linear-gradient(135deg, #a78bfa, #22d3ee);
1159
+ -webkit-background-clip: text;
1160
+ -webkit-text-fill-color: transparent;
1161
+ background-clip: text;
1162
+ }
1163
+ .hero-sub {
1164
+ color: var(--text-sec);
1165
+ font-size: 0.95rem;
1166
+ margin-bottom: 2rem;
1167
+ }
1168
+ .hero-badges {
1169
+ display: flex;
1170
+ justify-content: center;
1171
+ gap: 0.5rem;
1172
+ flex-wrap: wrap;
1173
+ margin-bottom: 1.5rem;
1174
+ }
1175
+ .hero-badge {
1176
+ display: inline-flex;
1177
+ align-items: center;
1178
+ gap: 0.35rem;
1179
+ padding: 0.35rem 0.85rem;
1180
+ border-radius: 100px;
1181
+ font-size: 0.78rem;
1182
+ font-weight: 600;
1183
+ background: rgba(255,255,255,0.03);
1184
+ border: 1px solid var(--border);
1185
+ color: var(--text-sec);
1186
+ transition: all 0.2s;
1187
+ }
1188
+ .hero-badge:hover {
1189
+ border-color: var(--border-hover);
1190
+ background: rgba(255,255,255,0.05);
1191
+ }
1192
+ .hero-badge .hb-icon { font-size: 0.9rem; }
1193
+ .hero-badge .hb-val { color: var(--text); font-family: var(--mono); font-weight: 700; }
1194
+
1195
+ /* ── Stats Counters ──────────────── */
1196
+ .stats-row {
1197
+ display: grid;
1198
+ grid-template-columns: repeat(4, 1fr);
1199
+ gap: 0.75rem;
1200
+ margin-bottom: 1.5rem;
1201
+ }
1202
+ @media (max-width: 600px) {
1203
+ .stats-row { grid-template-columns: repeat(2, 1fr); }
1204
+ }
1205
+ .stat-card {
1206
+ text-align: center;
1207
+ padding: 1.2rem 0.5rem;
1208
+ background: var(--bg-card);
1209
+ border: 1px solid var(--border);
1210
+ border-radius: var(--radius-sm);
1211
+ transition: all 0.3s;
1212
+ }
1213
+ .stat-card:hover {
1214
+ border-color: var(--border-accent);
1215
+ box-shadow: 0 4px 20px rgba(139,92,246,0.08);
1216
+ }
1217
+ .stat-num {
1218
+ font-size: 1.6rem;
1219
+ font-weight: 800;
1220
+ font-family: var(--mono);
1221
+ background: linear-gradient(135deg, var(--accent-3), var(--cyan));
1222
+ -webkit-background-clip: text;
1223
+ -webkit-text-fill-color: transparent;
1224
+ background-clip: text;
1225
+ line-height: 1.2;
1226
+ }
1227
+ .stat-label {
1228
+ font-size: 0.72rem;
1229
+ color: var(--text-muted);
1230
+ font-weight: 500;
1231
+ margin-top: 0.25rem;
1232
+ text-transform: uppercase;
1233
+ letter-spacing: 0.06em;
1234
+ }
1235
+
1236
+ /* ── Feature Chips ───────────────── */
1237
+ .feature-row {
1238
+ display: flex;
1239
+ justify-content: center;
1240
+ gap: 0.75rem;
1241
+ flex-wrap: wrap;
1242
+ margin-bottom: 2rem;
1243
+ }
1244
+ .feat-chip {
1245
+ display: flex;
1246
+ align-items: center;
1247
+ gap: 0.5rem;
1248
+ padding: 0.6rem 1rem;
1249
+ background: var(--bg-card);
1250
+ border: 1px solid var(--border);
1251
+ border-radius: var(--radius-sm);
1252
+ font-size: 0.82rem;
1253
+ color: var(--text-sec);
1254
+ transition: all 0.25s;
1255
+ cursor: default;
1256
+ }
1257
+ .feat-chip:hover {
1258
+ border-color: var(--border-accent);
1259
+ color: var(--text);
1260
+ transform: translateY(-1px);
1261
+ }
1262
+ .feat-chip .fc-icon {
1263
+ font-size: 1.1rem;
1264
+ width: 28px;
1265
+ height: 28px;
1266
+ display: flex;
1267
+ align-items: center;
1268
+ justify-content: center;
1269
+ border-radius: 8px;
1270
+ background: rgba(139,92,246,0.1);
1271
+ flex-shrink: 0;
1272
+ }
1273
+ .feat-chip .fc-text { font-weight: 600; }
1274
+ .feat-chip .fc-sub { font-size: 0.7rem; color: var(--text-muted); }
1275
+
1276
+ /* ── CTA Install Bar ─────────────── */
1277
+ .cta-bar {
1278
+ display: flex;
1279
+ justify-content: center;
1280
+ gap: 0.75rem;
1281
+ margin-bottom: 2.5rem;
1282
+ flex-wrap: wrap;
1283
+ }
1284
+ .cta-cmd {
1285
+ display: flex;
1286
+ align-items: center;
1287
+ gap: 0.6rem;
1288
+ padding: 0.65rem 1.2rem;
1289
+ background: rgba(0,0,0,0.4);
1290
+ border: 1px solid var(--border);
1291
+ border-radius: var(--radius-sm);
1292
+ font-family: var(--mono);
1293
+ font-size: 0.85rem;
1294
+ color: var(--text);
1295
+ cursor: pointer;
1296
+ transition: all 0.2s;
1297
+ }
1298
+ .cta-cmd:hover {
1299
+ border-color: var(--border-accent);
1300
+ background: rgba(0,0,0,0.5);
1301
+ }
1302
+ .cta-cmd .cta-prompt { color: var(--green); font-weight: 600; }
1303
+ .cta-cmd .cta-copy {
1304
+ font-size: 0.7rem;
1305
+ color: var(--text-muted);
1306
+ padding: 0.15rem 0.4rem;
1307
+ border-radius: 4px;
1308
+ background: rgba(255,255,255,0.06);
1309
+ border: 1px solid var(--border);
1310
+ font-family: var(--sans);
1311
+ font-weight: 600;
1312
+ cursor: pointer;
1313
+ transition: all 0.15s;
1314
+ }
1315
+ .cta-cmd .cta-copy:hover { color: var(--text); border-color: var(--border-hover); }
1316
+
1317
+ .divider {
1318
+ width: 60px;
1319
+ height: 2px;
1320
+ background: linear-gradient(90deg, transparent, var(--border-accent), transparent);
1321
+ margin: 0 auto 2rem;
1322
+ border-radius: 1px;
1323
+ }
1324
+
1325
+ /* ── Responsive hero ─────────────── */
1326
+ @media (max-width: 699px) {
1327
+ .hero { padding: 2.5rem 0 0.5rem; }
1328
+ .hero .logo { font-size: 2.4rem; }
1329
+ .hero-tagline { font-size: 1.15rem; }
1330
+ .stats-row { gap: 0.5rem; }
1331
+ .stat-num { font-size: 1.2rem; }
1332
+ .feature-row { gap: 0.5rem; }
1333
+ .feat-chip { padding: 0.5rem 0.75rem; font-size: 0.78rem; }
1334
+ }
1335
+ </style>
1336
+ </head>
1337
+ <body>
1338
+ <div class="app">
1339
+
1340
+ <header class="hero">
1341
+ <div class="header-top">
1342
+ <div class="logo">gitinstall</div>
1343
+ <div class="user-area" id="user-area">
1344
+ <button class="btn-user" id="btn-user" onclick="toggleUserPanel()">👤 登录</button>
1345
+ </div>
1346
+ </div>
1347
+ <p class="hero-tagline"><strong>一句话安装</strong>任何 GitHub 开源项目</p>
1348
+ <p class="hero-sub">零外部依赖 · 零配置上手 · 零 AI 也能用 — 来自<strong style="color:var(--text)">蹄门科技</strong></p>
1349
+
1350
+ <div class="hero-badges">
1351
+ <span class="hero-badge"><span class="hb-icon">✅</span> <span class="hb-val">100%</span> 覆盖率</span>
1352
+ <span class="hero-badge"><span class="hb-icon">👥</span> <span class="hb-val">5,000</span> 人模拟测试</span>
1353
+ <span class="hero-badge"><span class="hb-icon">📦</span> <span class="hb-val">110+</span> 项目支持</span>
1354
+ <span class="hero-badge"><span class="hb-icon">🪶</span> <span class="hb-val">0</span> 外部依赖</span>
1355
+ </div>
1356
+ </header>
1357
+
1358
+ <!-- ── Stats ──────────────────────── -->
1359
+ <div class="stats-row">
1360
+ <div class="stat-card"><div class="stat-num">22,517</div><div class="stat-label">模拟安装事件</div></div>
1361
+ <div class="stat-card"><div class="stat-num">93.4</div><div class="stat-label">满意度 / 100</div></div>
1362
+ <div class="stat-card"><div class="stat-num">80+</div><div class="stat-label">精确匹配项目</div></div>
1363
+ <div class="stat-card"><div class="stat-num">30+</div><div class="stat-label">语言模板</div></div>
1364
+ </div>
1365
+
1366
+ <!-- ── Features ───────────────────── -->
1367
+ <div class="feature-row">
1368
+ <div class="feat-chip"><span class="fc-icon">🧠</span><div><div class="fc-text">9 级 LLM 降级</div><div class="fc-sub">Claude → GPT → Ollama → 无 AI</div></div></div>
1369
+ <div class="feat-chip"><span class="fc-icon">🎮</span><div><div class="fc-text">GPU 自动适配</div><div class="fc-sub">CUDA · ROCm · MPS · CPU</div></div></div>
1370
+ <div class="feat-chip"><span class="fc-icon">🌍</span><div><div class="fc-text">跨平台</div><div class="fc-sub">macOS · Linux · Windows</div></div></div>
1371
+ <div class="feat-chip"><span class="fc-icon">🔒</span><div><div class="fc-text">安全审计</div><div class="fc-sub">CVE · 许可证 · 供应链</div></div></div>
1372
+ <div class="feat-chip"><span class="fc-icon">🔄</span><div><div class="fc-text">自动修复</div><div class="fc-sub">依赖冲突 · 权限 · 版本</div></div></div>
1373
+ <div class="feat-chip"><span class="fc-icon">📡</span><div><div class="fc-text">多平台源</div><div class="fc-sub">GitHub · GitLab · Gitee</div></div></div>
1374
+ </div>
1375
+
1376
+ <!-- ── CTA ────────────────────────── -->
1377
+ <div class="cta-bar">
1378
+ <div class="cta-cmd" onclick="copyCmd(this.querySelector('.cta-copy'),'pip install gitinstall')">
1379
+ <span class="cta-prompt">$</span> pip install gitinstall
1380
+ <span class="cta-copy">复制</span>
1381
+ </div>
1382
+ <div class="cta-cmd" onclick="copyCmd(this.querySelector('.cta-copy'),'gitinstall ollama/ollama')">
1383
+ <span class="cta-prompt">$</span> gitinstall ollama/ollama
1384
+ <span class="cta-copy">复制</span>
1385
+ </div>
1386
+ </div>
1387
+
1388
+ <div class="divider"></div>
1389
+
1390
+ <!-- ── Search ─────────────────────── -->
1391
+ <div class="glass" style="padding: 1.25rem 1.25rem 1rem">
1392
+ <div class="search-wrap">
1393
+ <div class="search-glow"></div>
1394
+ <div class="search-inner">
1395
+ <input id="search" class="search-input" type="text"
1396
+ placeholder="输入项目名称、URL、或搜索关键词…"
1397
+ autocomplete="off" autofocus>
1398
+ <button id="btn-gh-search" class="btn-gh-search" onclick="searchGitHub()">🔍 搜索</button>
1399
+ <button id="btn-analyze" class="btn-search" onclick="analyze()">分析项目</button>
1400
+ </div>
1401
+ <div id="search-results" class="search-results" hidden></div>
1402
+ </div>
1403
+ <div class="path-bar">
1404
+ <span class="path-icon">📂</span>
1405
+ <span class="path-label">安装到</span>
1406
+ <span class="path-sep"></span>
1407
+ <input id="install-path" class="path-input" type="text"
1408
+ placeholder="~/ (留空 = 用户主目录)">
1409
+ </div>
1410
+ </div>
1411
+
1412
+ <!-- ── Analysis Progress ───────────── -->
1413
+ <div class="glass" id="analyze-card" hidden>
1414
+ <div class="section-label">🔍 正在分析项目</div>
1415
+ <div class="analyze-bar"><div class="analyze-bar-fill"></div></div>
1416
+ <div class="analyze-steps" id="analyze-steps">
1417
+ <div class="a-step" id="astep-0"><span class="a-dot"></span><span class="a-check">✓</span>连接 GitHub 获取仓库信息</div>
1418
+ <div class="a-step" id="astep-1"><span class="a-dot"></span><span class="a-check">✓</span>分析项目结构与依赖</div>
1419
+ <div class="a-step" id="astep-2"><span class="a-dot"></span><span class="a-check">✓</span>AI 生成安装计划</div>
1420
+ <div class="a-step" id="astep-3"><span class="a-dot"></span><span class="a-check">✓</span>校验并优化安装步骤</div>
1421
+ </div>
1422
+ <div class="analyze-timer">⏱ 已用时 <span id="analyze-time">0</span>s</div>
1423
+ </div>
1424
+
1425
+ <!-- ── Trending ───────────────────── -->
1426
+ <div class="glass" id="trending-card">
1427
+ <div class="trending-header">
1428
+ <div class="section-label" style="margin-bottom:0">🔥 热门开源项目</div>
1429
+ <div class="trending-meta">
1430
+ <span id="trending-count"></span>
1431
+ <span id="trending-updated"></span>
1432
+ <button class="btn-refresh" id="btn-refresh" onclick="refreshTrending()" title="刷新热门项目">↻ 刷新</button>
1433
+ </div>
1434
+ </div>
1435
+ <div class="filter-bar" id="trending-filter">
1436
+ <span class="filter-pill active" onclick="filterTrending('all')">全部</span>
1437
+ <span class="filter-pill" onclick="filterTrending('AI')">🤖 AI</span>
1438
+ <span class="filter-pill" onclick="filterTrending('Web')">🌐 Web</span>
1439
+ <span class="filter-pill" onclick="filterTrending('工具')">🔧 工具</span>
1440
+ <span class="filter-pill" onclick="filterTrending('IoT')">🏠 IoT</span>
1441
+ </div>
1442
+ <div class="trending-grid" id="trending-grid">
1443
+ <div class="loading-text"><span class="spinner"></span>加载中…</div>
1444
+ </div>
1445
+ </div>
1446
+
1447
+ <!-- ── Error ──────────────────────── -->
1448
+ <div id="error-area" hidden></div>
1449
+
1450
+ <!-- ── Info Grid ──────────────────── -->
1451
+ <div class="info-grid">
1452
+ <div class="glass" id="env-card">
1453
+ <div class="section-label">💻 系统环境</div>
1454
+ <div id="env-content" class="env-grid">
1455
+ <div class="loading-text"><span class="spinner"></span>检测中…</div>
1456
+ </div>
1457
+ </div>
1458
+ <div class="glass" id="plan-card" hidden>
1459
+ <div class="section-label">📋 安装计划</div>
1460
+ <div id="plan-content"></div>
1461
+ </div>
1462
+ </div>
1463
+
1464
+ <!-- ── Progress ───────────────────── -->
1465
+ <div id="progress-section" hidden>
1466
+ <div class="progress-wrap">
1467
+ <div id="progress-fill" class="progress-fill"></div>
1468
+ </div>
1469
+ </div>
1470
+
1471
+ <!-- ── Terminal ───────────────────── -->
1472
+ <div class="glass" id="terminal-card" hidden>
1473
+ <div class="section-label">⬛ 终端输出</div>
1474
+ <div id="terminal" class="terminal"></div>
1475
+ </div>
1476
+
1477
+ <!-- ── Result ─────────────────────── -->
1478
+ <div id="result-area" hidden></div>
1479
+
1480
+ <!-- ── User Panel (Modal) ─────────── -->
1481
+ <div class="modal-overlay" id="user-modal" hidden>
1482
+ <div class="modal-box">
1483
+ <button class="modal-close" onclick="toggleUserPanel()">✕</button>
1484
+ <div id="user-panel-content">
1485
+ <div class="modal-tabs">
1486
+ <span class="modal-tab active" onclick="switchAuthTab('login')">登录</span>
1487
+ <span class="modal-tab" onclick="switchAuthTab('register')">注册</span>
1488
+ </div>
1489
+ <form id="form-login" onsubmit="handleLogin(event)">
1490
+ <input type="email" id="login-email" class="modal-input" placeholder="邮箱" required>
1491
+ <input type="password" id="login-pw" class="modal-input" placeholder="密码" required minlength="8">
1492
+ <button type="submit" class="btn-search" style="width:100%;margin-top:0.5rem">登录</button>
1493
+ <div class="auth-msg" id="login-msg"></div>
1494
+ <div style="text-align:right;margin-top:0.4rem">
1495
+ <a href="#" class="link-forgot" onclick="switchAuthTab('forgot');return false">忘记密码?</a>
1496
+ </div>
1497
+ </form>
1498
+ <form id="form-register" onsubmit="handleRegister(event)" hidden>
1499
+ <input type="text" id="reg-user" class="modal-input" placeholder="用户名" required>
1500
+ <input type="email" id="reg-email" class="modal-input" placeholder="邮箱" required>
1501
+ <input type="password" id="reg-pw" class="modal-input" placeholder="密码(至少 8 位)" required minlength="8">
1502
+ <button type="submit" class="btn-search" style="width:100%;margin-top:0.5rem">注册</button>
1503
+ <div class="auth-msg" id="reg-msg"></div>
1504
+ </form>
1505
+ <form id="form-forgot" onsubmit="handleForgot(event)" hidden>
1506
+ <p style="font-size:0.8rem;color:var(--text-sec);margin-bottom:0.8rem">输入注册邮箱,我们将发送密码重置链接</p>
1507
+ <input type="email" id="forgot-email" class="modal-input" placeholder="注册邮箱" required>
1508
+ <button type="submit" class="btn-search" style="width:100%;margin-top:0.5rem">发送重置链接</button>
1509
+ <div class="auth-msg" id="forgot-msg"></div>
1510
+ <div style="text-align:center;margin-top:0.6rem">
1511
+ <a href="#" class="link-forgot" onclick="switchAuthTab('login');return false">返回登录</a>
1512
+ </div>
1513
+ </form>
1514
+ <form id="form-reset" onsubmit="handleReset(event)" hidden>
1515
+ <p style="font-size:0.8rem;color:var(--text-sec);margin-bottom:0.8rem">设置新密码</p>
1516
+ <input type="password" id="reset-pw" class="modal-input" placeholder="新密码(至少 8 位)" required minlength="8">
1517
+ <input type="password" id="reset-pw2" class="modal-input" placeholder="确认新密码" required minlength="8">
1518
+ <button type="submit" class="btn-search" style="width:100%;margin-top:0.5rem">重置密码</button>
1519
+ <div class="auth-msg" id="reset-msg"></div>
1520
+ </form>
1521
+ </div>
1522
+ <div id="user-info-content" hidden>
1523
+ <div class="user-profile" id="user-profile"></div>
1524
+ <button class="btn-logout" onclick="handleLogout()">退出登录</button>
1525
+ </div>
1526
+ </div>
1527
+ </div>
1528
+
1529
+ <footer>
1530
+ <div class="footer-line"></div>
1531
+ <div style="margin-bottom:0.8rem">
1532
+ <span style="font-size:1.1rem;font-weight:700;background:linear-gradient(135deg,#a78bfa,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">gitinstall</span>
1533
+ <span style="margin-left:0.3rem;font-size:0.65rem;padding:0.1rem 0.4rem;border-radius:4px;background:rgba(139,92,246,0.15);color:var(--accent-3);font-weight:600">v1.0.0</span>
1534
+ </div>
1535
+ MIT License · 零外部依赖 · 纯 Python 标准库 · 蹄门科技出品
1536
+ <div style="margin-top:0.5rem;display:flex;justify-content:center;gap:1rem">
1537
+ <a href="/clawhub" style="color:var(--accent-3)">ClawHub</a>
1538
+ <a href="/admin" style="color:var(--text-sec)">管理后台</a>
1539
+ </div>
1540
+ </footer>
1541
+
1542
+ </div>
1543
+
1544
+ <script>
1545
+ /* ── State ──────────────────────────────── */
1546
+ let currentPlan = null;
1547
+ let currentPlanId = null;
1548
+ let eventSource = null;
1549
+ let trendingData = [];
1550
+ let currentFilter = 'all';
1551
+
1552
+ const $ = id => document.getElementById(id);
1553
+ const searchInput = $('search');
1554
+
1555
+ searchInput.addEventListener('keydown', e => {
1556
+ if (e.key === 'Enter') analyze();
1557
+ if (e.key === 'Escape') hideSearchResults();
1558
+ });
1559
+
1560
+ // Close search results when clicking outside
1561
+ document.addEventListener('click', e => {
1562
+ if (!e.target.closest('.search-wrap')) hideSearchResults();
1563
+ });
1564
+
1565
+ /* ── Init ───────────────────────────────── */
1566
+ (async function init() {
1567
+ const envP = fetch('/api/detect').then(r => r.json()).catch(() => null);
1568
+ const trendP = fetch('/api/trending').then(r => r.json()).catch(() => null);
1569
+ const [envData, trendData] = await Promise.all([envP, trendP]);
1570
+
1571
+ if (envData?.status === 'ok') renderEnv(envData.env);
1572
+ else $('env-content').innerHTML = '<div class="loading-text" style="color:var(--red)">检测失败</div>';
1573
+
1574
+ if (trendData?.status === 'ok') {
1575
+ trendingData = trendData.projects;
1576
+ renderTrending(trendingData);
1577
+ $('trending-count').textContent = trendingData.length + ' 个项目';
1578
+ } else {
1579
+ $('trending-grid').innerHTML = '<div class="loading-text" style="color:var(--red)">加载失败</div>';
1580
+ }
1581
+ })();
1582
+
1583
+ /* ── Trending ───────────────────────────── */
1584
+ function renderTrending(projects) {
1585
+ const grid = $('trending-grid');
1586
+ if (!projects.length) { grid.innerHTML = '<div class="loading-text">暂无项目</div>'; return; }
1587
+ grid.innerHTML = projects.map(p => `
1588
+ <div class="t-card" onclick="selectProject(this.dataset.repo)" data-repo="${escAttr(p.repo)}">
1589
+ <div class="t-icon">${p.icon}</div>
1590
+ <div class="t-body">
1591
+ <div class="t-head">
1592
+ <span class="t-name">${esc(p.name)}</span>
1593
+ <span class="t-tag" data-tag="${esc(p.tag)}">${esc(p.tag)}</span>
1594
+ </div>
1595
+ <div class="t-desc">${esc(p.desc)}</div>
1596
+ <div class="t-foot">
1597
+ <span class="t-stars">★ ${esc(p.stars)}</span>
1598
+ <span>${esc(p.lang)}</span>
1599
+ </div>
1600
+ </div>
1601
+ </div>
1602
+ `).join('');
1603
+ }
1604
+
1605
+ function filterTrending(tag) {
1606
+ currentFilter = tag;
1607
+ document.querySelectorAll('.filter-pill').forEach(el => {
1608
+ el.classList.toggle('active', el.textContent.includes(tag === 'all' ? '全部' : tag));
1609
+ });
1610
+ renderTrending(tag === 'all' ? trendingData : trendingData.filter(p => p.tag === tag));
1611
+ }
1612
+
1613
+ function selectProject(repo) {
1614
+ searchInput.value = repo;
1615
+ searchInput.focus();
1616
+ searchInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
1617
+ const inner = document.querySelector('.search-inner');
1618
+ inner.style.borderColor = 'rgba(52,211,153,0.5)';
1619
+ inner.style.boxShadow = '0 0 20px rgba(52,211,153,0.1)';
1620
+ setTimeout(() => { inner.style.borderColor = ''; inner.style.boxShadow = ''; }, 1000);
1621
+ }
1622
+
1623
+ /* ── Analyze ────────────────────────────── */
1624
+ let analyzeTimer = null;
1625
+ let analyzeStart = 0;
1626
+ const ANALYZE_PHASES = [
1627
+ { at: 0, step: 0 },
1628
+ { at: 2, step: 1 },
1629
+ { at: 5, step: 2 },
1630
+ { at: 12, step: 3 },
1631
+ ];
1632
+
1633
+ function showAnalyzeProgress() {
1634
+ $('analyze-card').hidden = false;
1635
+ analyzeStart = Date.now();
1636
+ for (let i = 0; i < 4; i++) $('astep-' + i).className = 'a-step';
1637
+ $('astep-0').classList.add('active');
1638
+ $('analyze-time').textContent = '0';
1639
+ analyzeTimer = setInterval(() => {
1640
+ const elapsed = Math.round((Date.now() - analyzeStart) / 1000);
1641
+ $('analyze-time').textContent = elapsed;
1642
+ for (let i = 0; i < ANALYZE_PHASES.length; i++) {
1643
+ const el = $('astep-' + i);
1644
+ const nextAt = ANALYZE_PHASES[i + 1]?.at ?? Infinity;
1645
+ if (elapsed >= ANALYZE_PHASES[i].at) {
1646
+ el.className = elapsed >= nextAt ? 'a-step done' : 'a-step active';
1647
+ }
1648
+ }
1649
+ }, 250);
1650
+ }
1651
+
1652
+ function hideAnalyzeProgress(success) {
1653
+ if (analyzeTimer) { clearInterval(analyzeTimer); analyzeTimer = null; }
1654
+ if (success) {
1655
+ for (let i = 0; i < 4; i++) $('astep-' + i).className = 'a-step done';
1656
+ setTimeout(() => { $('analyze-card').hidden = true; }, 600);
1657
+ } else {
1658
+ $('analyze-card').hidden = true;
1659
+ }
1660
+ }
1661
+
1662
+ async function analyze() {
1663
+ const project = searchInput.value.trim();
1664
+ if (!project) { searchInput.focus(); return; }
1665
+
1666
+ clearError(); hidePlan(); hideTerminal(); hideResult();
1667
+ hideSearchResults();
1668
+ $('btn-analyze').disabled = true;
1669
+ $('btn-analyze').innerHTML = '<span class="spinner"></span>分析中…';
1670
+ showAnalyzeProgress();
1671
+
1672
+ try {
1673
+ const res = await fetch('/api/plan', {
1674
+ method: 'POST',
1675
+ headers: { 'Content-Type': 'application/json' },
1676
+ body: JSON.stringify({ project }),
1677
+ });
1678
+ const data = await res.json();
1679
+ if (data.status === 'ok') {
1680
+ hideAnalyzeProgress(true);
1681
+ currentPlan = data;
1682
+ currentPlanId = data.plan_id || null;
1683
+ renderPlan(data);
1684
+ } else {
1685
+ hideAnalyzeProgress(false);
1686
+ showError(data.message || '分析失败,请检查项目名称');
1687
+ }
1688
+ } catch (e) {
1689
+ hideAnalyzeProgress(false);
1690
+ showError('网络错误:' + e.message);
1691
+ } finally {
1692
+ $('btn-analyze').disabled = false;
1693
+ $('btn-analyze').textContent = '分析项目';
1694
+ }
1695
+ }
1696
+
1697
+ /* ── Render Env ─────────────────────────── */
1698
+ function renderEnv(env) {
1699
+ const os = env.os || {}, hw = env.hardware || {}, gpu = env.gpu || {};
1700
+ const rt = env.runtimes || {}, pms = env.package_managers || {}, disk = env.disk || {};
1701
+ const chip = os.chip || os.arch || '';
1702
+ const osN = os.type === 'macos' ? 'macOS' : os.type === 'linux' ? 'Linux' : os.type === 'windows' ? 'Windows' : os.type;
1703
+ const pmList = Object.entries(pms).filter(([,v]) => v.available).map(([k]) => k).join(' · ');
1704
+ const gpuL = gpu.type === 'nvidia' ? `NVIDIA (CUDA ${gpu.cuda_version||''})` :
1705
+ gpu.type === 'apple_mps' ? 'Apple GPU (MPS)' : gpu.name || 'CPU';
1706
+ const items = [
1707
+ ['💻', `${osN} ${os.version||''}`],
1708
+ ['⚡', `${chip} · ${hw.cpu_count||'?'} 核`],
1709
+ ['🎮', gpuL],
1710
+ ['🐍', `Python ${rt.python?.version||'—'}`],
1711
+ ['📦', pmList || '—'],
1712
+ ['💾', `${disk.free_gb||'?'} GB 可用`],
1713
+ ];
1714
+ $('env-content').innerHTML = items.map(([ic, val]) => `
1715
+ <div class="env-item"><span class="env-icon">${ic}</span><span class="env-value">${val}</span></div>
1716
+ `).join('');
1717
+ }
1718
+
1719
+ /* ── Render Plan ────────────────────────── */
1720
+ function renderPlan(data) {
1721
+ const plan = data.plan || {}, steps = plan.steps || [];
1722
+ const project = data.project || '', confidence = data.confidence || '', llm = data.llm_used || '';
1723
+ const bc = confidence === 'high' ? 'badge-high' : confidence === 'medium' ? 'badge-medium' : 'badge-low';
1724
+ const bt = confidence === 'high' ? '高置信' : confidence === 'medium' ? '中置信' : '低置信';
1725
+
1726
+ let h = `
1727
+ <div class="plan-header">
1728
+ <div class="plan-project">${esc(project)}</div>
1729
+ <div class="plan-meta"><span class="badge ${bc}">${bt}</span></div>
1730
+ </div>
1731
+ <div style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
1732
+ ${steps.length} 步 · LLM: ${esc(llm)}
1733
+ </div>
1734
+ <div class="steps-list" id="steps-list">
1735
+ `;
1736
+ steps.forEach((s, i) => {
1737
+ const cmd = s.command || '';
1738
+ h += `<div class="step" id="step-${i}">
1739
+ <div class="step-indicator pending" id="step-ind-${i}">${i+1}</div>
1740
+ <div class="step-body">
1741
+ <div class="step-desc">${esc(s.description||'')}</div>
1742
+ <div class="step-cmd-row">
1743
+ <div class="step-cmd">$ ${esc(cmd)}</div>
1744
+ <button class="btn-copy-step" onclick="copyStep(this,'${escAttr(cmd)}')">复制</button>
1745
+ </div>
1746
+ </div>
1747
+ <div class="step-dur" id="step-dur-${i}"></div>
1748
+ </div>`;
1749
+ });
1750
+ h += '</div>';
1751
+ if (plan.launch_command) {
1752
+ h += `<div style="margin-top:0.75rem;font-size:0.82rem;color:var(--text-sec);display:flex;align-items:center;gap:0.4rem">
1753
+ ▶ 启动:<code style="color:var(--accent-3);font-family:var(--mono)">${esc(plan.launch_command)}</code>
1754
+ <button class="btn-copy-step" onclick="copyStep(this,'${escAttr(plan.launch_command)}')">复制</button>
1755
+ </div>`;
1756
+ }
1757
+
1758
+ // ── 安装指导 ──
1759
+ const guideSteps = buildGuide(steps, plan, project, confidence);
1760
+ h += `<div class="guide-section">
1761
+ <div class="guide-toggle" onclick="toggleGuide()">
1762
+ <span class="arrow" id="guide-arrow">▶</span> 📖 安装指导(手动安装参考)
1763
+ </div>
1764
+ <div class="guide-body" id="guide-body">
1765
+ <div class="guide-content">${guideSteps}</div>
1766
+ </div>
1767
+ </div>`;
1768
+
1769
+ h += `<div class="plan-actions">
1770
+ <button class="btn btn-outline" onclick="analyze()">重新分析</button>
1771
+ <button class="btn btn-success" id="btn-install" onclick="startInstall()">开始安装</button>
1772
+ </div>`;
1773
+ $('plan-content').innerHTML = h;
1774
+ $('plan-card').hidden = false;
1775
+ }
1776
+
1777
+ /* ── Install (SSE) ──────────────────────── */
1778
+ function startInstall() {
1779
+ if (!currentPlan) return;
1780
+ const project = currentPlan.project || searchInput.value.trim();
1781
+ $('btn-install').disabled = true;
1782
+ $('btn-install').innerHTML = '<span class="spinner"></span>安装中…';
1783
+ $('btn-analyze').disabled = true;
1784
+ clearError(); hideResult();
1785
+
1786
+ $('terminal-card').hidden = false;
1787
+ $('terminal').innerHTML = '';
1788
+ $('progress-section').hidden = false;
1789
+ $('progress-fill').style.width = '0%';
1790
+
1791
+ let totalSteps = 0, stepsDone = 0;
1792
+
1793
+ const params = new URLSearchParams();
1794
+ if (currentPlanId) params.set('plan_id', currentPlanId);
1795
+ params.set('project', project);
1796
+ const installDir = $('install-path').value.trim();
1797
+ if (installDir) params.set('install_dir', installDir);
1798
+
1799
+ eventSource = new EventSource('/api/install?' + params.toString());
1800
+
1801
+ eventSource.addEventListener('plan', e => {
1802
+ totalSteps = (JSON.parse(e.data).steps || []).length;
1803
+ });
1804
+ eventSource.addEventListener('step_start', e => {
1805
+ const d = JSON.parse(e.data);
1806
+ setStepStatus(d.index, 'active');
1807
+ appendTerm(`\n❯ ${d.command}`, 'prompt');
1808
+ });
1809
+ eventSource.addEventListener('output', e => {
1810
+ appendTerm(JSON.parse(e.data).line);
1811
+ });
1812
+ eventSource.addEventListener('step_done', e => {
1813
+ const d = JSON.parse(e.data);
1814
+ setStepStatus(d.index, d.success ? 'success' : 'error');
1815
+ if (d.duration > 0) { const el = $('step-dur-' + d.index); if (el) el.textContent = d.duration + 's'; }
1816
+ if (d.success) {
1817
+ stepsDone++;
1818
+ if (totalSteps > 0) $('progress-fill').style.width = (stepsDone / totalSteps * 100) + '%';
1819
+ appendTerm(`✓ 完成 (${d.duration}s)`, 'success');
1820
+ }
1821
+ });
1822
+ eventSource.addEventListener('step_error', e => {
1823
+ appendTerm(`✗ ${JSON.parse(e.data).message}`, 'error');
1824
+ });
1825
+ eventSource.addEventListener('done', e => {
1826
+ eventSource.close(); eventSource = null;
1827
+ const d = JSON.parse(e.data);
1828
+ $('progress-fill').style.width = d.success ? '100%' : ((stepsDone / Math.max(totalSteps,1)) * 100) + '%';
1829
+ $('btn-install').disabled = false;
1830
+ $('btn-install').textContent = '开始安装';
1831
+ $('btn-analyze').disabled = false;
1832
+ showResult(d);
1833
+ });
1834
+ eventSource.onerror = () => {
1835
+ if (eventSource) { eventSource.close(); eventSource = null; }
1836
+ $('btn-install').disabled = false;
1837
+ $('btn-install').textContent = '开始安装';
1838
+ $('btn-analyze').disabled = false;
1839
+ showError('连接断开,请重试');
1840
+ };
1841
+ }
1842
+
1843
+ /* ── Helpers ────────────────────────────── */
1844
+ function setStepStatus(idx, status) {
1845
+ const ind = $('step-ind-' + idx), step = $('step-' + idx);
1846
+ if (!ind || !step) return;
1847
+ ind.className = 'step-indicator ' + status;
1848
+ step.className = 'step ' + status;
1849
+ if (status === 'success') ind.innerHTML = '✓';
1850
+ else if (status === 'error') ind.innerHTML = '✗';
1851
+ }
1852
+
1853
+ function appendTerm(text, type) {
1854
+ const term = $('terminal');
1855
+ const line = document.createElement('div');
1856
+ if (type === 'prompt') line.className = 'term-prompt';
1857
+ else if (type === 'success') line.className = 'term-success';
1858
+ else if (type === 'error') line.className = 'term-error';
1859
+ line.textContent = text;
1860
+ term.appendChild(line);
1861
+ term.scrollTop = term.scrollHeight;
1862
+ }
1863
+
1864
+ function showResult(data) {
1865
+ const ok = data.success;
1866
+ let h = `<div class="result-banner ${ok ? 'success' : 'fail'}">
1867
+ <div class="result-icon">${ok ? '🎉' : '❌'}</div>
1868
+ <div class="result-body">
1869
+ <div class="result-title">${ok ? '安装完成!' : '安装未成功'}</div>
1870
+ <div class="result-sub">${esc(data.message||'')}${data.total_duration ? ' · 耗时 '+data.total_duration+'s' : ''}</div>`;
1871
+ if (ok && data.launch_command) {
1872
+ h += `<div class="result-launch"><span>$ ${esc(data.launch_command)}</span>
1873
+ <button class="copy-btn" onclick="copyCmd(this,'${escAttr(data.launch_command)}')">复制</button></div>`;
1874
+ }
1875
+ h += '</div></div>';
1876
+ $('result-area').innerHTML = h;
1877
+ $('result-area').hidden = false;
1878
+ }
1879
+
1880
+ function showError(msg) {
1881
+ $('error-area').innerHTML = `<div class="error-toast">⚠ ${esc(msg)}</div>`;
1882
+ $('error-area').hidden = false;
1883
+ }
1884
+ function clearError() { $('error-area').innerHTML = ''; $('error-area').hidden = true; }
1885
+ function hidePlan() { $('plan-card').hidden = true; }
1886
+ function hideTerminal() { $('terminal-card').hidden = true; $('progress-section').hidden = true; }
1887
+ function hideResult() { $('result-area').hidden = true; }
1888
+
1889
+ function copyCmd(btn, text) {
1890
+ navigator.clipboard.writeText(text).then(() => {
1891
+ btn.textContent = '已复制 ✓';
1892
+ setTimeout(() => btn.textContent = '复制', 1500);
1893
+ });
1894
+ }
1895
+
1896
+ function copyStep(btn, text) {
1897
+ navigator.clipboard.writeText(text).then(() => {
1898
+ btn.textContent = '已复制 ✓';
1899
+ btn.classList.add('copied');
1900
+ setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 1500);
1901
+ });
1902
+ }
1903
+
1904
+ function toggleGuide() {
1905
+ const body = $('guide-body'), arrow = $('guide-arrow');
1906
+ body.classList.toggle('open');
1907
+ arrow.classList.toggle('open');
1908
+ }
1909
+
1910
+ function buildGuide(steps, plan, project, confidence) {
1911
+ const allCmds = steps.map(s => s.command || '').filter(Boolean);
1912
+ let h = '<ol>';
1913
+ // Pre-requisite
1914
+ h += '<li><strong>前置条件</strong>:确保已安装 <code>git</code>';
1915
+ const hasPython = allCmds.some(c => /pip |python |conda /.test(c));
1916
+ const hasNode = allCmds.some(c => /npm |yarn |pnpm |node /.test(c));
1917
+ const hasDocker = allCmds.some(c => /docker /.test(c));
1918
+ if (hasPython) h += '、<code>Python 3.8+</code>';
1919
+ if (hasNode) h += '、<code>Node.js 16+</code>';
1920
+ if (hasDocker) h += '、<code>Docker</code>';
1921
+ h += '</li>';
1922
+
1923
+ // Clone step
1924
+ const cloneCmd = allCmds.find(c => c.includes('git clone'));
1925
+ if (cloneCmd) {
1926
+ h += `<li><strong>克隆仓库</strong>:打开终端,执行 <code>${esc(cloneCmd)}</code></li>`;
1927
+ }
1928
+
1929
+ // Dependency install
1930
+ const depCmd = allCmds.find(c => /pip install|npm install|yarn|pnpm install|conda/.test(c));
1931
+ if (depCmd) {
1932
+ h += `<li><strong>安装依赖</strong>:进入项目目录后执行 <code>${esc(depCmd)}</code></li>`;
1933
+ }
1934
+
1935
+ // Other steps
1936
+ const otherSteps = steps.filter(s => {
1937
+ const c = s.command || '';
1938
+ return c && !c.includes('git clone') && !/pip install|npm install|yarn|pnpm install/.test(c) && c !== (plan.launch_command||'');
1939
+ });
1940
+ if (otherSteps.length) {
1941
+ h += '<li><strong>配置与构建</strong>:按顺序执行以下命令:';
1942
+ h += '<br>' + otherSteps.map(s => `<code>${esc(s.command)}</code>`).join('<br>');
1943
+ h += '</li>';
1944
+ }
1945
+
1946
+ // Launch
1947
+ if (plan.launch_command) {
1948
+ h += `<li><strong>启动项目</strong>:执行 <code>${esc(plan.launch_command)}</code></li>`;
1949
+ }
1950
+
1951
+ h += '</ol>';
1952
+
1953
+ // Tips
1954
+ h += `<div class="guide-tip">💡 如遇到权限问题,Linux/macOS 可在命令前加 <code>sudo</code>;如遇依赖冲突建议使用虚拟环境(<code>python -m venv .venv</code>)</div>`;
1955
+
1956
+ // Copy all button
1957
+ const allScript = allCmds.join(' && ');
1958
+ h += `<button class="guide-copy-all" onclick="copyStep(this,'${escAttr(allScript)}')">📋 一键复制所有命令</button>`;
1959
+ return h;
1960
+ }
1961
+
1962
+ function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
1963
+ function escAttr(s) { return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\\/g,'\\\\'); }
1964
+
1965
+ /* ── GitHub Search ──────────────────────── */
1966
+ async function searchGitHub() {
1967
+ const query = searchInput.value.trim();
1968
+ if (!query) { searchInput.focus(); return; }
1969
+
1970
+ $('btn-gh-search').disabled = true;
1971
+ $('btn-gh-search').innerHTML = '<span class="spinner"></span>';
1972
+
1973
+ try {
1974
+ const res = await fetch('/api/search?q=' + encodeURIComponent(query));
1975
+ const data = await res.json();
1976
+ if (data.status === 'ok') {
1977
+ renderSearchResults(data.results, data.total);
1978
+ } else {
1979
+ showError(data.message || '搜索失败');
1980
+ }
1981
+ } catch (e) {
1982
+ showError('搜索失败:' + e.message);
1983
+ } finally {
1984
+ $('btn-gh-search').disabled = false;
1985
+ $('btn-gh-search').textContent = '🔍 搜索';
1986
+ }
1987
+ }
1988
+
1989
+ function fmtStars(n) {
1990
+ if (typeof n !== 'number') return n;
1991
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1992
+ return String(n);
1993
+ }
1994
+
1995
+ function renderSearchResults(results, total) {
1996
+ const container = $('search-results');
1997
+ if (!results || !results.length) {
1998
+ container.innerHTML = '<div style="padding:1.2rem;color:var(--text-muted);font-size:0.85rem;text-align:center">未找到相关项目</div>';
1999
+ container.hidden = false;
2000
+ return;
2001
+ }
2002
+ let h = `<div class="sr-header">找到 ${total > 1000 ? '1000+' : total} 个项目</div>`;
2003
+ h += results.map(r => `
2004
+ <div class="sr-item" onclick="pickSearchResult('${escAttr(r.repo)}')">
2005
+ <div style="flex:1;min-width:0">
2006
+ <div class="sr-name">${esc(r.repo)}</div>
2007
+ <div class="sr-desc">${esc(r.desc)}</div>
2008
+ <div class="sr-meta">
2009
+ <span class="sr-stars">★ ${fmtStars(r.stars)}</span>
2010
+ <span>${esc(r.lang)}</span>
2011
+ </div>
2012
+ </div>
2013
+ </div>
2014
+ `).join('');
2015
+ container.innerHTML = h;
2016
+ container.hidden = false;
2017
+ }
2018
+
2019
+ function pickSearchResult(repo) {
2020
+ searchInput.value = repo;
2021
+ hideSearchResults();
2022
+ searchInput.focus();
2023
+ }
2024
+
2025
+ function hideSearchResults() {
2026
+ $('search-results').hidden = true;
2027
+ }
2028
+
2029
+ /* ── Refresh Trending ───────────────────── */
2030
+ async function refreshTrending() {
2031
+ const btn = $('btn-refresh');
2032
+ btn.disabled = true;
2033
+ btn.textContent = '⟳ 刷新中…';
2034
+ try {
2035
+ const res = await fetch('/api/trending/refresh');
2036
+ const data = await res.json();
2037
+ if (data?.status === 'ok') {
2038
+ trendingData = data.projects;
2039
+ filterTrending(currentFilter);
2040
+ $('trending-count').textContent = trendingData.length + ' 个项目';
2041
+ $('trending-updated').textContent = '刚刚更新';
2042
+ }
2043
+ } catch (e) {
2044
+ // silent fail
2045
+ } finally {
2046
+ btn.disabled = false;
2047
+ btn.textContent = '↻ 刷新';
2048
+ }
2049
+ }
2050
+
2051
+ function fmtNum(n) {
2052
+ if (typeof n !== 'number') return '0';
2053
+ if (n >= 10000) return (n / 10000).toFixed(1) + 'w';
2054
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
2055
+ return String(n);
2056
+ }
2057
+
2058
+ /* ── User Auth System ──────────────────── */
2059
+ let authToken = localStorage.getItem('gitinstall_token') || '';
2060
+ let currentUser = null;
2061
+
2062
+ async function checkAuth() {
2063
+ if (!authToken) return;
2064
+ try {
2065
+ const res = await fetch('/api/user', {
2066
+ headers: { 'Authorization': 'Bearer ' + authToken },
2067
+ });
2068
+ const d = await res.json();
2069
+ if (d.status === 'ok' && d.user) {
2070
+ currentUser = d.user;
2071
+ currentUser.quota = d.quota;
2072
+ renderUserButton();
2073
+ } else {
2074
+ authToken = '';
2075
+ localStorage.removeItem('gitinstall_token');
2076
+ }
2077
+ } catch (e) {
2078
+ // silent
2079
+ }
2080
+ }
2081
+
2082
+ function renderUserButton() {
2083
+ const btn = $('btn-user');
2084
+ if (currentUser) {
2085
+ btn.textContent = '👤 ' + currentUser.username;
2086
+ btn.classList.add('logged-in');
2087
+ } else {
2088
+ btn.textContent = '👤 登录';
2089
+ btn.classList.remove('logged-in');
2090
+ }
2091
+ }
2092
+
2093
+ function toggleUserPanel() {
2094
+ const modal = $('user-modal');
2095
+ modal.hidden = !modal.hidden;
2096
+ if (!modal.hidden) {
2097
+ if (currentUser) {
2098
+ $('user-panel-content').hidden = true;
2099
+ $('user-info-content').hidden = false;
2100
+ renderUserProfile();
2101
+ } else {
2102
+ $('user-panel-content').hidden = false;
2103
+ $('user-info-content').hidden = true;
2104
+ }
2105
+ }
2106
+ }
2107
+
2108
+ function renderUserProfile() {
2109
+ const q = currentUser.quota || {};
2110
+ const limitText = q.limit < 0 ? '无限制' : `${q.used}/${q.limit}`;
2111
+ const tierName = { free: '免费用户', pro: 'Pro 会员', guest: '游客' }[currentUser.tier] || currentUser.tier;
2112
+ $('user-profile').innerHTML = `
2113
+ <div class="user-name">${esc(currentUser.username)}</div>
2114
+ <div class="user-tier">${tierName}</div>
2115
+ <div class="quota-info">本月已用: ${limitText} 次计划生成</div>
2116
+ `;
2117
+ }
2118
+
2119
+ function switchAuthTab(tab) {
2120
+ document.querySelectorAll('.modal-tab').forEach(el => {
2121
+ el.classList.toggle('active',
2122
+ (tab === 'login' && el.textContent.includes('登录')) ||
2123
+ (tab === 'register' && el.textContent.includes('注册'))
2124
+ );
2125
+ });
2126
+ $('form-login').hidden = tab !== 'login';
2127
+ $('form-register').hidden = tab !== 'register';
2128
+ $('form-forgot').hidden = tab !== 'forgot';
2129
+ $('form-reset').hidden = tab !== 'reset';
2130
+ }
2131
+
2132
+ async function handleLogin(e) {
2133
+ e.preventDefault();
2134
+ const msg = $('login-msg');
2135
+ msg.className = 'auth-msg';
2136
+ msg.textContent = '登录中...';
2137
+ try {
2138
+ const res = await fetch('/api/login', {
2139
+ method: 'POST',
2140
+ headers: { 'Content-Type': 'application/json' },
2141
+ body: JSON.stringify({
2142
+ email: $('login-email').value,
2143
+ password: $('login-pw').value,
2144
+ }),
2145
+ });
2146
+ const d = await res.json();
2147
+ if (d.status === 'ok') {
2148
+ authToken = d.token;
2149
+ localStorage.setItem('gitinstall_token', authToken);
2150
+ currentUser = { id: d.user_id, username: d.username, tier: d.tier };
2151
+ msg.className = 'auth-msg ok';
2152
+ msg.textContent = '登录成功!';
2153
+ renderUserButton();
2154
+ setTimeout(() => toggleUserPanel(), 600);
2155
+ checkAuth(); // refresh quota
2156
+ } else {
2157
+ msg.className = 'auth-msg err';
2158
+ msg.textContent = d.message || '登录失败';
2159
+ }
2160
+ } catch (err) {
2161
+ msg.className = 'auth-msg err';
2162
+ msg.textContent = '网络错误';
2163
+ }
2164
+ }
2165
+
2166
+ async function handleRegister(e) {
2167
+ e.preventDefault();
2168
+ const msg = $('reg-msg');
2169
+ msg.className = 'auth-msg';
2170
+ msg.textContent = '注册中...';
2171
+ try {
2172
+ const res = await fetch('/api/register', {
2173
+ method: 'POST',
2174
+ headers: { 'Content-Type': 'application/json' },
2175
+ body: JSON.stringify({
2176
+ username: $('reg-user').value,
2177
+ email: $('reg-email').value,
2178
+ password: $('reg-pw').value,
2179
+ }),
2180
+ });
2181
+ const d = await res.json();
2182
+ if (d.status === 'ok') {
2183
+ msg.className = 'auth-msg ok';
2184
+ msg.textContent = '注册成功!请登录';
2185
+ setTimeout(() => switchAuthTab('login'), 800);
2186
+ } else {
2187
+ msg.className = 'auth-msg err';
2188
+ msg.textContent = d.message || '注册失败';
2189
+ }
2190
+ } catch (err) {
2191
+ msg.className = 'auth-msg err';
2192
+ msg.textContent = '网络错误';
2193
+ }
2194
+ }
2195
+
2196
+ function handleLogout() {
2197
+ authToken = '';
2198
+ currentUser = null;
2199
+ localStorage.removeItem('gitinstall_token');
2200
+ renderUserButton();
2201
+ toggleUserPanel();
2202
+ }
2203
+
2204
+ /* ── Forgot / Reset Password ────────────── */
2205
+ async function handleForgot(e) {
2206
+ e.preventDefault();
2207
+ const msg = $('forgot-msg');
2208
+ msg.className = 'auth-msg';
2209
+ msg.textContent = '发送中...';
2210
+ try {
2211
+ const res = await fetch('/api/forgot-password', {
2212
+ method: 'POST',
2213
+ headers: { 'Content-Type': 'application/json' },
2214
+ body: JSON.stringify({ email: $('forgot-email').value }),
2215
+ });
2216
+ const d = await res.json();
2217
+ msg.className = 'auth-msg ok';
2218
+ msg.textContent = d.message || '重置链接已发送,请查收邮件';
2219
+ } catch (err) {
2220
+ msg.className = 'auth-msg err';
2221
+ msg.textContent = '网络错误';
2222
+ }
2223
+ }
2224
+
2225
+ let resetToken = '';
2226
+
2227
+ async function handleReset(e) {
2228
+ e.preventDefault();
2229
+ const msg = $('reset-msg');
2230
+ const pw = $('reset-pw').value;
2231
+ const pw2 = $('reset-pw2').value;
2232
+ if (pw !== pw2) {
2233
+ msg.className = 'auth-msg err';
2234
+ msg.textContent = '两次密码不一致';
2235
+ return;
2236
+ }
2237
+ msg.className = 'auth-msg';
2238
+ msg.textContent = '重置中...';
2239
+ try {
2240
+ const res = await fetch('/api/reset-password', {
2241
+ method: 'POST',
2242
+ headers: { 'Content-Type': 'application/json' },
2243
+ body: JSON.stringify({ token: resetToken, password: pw }),
2244
+ });
2245
+ const d = await res.json();
2246
+ if (d.status === 'ok') {
2247
+ msg.className = 'auth-msg ok';
2248
+ msg.textContent = d.message || '密码已重置,请登录';
2249
+ // 清除 URL 中的 reset_token
2250
+ history.replaceState(null, '', '/');
2251
+ setTimeout(() => switchAuthTab('login'), 1000);
2252
+ } else {
2253
+ msg.className = 'auth-msg err';
2254
+ msg.textContent = d.message || '重置失败';
2255
+ }
2256
+ } catch (err) {
2257
+ msg.className = 'auth-msg err';
2258
+ msg.textContent = '网络错误';
2259
+ }
2260
+ }
2261
+
2262
+ // 检查 URL 是否携带 reset_token(从邮件链接跳转)
2263
+ (function checkResetToken() {
2264
+ const params = new URLSearchParams(window.location.search);
2265
+ const rt = params.get('reset_token');
2266
+ if (rt) {
2267
+ resetToken = rt;
2268
+ toggleUserPanel();
2269
+ switchAuthTab('reset');
2270
+ }
2271
+ })();
2272
+
2273
+ // Check auth on page load
2274
+ checkAuth();
2275
+ </script>
2276
+ </body>
2277
+ </html>