vibe-roast 0.98.0

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 (164) hide show
  1. package/README.md +418 -0
  2. package/assests/README.md +43 -0
  3. package/assests/badges/.gitkeep +0 -0
  4. package/assests/badges/01-builder/builder-badge.png +0 -0
  5. package/assests/badges/01-builder/builder-badge.svg +4 -0
  6. package/assests/badges/02-debugger/debugger-badge.png +0 -0
  7. package/assests/badges/02-debugger/debugger-badge.svg +4 -0
  8. package/assests/badges/03-architect/architect-badge.png +0 -0
  9. package/assests/badges/03-architect/architect-badge.svg +4 -0
  10. package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
  11. package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
  12. package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
  13. package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
  14. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
  15. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
  16. package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
  17. package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
  18. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
  19. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
  20. package/assests/banners/.gitkeep +0 -0
  21. package/assests/banners/home-hero.png +0 -0
  22. package/assests/banners/home-hero.svg +3 -0
  23. package/assests/banners/result-header.png +0 -0
  24. package/assests/banners/result-header.svg +3 -0
  25. package/assests/banners/share-strip.png +0 -0
  26. package/assests/banners/share-strip.svg +3 -0
  27. package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
  28. package/assests/characters/01-builder/builder-card.svg +3 -0
  29. package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
  30. package/assests/characters/01-builder/builder-character.svg +3 -0
  31. package/assests/characters/01-builder/builder-figure.png +0 -0
  32. package/assests/characters/01-builder/builder-mascot.png +0 -0
  33. package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
  34. package/assests/characters/02-debugger/debugger-card.svg +3 -0
  35. package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
  36. package/assests/characters/02-debugger/debugger-character.svg +3 -0
  37. package/assests/characters/02-debugger/debugger-figure.png +0 -0
  38. package/assests/characters/02-debugger/debugger-mascot.png +0 -0
  39. package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
  40. package/assests/characters/03-architect/architect-card.svg +3 -0
  41. package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
  42. package/assests/characters/03-architect/architect-character.svg +3 -0
  43. package/assests/characters/03-architect/architect-figure.png +0 -0
  44. package/assests/characters/03-architect/architect-mascot.png +0 -0
  45. package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
  46. package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
  47. package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
  48. package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
  49. package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
  50. package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
  51. package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
  52. package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
  53. package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
  54. package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
  55. package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
  56. package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
  57. package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
  58. package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
  59. package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
  60. package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
  61. package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
  62. package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
  63. package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
  64. package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
  65. package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
  66. package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
  67. package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
  68. package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
  69. package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
  70. package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
  71. package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
  72. package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
  73. package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
  74. package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
  75. package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
  76. package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
  77. package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
  78. package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
  79. package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
  80. package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
  81. package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
  82. package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
  83. package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
  84. package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
  85. package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
  86. package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
  87. package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
  88. package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
  89. package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
  90. package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
  91. package/assests/live-report.html +1403 -0
  92. package/assests/notes/live-inspect-summary.json +3487 -0
  93. package/assests/scripts/compose-banners.py +148 -0
  94. package/assests/scripts/live-report-boot.js +2 -0
  95. package/assests/scripts/profiles.json +104 -0
  96. package/assests/scripts/qa-faces.mjs +72 -0
  97. package/assests/scripts/roast-extras.js +619 -0
  98. package/assests/scripts/score-engine.js +62 -0
  99. package/assests/scripts/split-badges.py +49 -0
  100. package/assests/scripts/split-reference.py +38 -0
  101. package/assests/scripts/sync-banners.mjs +45 -0
  102. package/assests/scripts/sync-cards.mjs +50 -0
  103. package/assests/scripts/sync-raster.mjs +118 -0
  104. package/assests/scripts/vibe-types.json +22 -0
  105. package/assests/source/design-system.md +50 -0
  106. package/assests/source/prompts/01-builder.md +19 -0
  107. package/assests/source/prompts/02-debugger.md +17 -0
  108. package/assests/source/prompts/03-architect.md +16 -0
  109. package/assests/source/prompts/04-codex-believer.md +13 -0
  110. package/assests/source/prompts/05-prompt-priest.md +13 -0
  111. package/assests/source/prompts/06-tab-hoarder.md +13 -0
  112. package/assests/source/prompts/07-context-maxxer.md +13 -0
  113. package/assests/source/prompts/08-yolo-shipper.md +13 -0
  114. package/assests/source/references/sbti-style-reference-board.png +0 -0
  115. package/assests/source/references/vibe-badges-v6-board.png +0 -0
  116. package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
  117. package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
  118. package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
  119. package/assests/source/vibe-types-visual-brief.md +38 -0
  120. package/bin/hook.js +121 -0
  121. package/bin/vibe-roast.js +98 -0
  122. package/bin/vibe-wrapper.js +4 -0
  123. package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
  124. package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
  125. package/dashboard/dist/index.html +32 -0
  126. package/package.json +58 -0
  127. package/src/extract/environment.js +160 -0
  128. package/src/extract/phrase-stats.js +684 -0
  129. package/src/extract/prompt-analysis.js +368 -0
  130. package/src/extract/text.js +30 -0
  131. package/src/hooks/install.js +184 -0
  132. package/src/inspect.js +261 -0
  133. package/src/lib/activity-metrics.js +238 -0
  134. package/src/lib/agent-score.js +187 -0
  135. package/src/lib/ai-roast.js +446 -0
  136. package/src/lib/dates.js +43 -0
  137. package/src/lib/hashtags.js +150 -0
  138. package/src/lib/jsonl.js +48 -0
  139. package/src/lib/load-env.js +59 -0
  140. package/src/lib/roast-evidence.js +186 -0
  141. package/src/lib/roast-snapshot.js +58 -0
  142. package/src/server.js +644 -0
  143. package/src/sources/aider.js +150 -0
  144. package/src/sources/amazonq.js +113 -0
  145. package/src/sources/antigravity.js +147 -0
  146. package/src/sources/claude.js +262 -0
  147. package/src/sources/cline.js +11 -0
  148. package/src/sources/codex.js +222 -0
  149. package/src/sources/common.js +133 -0
  150. package/src/sources/continue.js +67 -0
  151. package/src/sources/copilot.js +170 -0
  152. package/src/sources/cursor.js +238 -0
  153. package/src/sources/gemini.js +75 -0
  154. package/src/sources/index.js +61 -0
  155. package/src/sources/roo.js +11 -0
  156. package/src/sources/token-tracker.js +171 -0
  157. package/src/sources/vibe-tracker.js +79 -0
  158. package/src/sources/vscode-tasks.js +142 -0
  159. package/src/sources/windsurf.js +109 -0
  160. package/worker/README.md +96 -0
  161. package/worker/package-lock.json +1547 -0
  162. package/worker/package.json +20 -0
  163. package/worker/src/index.mjs +694 -0
  164. package/worker/wrangler.jsonc +40 -0
@@ -0,0 +1,1403 @@
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>