gsarti Claude Opus 4.6 commited on
Commit
1f2f100
·
1 Parent(s): 25e5cb2

Add trace viewer with HuggingFace dataset loading

Browse files

Interactive trace viewer that loads trajectories from project-telos/trajectories_test_full
via size/complexity/index selectors, or accepts uploaded JSON files for trajectories
with probe predictions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +11 -4
  2. index.html +1971 -18
  3. style.css +0 -28
README.md CHANGED
@@ -1,10 +1,17 @@
1
  ---
2
  title: Trace Viewer
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
  title: Trace Viewer
3
+ emoji: 🔍
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ license: apache-2.0
9
  ---
10
 
11
+ Interactive trace viewer for visualizing language model agent trajectories in 2D grid environments, with probing results overlaid on tokens.
12
+
13
+ Trajectories can be loaded directly from the [project-telos/trajectories_test_full](https://huggingface.co/datasets/project-telos/trajectories_test_full) dataset, or uploaded as JSON files (e.g. from [project-telos/trajectories_test_full_with_cognitive_map_probes](https://huggingface.co/datasets/project-telos/trajectories_test_full_with_cognitive_map_probes) for probe visualizations).
14
+
15
+ Part of the paper: [A Behavioural and Representational Evaluation of Goal-Directedness in Language Model Agents](https://arxiv.org/abs/2602.08964).
16
+
17
+ Source code: [SPAR-Telos/interp](https://github.com/SPAR-Telos/interp)
index.html CHANGED
@@ -1,19 +1,1972 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trace Viewer</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: black;
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ padding: 20px;
22
+ color: white;
23
+ }
24
+
25
+ .container {
26
+ background: rgba(255, 255, 255, 0.1);
27
+ backdrop-filter: blur(10px);
28
+ border-radius: 20px;
29
+ padding: 30px;
30
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
31
+ border: 1px solid rgba(255, 255, 255, 0.18);
32
+ width: 100%;
33
+ }
34
+
35
+ h1 {
36
+ text-align: center;
37
+ margin-bottom: 30px;
38
+ font-size: 2.5em;
39
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
40
+ }
41
+
42
+ .file-input-container {
43
+ margin-bottom: 20px;
44
+ text-align: center;
45
+ }
46
+
47
+ .file-input-label {
48
+ background: rgba(255, 255, 255, 0.2);
49
+ padding: 10px 20px;
50
+ border-radius: 8px;
51
+ cursor: pointer;
52
+ display: inline-block;
53
+ transition: all 0.3s ease;
54
+ border: 2px solid rgba(255, 255, 255, 0.5);
55
+ color: white;
56
+ font-size: 16px;
57
+ min-width: 80px;
58
+ text-align: center;
59
+ }
60
+
61
+ .file-input-label:hover {
62
+ background: rgba(255, 255, 255, 0.3);
63
+ transform: translateY(-2px);
64
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
65
+ }
66
+
67
+ .control-separator {
68
+ width: 2px;
69
+ height: 30px;
70
+ background: rgba(255, 255, 255, 0.2);
71
+ margin-top: 8px;
72
+ }
73
+
74
+ #fileInput {
75
+ display: none;
76
+ }
77
+
78
+ .visualization-container {
79
+ display: flex;
80
+ flex-direction: row;
81
+ align-items: center;
82
+ gap: 30px;
83
+ margin: 20px 0;
84
+ padding-left: 20px;
85
+ padding-right: 20px;
86
+ }
87
+
88
+ .canvas-container {
89
+ display: flex;
90
+ justify-content: center;
91
+ }
92
+
93
+ #gridCanvas {
94
+ border-radius: 10px;
95
+ }
96
+
97
+ .action-bars {
98
+ width: 100%;
99
+ max-width: 600px;
100
+ transition: opacity 0.3s ease;
101
+ }
102
+
103
+ .state-tracker-container {
104
+ display: flex;
105
+ flex-direction: column;
106
+ align-items: center;
107
+ gap: 20px;
108
+ width: 100%;
109
+ justify-content: center;
110
+ }
111
+
112
+ .action-bars h3 {
113
+ text-align: center;
114
+ margin-bottom: 20px;
115
+ font-size: 1.3em;
116
+ color: rgba(255, 255, 255, 0.9);
117
+ }
118
+
119
+ .bars-container {
120
+ display: flex;
121
+ justify-content: space-around;
122
+ align-items: flex-end;
123
+ height: 200px;
124
+ gap: 20px;
125
+ }
126
+
127
+ .bar-wrapper {
128
+ flex: 1;
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ gap: 10px;
133
+ }
134
+
135
+ .bar {
136
+ width: 100%;
137
+ max-width: 80px;
138
+ background: linear-gradient(to top, #4CAF50, #8BC34A);
139
+ border-radius: 5px 5px 0 0;
140
+ transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
141
+ display: flex;
142
+ align-items: flex-start;
143
+ justify-content: center;
144
+ padding-top: 8px;
145
+ min-height: 5px;
146
+ }
147
+
148
+ .bar-value {
149
+ color: white;
150
+ font-weight: bold;
151
+ font-size: 0.9em;
152
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
153
+ }
154
+
155
+ .bar-label {
156
+ font-weight: bold;
157
+ font-size: 1em;
158
+ color: rgba(255, 255, 255, 0.95);
159
+ padding-top: 10px;
160
+ }
161
+
162
+ .controls {
163
+ display: flex;
164
+ justify-content: center;
165
+ gap: 15px;
166
+ margin-top: 30px;
167
+ flex-wrap: wrap;
168
+ }
169
+
170
+ button {
171
+ background: rgba(255, 255, 255, 0.2);
172
+ border: 2px solid rgba(255, 255, 255, 0.5);
173
+ color: white;
174
+ padding: 10px 20px;
175
+ border-radius: 8px;
176
+ cursor: pointer;
177
+ font-size: 16px;
178
+ transition: all 0.3s ease;
179
+ min-width: 80px;
180
+ }
181
+
182
+ button:hover:not(:disabled) {
183
+ background: rgba(255, 255, 255, 0.3);
184
+ transform: translateY(-2px);
185
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
186
+ }
187
+
188
+ button:disabled {
189
+ opacity: 0.5;
190
+ cursor: not-allowed;
191
+ }
192
+
193
+ button.active {
194
+ background: rgba(76, 175, 80, 0.5);
195
+ border-color: #4CAF50;
196
+ }
197
+
198
+ .speed-control {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 10px;
202
+ background: rgba(255, 255, 255, 0.1);
203
+ padding: 10px 15px;
204
+ border-radius: 8px;
205
+ }
206
+
207
+ #speedSlider {
208
+ width: 100px;
209
+ cursor: pointer;
210
+ }
211
+
212
+ .legend {
213
+ display: flex;
214
+ gap: 15px;
215
+ justify-content: center;
216
+ margin-top: 20px;
217
+ padding: 10px;
218
+ background: rgba(255, 255, 255,0.2);
219
+ border-radius: 10px;
220
+ }
221
+
222
+ .legend-item {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 10px;
226
+ }
227
+
228
+ .legend-color {
229
+ width: 30px;
230
+ height: 30px;
231
+ border-radius: 5px;
232
+ border: 2px solid rgba(255, 255, 255, 0.5);
233
+ text-align: center;
234
+ line-height: 25px;
235
+ }
236
+
237
+ .placeholder-message {
238
+ text-align: center;
239
+ padding: 40px;
240
+ color: rgba(255, 255, 255, 0.7);
241
+ font-size: 1.2em;
242
+ }
243
+
244
+ /* HuggingFace dataset loader */
245
+ .hf-loader {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 10px;
249
+ flex-wrap: wrap;
250
+ }
251
+
252
+ .hf-loader select {
253
+ padding: 8px 12px;
254
+ border-radius: 8px;
255
+ background: rgba(255, 255, 255, 0.9);
256
+ color: #333;
257
+ border: 2px solid rgba(255, 255, 255, 0.5);
258
+ cursor: pointer;
259
+ font-size: 14px;
260
+ }
261
+
262
+ .hf-loader label {
263
+ font-size: 14px;
264
+ color: rgba(255, 255, 255, 0.9);
265
+ }
266
+
267
+ .hf-loader button {
268
+ min-width: auto;
269
+ }
270
+
271
+ .hf-loader button:disabled {
272
+ opacity: 0.5;
273
+ cursor: not-allowed;
274
+ }
275
+
276
+ .hf-loader .hf-icon {
277
+ font-size: 16px;
278
+ }
279
+
280
+ /* Header section */
281
+ .header-section {
282
+ display: flex;
283
+ justify-content: space-between;
284
+ align-items: center;
285
+ margin-bottom: 20px;
286
+ }
287
+
288
+ #paramsMenuToggle {
289
+ background: rgba(255, 255, 255, 0.2);
290
+ border: 2px solid rgba(255, 255, 255, 0.5);
291
+ color: white;
292
+ padding: 8px 16px;
293
+ border-radius: 8px;
294
+ cursor: pointer;
295
+ font-size: 14px;
296
+ transition: all 0.3s ease;
297
+ }
298
+
299
+ #paramsMenuToggle:hover {
300
+ background: rgba(255, 255, 255, 0.3);
301
+ transform: translateY(-2px);
302
+ }
303
+
304
+ /* Collapsible params menu */
305
+ .params-menu {
306
+ position: fixed;
307
+ top: 80px;
308
+ right: 20px;
309
+ max-width: 400px;
310
+ max-height: 60vh;
311
+ overflow-y: auto;
312
+ background: rgba(255, 255, 255, 0.95);
313
+ backdrop-filter: blur(10px);
314
+ border-radius: 10px;
315
+ padding: 20px;
316
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
317
+ z-index: 1000;
318
+ transition: transform 0.3s, opacity 0.3s;
319
+ color: #333;
320
+ }
321
+
322
+ .params-menu.collapsed {
323
+ transform: translateY(-20px);
324
+ opacity: 0;
325
+ pointer-events: none;
326
+ }
327
+
328
+ .params-section {
329
+ margin-bottom: 20px;
330
+ }
331
+
332
+ .params-section h3 {
333
+ color: #667eea;
334
+ margin-bottom: 10px;
335
+ font-size: 1.1em;
336
+ }
337
+
338
+ #gridParamsContent,
339
+ #modelParamsContent {
340
+ font-size: 0.9em;
341
+ line-height: 1.6;
342
+ }
343
+
344
+ #gridParamsContent div,
345
+ #modelParamsContent div {
346
+ margin: 4px 0;
347
+ }
348
+
349
+ /* Updated visualization container - side by side */
350
+ .visualization-container {
351
+ display: flex;
352
+ gap: 0;
353
+ justify-content: space-between;
354
+ align-items: flex-start;
355
+ margin: 20px 0;
356
+ min-height: 100%;
357
+ }
358
+
359
+ .canvas-container {
360
+ flex: 1;
361
+ display: flex;
362
+ flex-direction: column;
363
+ align-items: center;
364
+ justify-content: flex-start;
365
+ }
366
+
367
+ .canvas-container h3 {
368
+ text-align: center;
369
+ margin-bottom: 50px;
370
+ font-size: 1.2em;
371
+ color: rgba(255, 255, 255, 0.9);
372
+ }
373
+
374
+ .canvas-container h3 #stepInfo {
375
+ font-size: 0.9em;
376
+ color: rgba(255, 255, 255, 0.7);
377
+ }
378
+
379
+ /* Decoded grid container */
380
+ .decoded-grid-container {
381
+ flex: 1;
382
+ display: flex;
383
+ flex-direction: column;
384
+ align-items: center;
385
+ justify-content: flex-start;
386
+ transition: opacity 0.3s ease;
387
+ }
388
+
389
+ .decoded-grid-container.hidden {
390
+ opacity: 0;
391
+ pointer-events: none;
392
+ visibility: hidden;
393
+ }
394
+
395
+ .decoded-grid-container h3 {
396
+ text-align: center;
397
+ margin-bottom: 15px;
398
+ font-size: 1.2em;
399
+ color: rgba(255, 255, 255, 0.9);
400
+ }
401
+
402
+ #decodedGridCanvas {
403
+ border-radius: 10px;
404
+ cursor: pointer;
405
+ }
406
+
407
+ .layer-selector-container {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 10px;
411
+ margin-bottom: 10px;
412
+ justify-content: center;
413
+ }
414
+
415
+ .layer-selector-container label {
416
+ font-size: 0.9em;
417
+ color: rgba(255, 255, 255, 0.9);
418
+ }
419
+
420
+ .layer-selector-container select {
421
+ padding: 4px 8px;
422
+ border-radius: 4px;
423
+ background: rgba(255, 255, 255, 0.9);
424
+ color: #333;
425
+ border: 1px solid rgba(255, 255, 255, 0.5);
426
+ cursor: pointer;
427
+ font-size: 0.85em;
428
+ }
429
+
430
+ /* Controls and legend section */
431
+ .controls-legend-section {
432
+ margin: 30px 0;
433
+ }
434
+
435
+ /* Token displays section */
436
+ .token-displays-section {
437
+ margin-top: 30px;
438
+ padding: 20px;
439
+ background: rgba(255, 255, 255, 0.1);
440
+ border-radius: 15px;
441
+ }
442
+
443
+ .token-display {
444
+ margin-bottom: 25px;
445
+ }
446
+
447
+ .token-display h3 {
448
+ margin-bottom: 10px;
449
+ font-size: 1.2em;
450
+ color: rgba(255, 255, 255, 0.95);
451
+ }
452
+
453
+ .collapsible-header {
454
+ cursor: pointer;
455
+ user-select: none;
456
+ transition: color 0.2s;
457
+ }
458
+
459
+ .collapsible-header:hover {
460
+ color: rgba(255, 255, 255, 1);
461
+ }
462
+
463
+ .collapse-icon {
464
+ display: inline-block;
465
+ transition: transform 0.2s;
466
+ font-size: 0.8em;
467
+ }
468
+
469
+ .token-container {
470
+ background: rgba(0, 0, 0, 0.2);
471
+ padding: 15px;
472
+ border-radius: 8px;
473
+ line-height: 1.8;
474
+ min-height: 60px;
475
+ font-size: 0.85em;
476
+ transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
477
+ max-height: 2000px;
478
+ overflow: hidden;
479
+ }
480
+
481
+ .token-container.collapsed {
482
+ max-height: 0;
483
+ padding: 0 15px;
484
+ opacity: 0;
485
+ min-height: 0;
486
+ }
487
+
488
+ /* Help section */
489
+ .help-section {
490
+ margin-top: 30px;
491
+ padding: 20px;
492
+ background: rgba(255, 255, 255, 0.05);
493
+ border-radius: 10px;
494
+ border: 1px solid rgba(255, 255, 255, 0.1);
495
+ }
496
+
497
+ .help-content {
498
+ padding: 15px;
499
+ line-height: 1.6;
500
+ font-size: 0.9em;
501
+ transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
502
+ max-height: 2000px;
503
+ overflow: hidden;
504
+ }
505
+
506
+ .help-content.collapsed {
507
+ max-height: 0;
508
+ padding: 0 15px;
509
+ opacity: 0;
510
+ }
511
+
512
+ .help-content h4 {
513
+ margin-top: 15px;
514
+ margin-bottom: 10px;
515
+ color: rgba(255, 255, 255, 0.9);
516
+ font-size: 1.05em;
517
+ }
518
+
519
+ .help-content h4:first-child {
520
+ margin-top: 0;
521
+ }
522
+
523
+ .help-content ul {
524
+ margin: 10px 0;
525
+ padding-left: 25px;
526
+ }
527
+
528
+ .help-content li {
529
+ margin: 6px 0;
530
+ }
531
+
532
+ .help-content ul ul {
533
+ margin: 5px 0;
534
+ }
535
+
536
+ .help-content p {
537
+ margin: 10px 0;
538
+ }
539
+
540
+ /* Token styling */
541
+ .token-span {
542
+ padding: 2px 2px;
543
+ border-radius: 3px;
544
+ cursor: pointer;
545
+ white-space: pre;
546
+ transition: all 0.2s;
547
+ display: inline-block;
548
+ font-size: 1em;
549
+ }
550
+
551
+ .token-span:hover {
552
+ transform: scale(1.05);
553
+ box-shadow: 0 2px 8px rgba(255, 255, 255,0.3);
554
+ }
555
+
556
+ /* Token group underlines (no background) */
557
+ .token-span.group-analysis {
558
+ border-bottom: 2px solid rgba(33, 150, 243, 0.8);
559
+ }
560
+
561
+ .token-span.group-final {
562
+ border-bottom: 2px solid rgba(255, 152, 0, 0.8);
563
+ }
564
+
565
+ .token-span.group-action {
566
+ border-bottom: 2px solid rgba(76, 175, 80, 0.8);
567
+ }
568
+
569
+ .token-span.group-template {
570
+ border-bottom: 2px solid rgba(158, 158, 158, 0.8);
571
+ }
572
+
573
+ /* Highlight for tokens with probabilities */
574
+ .token-span.has-probabilities {
575
+ box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.5);
576
+ background: rgba(255, 235, 59, 0.15);
577
+ }
578
+
579
+ /* Highlight for tokens with probes */
580
+ .token-span.has-probe {
581
+ box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.5);
582
+ background: rgba(102, 126, 234, 0.15);
583
+ }
584
+
585
+ /* Combined: token with both group and probabilities */
586
+ .token-span.group-analysis.has-probabilities,
587
+ .token-span.group-final.has-probabilities,
588
+ .token-span.group-action.has-probabilities,
589
+ .token-span.group-template.has-probabilities {
590
+ box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.7);
591
+ }
592
+
593
+ /* Combined: token with both group and probes */
594
+ .token-span.group-analysis.has-probe,
595
+ .token-span.group-final.has-probe,
596
+ .token-span.group-action.has-probe,
597
+ .token-span.group-template.has-probe {
598
+ box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.7);
599
+ }
600
+
601
+ .token-span.pinned {
602
+ border: 2px solid #667eea !important;
603
+ box-shadow: 0 0 8px rgba(102, 126, 234, 0.5) !important;
604
+ }
605
+
606
+ /* Tooltips */
607
+ .token-tooltip {
608
+ position: fixed;
609
+ background: rgba(40, 40, 40, 0.95);
610
+ color: white;
611
+ padding: 12px;
612
+ border-radius: 8px;
613
+ z-index: 2000;
614
+ min-width: 200px;
615
+ max-width: 350px;
616
+ font-size: 0.9em;
617
+ box-shadow: 0 4px 12px rgba(255, 255, 255,0.5);
618
+ pointer-events: none;
619
+ }
620
+
621
+ .token-tooltip .tooltip-section {
622
+ margin: 8px 0;
623
+ display: flex;
624
+ flex-direction: row;
625
+ }
626
+
627
+ .token-tooltip .tooltip-label {
628
+ font-weight: bold;
629
+ margin-right: 4px;
630
+ color: #a0c4ff;
631
+ }
632
+
633
+ .token-tooltip select {
634
+ width: 100%;
635
+ padding: 4px;
636
+ border-radius: 4px;
637
+ background: rgba(255, 255, 255, 0.9);
638
+ color: #333;
639
+ border: none;
640
+ pointer-events: auto;
641
+ cursor: pointer;
642
+ }
643
+
644
+ .tile-tooltip {
645
+ position: fixed;
646
+ background: rgba(40, 40, 40, 0.95);
647
+ color: white;
648
+ padding: 12px;
649
+ border-radius: 8px;
650
+ z-index: 2000;
651
+ min-width: 200px;
652
+ font-size: 0.9em;
653
+ box-shadow: 0 4px 12px rgba(255, 255, 255,0.5);
654
+ }
655
+
656
+ .tile-tooltip .tooltip-title {
657
+ font-weight: bold;
658
+ margin-bottom: 10px;
659
+ color: #a0c4ff;
660
+ }
661
+
662
+ /* Mini bar chart (for both token and tile tooltips) */
663
+ .tile-tooltip .bar-item,
664
+ .token-tooltip .bar-item {
665
+ display: flex;
666
+ align-items: center;
667
+ gap: 8px;
668
+ margin: 6px 0;
669
+ }
670
+
671
+ .tile-tooltip .bar-label,
672
+ .token-tooltip .bar-label {
673
+ min-width: 60px;
674
+ font-size: 0.85em;
675
+ }
676
+
677
+ .tile-tooltip .bar-wrapper,
678
+ .token-tooltip .bar-wrapper {
679
+ flex: 1;
680
+ background: rgba(255, 255, 255, 0.1);
681
+ border-radius: 3px;
682
+ overflow: hidden;
683
+ position: relative;
684
+ height: 16px;
685
+ }
686
+
687
+ .tile-tooltip .bar-fill,
688
+ .token-tooltip .bar-fill {
689
+ height: 100%;
690
+ background: black;
691
+ border-radius: 3px;
692
+ transition: width 0.3s ease;
693
+ position: absolute;
694
+ left: 0;
695
+ top: 0;
696
+ }
697
+
698
+ .tile-tooltip .bar-wrapper::after,
699
+ .token-tooltip .bar-wrapper::after {
700
+ content: attr(data-percentage);
701
+ position: absolute;
702
+ right: 4px;
703
+ top: 50%;
704
+ transform: translateY(-50%);
705
+ color: white;
706
+ font-size: 0.75em;
707
+ font-weight: bold;
708
+ pointer-events: none;
709
+ }
710
+ </style>
711
+ </head>
712
+ <body>
713
+ <div class="container">
714
+ <div class="header-section">
715
+ <h2>Project Telos Trace Viewer</h2>
716
+ <button id="paramsMenuToggle">⚙️ Config</button>
717
+ </div>
718
+
719
+ <div id="paramsMenu" class="params-menu collapsed">
720
+ <div class="params-section">
721
+ <h3>Grid Parameters</h3>
722
+ <div id="gridParamsContent"></div>
723
+ </div>
724
+ <div class="params-section">
725
+ <h3>Model Parameters</h3>
726
+ <div id="modelParamsContent"></div>
727
+ </div>
728
+ </div>
729
+
730
+ <div id="placeholderMessage" class="placeholder-message">
731
+ Select a trajectory from the 🤗 HuggingFace dataset or upload a JSON file to start
732
+ </div>
733
+
734
+ <div class="visualization-container">
735
+ <div class="canvas-container" id="canvasContainer" style="display: none;">
736
+ <h3>Grid State <span id="stepInfo"></span></h3>
737
+ <canvas id="gridCanvas"></canvas>
738
+ </div>
739
+ <div id="decodedGridContainer" class="decoded-grid-container hidden">
740
+ <h3>Decoded Grid State</h3>
741
+ <div class="layer-selector-container">
742
+ <label for="layerSelector">Probe Layer:</label>
743
+ <select id="layerSelector"></select>
744
+ </div>
745
+ <canvas id="decodedGridCanvas"></canvas>
746
+ </div>
747
+ </div>
748
+
749
+ <div class="controls-legend-section">
750
+ <div class="controls">
751
+ <div class="hf-loader">
752
+ <span class="hf-icon">🤗</span>
753
+ <label>Size:</label>
754
+ <select id="hfSize">
755
+ <option value="7">7</option>
756
+ <option value="9">9</option>
757
+ <option value="11">11</option>
758
+ <option value="13">13</option>
759
+ <option value="15">15</option>
760
+ </select>
761
+ <label>Complexity:</label>
762
+ <select id="hfComplexity">
763
+ <option value="0.0">0.0</option>
764
+ <option value="0.2">0.2</option>
765
+ <option value="0.4">0.4</option>
766
+ <option value="0.6">0.6</option>
767
+ <option value="0.8">0.8</option>
768
+ <option value="1.0">1.0</option>
769
+ </select>
770
+ <label>#</label>
771
+ <select id="hfIndex">
772
+ <option value="0">0</option>
773
+ <option value="1">1</option>
774
+ <option value="2">2</option>
775
+ <option value="3">3</option>
776
+ <option value="4">4</option>
777
+ <option value="5">5</option>
778
+ <option value="6">6</option>
779
+ <option value="7">7</option>
780
+ <option value="8">8</option>
781
+ <option value="9">9</option>
782
+ </select>
783
+ <button id="hfLoadBtn">Load</button>
784
+ </div>
785
+ <div class="control-separator"></div>
786
+ <label for="fileInput" class="file-input-label" id="fileInputLabel">
787
+ 📁 Load JSON
788
+ </label>
789
+ <input type="file" id="fileInput" accept=".json" style="display: none;">
790
+ <div class="control-separator"></div>
791
+ <button id="playPauseBtn">▶️ Play</button>
792
+ <button id="backStepBtn">⏪ Back</button>
793
+ <button id="stepBtn">⏩ Step</button>
794
+ <button id="resetBtn">🔄 Reset</button>
795
+ <div class="speed-control">
796
+ <label>Speed:</label>
797
+ <input type="range" id="speedSlider" min="100" max="2000" value="1000" step="100">
798
+ <span id="speedValue">1.0s</span>
799
+ </div>
800
+ </div>
801
+ <div class="legend" id="dynamicLegend" style="display: none;"></div>
802
+ </div>
803
+
804
+ <div class="help-section">
805
+ <h3 class="collapsible-header" id="helpHeader">
806
+ <span class="collapse-icon">▶</span> How to Use
807
+ </h3>
808
+ <div id="helpContent" class="help-content collapsed">
809
+ <h4>Getting Started with Trace Viewer</h4>
810
+
811
+ <p>To use Trace Viewer, select a trajectory from the <b>🤗 HuggingFace dataset</b> using the size, complexity, and index selectors, or upload a <b>JSON trace file</b> using the 📁 Load JSON button. Once loaded, the first grid state will become visible, alongside two prompt and model output sections containing input/output tokens for the current trace step.</p>
812
+ <p>Use the buttons ▶️ Play, ⏸️ Pause, ⏩ Next, ⏪ Back and 🔄 Reset to move across trace steps, and use the slider to control animation speed. A white arrow on the agent tile (A) indicates the predicted action direction from the model. The arrow points in the direction predicted by the agent. A summary of grid and model parameters is available in the ⚙️ Config tab.</p>
813
+
814
+ <h4>Token Visual Indicators</h4>
815
+ <ul>
816
+ <li><strong>Colored Underlines</strong> indicate token types:
817
+ <ul>
818
+ <li><span style="border-bottom: 2px solid rgba(33, 150, 243, 0.8);">Blue</span> = Reasoning tokens</li>
819
+ <li><span style="border-bottom: 2px solid rgba(255, 152, 0, 0.8);">Orange</span> = Final answer tokens</li>
820
+ <li><span style="border-bottom: 2px solid rgba(76, 175, 80, 0.8);">Green</span> = Tokens matching valid output actions</li>
821
+ <li><span style="border-bottom: 2px solid rgba(158, 158, 158, 0.8);">Gray</span> = Special tokens used by the tokenizer</li>
822
+ </ul>
823
+ </li>
824
+ <li><strong>Colored Highlights</strong> indicate the token has additional information visible upon hovering:
825
+ <ul>
826
+ <li><span style="box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.5);">Yellow</span> = Prediction probabilities</li>
827
+ <li><span style="box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.5);">Purple</span> = Probing results</li>
828
+ </ul>
829
+ </li>
830
+ </ul>
831
+ <p>Tokens can be hovered to show an info tooltip with next-token probabilities displayed as bar charts. Tokens with probe information will also display a decoded grid showing the model's internal representation of tile types. Tiles marked with "X" have no probe data available. Clicking on a token pins its position; when advancing steps, the decoded grid will update to show probe data for the same token position in the new step (if available). This allows tracking how the model's internal representation evolves across steps. Use the layer selector dropdown above the decoded grid to switch between different model layers. Hovering on decoded grid tiles shows the prediction probabilities for all tile classes as bar charts. Click the pinned token again to unpin.</p>
832
+ </div>
833
+ </div>
834
+
835
+ <div class="token-displays-section" style="display: none;">
836
+ <div class="token-display">
837
+ <h3 class="collapsible-header" id="promptHeader">
838
+ <span class="collapse-icon">▶</span> Prompt Template
839
+ </h3>
840
+ <div id="promptTokens" class="token-container collapsed"></div>
841
+ </div>
842
+
843
+ <div class="token-display">
844
+ <h3 class="collapsible-header" id="outputHeader">
845
+ <span class="collapse-icon">▼</span> Model Output
846
+ </h3>
847
+ <div id="outputTokens" class="token-container"></div>
848
+ </div>
849
+ </div>
850
+ <div style="height: 200px"></div>
851
+ </div>
852
+
853
+ <script>
854
+ // DOM references
855
+ const canvas = document.getElementById('gridCanvas');
856
+ const ctx = canvas.getContext('2d');
857
+ const canvasContainer = document.getElementById('canvasContainer');
858
+ const decodedCanvas = document.getElementById('decodedGridCanvas');
859
+ const decodedCtx = decodedCanvas.getContext('2d');
860
+ const fileInput = document.getElementById('fileInput');
861
+ const playPauseBtn = document.getElementById('playPauseBtn');
862
+ const backStepBtn = document.getElementById('backStepBtn');
863
+ const stepBtn = document.getElementById('stepBtn');
864
+ const resetBtn = document.getElementById('resetBtn');
865
+ const speedSlider = document.getElementById('speedSlider');
866
+ const speedValue = document.getElementById('speedValue');
867
+ const stepInfo = document.getElementById('stepInfo');
868
+ const placeholderMessage = document.getElementById('placeholderMessage');
869
+ const paramsMenuToggle = document.getElementById('paramsMenuToggle');
870
+ const paramsMenu = document.getElementById('paramsMenu');
871
+ const gridParamsContent = document.getElementById('gridParamsContent');
872
+ const modelParamsContent = document.getElementById('modelParamsContent');
873
+ const dynamicLegend = document.getElementById('dynamicLegend');
874
+ const promptTokens = document.getElementById('promptTokens');
875
+ const outputTokens = document.getElementById('outputTokens');
876
+ const decodedGridContainer = document.getElementById('decodedGridContainer');
877
+ const tokenDisplaysSection = document.querySelector('.token-displays-section');
878
+ const fileInputLabel = document.getElementById('fileInputLabel');
879
+ const promptHeader = document.getElementById('promptHeader');
880
+ const outputHeader = document.getElementById('outputHeader');
881
+ const helpHeader = document.getElementById('helpHeader');
882
+ const helpContent = document.getElementById('helpContent');
883
+ const layerSelector = document.getElementById('layerSelector');
884
+ const hfSize = document.getElementById('hfSize');
885
+ const hfComplexity = document.getElementById('hfComplexity');
886
+ const hfIndex = document.getElementById('hfIndex');
887
+ const hfLoadBtn = document.getElementById('hfLoadBtn');
888
+
889
+ // Global state
890
+ let viewerData = null;
891
+ let currentStepIndex = 0;
892
+ let isPlaying = false;
893
+ let animationTimeout = null;
894
+
895
+ let uiState = {
896
+ currentStepIndex: 0,
897
+ isPlaying: false,
898
+ pinnedToken: null, // { stepIndex, context, tokenIndex, element }
899
+ hoveredToken: null,
900
+ selectedLayer: {}, // Map: tokenKey -> layerName
901
+ paramsMenuCollapsed: true,
902
+ tokenGroupColors: {
903
+ "analysis": "rgba(227, 242, 253, 0.6)",
904
+ "final": "rgba(255, 243, 224, 0.6)",
905
+ "action": "rgba(241, 248, 233, 0.6)",
906
+ "template": "rgba(245, 245, 245, 0.6)"
907
+ }
908
+ };
909
+
910
+ let currentTooltip = null;
911
+ let decodedGridTileProbes = null;
912
+
913
+ // Tile color mapping (will be dynamically built from legend)
914
+ let tileColorMap = {};
915
+
916
+ // Helper functions
917
+ function getTileColor(tileType) {
918
+ // Return color from dynamic legend mapping
919
+ if (tileColorMap[tileType]) {
920
+ return tileColorMap[tileType];
921
+ }
922
+ return '#CCCCCC'; // Default gray
923
+ }
924
+
925
+ function buildTileColorMap() {
926
+ if (!viewerData || !viewerData.grid_params || !viewerData.grid_params.legend) {
927
+ return;
928
+ }
929
+
930
+ const legend = viewerData.grid_params.legend;
931
+ const colorPalette = {
932
+ 'wall': '#666666',
933
+ 'empty': '#E8F5E9',
934
+ 'agent': '#F44336',
935
+ 'goal': '#4CAF50',
936
+ 'start': '#2196F3',
937
+ 'fog': '#444444',
938
+ 'unknown_obj': '#FF9800'
939
+ };
940
+
941
+ tileColorMap = {};
942
+ Object.keys(legend).forEach((tileType, index) => {
943
+ tileColorMap[tileType] = colorPalette[tileType] || `hsl(${index * 45}, 70%, 60%)`;
944
+ });
945
+ }
946
+
947
+ function validateData(data) {
948
+ if (!data.grid_params) throw new Error('Missing grid_params');
949
+ if (!data.model_params) throw new Error('Missing model_params');
950
+ if (!data.prompt) throw new Error('Missing prompt');
951
+ if (!data.steps || !Array.isArray(data.steps)) throw new Error('Invalid steps');
952
+
953
+ // Validate legend structure
954
+ if (!data.grid_params.legend) throw new Error('Missing legend');
955
+ for (const [key, val] of Object.entries(data.grid_params.legend)) {
956
+ if (!val.symbol || !val.description) {
957
+ throw new Error(`Invalid legend entry: ${key}`);
958
+ }
959
+ }
960
+ }
961
+
962
+ // Event listeners
963
+ speedSlider.addEventListener('input', (e) => {
964
+ speedValue.textContent = (e.target.value / 1000).toFixed(1) + 's';
965
+ });
966
+
967
+ paramsMenuToggle.addEventListener('click', () => {
968
+ uiState.paramsMenuCollapsed = !uiState.paramsMenuCollapsed;
969
+ if (uiState.paramsMenuCollapsed) {
970
+ paramsMenu.classList.add('collapsed');
971
+ } else {
972
+ paramsMenu.classList.remove('collapsed');
973
+ }
974
+ });
975
+
976
+ function loadTrajectoryData(data, label) {
977
+ // Validate data structure
978
+ validateData(data);
979
+
980
+ // Store data
981
+ viewerData = data;
982
+ currentStepIndex = 0;
983
+ uiState.currentStepIndex = 0;
984
+ uiState.pinnedToken = null;
985
+ uiState.selectedLayer = {};
986
+
987
+ // Build color map from legend
988
+ buildTileColorMap();
989
+
990
+ // Update file label
991
+ const displayLabel = label.length > 20 ? label.substring(0, 17) + '...' : label;
992
+ fileInputLabel.textContent = `📁 ${displayLabel}`;
993
+
994
+ // Render UI components
995
+ renderParamsMenu();
996
+ renderLegend();
997
+ resetAnimation();
998
+ displayStep(0);
999
+
1000
+ // Show/hide UI elements
1001
+ placeholderMessage.style.display = 'none';
1002
+ canvasContainer.style.display = 'flex';
1003
+ dynamicLegend.style.display = 'flex';
1004
+ tokenDisplaysSection.style.display = 'block';
1005
+ }
1006
+
1007
+ fileInput.addEventListener('change', async (e) => {
1008
+ const file = e.target.files[0];
1009
+ if (file) {
1010
+ try {
1011
+ const text = await file.text();
1012
+ const data = JSON.parse(text);
1013
+ loadTrajectoryData(data, file.name);
1014
+ } catch (error) {
1015
+ alert('Error loading JSON file: ' + error.message);
1016
+ console.error(error);
1017
+ }
1018
+ }
1019
+ });
1020
+
1021
+ hfLoadBtn.addEventListener('click', async () => {
1022
+ const size = hfSize.value;
1023
+ const comp = hfComplexity.value;
1024
+ const idx = hfIndex.value;
1025
+ const fileName = `together_ai_openai_gpt-oss-20b_size${size}_comp${comp}_${idx}.json`;
1026
+ const url = `https://huggingface.co/datasets/project-telos/trajectories_test_full/resolve/main/size${size}/${fileName}`;
1027
+
1028
+ hfLoadBtn.disabled = true;
1029
+ hfLoadBtn.textContent = 'Loading...';
1030
+ try {
1031
+ const response = await fetch(url);
1032
+ if (!response.ok) {
1033
+ throw new Error(`Failed to fetch trajectory (${response.status})`);
1034
+ }
1035
+ const data = await response.json();
1036
+ loadTrajectoryData(data, `s${size}_c${comp}_${idx}`);
1037
+ } catch (error) {
1038
+ alert('Error loading from HuggingFace: ' + error.message);
1039
+ console.error(error);
1040
+ } finally {
1041
+ hfLoadBtn.disabled = false;
1042
+ hfLoadBtn.textContent = 'Load';
1043
+ }
1044
+ });
1045
+
1046
+ function renderParamsMenu() {
1047
+ if (!viewerData) return;
1048
+
1049
+ // Render grid params (exclude legend)
1050
+ gridParamsContent.innerHTML = '';
1051
+ const gridParams = viewerData.grid_params;
1052
+ for (const [key, value] of Object.entries(gridParams)) {
1053
+ if (key !== 'legend') {
1054
+ const div = document.createElement('div');
1055
+ div.innerHTML = `<strong>${key}:</strong> ${JSON.stringify(value)}`;
1056
+ gridParamsContent.appendChild(div);
1057
+ }
1058
+ }
1059
+
1060
+ // Render model params
1061
+ modelParamsContent.innerHTML = '';
1062
+ const modelParams = viewerData.model_params;
1063
+ for (const [key, value] of Object.entries(modelParams)) {
1064
+ const div = document.createElement('div');
1065
+ div.innerHTML = `<strong>${key}:</strong> ${JSON.stringify(value)}`;
1066
+ modelParamsContent.appendChild(div);
1067
+ }
1068
+ }
1069
+
1070
+ function renderLegend() {
1071
+ if (!viewerData || !viewerData.grid_params || !viewerData.grid_params.legend) return;
1072
+
1073
+ dynamicLegend.innerHTML = '';
1074
+ const legend = viewerData.grid_params.legend;
1075
+
1076
+ for (const [tileType, info] of Object.entries(legend)) {
1077
+ const legendItem = document.createElement('div');
1078
+ legendItem.className = 'legend-item';
1079
+
1080
+ const colorBox = document.createElement('div');
1081
+ colorBox.className = 'legend-color';
1082
+ colorBox.style.background = getTileColor(tileType);
1083
+ colorBox.textContent = info.symbol;
1084
+
1085
+ const label = document.createElement('span');
1086
+ label.textContent = info.description;
1087
+
1088
+ legendItem.appendChild(colorBox);
1089
+ legendItem.appendChild(label);
1090
+ dynamicLegend.appendChild(legendItem);
1091
+ }
1092
+
1093
+ // Add special legend item for missing probe data
1094
+ const missingItem = document.createElement('div');
1095
+ missingItem.className = 'legend-item';
1096
+
1097
+ const missingBox = document.createElement('canvas');
1098
+ missingBox.className = 'legend-color';
1099
+ missingBox.width = 30;
1100
+ missingBox.height = 30;
1101
+ const ctx = missingBox.getContext('2d');
1102
+
1103
+ // Draw gray background
1104
+ ctx.fillStyle = '#888888';
1105
+ ctx.fillRect(0, 0, 30, 30);
1106
+
1107
+ // Draw X
1108
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
1109
+ ctx.lineWidth = 2;
1110
+ ctx.beginPath();
1111
+ ctx.moveTo(5, 5);
1112
+ ctx.lineTo(25, 25);
1113
+ ctx.moveTo(25, 5);
1114
+ ctx.lineTo(5, 25);
1115
+ ctx.stroke();
1116
+
1117
+ const missingLabel = document.createElement('span');
1118
+ missingLabel.textContent = 'Missing Info';
1119
+
1120
+ missingItem.appendChild(missingBox);
1121
+ missingItem.appendChild(missingLabel);
1122
+ dynamicLegend.appendChild(missingItem);
1123
+ }
1124
+
1125
+ function drawOriginalGrid(gridStateLines, agentAction = null) {
1126
+ if (!gridStateLines || gridStateLines.length === 0) return;
1127
+
1128
+ // Grid state is an array of strings, first line is coordinates
1129
+ // Skip the first line (coordinate headers) and parse the rest
1130
+ const gridLines = gridStateLines.slice(1).map(line => line.trim()).filter(line => line.length > 0);
1131
+
1132
+ const cellSize = 30;
1133
+ const padding = 20;
1134
+
1135
+ // Parse grid to extract symbols (skip row number at start of each line)
1136
+ const grid = gridLines.map(line => {
1137
+ const parts = line.split(/\s+/);
1138
+ // First part is row number, rest are symbols
1139
+ return parts.slice(1);
1140
+ });
1141
+
1142
+ if (grid.length === 0 || !grid[0]) return;
1143
+
1144
+ const numRows = grid.length;
1145
+ const numCols = grid[0].length;
1146
+
1147
+ canvas.width = numCols * cellSize + padding * 2;
1148
+ canvas.height = numRows * cellSize + padding * 2;
1149
+
1150
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
1151
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1152
+
1153
+ let agentPosition = null;
1154
+
1155
+ grid.forEach((row, y) => {
1156
+ row.forEach((symbol, x) => {
1157
+ const xPos = x * cellSize + padding;
1158
+ const yPos = y * cellSize + padding;
1159
+
1160
+ // Track agent position
1161
+ if (symbol === 'A') {
1162
+ agentPosition = { row: y, col: x, xPos, yPos };
1163
+ }
1164
+
1165
+ // Map symbol to tile type from legend
1166
+ let tileType = null;
1167
+ if (viewerData && viewerData.grid_params && viewerData.grid_params.legend) {
1168
+ for (const [type, info] of Object.entries(viewerData.grid_params.legend)) {
1169
+ if (info.symbol === symbol) {
1170
+ tileType = type;
1171
+ break;
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ // Draw cell background
1177
+ ctx.fillStyle = tileType ? getTileColor(tileType) : '#CCCCCC';
1178
+ ctx.fillRect(xPos, yPos, cellSize, cellSize);
1179
+
1180
+ // Draw grid lines
1181
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
1182
+ ctx.strokeRect(xPos, yPos, cellSize, cellSize);
1183
+
1184
+ // Draw symbol
1185
+ if (symbol && symbol !== '_') {
1186
+ ctx.fillStyle = 'white';
1187
+ ctx.font = 'bold 16px Arial';
1188
+ ctx.textAlign = 'center';
1189
+ ctx.textBaseline = 'middle';
1190
+ ctx.fillText(symbol, xPos + cellSize/2, yPos + cellSize/2);
1191
+ }
1192
+ });
1193
+ });
1194
+
1195
+ // Draw arrow on agent tile if agent_action is provided
1196
+ if (agentAction && agentPosition) {
1197
+ drawActionArrow(agentPosition, agentAction, cellSize);
1198
+ }
1199
+ }
1200
+
1201
+ function drawActionArrow(agentPosition, agentAction, cellSize) {
1202
+ const centerX = agentPosition.xPos + cellSize / 2;
1203
+ const centerY = agentPosition.yPos + cellSize / 2;
1204
+ const arrowLength = cellSize * 0.50;
1205
+ const arrowHeadSize = 7;
1206
+
1207
+ // Calculate arrow end point based on direction
1208
+ let endX = centerX;
1209
+ let endY = centerY;
1210
+
1211
+ switch (agentAction) {
1212
+ case 'UP':
1213
+ endY -= arrowLength;
1214
+ break;
1215
+ case 'DOWN':
1216
+ endY += arrowLength;
1217
+ break;
1218
+ case 'LEFT':
1219
+ endX -= arrowLength;
1220
+ break;
1221
+ case 'RIGHT':
1222
+ endX += arrowLength;
1223
+ break;
1224
+ default:
1225
+ return; // Unknown action, don't draw
1226
+ }
1227
+
1228
+ // Calculate arrowhead points
1229
+ const angle = Math.atan2(endY - centerY, endX - centerX);
1230
+ const arrowHead1X = endX - arrowHeadSize * Math.cos(angle - Math.PI / 6);
1231
+ const arrowHead1Y = endY - arrowHeadSize * Math.sin(angle - Math.PI / 6);
1232
+ const arrowHead2X = endX - arrowHeadSize * Math.cos(angle + Math.PI / 6);
1233
+ const arrowHead2Y = endY - arrowHeadSize * Math.sin(angle + Math.PI / 6);
1234
+
1235
+ // Draw arrowhead
1236
+ ctx.fillStyle = 'rgba(255, 255, 255, 1)';
1237
+ ctx.beginPath();
1238
+ ctx.moveTo(endX, endY);
1239
+ ctx.lineTo(arrowHead1X, arrowHead1Y);
1240
+ ctx.lineTo(arrowHead2X, arrowHead2Y);
1241
+ ctx.closePath();
1242
+ ctx.fill();
1243
+ }
1244
+
1245
+ // Token rendering functions
1246
+ function renderPromptTokens(step) {
1247
+ promptTokens.innerHTML = '';
1248
+
1249
+ // Assemble full prompt: prefix + grid_state + suffix
1250
+ const allTokens = [
1251
+ ...(viewerData.prompt.prompt_prefix_tokens || []),
1252
+ ...(step.grid_state_tokens || []),
1253
+ ...(step.prompt_suffix_tokens || [])
1254
+ ];
1255
+
1256
+ const fragment = document.createDocumentFragment();
1257
+ allTokens.forEach((token, index) => {
1258
+ const tokenElement = createTokenSpan(token, 'prompt', index);
1259
+ fragment.appendChild(tokenElement);
1260
+ });
1261
+
1262
+ promptTokens.appendChild(fragment);
1263
+ }
1264
+
1265
+ function renderOutputTokens(step) {
1266
+ outputTokens.innerHTML = '';
1267
+
1268
+ if (!step.output_tokens || step.output_tokens.length === 0) {
1269
+ outputTokens.textContent = '(No output)';
1270
+ return;
1271
+ }
1272
+
1273
+ const fragment = document.createDocumentFragment();
1274
+ step.output_tokens.forEach((token, index) => {
1275
+ const tokenElement = createTokenSpan(token, 'output', index);
1276
+ fragment.appendChild(tokenElement);
1277
+ });
1278
+
1279
+ outputTokens.appendChild(fragment);
1280
+ }
1281
+
1282
+ function createTokenSpan(token, context, index) {
1283
+ // Check if token contains newlines (Ċ)
1284
+ if (token.token.includes('Ċ')) {
1285
+ // Split by newline and create multiple elements
1286
+ const parts = token.token.split('Ċ');
1287
+ const fragment = document.createDocumentFragment();
1288
+
1289
+ parts.forEach((part, i) => {
1290
+ if (part.length > 0) {
1291
+ // Create span for non-empty part
1292
+ const span = document.createElement('span');
1293
+ span.className = 'token-span';
1294
+
1295
+ // Replace Ġ with space for display
1296
+ let displayText = part.replace(/Ġ/g, ' ');
1297
+ span.textContent = displayText;
1298
+
1299
+ // Apply token group colors
1300
+ const groupColor = getTokenGroupColor(token.token_groups || []);
1301
+ if (groupColor) {
1302
+ const groupName = getTokenGroupName(token.token_groups || []);
1303
+ span.classList.add(`group-${groupName}`);
1304
+ }
1305
+
1306
+ // Check if token has tile_identity_probes
1307
+ const hasProbes = hasTileIdentityProbes(token);
1308
+ if (hasProbes) {
1309
+ span.classList.add('has-probe');
1310
+ }
1311
+
1312
+ // Check if token has probabilities
1313
+ if (token.probabilities && Object.keys(token.probabilities).length > 0) {
1314
+ span.classList.add('has-probabilities');
1315
+ }
1316
+
1317
+ // Store token data in dataset
1318
+ span.dataset.tokenId = token.token_id;
1319
+ span.dataset.context = context;
1320
+ span.dataset.index = index;
1321
+ span.dataset.tokenData = JSON.stringify(token);
1322
+
1323
+ // Attach event listeners
1324
+ span.addEventListener('mouseenter', (e) => handleTokenHover(e, token, context, index, span));
1325
+ span.addEventListener('mouseleave', () => handleTokenUnhover());
1326
+ span.addEventListener('click', (e) => handleTokenClick(e, token, context, index, span));
1327
+
1328
+ fragment.appendChild(span);
1329
+ }
1330
+
1331
+ // Add <br> after each part except the last
1332
+ if (i < parts.length - 1) {
1333
+ const br = document.createElement('br');
1334
+ fragment.appendChild(br);
1335
+ }
1336
+ });
1337
+
1338
+ return fragment;
1339
+ }
1340
+
1341
+ // No newlines - create single span
1342
+ const span = document.createElement('span');
1343
+ span.className = 'token-span';
1344
+
1345
+ // Replace Ġ with space for display
1346
+ let displayText = token.token.replace(/Ġ/g, ' ');
1347
+ span.textContent = displayText;
1348
+
1349
+ // Apply token group colors (priority: action > final > analysis > template)
1350
+ const groupColor = getTokenGroupColor(token.token_groups || []);
1351
+ if (groupColor) {
1352
+ const groupName = getTokenGroupName(token.token_groups || []);
1353
+ span.classList.add(`group-${groupName}`);
1354
+ }
1355
+
1356
+ // Check if token has tile_identity_probes
1357
+ const hasProbes = hasTileIdentityProbes(token);
1358
+ if (hasProbes) {
1359
+ span.classList.add('has-probe');
1360
+ }
1361
+
1362
+ // Check if token has probabilities
1363
+ if (token.probabilities && Object.keys(token.probabilities).length > 0) {
1364
+ span.classList.add('has-probabilities');
1365
+ }
1366
+
1367
+ // Store token data in dataset
1368
+ span.dataset.tokenId = token.token_id;
1369
+ span.dataset.context = context;
1370
+ span.dataset.index = index;
1371
+ span.dataset.tokenData = JSON.stringify(token);
1372
+
1373
+ // Attach event listeners
1374
+ span.addEventListener('mouseenter', (e) => handleTokenHover(e, token, context, index, span));
1375
+ span.addEventListener('mouseleave', () => handleTokenUnhover());
1376
+ span.addEventListener('click', (e) => handleTokenClick(e, token, context, index, span));
1377
+
1378
+ return span;
1379
+ }
1380
+
1381
+ function getTokenGroupColor(tokenGroups) {
1382
+ // Priority order: action > final > analysis > template
1383
+ const priority = ['action', 'final', 'analysis', 'template'];
1384
+
1385
+ for (const group of priority) {
1386
+ if (tokenGroups.includes(group)) {
1387
+ return uiState.tokenGroupColors[group];
1388
+ }
1389
+ }
1390
+ return null;
1391
+ }
1392
+
1393
+ function getTokenGroupName(tokenGroups) {
1394
+ const priority = ['action', 'final', 'analysis', 'template'];
1395
+ for (const group of priority) {
1396
+ if (tokenGroups.includes(group)) {
1397
+ return group;
1398
+ }
1399
+ }
1400
+ return null;
1401
+ }
1402
+
1403
+ function hasTileIdentityProbes(token) {
1404
+ if (!token.probes) return false;
1405
+ return Object.keys(token.probes).some(key =>
1406
+ key.startsWith('tile_identity_probe') || key.startsWith('cognitive_map_probe')
1407
+ );
1408
+ }
1409
+
1410
+ function getAvailableLayers(token) {
1411
+ if (!token.probes) return [];
1412
+
1413
+ const layers = new Set();
1414
+ for (const probeKey of Object.keys(token.probes)) {
1415
+ if (probeKey.startsWith('tile_identity_probe') || probeKey.startsWith('cognitive_map_probe')) {
1416
+ const probeData = token.probes[probeKey];
1417
+ Object.keys(probeData).forEach(layer => layers.add(layer));
1418
+ }
1419
+ }
1420
+ return Array.from(layers);
1421
+ }
1422
+
1423
+ function getTokenKey(tokenRef) {
1424
+ // Key based on context and position only (not step), so layer selection persists across steps
1425
+ return `${tokenRef.context}_${tokenRef.tokenIndex}`;
1426
+ }
1427
+
1428
+ function getTokenAtPosition(stepIndex, context, tokenIndex) {
1429
+ // Get token at a specific position for any step
1430
+ const step = viewerData.steps[stepIndex];
1431
+ if (!step) return null;
1432
+
1433
+ if (context === 'prompt') {
1434
+ const allTokens = [
1435
+ ...(viewerData.prompt.prompt_prefix_tokens || []),
1436
+ ...(step.grid_state_tokens || []),
1437
+ ...(step.prompt_suffix_tokens || [])
1438
+ ];
1439
+ return allTokens[tokenIndex] || null;
1440
+ } else if (context === 'output') {
1441
+ return (step.output_tokens && step.output_tokens[tokenIndex]) || null;
1442
+ }
1443
+
1444
+ return null;
1445
+ }
1446
+
1447
+ function getPinnedTokenData() {
1448
+ if (!uiState.pinnedToken) return null;
1449
+ // Get the token at the pinned position for the CURRENT step
1450
+ return getTokenAtPosition(currentStepIndex, uiState.pinnedToken.context, uiState.pinnedToken.tokenIndex);
1451
+ }
1452
+
1453
+ function unpinToken() {
1454
+ if (uiState.pinnedToken && uiState.pinnedToken.element) {
1455
+ uiState.pinnedToken.element.classList.remove('pinned');
1456
+ }
1457
+ uiState.pinnedToken = null;
1458
+ hideDecodedGrid();
1459
+ }
1460
+
1461
+ function hideDecodedGrid() {
1462
+ decodedGridContainer.classList.add('hidden');
1463
+ }
1464
+
1465
+ function showDecodedGrid() {
1466
+ decodedGridContainer.classList.remove('hidden');
1467
+ }
1468
+
1469
+ // Token interaction handlers
1470
+ function handleTokenHover(event, token, context, index, element) {
1471
+ // Remove existing tooltip
1472
+ if (currentTooltip) {
1473
+ document.body.removeChild(currentTooltip);
1474
+ }
1475
+
1476
+ // Create tooltip
1477
+ const tooltip = document.createElement('div');
1478
+ tooltip.className = 'token-tooltip';
1479
+
1480
+ let tooltipHTML = `<div class="tooltip-section"><div class="tooltip-label">ID:</div>${token.token_id}</div>`;
1481
+
1482
+ // Show probabilities if present (using bar chart format like tile tooltip)
1483
+ if (token.probabilities && Object.keys(token.probabilities).length > 0) {
1484
+ tooltipHTML += `<div style="margin-top: 10px;"><div class="tooltip-label" style="margin-bottom: 8px;">Next Token Probabilities:</div>`;
1485
+
1486
+ // Sort by probability descending
1487
+ const sorted = Object.entries(token.probabilities).sort((a, b) => b[1] - a[1]);
1488
+
1489
+ for (const [tokenText, prob] of sorted) {
1490
+ const percentage = (prob * 100).toFixed(1);
1491
+ const barWidthPercent = Math.max(1, prob * 100); // Percentage width
1492
+
1493
+ tooltipHTML += `
1494
+ <div class="bar-item">
1495
+ <div class="bar-label">${tokenText}</div>
1496
+ <div class="bar-wrapper" data-percentage="${percentage}%">
1497
+ <div class="bar-fill" style="width: ${barWidthPercent}%;"></div>
1498
+ </div>
1499
+ </div>
1500
+ `;
1501
+ }
1502
+ tooltipHTML += `</div>`;
1503
+ }
1504
+
1505
+ tooltip.innerHTML = tooltipHTML;
1506
+
1507
+ // Position tooltip near cursor
1508
+ tooltip.style.left = (event.clientX + 10) + 'px';
1509
+ tooltip.style.top = (event.clientY + 10) + 'px';
1510
+
1511
+ document.body.appendChild(tooltip);
1512
+ currentTooltip = tooltip;
1513
+
1514
+ // If token has tile_identity_probes, show decoded grid
1515
+ if (hasTileIdentityProbes(token) && !uiState.pinnedToken) {
1516
+ const tokenKey = `${context}_${index}`;
1517
+ const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0];
1518
+ updateDecodedGrid(token, selectedLayer);
1519
+ }
1520
+ }
1521
+
1522
+ function handleTokenUnhover() {
1523
+ // Remove tooltip
1524
+ if (currentTooltip) {
1525
+ document.body.removeChild(currentTooltip);
1526
+ currentTooltip = null;
1527
+ }
1528
+
1529
+ // Hide decoded grid if not pinned
1530
+ if (!uiState.pinnedToken) {
1531
+ hideDecodedGrid();
1532
+ }
1533
+ }
1534
+
1535
+ function handleTokenClick(event, token, context, index, element) {
1536
+ event.stopPropagation();
1537
+
1538
+ // Check if this position is already pinned (compare by position, not step)
1539
+ if (uiState.pinnedToken &&
1540
+ uiState.pinnedToken.context === context &&
1541
+ uiState.pinnedToken.tokenIndex === index) {
1542
+ // Unpin
1543
+ unpinToken();
1544
+ } else {
1545
+ // Unpin previous token if any
1546
+ if (uiState.pinnedToken && uiState.pinnedToken.element) {
1547
+ uiState.pinnedToken.element.classList.remove('pinned');
1548
+ }
1549
+
1550
+ // Pin this position (persists across steps)
1551
+ uiState.pinnedToken = {
1552
+ context: context,
1553
+ tokenIndex: index,
1554
+ element: element
1555
+ };
1556
+
1557
+ element.classList.add('pinned');
1558
+
1559
+ // Show decoded grid if token has probes
1560
+ if (hasTileIdentityProbes(token)) {
1561
+ const tokenKey = getTokenKey(uiState.pinnedToken);
1562
+ const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0];
1563
+ updateDecodedGrid(token, selectedLayer);
1564
+ showDecodedGrid();
1565
+ } else {
1566
+ // Token is pinned but has no probes - hide decoded grid
1567
+ hideDecodedGrid();
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ function handleLayerChange(newLayer) {
1573
+ if (!uiState.pinnedToken) return;
1574
+
1575
+ const tokenKey = getTokenKey(uiState.pinnedToken);
1576
+ uiState.selectedLayer[tokenKey] = newLayer;
1577
+
1578
+ // Get token and update decoded grid
1579
+ const token = getPinnedTokenData();
1580
+ if (token) {
1581
+ updateDecodedGrid(token, newLayer);
1582
+ }
1583
+ }
1584
+
1585
+ // Add event listener for layer selector
1586
+ layerSelector.addEventListener('change', (e) => {
1587
+ handleLayerChange(e.target.value);
1588
+ });
1589
+
1590
+ // Decoded grid logic
1591
+ function updateDecodedGrid(token, selectedLayer) {
1592
+ if (!token || !token.probes) {
1593
+ hideDecodedGrid();
1594
+ return;
1595
+ }
1596
+
1597
+ // Populate layer selector
1598
+ const layers = getAvailableLayers(token);
1599
+ layerSelector.innerHTML = '';
1600
+ layers.forEach(layer => {
1601
+ const option = document.createElement('option');
1602
+ option.value = layer;
1603
+ option.textContent = layer;
1604
+ if (layer === selectedLayer) {
1605
+ option.selected = true;
1606
+ }
1607
+ layerSelector.appendChild(option);
1608
+ });
1609
+
1610
+ // Extract tile identity probes
1611
+ const tileProbes = extractTileIdentityProbes(token.probes, selectedLayer);
1612
+
1613
+ // Render decoded grid
1614
+ const n_rows = viewerData.grid_params.grid_height;
1615
+ const n_cols = viewerData.grid_params.grid_width;
1616
+ renderDecodedGrid(tileProbes, n_rows, n_cols);
1617
+
1618
+ // Store for hover interactions
1619
+ decodedGridTileProbes = tileProbes;
1620
+
1621
+ showDecodedGrid();
1622
+ }
1623
+
1624
+ function extractTileIdentityProbes(probes, selectedLayer) {
1625
+ const tileProbes = {};
1626
+
1627
+ for (const probeKey of Object.keys(probes)) {
1628
+ if (probeKey.startsWith('tile_identity_probe') || probeKey.startsWith('cognitive_map_probe')) {
1629
+ // Extract row and column from probe key
1630
+ const match = probeKey.match(/r(\d+)_c(\d+)/);
1631
+ if (match) {
1632
+ const row = parseInt(match[1]);
1633
+ const col = parseInt(match[2]);
1634
+ const key = `${row}_${col}`;
1635
+
1636
+ // Get probabilities for the selected layer
1637
+ const probeData = probes[probeKey];
1638
+ if (probeData[selectedLayer]) {
1639
+ tileProbes[key] = probeData[selectedLayer];
1640
+ }
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ return tileProbes;
1646
+ }
1647
+
1648
+ function renderDecodedGrid(tileProbes, n_rows, n_cols) {
1649
+ const cellSize = 30;
1650
+ const padding = 20;
1651
+
1652
+ decodedCanvas.width = n_cols * cellSize + padding * 2;
1653
+ decodedCanvas.height = n_rows * cellSize + padding * 2;
1654
+
1655
+ decodedCtx.fillStyle = 'rgba(0, 0, 0, 0.3)';
1656
+ decodedCtx.fillRect(0, 0, decodedCanvas.width, decodedCanvas.height);
1657
+
1658
+ for (let row = 0; row < n_rows; row++) {
1659
+ for (let col = 0; col < n_cols; col++) {
1660
+ const xPos = col * cellSize + padding;
1661
+ const yPos = row * cellSize + padding;
1662
+ const key = `${row}_${col}`;
1663
+
1664
+ if (tileProbes[key]) {
1665
+ // Find tile type with highest probability
1666
+ const probabilities = tileProbes[key];
1667
+ let maxProb = -1;
1668
+ let maxTileType = null;
1669
+
1670
+ for (const [tileType, prob] of Object.entries(probabilities)) {
1671
+ if (prob > maxProb) {
1672
+ maxProb = prob;
1673
+ maxTileType = tileType;
1674
+ }
1675
+ }
1676
+
1677
+ // Draw tile with color and symbol
1678
+ if (maxTileType) {
1679
+ decodedCtx.fillStyle = getTileColor(maxTileType);
1680
+ decodedCtx.fillRect(xPos, yPos, cellSize, cellSize);
1681
+
1682
+ // Get symbol from legend
1683
+ const legend = viewerData.grid_params.legend;
1684
+ const symbol = legend[maxTileType]?.symbol || '?';
1685
+
1686
+ if (symbol && symbol !== '_') {
1687
+ decodedCtx.fillStyle = 'white';
1688
+ decodedCtx.font = 'bold 16px Arial';
1689
+ decodedCtx.textAlign = 'center';
1690
+ decodedCtx.textBaseline = 'middle';
1691
+ decodedCtx.fillText(symbol, xPos + cellSize/2, yPos + cellSize/2);
1692
+ }
1693
+ }
1694
+ } else {
1695
+ // No probe data - draw X (crossed out)
1696
+ decodedCtx.fillStyle = '#888888';
1697
+ decodedCtx.fillRect(xPos, yPos, cellSize, cellSize);
1698
+
1699
+ // Draw X
1700
+ decodedCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
1701
+ decodedCtx.lineWidth = 2;
1702
+ decodedCtx.beginPath();
1703
+ decodedCtx.moveTo(xPos + 5, yPos + 5);
1704
+ decodedCtx.lineTo(xPos + cellSize - 5, yPos + cellSize - 5);
1705
+ decodedCtx.moveTo(xPos + cellSize - 5, yPos + 5);
1706
+ decodedCtx.lineTo(xPos + 5, yPos + cellSize - 5);
1707
+ decodedCtx.stroke();
1708
+ }
1709
+
1710
+ // Draw grid lines
1711
+ decodedCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
1712
+ decodedCtx.lineWidth = 1;
1713
+ decodedCtx.strokeRect(xPos, yPos, cellSize, cellSize);
1714
+ }
1715
+ }
1716
+ }
1717
+
1718
+ // Add hover interaction for decoded grid
1719
+ decodedCanvas.addEventListener('mousemove', (e) => {
1720
+ if (!decodedGridTileProbes || !uiState.pinnedToken) return;
1721
+
1722
+ handleDecodedGridHover(e, decodedGridTileProbes);
1723
+ });
1724
+
1725
+ decodedCanvas.addEventListener('mouseleave', () => {
1726
+ removeTileTooltip();
1727
+ });
1728
+
1729
+ let currentTileTooltip = null;
1730
+
1731
+ function handleDecodedGridHover(event, tileProbes) {
1732
+ const cellSize = 30;
1733
+ const padding = 20;
1734
+
1735
+ const rect = decodedCanvas.getBoundingClientRect();
1736
+ const x = event.clientX - rect.left;
1737
+ const y = event.clientY - rect.top;
1738
+
1739
+ // Calculate tile coordinates
1740
+ const col = Math.floor((x - padding) / cellSize);
1741
+ const row = Math.floor((y - padding) / cellSize);
1742
+
1743
+ const n_rows = viewerData.grid_params.grid_height;
1744
+ const n_cols = viewerData.grid_params.grid_width;
1745
+
1746
+ // Check if within grid bounds
1747
+ if (row < 0 || row >= n_rows || col < 0 || col >= n_cols) {
1748
+ removeTileTooltip();
1749
+ return;
1750
+ }
1751
+
1752
+ const key = `${row}_${col}`;
1753
+ const probabilities = tileProbes[key];
1754
+
1755
+ if (probabilities) {
1756
+ createTileTooltip(probabilities, row, col, event.clientX, event.clientY);
1757
+ } else {
1758
+ removeTileTooltip();
1759
+ }
1760
+ }
1761
+
1762
+ function createTileTooltip(probabilities, row, col, x, y) {
1763
+ // Remove existing tooltip
1764
+ removeTileTooltip();
1765
+
1766
+ const tooltip = document.createElement('div');
1767
+ tooltip.className = 'tile-tooltip';
1768
+
1769
+ let tooltipHTML = `<div class="tooltip-title">Tile (${row}, ${col}) Probabilities</div>`;
1770
+
1771
+ // Keep consistent order (same as legend order)
1772
+ const legendOrder = viewerData.grid_params.legend;
1773
+ const entries = Object.keys(legendOrder).map(tileType => {
1774
+ return [tileType, probabilities[tileType] || 0];
1775
+ });
1776
+
1777
+ for (const [tileType, prob] of entries) {
1778
+ const percentage = (prob * 100).toFixed(1);
1779
+ const barWidthPercent = Math.max(1, prob * 100); // Percentage width
1780
+
1781
+ tooltipHTML += `
1782
+ <div class="bar-item">
1783
+ <div class="bar-label">${tileType}</div>
1784
+ <div class="bar-wrapper" data-percentage="${percentage}%">
1785
+ <div class="bar-fill" style="width: ${barWidthPercent}%;"></div>
1786
+ </div>
1787
+ </div>
1788
+ `;
1789
+ }
1790
+
1791
+ tooltip.innerHTML = tooltipHTML;
1792
+
1793
+ // Position tooltip
1794
+ tooltip.style.left = (x + 15) + 'px';
1795
+ tooltip.style.top = (y + 15) + 'px';
1796
+
1797
+ document.body.appendChild(tooltip);
1798
+ currentTileTooltip = tooltip;
1799
+ }
1800
+
1801
+ function removeTileTooltip() {
1802
+ if (currentTileTooltip) {
1803
+ document.body.removeChild(currentTileTooltip);
1804
+ currentTileTooltip = null;
1805
+ }
1806
+ }
1807
+
1808
+ function displayStep(stepIndex) {
1809
+ if (!viewerData || !viewerData.steps || stepIndex >= viewerData.steps.length) return;
1810
+
1811
+ const step = viewerData.steps[stepIndex];
1812
+ currentStepIndex = stepIndex;
1813
+ uiState.currentStepIndex = stepIndex;
1814
+
1815
+ // Draw original grid with agent action arrow
1816
+ drawOriginalGrid(step.grid_state, step.agent_action);
1817
+
1818
+ // Render token displays
1819
+ renderPromptTokens(step);
1820
+ renderOutputTokens(step);
1821
+
1822
+ // Update step info
1823
+ updateInfo();
1824
+
1825
+ // If a position is pinned, update the pin for the new step
1826
+ if (uiState.pinnedToken) {
1827
+ // Clear old element reference (it's from the previous render)
1828
+ uiState.pinnedToken.element = null;
1829
+
1830
+ // Find and mark the element at the pinned position in the new step
1831
+ const pinnedElement = findTokenElement(uiState.pinnedToken.context, uiState.pinnedToken.tokenIndex);
1832
+ if (pinnedElement) {
1833
+ pinnedElement.classList.add('pinned');
1834
+ uiState.pinnedToken.element = pinnedElement;
1835
+ }
1836
+
1837
+ // Get the token data at the pinned position for this step
1838
+ const token = getPinnedTokenData();
1839
+ if (token && hasTileIdentityProbes(token)) {
1840
+ const tokenKey = getTokenKey(uiState.pinnedToken);
1841
+ const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0];
1842
+ updateDecodedGrid(token, selectedLayer);
1843
+ showDecodedGrid();
1844
+ } else {
1845
+ // Token exists but has no probes at this step
1846
+ hideDecodedGrid();
1847
+ }
1848
+ }
1849
+ }
1850
+
1851
+ function findTokenElement(context, tokenIndex) {
1852
+ // Find the DOM element for a token at a specific position
1853
+ const container = context === 'prompt' ? promptTokens : outputTokens;
1854
+ const tokenSpans = container.querySelectorAll('.token-span');
1855
+
1856
+ for (const span of tokenSpans) {
1857
+ if (span.dataset.context === context && parseInt(span.dataset.index) === tokenIndex) {
1858
+ return span;
1859
+ }
1860
+ }
1861
+ return null;
1862
+ }
1863
+
1864
+ function animate() {
1865
+ if (!isPlaying || !viewerData || !viewerData.steps) {
1866
+ return;
1867
+ }
1868
+
1869
+ if (currentStepIndex >= viewerData.steps.length - 1) {
1870
+ stopAnimation();
1871
+ return;
1872
+ }
1873
+
1874
+ currentStepIndex++;
1875
+ displayStep(currentStepIndex);
1876
+
1877
+ const delay = parseInt(speedSlider.value);
1878
+ animationTimeout = setTimeout(animate, delay);
1879
+ }
1880
+
1881
+ function step() {
1882
+ if (!viewerData || !viewerData.steps || currentStepIndex >= viewerData.steps.length - 1) return;
1883
+
1884
+ stopAnimation();
1885
+ currentStepIndex++;
1886
+ displayStep(currentStepIndex);
1887
+ }
1888
+
1889
+ function backStep() {
1890
+ if (!viewerData || !viewerData.steps || currentStepIndex <= 0) return;
1891
+
1892
+ stopAnimation();
1893
+ currentStepIndex--;
1894
+ displayStep(currentStepIndex);
1895
+ }
1896
+
1897
+ function startAnimation() {
1898
+ if (!viewerData || !viewerData.steps || viewerData.steps.length === 0) return;
1899
+
1900
+ isPlaying = true;
1901
+ uiState.isPlaying = true;
1902
+ playPauseBtn.textContent = '⏸️ Pause';
1903
+ playPauseBtn.classList.add('active');
1904
+ animate();
1905
+ }
1906
+
1907
+ function stopAnimation() {
1908
+ isPlaying = false;
1909
+ uiState.isPlaying = false;
1910
+ playPauseBtn.textContent = '▶️ Play';
1911
+ playPauseBtn.classList.remove('active');
1912
+ if (animationTimeout) {
1913
+ clearTimeout(animationTimeout);
1914
+ animationTimeout = null;
1915
+ }
1916
+ }
1917
+
1918
+ function resetAnimation() {
1919
+ stopAnimation();
1920
+ currentStepIndex = 0;
1921
+ uiState.currentStepIndex = 0;
1922
+ if (viewerData && viewerData.steps && viewerData.steps.length > 0) {
1923
+ displayStep(0);
1924
+ }
1925
+ }
1926
+
1927
+ function updateInfo() {
1928
+ if (!viewerData || !viewerData.steps) {
1929
+ stepInfo.innerHTML = '';
1930
+ } else {
1931
+ stepInfo.innerHTML = `(${currentStepIndex + 1} / ${viewerData.steps.length})`;
1932
+ }
1933
+ }
1934
+
1935
+ playPauseBtn.addEventListener('click', () => {
1936
+ if (!viewerData || !viewerData.steps) return;
1937
+
1938
+ if (isPlaying) {
1939
+ stopAnimation();
1940
+ } else {
1941
+ if (currentStepIndex >= viewerData.steps.length - 1) {
1942
+ resetAnimation();
1943
+ }
1944
+ startAnimation();
1945
+ }
1946
+ });
1947
+
1948
+ backStepBtn.addEventListener('click', backStep);
1949
+ stepBtn.addEventListener('click', step);
1950
+ resetBtn.addEventListener('click', resetAnimation);
1951
+
1952
+ // Collapsible sections for prompt template and model output
1953
+ promptHeader.addEventListener('click', () => {
1954
+ promptTokens.classList.toggle('collapsed');
1955
+ const icon = promptHeader.querySelector('.collapse-icon');
1956
+ icon.textContent = promptTokens.classList.contains('collapsed') ? '▶' : '▼';
1957
+ });
1958
+
1959
+ outputHeader.addEventListener('click', () => {
1960
+ outputTokens.classList.toggle('collapsed');
1961
+ const icon = outputHeader.querySelector('.collapse-icon');
1962
+ icon.textContent = outputTokens.classList.contains('collapsed') ? '▶' : '▼';
1963
+ });
1964
+
1965
+ helpHeader.addEventListener('click', () => {
1966
+ helpContent.classList.toggle('collapsed');
1967
+ const icon = helpHeader.querySelector('.collapse-icon');
1968
+ icon.textContent = helpContent.classList.contains('collapsed') ? '▶' : '▼';
1969
+ });
1970
+ </script>
1971
+ </body>
1972
  </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }