vibe-roast 0.98.0 → 0.98.2

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.
@@ -1,1403 +0,0 @@
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" />
6
- <title>Live Report · Vibe Roaster</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link
10
- href="https://fonts.googleapis.com/css2?family=Great+Vibes&family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap"
11
- rel="stylesheet"
12
- />
13
- <style>
14
- :root {
15
- --bg: #f3f1ec;
16
- --bg-deep: #e8e4db;
17
- --card: #fffcf7;
18
- --ink: #1a1a1a;
19
- --muted: #6b6560;
20
- --line: #e4dfd6;
21
- --orange: #ff5a1f;
22
- --orange-soft: #fff0e8;
23
- --shadow: 0 10px 30px rgba(40, 28, 12, 0.06);
24
- --radius: 18px;
25
- --accent: #ff5a1f;
26
- }
27
-
28
- * {
29
- box-sizing: border-box;
30
- }
31
-
32
- body {
33
- margin: 0;
34
- min-height: 100vh;
35
- color: var(--ink);
36
- font-family: Outfit, sans-serif;
37
- background:
38
- radial-gradient(1200px 600px at 10% -10%, #ffe7d6 0%, transparent 55%),
39
- radial-gradient(900px 500px at 100% 0%, #d9fff3 0%, transparent 50%),
40
- linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);
41
- }
42
-
43
- a {
44
- color: inherit;
45
- text-decoration: none;
46
- }
47
-
48
- .topnav {
49
- display: flex;
50
- align-items: flex-end;
51
- justify-content: space-between;
52
- gap: 16px;
53
- margin-bottom: 22px;
54
- padding: 0;
55
- background: transparent;
56
- border: none;
57
- }
58
-
59
- .brand {
60
- margin: 0;
61
- font-family: "Great Vibes", cursive;
62
- font-weight: 400;
63
- font-size: clamp(40px, 6vw, 52px);
64
- line-height: 0.95;
65
- letter-spacing: 0.01em;
66
- color: var(--ink);
67
- }
68
-
69
- .nav-links {
70
- display: flex;
71
- gap: 18px;
72
- color: var(--muted);
73
- font-size: 14px;
74
- font-weight: 600;
75
- padding-bottom: 6px;
76
- }
77
-
78
- .nav-links a:hover {
79
- color: var(--ink);
80
- }
81
-
82
- .shell {
83
- width: min(1120px, calc(100% - 32px));
84
- margin: 0 auto;
85
- padding: 28px 0 80px;
86
- overflow-x: clip;
87
- }
88
-
89
- .back {
90
- display: inline-flex;
91
- align-items: center;
92
- gap: 6px;
93
- margin-bottom: 18px;
94
- color: var(--muted);
95
- font-size: 14px;
96
- font-weight: 600;
97
- }
98
-
99
- .picker {
100
- display: flex;
101
- flex-wrap: wrap;
102
- gap: 8px;
103
- margin-bottom: 22px;
104
- }
105
-
106
- .picker button {
107
- appearance: none;
108
- border: 1px solid var(--line);
109
- background: rgba(255, 252, 247, 0.8);
110
- color: var(--muted);
111
- border-radius: 999px;
112
- padding: 8px 12px;
113
- font: inherit;
114
- font-size: 13px;
115
- font-weight: 650;
116
- cursor: pointer;
117
- transition: 160ms ease;
118
- }
119
-
120
- .picker button:hover,
121
- .picker button.is-active {
122
- color: #fff;
123
- border-color: transparent;
124
- background: var(--accent);
125
- transform: translateY(-1px);
126
- }
127
-
128
- .layout {
129
- display: grid;
130
- grid-template-columns: minmax(260px, 300px) minmax(0, 1fr);
131
- gap: 20px;
132
- align-items: start;
133
- min-width: 0;
134
- }
135
-
136
- .card {
137
- background: var(--card);
138
- border: 1px solid rgba(0, 0, 0, 0.04);
139
- border-radius: var(--radius);
140
- box-shadow: var(--shadow);
141
- }
142
-
143
- .profile-card {
144
- padding: 8px 4px 0;
145
- text-align: center;
146
- position: relative;
147
- overflow: visible;
148
- background: transparent;
149
- border: 0;
150
- box-shadow: none;
151
- min-width: 0;
152
- }
153
-
154
- .profile-stage {
155
- position: relative;
156
- margin: 0 auto 8px;
157
- padding: 12px 8px 0;
158
- isolation: isolate;
159
- }
160
-
161
- .profile-stage::before {
162
- content: "";
163
- position: absolute;
164
- left: 50%;
165
- top: 8%;
166
- width: 100%;
167
- height: 78%;
168
- transform: translateX(-50%);
169
- border-radius: 46% 54% 42% 58% / 48% 44% 56% 52%;
170
- background:
171
- radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--accent) 28%, #fff), transparent 58%),
172
- radial-gradient(circle at 70% 65%, color-mix(in srgb, var(--accent) 16%, #f3f1ec), transparent 62%);
173
- z-index: 0;
174
- pointer-events: none;
175
- }
176
-
177
- .profile-stage::after {
178
- content: "";
179
- position: absolute;
180
- left: 50%;
181
- bottom: 6%;
182
- width: 58%;
183
- height: 18px;
184
- transform: translateX(-50%);
185
- border-radius: 50%;
186
- background: color-mix(in srgb, var(--accent) 18%, transparent);
187
- filter: blur(8px);
188
- z-index: 0;
189
- pointer-events: none;
190
- }
191
-
192
- .handle {
193
- position: relative;
194
- z-index: 1;
195
- margin: 0;
196
- font-size: 28px;
197
- font-weight: 800;
198
- letter-spacing: -0.04em;
199
- color: var(--accent);
200
- }
201
-
202
- .name {
203
- position: relative;
204
- z-index: 1;
205
- margin: 4px 0 6px;
206
- color: var(--muted);
207
- font-size: 14px;
208
- font-weight: 500;
209
- }
210
-
211
- .mascot-wrap {
212
- position: relative;
213
- z-index: 1;
214
- width: min(100%, 300px);
215
- margin: 0 auto;
216
- padding: 0;
217
- border: 0;
218
- background: transparent;
219
- box-shadow: none;
220
- animation: floaty 4.8s ease-in-out infinite;
221
- }
222
-
223
- .mascot-wrap img {
224
- position: relative;
225
- z-index: 1;
226
- width: 100%;
227
- height: auto;
228
- display: block;
229
- border: 0;
230
- border-radius: 0;
231
- background: transparent;
232
- box-shadow: none;
233
- /* transparent PNG sits unframed on stage blob */
234
- image-rendering: auto;
235
- }
236
-
237
- .meta-panel {
238
- margin-top: 6px;
239
- padding: 16px 14px 14px;
240
- border-radius: 18px;
241
- background: rgba(255, 252, 247, 0.78);
242
- border: 1px solid rgba(0, 0, 0, 0.04);
243
- box-shadow: var(--shadow);
244
- backdrop-filter: blur(10px);
245
- }
246
-
247
- .score-block {
248
- position: relative;
249
- margin: 8px 0 6px;
250
- }
251
-
252
- .score-num {
253
- font-family: "JetBrains Mono", monospace;
254
- font-size: 44px;
255
- font-weight: 700;
256
- letter-spacing: -0.05em;
257
- line-height: 1;
258
- color: var(--accent);
259
- white-space: nowrap;
260
- }
261
-
262
- .score-num span {
263
- font-size: 18px;
264
- color: var(--muted);
265
- font-weight: 500;
266
- }
267
-
268
- .tier {
269
- margin: 8px 0 4px;
270
- font-size: 28px;
271
- font-weight: 800;
272
- letter-spacing: 0.04em;
273
- color: var(--accent);
274
- white-space: nowrap;
275
- }
276
-
277
- .tier-blurb {
278
- margin: 0 0 14px;
279
- color: var(--muted);
280
- font-size: 13px;
281
- }
282
-
283
- .tags {
284
- display: flex;
285
- flex-wrap: wrap;
286
- justify-content: center;
287
- gap: 6px;
288
- margin-bottom: 16px;
289
- }
290
-
291
- .tags span {
292
- padding: 5px 10px;
293
- border-radius: 999px;
294
- background: color-mix(in srgb, var(--accent) 12%, white);
295
- color: color-mix(in srgb, var(--accent) 70%, #222);
296
- font-size: 12px;
297
- font-weight: 700;
298
- }
299
-
300
- .hashtag-row {
301
- display: flex;
302
- flex-wrap: wrap;
303
- justify-content: center;
304
- gap: 6px;
305
- margin: 12px 0 4px;
306
- }
307
-
308
- .hashtag-row span {
309
- padding: 5px 10px;
310
- border-radius: 999px;
311
- background: color-mix(in srgb, var(--accent) 14%, #fffcf7);
312
- color: color-mix(in srgb, var(--accent) 72%, #222);
313
- font-size: 12px;
314
- font-weight: 700;
315
- letter-spacing: -0.01em;
316
- }
317
-
318
- .cloud-stage,
319
- .activity-stage {
320
- border-radius: 14px;
321
- background: #f7f4ef;
322
- overflow: hidden;
323
- }
324
-
325
- .cloud-stage {
326
- height: 140px;
327
- display: block;
328
- position: relative;
329
- }
330
-
331
- .cloud-stage canvas {
332
- width: 100%;
333
- height: 100%;
334
- display: block;
335
- }
336
-
337
- .activity-stage {
338
- min-height: 280px;
339
- max-height: 360px;
340
- padding: 0;
341
- position: relative;
342
- }
343
-
344
- .activity-view-toggle {
345
- position: absolute;
346
- top: 6px;
347
- right: 6px;
348
- z-index: 2;
349
- display: inline-flex;
350
- gap: 2px;
351
- padding: 2px;
352
- border-radius: 999px;
353
- border: 1px solid rgba(0, 0, 0, 0.06);
354
- background: color-mix(in srgb, #fffcf7 92%, transparent);
355
- }
356
-
357
- .activity-view-toggle button {
358
- border: 0;
359
- background: transparent;
360
- color: #6b6560;
361
- font: 700 10px/1 "JetBrains Mono", ui-monospace, monospace;
362
- letter-spacing: 0.08em;
363
- text-transform: uppercase;
364
- border-radius: 999px;
365
- padding: 6px 10px;
366
- cursor: pointer;
367
- }
368
-
369
- .activity-view-toggle button.is-active {
370
- background: #10b981;
371
- color: #fff;
372
- }
373
-
374
- .activity-view-body {
375
- padding-top: 0;
376
- transform-origin: 50% 85%;
377
- }
378
-
379
- .activity-view-body.is-enter {
380
- animation: heatmapViewIn 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
381
- }
382
-
383
- .activity-view-body.is-exit {
384
- animation: heatmapViewOut 0.16s ease-in both;
385
- }
386
-
387
- @keyframes heatmapViewIn {
388
- from {
389
- opacity: 0;
390
- transform: perspective(900px) rotateX(10deg) translateY(14px) scale(0.97);
391
- }
392
- to {
393
- opacity: 1;
394
- transform: perspective(900px) rotateX(0deg) translateY(0) scale(1);
395
- }
396
- }
397
-
398
- @keyframes heatmapViewOut {
399
- from {
400
- opacity: 1;
401
- transform: perspective(900px) rotateX(0deg) translateY(0) scale(1);
402
- }
403
- to {
404
- opacity: 0;
405
- transform: perspective(900px) rotateX(6deg) translateY(8px) scale(0.98);
406
- }
407
- }
408
-
409
- .activity-2d-wrap {
410
- overflow-x: auto;
411
- display: flex;
412
- justify-content: center;
413
- padding: 0;
414
- }
415
-
416
- .activity-kpi {
417
- display: grid;
418
- grid-template-columns: repeat(4, minmax(0, 1fr));
419
- gap: 3px;
420
- margin-bottom: 2px;
421
- padding-right: 64px;
422
- }
423
-
424
- .activity-kpi > div {
425
- border-radius: 8px;
426
- background: #fffcf7;
427
- border: 1px solid rgba(0, 0, 0, 0.04);
428
- padding: 3px 5px;
429
- }
430
-
431
- .activity-kpi span {
432
- display: block;
433
- font-size: 9px;
434
- font-weight: 700;
435
- letter-spacing: 0.12em;
436
- text-transform: uppercase;
437
- color: #8b8680;
438
- }
439
-
440
- .activity-kpi strong {
441
- display: block;
442
- margin-top: 2px;
443
- font-family: "JetBrains Mono", ui-monospace, monospace;
444
- font-size: 14px;
445
- font-weight: 800;
446
- color: #059669;
447
- }
448
-
449
- @media (max-width: 720px) {
450
- .activity-kpi {
451
- grid-template-columns: repeat(2, minmax(0, 1fr));
452
- padding-right: 0;
453
- }
454
- }
455
-
456
- .poster-bar {
457
- display: flex;
458
- flex-wrap: wrap;
459
- align-items: center;
460
- justify-content: space-between;
461
- gap: 12px;
462
- margin-bottom: 18px;
463
- }
464
-
465
- .poster-bar .hint {
466
- color: var(--muted);
467
- font-size: 13px;
468
- font-weight: 600;
469
- }
470
-
471
- .share-icon-btn {
472
- display: inline-flex;
473
- align-items: center;
474
- justify-content: center;
475
- width: 48px;
476
- height: 48px;
477
- border: 0;
478
- border-radius: 14px;
479
- color: #fff;
480
- background: var(--accent, #ff5a1f);
481
- box-shadow: 0 10px 20px color-mix(in srgb, var(--accent, #ff5a1f) 28%, transparent);
482
- cursor: pointer;
483
- }
484
-
485
- .share-icon-btn:disabled {
486
- opacity: 0.6;
487
- cursor: wait;
488
- }
489
-
490
- .poster-modal {
491
- position: fixed;
492
- inset: 0;
493
- z-index: 80;
494
- display: none;
495
- align-items: flex-end;
496
- justify-content: center;
497
- background: rgba(0, 0, 0, 0.45);
498
- backdrop-filter: blur(2px);
499
- padding: 0;
500
- }
501
-
502
- .poster-modal.is-open {
503
- display: flex;
504
- }
505
-
506
- .poster-sheet {
507
- width: min(100%, 440px);
508
- max-height: 96vh;
509
- display: flex;
510
- flex-direction: column;
511
- overflow: hidden;
512
- background: #fffcf7;
513
- border-radius: 22px 22px 0 0;
514
- box-shadow: 0 -12px 40px rgba(40, 28, 12, 0.18);
515
- }
516
-
517
- .poster-sheet-head,
518
- .poster-sheet-foot {
519
- display: flex;
520
- align-items: center;
521
- justify-content: space-between;
522
- gap: 10px;
523
- padding: 14px 16px;
524
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
525
- }
526
-
527
- .poster-sheet-foot {
528
- border-bottom: 0;
529
- border-top: 1px solid rgba(0, 0, 0, 0.06);
530
- display: grid;
531
- grid-template-columns: 1fr 1fr;
532
- padding-bottom: max(14px, env(safe-area-inset-bottom));
533
- }
534
-
535
- .poster-sheet-body {
536
- overflow: auto;
537
- padding: 16px;
538
- }
539
-
540
- .poster-preview {
541
- aspect-ratio: 3 / 4;
542
- width: min(100%, 320px);
543
- margin: 0 auto;
544
- overflow: hidden;
545
- border-radius: 16px;
546
- border: 1px solid rgba(0, 0, 0, 0.06);
547
- background: #f3f1ec;
548
- box-shadow: 0 10px 28px rgba(40, 28, 12, 0.1);
549
- }
550
-
551
- .poster-preview img {
552
- display: block;
553
- width: 100%;
554
- height: 100%;
555
- object-fit: cover;
556
- }
557
-
558
- .poster-sheet-title {
559
- font-size: 14px;
560
- font-weight: 800;
561
- color: var(--ink);
562
- }
563
-
564
- .poster-sheet-sub {
565
- margin-top: 2px;
566
- font-size: 11px;
567
- color: var(--muted);
568
- }
569
-
570
- .poster-sheet button {
571
- min-height: 48px;
572
- border-radius: 12px;
573
- border: 1px solid rgba(0, 0, 0, 0.08);
574
- background: #fff;
575
- font-weight: 700;
576
- cursor: pointer;
577
- }
578
-
579
- .poster-sheet .download {
580
- border: 0;
581
- color: #fff;
582
- background: var(--accent, #ff5a1f);
583
- }
584
-
585
- .poster-sheet .download:disabled {
586
- opacity: 0.5;
587
- cursor: not-allowed;
588
- }
589
-
590
- @media (min-width: 640px) {
591
- .poster-modal {
592
- align-items: center;
593
- padding: 16px;
594
- }
595
- .poster-sheet {
596
- border-radius: 22px;
597
- max-height: 92vh;
598
- box-shadow: 0 24px 60px rgba(40, 28, 12, 0.22);
599
- }
600
- }
601
-
602
- .signal-grid {
603
- display: grid;
604
- grid-template-columns: 1fr;
605
- gap: 8px;
606
- text-align: left;
607
- }
608
-
609
- .signal {
610
- display: flex;
611
- justify-content: space-between;
612
- gap: 10px;
613
- padding: 10px 12px;
614
- border-radius: 12px;
615
- background: #f6f3ee;
616
- font-size: 13px;
617
- min-width: 0;
618
- }
619
-
620
- .signal span {
621
- min-width: 0;
622
- overflow-wrap: anywhere;
623
- }
624
-
625
- .signal strong {
626
- font-family: "JetBrains Mono", monospace;
627
- font-weight: 700;
628
- flex-shrink: 0;
629
- max-width: 58%;
630
- text-align: right;
631
- overflow-wrap: anywhere;
632
- }
633
-
634
- .stack {
635
- display: grid;
636
- gap: 16px;
637
- }
638
-
639
- .panel {
640
- padding: 22px 22px 20px;
641
- }
642
-
643
- .panel.panel-compact {
644
- padding: 16px 16px 12px;
645
- }
646
-
647
- .panel h2 {
648
- margin: 0 0 4px;
649
- font-size: 20px;
650
- letter-spacing: -0.02em;
651
- }
652
-
653
- .panel .sub {
654
- margin: 0 0 16px;
655
- color: var(--muted);
656
- font-size: 13px;
657
- }
658
-
659
- .panel.panel-compact .sub {
660
- margin: 0 0 8px;
661
- }
662
-
663
- .radar-wrap {
664
- display: grid;
665
- grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
666
- gap: 12px;
667
- align-items: center;
668
- }
669
-
670
- .radar-stage {
671
- position: relative;
672
- min-height: 340px;
673
- display: grid;
674
- place-items: center;
675
- padding: 8px;
676
- overflow: visible;
677
- min-width: 0;
678
- }
679
-
680
- .radar-stage svg {
681
- width: min(100%, 360px);
682
- height: auto;
683
- overflow: visible;
684
- display: block;
685
- }
686
-
687
- .axis-list {
688
- display: grid;
689
- gap: 10px;
690
- min-width: 0;
691
- }
692
-
693
- .axis-row {
694
- display: grid;
695
- grid-template-columns: 1fr auto;
696
- gap: 8px;
697
- align-items: baseline;
698
- padding: 10px 12px;
699
- border-radius: 12px;
700
- background: #f7f4ef;
701
- }
702
-
703
- .axis-row .label {
704
- font-size: 13px;
705
- font-weight: 700;
706
- }
707
-
708
- .axis-row .hint {
709
- grid-column: 1 / -1;
710
- color: var(--muted);
711
- font-size: 11px;
712
- }
713
-
714
- .axis-row .val {
715
- font-family: "JetBrains Mono", monospace;
716
- font-size: 13px;
717
- font-weight: 700;
718
- color: var(--accent);
719
- }
720
-
721
- .featured {
722
- display: grid;
723
- gap: 10px;
724
- }
725
-
726
- .feat {
727
- display: flex;
728
- justify-content: space-between;
729
- gap: 12px;
730
- align-items: center;
731
- padding: 12px 14px;
732
- border-radius: 12px;
733
- border: 1px solid var(--line);
734
- background: #fff;
735
- transition: 160ms ease;
736
- }
737
-
738
- .feat:hover {
739
- border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
740
- transform: translateX(2px);
741
- }
742
-
743
- .feat strong {
744
- display: block;
745
- font-size: 14px;
746
- }
747
-
748
- .feat span {
749
- color: var(--muted);
750
- font-size: 12px;
751
- }
752
-
753
- .roast {
754
- position: relative;
755
- padding: 20px 20px 18px;
756
- border-radius: 14px;
757
- background: linear-gradient(180deg, #fff7f2, #fff);
758
- border: 1px solid #ffd7c4;
759
- line-height: 1.7;
760
- font-size: 15px;
761
- }
762
-
763
- .roast .tldr {
764
- margin: 14px 0 0;
765
- padding-top: 12px;
766
- border-top: 1px dashed #ffd7c4;
767
- color: var(--muted);
768
- font-size: 13px;
769
- }
770
-
771
- .roast .tldr b {
772
- color: var(--ink);
773
- }
774
-
775
- .lang-toggle {
776
- display: inline-flex;
777
- margin-bottom: 10px;
778
- border: 1px solid var(--line);
779
- border-radius: 999px;
780
- overflow: hidden;
781
- }
782
-
783
- .lang-toggle button {
784
- appearance: none;
785
- border: 0;
786
- background: transparent;
787
- padding: 6px 12px;
788
- font: inherit;
789
- font-size: 12px;
790
- font-weight: 700;
791
- color: var(--muted);
792
- cursor: pointer;
793
- }
794
-
795
- .lang-toggle button.is-active {
796
- background: var(--ink);
797
- color: #fff;
798
- }
799
-
800
- .standing {
801
- margin-top: 14px;
802
- padding: 14px;
803
- border-radius: 14px;
804
- background: #fff;
805
- border: 1px solid var(--line);
806
- text-align: left;
807
- }
808
-
809
- .standing .rank {
810
- font-size: 13px;
811
- color: var(--muted);
812
- }
813
-
814
- .standing .pct {
815
- margin-top: 4px;
816
- font-size: 18px;
817
- font-weight: 800;
818
- letter-spacing: -0.03em;
819
- }
820
-
821
- .cta {
822
- margin-top: 12px;
823
- width: 100%;
824
- appearance: none;
825
- border: 0;
826
- border-radius: 12px;
827
- padding: 12px 14px;
828
- background: var(--orange);
829
- color: #fff;
830
- font: inherit;
831
- font-weight: 750;
832
- cursor: pointer;
833
- box-shadow: 0 10px 20px rgba(255, 90, 31, 0.25);
834
- }
835
-
836
- .cta:hover {
837
- filter: brightness(1.05);
838
- }
839
-
840
- .float-notes {
841
- display: flex;
842
- flex-wrap: wrap;
843
- gap: 8px;
844
- margin: 0 0 16px;
845
- position: static;
846
- height: auto;
847
- }
848
-
849
- .note {
850
- position: static;
851
- max-width: none;
852
- padding: 8px 12px;
853
- border-radius: 12px;
854
- background: rgba(255, 252, 247, 0.92);
855
- border: 1px solid var(--line);
856
- box-shadow: none;
857
- font-size: 12px;
858
- font-weight: 650;
859
- color: var(--muted);
860
- animation: none;
861
- pointer-events: none;
862
- }
863
-
864
- .note b {
865
- color: var(--ink);
866
- }
867
-
868
- .note.n1,
869
- .note.n2 {
870
- top: auto;
871
- right: auto;
872
- animation-delay: 0s;
873
- }
874
-
875
- footer.page-foot {
876
- margin-top: 28px;
877
- color: var(--muted);
878
- font-size: 12px;
879
- text-align: center;
880
- }
881
-
882
- footer.page-foot a {
883
- color: var(--orange);
884
- font-weight: 700;
885
- }
886
-
887
- @keyframes floaty {
888
- 0%,
889
- 100% {
890
- transform: translateY(0);
891
- }
892
- 50% {
893
- transform: translateY(-8px);
894
- }
895
- }
896
-
897
- @keyframes bob {
898
- 0%,
899
- 100% {
900
- transform: translateY(0) rotate(-1deg);
901
- }
902
- 50% {
903
- transform: translateY(-6px) rotate(1deg);
904
- }
905
- }
906
-
907
- @keyframes drawIn {
908
- from {
909
- opacity: 0;
910
- transform: scale(0.92);
911
- }
912
- to {
913
- opacity: 1;
914
- transform: scale(1);
915
- }
916
- }
917
-
918
- .is-animating .radar-stage svg {
919
- animation: drawIn 420ms ease both;
920
- }
921
-
922
- @media (max-width: 960px) {
923
- .layout {
924
- grid-template-columns: 1fr;
925
- }
926
- .radar-wrap {
927
- grid-template-columns: 1fr;
928
- }
929
- .radar-stage {
930
- min-height: 300px;
931
- }
932
- }
933
-
934
- @media (max-width: 720px) {
935
- .float-notes,
936
- .nav-links {
937
- display: none;
938
- }
939
- .shell {
940
- width: min(1120px, calc(100% - 24px));
941
- padding: 20px 0 64px;
942
- }
943
- .score-num {
944
- font-size: 38px;
945
- }
946
- .tier {
947
- font-size: 24px;
948
- }
949
- .mascot-wrap {
950
- width: min(100%, 260px);
951
- }
952
- }
953
-
954
- .live-banner {
955
- margin-bottom: 18px;
956
- padding: 12px 16px;
957
- border-radius: 14px;
958
- background: color-mix(in srgb, var(--accent) 12%, #fffcf7);
959
- border: 1px solid color-mix(in srgb, var(--accent) 28%, #e4dfd6);
960
- font-size: 13px;
961
- color: var(--muted);
962
- }
963
- .live-banner b { color: var(--ink); }
964
- .picker { display: none; }
965
- </style>
966
- </head>
967
- <body>
968
- <main class="shell">
969
- <header class="topnav">
970
- <h1 class="brand">Vibe Roaster</h1>
971
- <nav class="nav-links">
972
- <a href="/" id="navHome">Home</a>
973
- <a href="live-report.html" id="navLive">Live export</a>
974
- </nav>
975
- </header>
976
-
977
- <div class="live-banner" id="banner"></div>
978
-
979
- <div class="poster-bar">
980
- <div class="hint" id="posterHint">Share poster · 3:4</div>
981
- <button class="share-icon-btn" type="button" id="posterBtn" aria-label="Share poster" title="Share poster">
982
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
983
- <path d="M15 8a3 3 0 1 0-2.83-4H12a3 3 0 0 0 3 4Zm-6 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm10 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" stroke-width="1.8"/>
984
- <path d="M8.6 11.4 15 8.6M8.6 12.6 15.4 15.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
985
- </svg>
986
- </button>
987
- </div>
988
-
989
- <section class="layout is-animating" id="stage">
990
- <aside class="profile-card" id="profileCard"></aside>
991
- <div class="stack">
992
- <article class="card panel">
993
- <h2 id="scoreTitle">Score breakdown</h2>
994
- <p class="sub" id="scoreSub">Six agent-native axes · live inspect</p>
995
- <div class="radar-wrap">
996
- <div class="radar-stage" id="radar"></div>
997
- <div class="axis-list" id="axisList"></div>
998
- </div>
999
- </article>
1000
-
1001
- <article class="card panel">
1002
- <h2 id="roastTitle">🔥 Full roast</h2>
1003
- <div class="lang-toggle" role="group" aria-label="Roast language">
1004
- <button type="button" id="langZh" class="is-active">中文</button>
1005
- <button type="button" id="langEn">EN</button>
1006
- </div>
1007
- <div class="roast" id="roast"></div>
1008
- </article>
1009
-
1010
- <article class="card panel panel-compact">
1011
- <h2 id="cloudTitle">Word cloud</h2>
1012
- <p class="sub" id="cloudSub">From inspect word_frequencies</p>
1013
- <div class="cloud-stage"><canvas id="wordCloud"></canvas></div>
1014
- </article>
1015
-
1016
- <article class="card panel panel-compact">
1017
- <h2 id="activityTitle">Activity</h2>
1018
- <p class="sub" id="activitySub">Emerald heatmap · TokenTracker when available · toggle 3D</p>
1019
- <div class="activity-stage" id="activityMap"></div>
1020
- </article>
1021
- </div>
1022
- </section>
1023
-
1024
- <footer class="page-foot" id="pageFoot">
1025
- Static snapshot from redacted live inspect · no prompt bodies ·
1026
- <a href="notes/live-inspect-summary.json">JSON</a>
1027
- </footer>
1028
- </main>
1029
-
1030
- <div class="poster-modal" id="posterModal" aria-hidden="true">
1031
- <div class="poster-sheet" role="dialog" aria-modal="true" aria-labelledby="posterModalTitle">
1032
- <div class="poster-sheet-head">
1033
- <div>
1034
- <div class="poster-sheet-title" id="posterModalTitle">Share poster · 3:4</div>
1035
- <div class="poster-sheet-sub" id="posterModalHint">Preview first, then download when you’re ready.</div>
1036
- </div>
1037
- <button type="button" id="posterModalCloseTop">Close</button>
1038
- </div>
1039
- <div class="poster-sheet-body">
1040
- <div class="poster-preview">
1041
- <img id="posterPreviewImg" alt="Share poster preview" hidden />
1042
- </div>
1043
- </div>
1044
- <div class="poster-sheet-foot">
1045
- <button type="button" id="posterModalClose">Close</button>
1046
- <button type="button" class="download" id="posterModalDownload" disabled>Download PNG</button>
1047
- </div>
1048
- </div>
1049
- </div>
1050
-
1051
- <script type="module">
1052
- import {
1053
- buildHashtags,
1054
- renderWordCloud,
1055
- renderActivityMap,
1056
- renderSharePoster,
1057
- downloadCanvasPng,
1058
- } from "./scripts/roast-extras.js";
1059
- import { EMBED } from "./scripts/live-report-boot.js";
1060
-
1061
- const COPY = {
1062
- en: {
1063
- navHome: "Home",
1064
- navLive: "Live export",
1065
- posterHint: "Share poster · 3:4",
1066
- posterShare: "Share poster",
1067
- posterBusy: "Preparing…",
1068
- posterTitle: "Share poster · 3:4",
1069
- posterModalHint: "Preview first, then download when you’re ready.",
1070
- posterDownload: "Download PNG",
1071
- posterClose: "Close",
1072
- scoreTitle: "Score breakdown",
1073
- scoreSub: "Six agent-native axes · live inspect",
1074
- roastTitle: "🔥 Full roast",
1075
- cloudTitle: "Word cloud",
1076
- cloudSub: "From inspect word_frequencies",
1077
- activityTitle: "Activity",
1078
- activitySub: "Emerald heatmap · TokenTracker when available · toggle 3D",
1079
- foot: "Static snapshot from redacted live inspect · no prompt bodies ·",
1080
- banner: (meta, srcBits) =>
1081
- `<b>Live inspect</b> · ${meta.generated_at} · prompts ${meta.prompt_count} · sources ${meta.source_count} (${srcBits}) · redacted aggregates only`,
1082
- },
1083
- zh: {
1084
- navHome: "首页",
1085
- navLive: "静态导出",
1086
- posterHint: "分享海报 · 3:4",
1087
- posterShare: "分享海报",
1088
- posterBusy: "准备中…",
1089
- posterTitle: "分享海报 · 3:4",
1090
- posterModalHint: "先预览,确认后再下载。",
1091
- posterDownload: "下载 PNG",
1092
- posterClose: "关闭",
1093
- scoreTitle: "分数明细",
1094
- scoreSub: "六维 Agent 轴 · live inspect",
1095
- roastTitle: "🔥 完整 Roast",
1096
- cloudTitle: "词云",
1097
- cloudSub: "来自 inspect word_frequencies",
1098
- activityTitle: "活动",
1099
- activitySub: "翠绿热力图 · 可用时用 TokenTracker · 可切换 3D",
1100
- foot: "来自脱敏 live inspect 的静态快照 · 不含 prompt 正文 ·",
1101
- banner: (meta, srcBits) =>
1102
- `<b>Live inspect</b> · ${meta.generated_at} · 提示 ${meta.prompt_count} · 数据源 ${meta.source_count} (${srcBits}) · 仅脱敏聚合`,
1103
- },
1104
- };
1105
-
1106
- async function loadPayload() {
1107
- try {
1108
- const res = await fetch("notes/live-inspect-summary.json", { cache: "no-store" });
1109
- if (!res.ok) throw new Error("fetch failed");
1110
- const summary = await res.json();
1111
- const vibe = { ...summary.vibe_profile };
1112
- const rel = (p) => String(p || "").replace(/^\/assests\//, "");
1113
- vibe.figure = rel(vibe.figure);
1114
- vibe.badge = rel(vibe.badge);
1115
- return {
1116
- LIVE: vibe,
1117
- META: {
1118
- generated_at: summary.generated_at,
1119
- prompt_count: summary.summary?.prompt_count,
1120
- source_count: summary.summary?.source_count,
1121
- sources: Object.fromEntries(
1122
- Object.entries(summary.sources || {}).map(([k, v]) => [k, v.prompt_count || 0]),
1123
- ),
1124
- },
1125
- WORDS: summary.word_frequencies || [],
1126
- ACTIVITY: summary.activity || {},
1127
- CATEGORIES: summary.profile_signals?.prompt_analysis?.categories || {},
1128
- };
1129
- } catch {
1130
- return EMBED;
1131
- }
1132
- }
1133
-
1134
- const profileCard = document.getElementById("profileCard");
1135
- const radar = document.getElementById("radar");
1136
- const axisList = document.getElementById("axisList");
1137
- const roast = document.getElementById("roast");
1138
- const stage = document.getElementById("stage");
1139
- const langZh = document.getElementById("langZh");
1140
- const langEn = document.getElementById("langEn");
1141
- const banner = document.getElementById("banner");
1142
- const posterBtn = document.getElementById("posterBtn");
1143
- const posterHint = document.getElementById("posterHint");
1144
- const wordCloud = document.getElementById("wordCloud");
1145
- const activityMap = document.getElementById("activityMap");
1146
- const posterModal = document.getElementById("posterModal");
1147
- const posterPreviewImg = document.getElementById("posterPreviewImg");
1148
- const posterModalDownload = document.getElementById("posterModalDownload");
1149
- const posterModalClose = document.getElementById("posterModalClose");
1150
- const posterModalCloseTop = document.getElementById("posterModalCloseTop");
1151
-
1152
- let lang = "zh";
1153
- let payload = EMBED;
1154
- let posterCanvas = null;
1155
-
1156
- function copy() {
1157
- return COPY[lang] || COPY.en;
1158
- }
1159
-
1160
- function applyChrome() {
1161
- const c = copy();
1162
- document.getElementById("navHome").textContent = c.navHome;
1163
- document.getElementById("navLive").textContent = c.navLive;
1164
- posterHint.textContent = c.posterHint;
1165
- posterBtn.setAttribute("aria-label", c.posterShare);
1166
- posterBtn.title = c.posterShare;
1167
- document.getElementById("scoreTitle").textContent = c.scoreTitle;
1168
- document.getElementById("scoreSub").textContent = c.scoreSub;
1169
- document.getElementById("roastTitle").textContent = c.roastTitle;
1170
- document.getElementById("cloudTitle").textContent = c.cloudTitle;
1171
- document.getElementById("cloudSub").textContent = c.cloudSub;
1172
- document.getElementById("activityTitle").textContent = c.activityTitle;
1173
- document.getElementById("activitySub").textContent = c.activitySub;
1174
- document.getElementById("pageFoot").innerHTML = `${c.foot} <a href="notes/live-inspect-summary.json">JSON</a>`;
1175
- document.getElementById("posterModalTitle").textContent = c.posterTitle;
1176
- document.getElementById("posterModalHint").textContent = c.posterModalHint;
1177
- posterModalClose.textContent = c.posterClose;
1178
- posterModalCloseTop.textContent = c.posterClose;
1179
- posterModalDownload.textContent = c.posterDownload;
1180
- posterPreviewImg.alt = c.posterTitle;
1181
- }
1182
-
1183
- function closePosterModal() {
1184
- posterModal.classList.remove("is-open");
1185
- posterModal.setAttribute("aria-hidden", "true");
1186
- }
1187
-
1188
- function openPosterModal() {
1189
- posterModal.classList.add("is-open");
1190
- posterModal.setAttribute("aria-hidden", "false");
1191
- }
1192
-
1193
- langZh.addEventListener("click", () => { lang = "zh"; render(); });
1194
- langEn.addEventListener("click", () => { lang = "en"; render(); });
1195
- posterBtn.addEventListener("click", async () => {
1196
- const c = copy();
1197
- posterBtn.disabled = true;
1198
- posterBtn.title = c.posterBusy;
1199
- posterBtn.setAttribute("aria-label", c.posterBusy);
1200
- posterCanvas = null;
1201
- posterPreviewImg.hidden = true;
1202
- posterPreviewImg.removeAttribute("src");
1203
- posterModalDownload.disabled = true;
1204
- openPosterModal();
1205
- try {
1206
- const hashtags = buildHashtags(payload.LIVE, payload.CATEGORIES, { locale: lang });
1207
- posterCanvas = await renderSharePoster({ vibe: payload.LIVE, hashtags, lang });
1208
- posterPreviewImg.src = posterCanvas.toDataURL("image/png");
1209
- posterPreviewImg.hidden = false;
1210
- posterModalDownload.disabled = false;
1211
- } finally {
1212
- posterBtn.disabled = false;
1213
- posterBtn.title = c.posterShare;
1214
- posterBtn.setAttribute("aria-label", c.posterShare);
1215
- }
1216
- });
1217
- posterModalDownload.addEventListener("click", () => {
1218
- if (!posterCanvas) return;
1219
- const code = (payload.LIVE?.archetype?.code || "vibe").toLowerCase();
1220
- downloadCanvasPng(posterCanvas, `vibe-roast-${code}-3x4.png`);
1221
- });
1222
- posterModalClose.addEventListener("click", closePosterModal);
1223
- posterModalCloseTop.addEventListener("click", closePosterModal);
1224
- posterModal.addEventListener("click", (event) => {
1225
- if (event.target === posterModal) closePosterModal();
1226
- });
1227
- window.addEventListener("keydown", (event) => {
1228
- if (event.key === "Escape") closePosterModal();
1229
- });
1230
-
1231
- function escapeHtml(value) {
1232
- return String(value)
1233
- .replaceAll("&", "&amp;")
1234
- .replaceAll("<", "&lt;")
1235
- .replaceAll(">", "&gt;")
1236
- .replaceAll('"', "&quot;");
1237
- }
1238
- const escapeXml = escapeHtml;
1239
-
1240
- function render() {
1241
- const vibe = payload.LIVE;
1242
- const META = payload.META;
1243
- const arch = vibe.archetype;
1244
- const tier = vibe.tier;
1245
- const score = Number(vibe.total);
1246
- const c = copy();
1247
- document.documentElement.style.setProperty("--accent", arch.accent);
1248
- langZh.classList.toggle("is-active", lang === "zh");
1249
- langEn.classList.toggle("is-active", lang === "en");
1250
- applyChrome();
1251
-
1252
- const srcBits = Object.entries(META.sources || {})
1253
- .map(([k, v]) => `${k} ${v}`)
1254
- .join(" · ");
1255
- banner.innerHTML = c.banner(
1256
- {
1257
- generated_at: escapeHtml(META.generated_at),
1258
- prompt_count: META.prompt_count,
1259
- source_count: META.source_count,
1260
- },
1261
- escapeHtml(srcBits),
1262
- );
1263
-
1264
- const hook = lang === "zh" ? arch.hookZh : arch.hook;
1265
- const blurb = lang === "zh" ? tier.blurbZh : tier.blurb;
1266
- const percentile = Math.min(99.5, Math.round((score / 100) * 97 + 2));
1267
- const hashtags = buildHashtags(vibe, payload.CATEGORIES, { locale: lang });
1268
-
1269
- profileCard.innerHTML = `
1270
- <div class="profile-stage">
1271
- <h1 class="handle">@${escapeHtml(arch.code.toLowerCase())}</h1>
1272
- <p class="name">${escapeHtml(arch.title)} · ${escapeHtml(hook)}</p>
1273
- <div class="mascot-wrap">
1274
- <img src="${escapeHtml(vibe.figure)}" width="384" height="512" alt="${escapeHtml(arch.title)}" />
1275
- </div>
1276
- <div class="hashtag-row">${hashtags.map((t) => `<span>${escapeHtml(t)}</span>`).join("")}</div>
1277
- </div>
1278
- <div class="meta-panel">
1279
- <div class="score-block">
1280
- <div class="score-num">${score.toFixed(1)} <span>/ 100</span></div>
1281
- </div>
1282
- <div class="tier">${tier.emoji} ${escapeHtml(tier.id)}</div>
1283
- <p class="tier-blurb">${escapeHtml(blurb)}</p>
1284
- <div class="signal-grid">
1285
- ${vibe.signals
1286
- .map(
1287
- (s) =>
1288
- `<div class="signal"><span>${escapeHtml(lang === "zh" ? s.labelZh : s.label)}</span><strong>${escapeHtml(s.value)}</strong></div>`,
1289
- )
1290
- .join("")}
1291
- </div>
1292
- <div class="standing">
1293
- <div class="rank">${lang === "zh" ? "Agent 使用力分位" : "Agent-usage percentile"}</div>
1294
- <div class="pct">${lang === "zh" ? `击败 ${percentile}% 的 vibe coder` : `Beats ${percentile}% of vibe coders`}</div>
1295
- </div>
1296
- </div>
1297
- `;
1298
-
1299
- axisList.innerHTML = vibe.dimensions
1300
- .map(
1301
- (dim) => `<div class="axis-row">
1302
- <div class="label">${escapeHtml(lang === "zh" ? dim.labelZh : dim.label)}</div>
1303
- <div class="val">${Number(dim.value).toFixed(1)} / ${dim.max}</div>
1304
- <div class="hint">${escapeHtml(lang === "zh" ? (dim.hintZh || dim.hint) : dim.hint)}</div>
1305
- </div>`,
1306
- )
1307
- .join("");
1308
-
1309
- radar.innerHTML = renderRadar(vibe.dimensions, arch.accent);
1310
-
1311
- const body = lang === "zh" ? vibe.roastZh : vibe.roast;
1312
- const tldr = lang === "zh" ? vibe.tldrZh : vibe.tldr;
1313
- roast.innerHTML = `${escapeHtml(body)}<p class="tldr"><b>TL;DR</b> · ${escapeHtml(tldr)}</p>`;
1314
-
1315
- renderWordCloud(wordCloud, payload.WORDS, arch.accent);
1316
- renderActivityMap(activityMap, payload.ACTIVITY, 52, { defaultMode: "2d" });
1317
-
1318
- stage.classList.remove("is-animating");
1319
- void stage.offsetWidth;
1320
- stage.classList.add("is-animating");
1321
- document.title = `${arch.title} — ${score.toFixed(1)}/100 · ${tier.id} | live report`;
1322
- }
1323
-
1324
- function renderRadar(dimensions, accent) {
1325
- const size = 360;
1326
- const pad = 36;
1327
- const center = size / 2;
1328
- const maxRadius = 108;
1329
- const rings = [0.33, 0.66, 1];
1330
- const points = dimensions.map((dim, index) => {
1331
- const angle = -Math.PI / 2 + (index * Math.PI * 2) / dimensions.length;
1332
- const ratio = Math.min(1, Number(dim.score || 0));
1333
- const radius = 28 + ratio * (maxRadius - 28);
1334
- const labelR = maxRadius + 34;
1335
- return {
1336
- ...dim,
1337
- x: center + Math.cos(angle) * radius,
1338
- y: center + Math.sin(angle) * radius,
1339
- axisX: center + Math.cos(angle) * maxRadius,
1340
- axisY: center + Math.sin(angle) * maxRadius,
1341
- labelX: center + Math.cos(angle) * labelR,
1342
- labelY: center + Math.sin(angle) * labelR,
1343
- };
1344
- });
1345
- const polygon = points.map((p) => `${p.x},${p.y}`).join(" ");
1346
- const ringPolys = rings
1347
- .map((ring) => {
1348
- const pts = dimensions
1349
- .map((_, index) => {
1350
- const angle = -Math.PI / 2 + (index * Math.PI * 2) / dimensions.length;
1351
- return `${center + Math.cos(angle) * maxRadius * ring},${center + Math.sin(angle) * maxRadius * ring}`;
1352
- })
1353
- .join(" ");
1354
- return `<polygon points="${pts}" fill="none" stroke="rgba(40,30,20,0.12)" stroke-width="1" />`;
1355
- })
1356
- .join("");
1357
- const axes = points
1358
- .map((p) => `<line x1="${center}" y1="${center}" x2="${p.axisX}" y2="${p.axisY}" stroke="rgba(40,30,20,0.1)" stroke-width="1" />`)
1359
- .join("");
1360
- const dots = points
1361
- .map((p) => `<circle cx="${p.x}" cy="${p.y}" r="4.5" fill="${accent}" stroke="#fff" stroke-width="2" />`)
1362
- .join("");
1363
- const labels = points
1364
- .map((p) => {
1365
- const anchor = p.labelX < center - 8 ? "end" : p.labelX > center + 8 ? "start" : "middle";
1366
- const text = lang === "zh" ? p.labelZh : p.label;
1367
- const scoreText = `${Number(p.value).toFixed(1)}/${p.max}`;
1368
- const dy = p.labelY < center - 40 ? -2 : p.labelY > center + 40 ? 2 : 0;
1369
- return `<text x="${p.labelX}" y="${p.labelY - 7 + dy}" text-anchor="${anchor}" fill="#1a1a1a" font-size="11" font-weight="700" font-family="Outfit,sans-serif">${escapeXml(text)}</text>
1370
- <text x="${p.labelX}" y="${p.labelY + 8 + dy}" text-anchor="${anchor}" fill="${accent}" font-size="11" font-weight="700" font-family="JetBrains Mono,monospace">${scoreText}</text>`;
1371
- })
1372
- .join("");
1373
-
1374
- return `<svg viewBox="${-pad} ${-pad} ${size + pad * 2} ${size + pad * 2}" role="img" aria-label="Score radar">
1375
- ${ringPolys}
1376
- ${axes}
1377
- <polygon points="${polygon}" fill="${accent}33" stroke="${accent}" stroke-width="2.5" />
1378
- ${dots}
1379
- ${labels}
1380
- </svg>`;
1381
- }
1382
-
1383
- payload = await loadPayload();
1384
- render();
1385
-
1386
- let cloudResizeTimer = 0;
1387
- const redrawCloud = () => {
1388
- renderWordCloud(wordCloud, payload.WORDS, payload.LIVE?.archetype?.accent);
1389
- };
1390
- if (typeof ResizeObserver !== "undefined" && wordCloud?.parentElement) {
1391
- new ResizeObserver(() => {
1392
- clearTimeout(cloudResizeTimer);
1393
- cloudResizeTimer = setTimeout(redrawCloud, 80);
1394
- }).observe(wordCloud.parentElement);
1395
- }
1396
- window.addEventListener("resize", () => {
1397
- clearTimeout(cloudResizeTimer);
1398
- cloudResizeTimer = setTimeout(redrawCloud, 80);
1399
- });
1400
- </script>
1401
-
1402
- </body>
1403
- </html>