@empiricalrun/test-gen 0.77.0 → 0.78.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.
- package/CHANGELOG.md +14 -0
- package/dist/agent/base/index.d.ts +3 -3
- package/dist/agent/base/index.d.ts.map +1 -1
- package/dist/agent/base/index.js +5 -3
- package/dist/agent/chat/exports.d.ts +1 -1
- package/dist/agent/chat/exports.d.ts.map +1 -1
- package/dist/agent/chat/exports.js +1 -3
- package/dist/agent/chat/models.d.ts.map +1 -1
- package/dist/agent/chat/models.js +1 -1
- package/dist/agent/chat/state.d.ts +1 -8
- package/dist/agent/chat/state.d.ts.map +1 -1
- package/dist/agent/chat/state.js +0 -15
- package/dist/agent/cli.d.ts.map +1 -1
- package/dist/agent/cli.js +12 -25
- package/dist/agent/code-review/index.d.ts +1 -1
- package/dist/agent/code-review/index.d.ts.map +1 -1
- package/dist/agent/code-review/index.js +3 -0
- package/dist/agent/code-review/types.d.ts +9 -9
- package/dist/agent/code-review/types.d.ts.map +1 -1
- package/dist/agent/triage/index.d.ts +1 -1
- package/dist/agent/triage/index.d.ts.map +1 -1
- package/dist/agent/triage/index.js +9 -14
- package/dist/bin/index.js +0 -55
- package/dist/tools/analyse-video/index.d.ts.map +1 -1
- package/dist/tools/analyse-video/index.js +8 -2
- package/dist/tools/definitions/analyse-video.d.ts +4 -4
- package/dist/tools/definitions/analyse-video.js +2 -2
- package/dist/tools/executor/base.d.ts +1 -1
- package/dist/tools/executor/base.d.ts.map +1 -1
- package/dist/tools/executor/base.js +19 -2
- package/dist/tools/executor/utils/index.d.ts +5 -3
- package/dist/tools/executor/utils/index.d.ts.map +1 -1
- package/dist/tools/executor/utils/index.js +22 -1
- package/dist/tools/file-operations/replace.d.ts.map +1 -1
- package/dist/tools/file-operations/replace.js +20 -21
- package/dist/tools/file-operations/shared/helpers.d.ts +3 -5
- package/dist/tools/file-operations/shared/helpers.d.ts.map +1 -1
- package/dist/tools/file-operations/shared/helpers.js +1 -5
- package/dist/tools/review-pull-request/index.d.ts.map +1 -1
- package/dist/tools/review-pull-request/index.js +4 -11
- package/dist/tools/upgrade-packages/index.d.ts.map +1 -1
- package/dist/tools/upgrade-packages/index.js +4 -0
- package/dist/tools/upgrade-packages/utils.d.ts +1 -0
- package/dist/tools/upgrade-packages/utils.d.ts.map +1 -1
- package/dist/tools/upgrade-packages/utils.js +1 -0
- package/dist/trace-utils/index.d.ts +1 -1
- package/dist/trace-utils/index.d.ts.map +1 -1
- package/dist/trace-utils/index.js +1 -1
- package/dist/utils/dedup/dedup-image.d.ts +22 -0
- package/dist/utils/dedup/dedup-image.d.ts.map +1 -0
- package/dist/utils/dedup/dedup-image.js +26 -0
- package/dist/utils/dedup/find-threshold.d.ts +2 -0
- package/dist/utils/dedup/find-threshold.d.ts.map +1 -0
- package/dist/utils/{find-threshold.js → dedup/find-threshold.js} +0 -13
- package/dist/video-core/agent-orchestrator.d.ts +1 -2
- package/dist/video-core/agent-orchestrator.d.ts.map +1 -1
- package/dist/video-core/agent-orchestrator.js +11 -30
- package/dist/video-core/index.d.ts +11 -16
- package/dist/video-core/index.d.ts.map +1 -1
- package/dist/video-core/index.js +110 -180
- package/dist/video-core/model-limits.d.ts.map +1 -1
- package/dist/video-core/model-limits.js +8 -2
- package/dist/video-core/storage-manager.d.ts.map +1 -1
- package/dist/video-core/storage-manager.js +13 -6
- package/dist/video-core/utils.d.ts +0 -10
- package/dist/video-core/utils.d.ts.map +1 -1
- package/dist/video-core/utils.js +1 -18
- package/package.json +5 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/artifact-paths.d.ts +0 -20
- package/dist/utils/artifact-paths.d.ts.map +0 -1
- package/dist/utils/artifact-paths.js +0 -16
- package/dist/utils/dedup-image-fs.d.ts +0 -13
- package/dist/utils/dedup-image-fs.d.ts.map +0 -1
- package/dist/utils/dedup-image-fs.js +0 -84
- package/dist/utils/dedup-image.d.ts +0 -12
- package/dist/utils/dedup-image.d.ts.map +0 -1
- package/dist/utils/dedup-image.js +0 -25
- package/dist/utils/ffmpeg/index.d.ts +0 -26
- package/dist/utils/ffmpeg/index.d.ts.map +0 -1
- package/dist/utils/ffmpeg/index.js +0 -415
- package/dist/utils/find-threshold.d.ts +0 -8
- package/dist/utils/find-threshold.d.ts.map +0 -1
- package/dist/video-core/analysis-server.d.ts +0 -24
- package/dist/video-core/analysis-server.d.ts.map +0 -1
- package/dist/video-core/analysis-server.js +0 -398
- package/dist/video-core/analysis-viewer.html +0 -1374
|
@@ -1,1374 +0,0 @@
|
|
|
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>Video Analysis Debug UI</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
body {
|
|
15
|
-
font-family: 'Monaco', 'Menlo', monospace;
|
|
16
|
-
font-size: 12px;
|
|
17
|
-
line-height: 1.3;
|
|
18
|
-
background: #f8f9fa;
|
|
19
|
-
height: 100vh;
|
|
20
|
-
overflow: hidden;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.main-container {
|
|
24
|
-
display: grid;
|
|
25
|
-
grid-template-columns: 200px 1fr 500px;
|
|
26
|
-
height: 100vh;
|
|
27
|
-
gap: 2px;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.column {
|
|
31
|
-
background: white;
|
|
32
|
-
border: 1px solid #ddd;
|
|
33
|
-
display: flex;
|
|
34
|
-
flex-direction: column;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.column-header {
|
|
38
|
-
background: #2c3e50;
|
|
39
|
-
color: white;
|
|
40
|
-
padding: 4px 8px;
|
|
41
|
-
font-weight: bold;
|
|
42
|
-
font-size: 10px;
|
|
43
|
-
text-transform: uppercase;
|
|
44
|
-
border-bottom: 1px solid #ddd;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.column-content {
|
|
48
|
-
flex: 1;
|
|
49
|
-
overflow-y: auto;
|
|
50
|
-
padding: 4px;
|
|
51
|
-
min-height: 0; /* Allow flex child to shrink */
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/* Results column: make interleaved section handle its own scroll */
|
|
55
|
-
.results-column .column-content {
|
|
56
|
-
overflow: hidden;
|
|
57
|
-
display: flex;
|
|
58
|
-
flex-direction: column;
|
|
59
|
-
height: 100%;
|
|
60
|
-
min-height: 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* Column 1: Controls */
|
|
64
|
-
.control-section {
|
|
65
|
-
margin-bottom: 8px;
|
|
66
|
-
border: 1px solid #eee;
|
|
67
|
-
border-radius: 2px;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.control-header {
|
|
71
|
-
background: #f1f3f4;
|
|
72
|
-
padding: 2px 4px;
|
|
73
|
-
font-size: 9px;
|
|
74
|
-
font-weight: bold;
|
|
75
|
-
border-bottom: 1px solid #eee;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.control-content {
|
|
79
|
-
padding: 4px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.input-group {
|
|
83
|
-
margin-bottom: 4px;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.input-label {
|
|
87
|
-
font-size: 9px;
|
|
88
|
-
color: #666;
|
|
89
|
-
margin-bottom: 2px;
|
|
90
|
-
display: block;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.input-field {
|
|
94
|
-
width: 100%;
|
|
95
|
-
padding: 2px 4px;
|
|
96
|
-
border: 1px solid #ccc;
|
|
97
|
-
border-radius: 2px;
|
|
98
|
-
font-size: 10px;
|
|
99
|
-
font-family: 'Monaco', 'Menlo', monospace;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.btn {
|
|
103
|
-
width: 100%;
|
|
104
|
-
padding: 4px 8px;
|
|
105
|
-
margin: 2px 0;
|
|
106
|
-
border: none;
|
|
107
|
-
border-radius: 2px;
|
|
108
|
-
cursor: pointer;
|
|
109
|
-
font-size: 9px;
|
|
110
|
-
font-weight: bold;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.btn-primary {
|
|
114
|
-
background: #007bff;
|
|
115
|
-
color: white;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.btn-secondary {
|
|
119
|
-
background: #6c757d;
|
|
120
|
-
color: white;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.status-indicator {
|
|
124
|
-
padding: 2px 4px;
|
|
125
|
-
border-radius: 2px;
|
|
126
|
-
font-size: 9px;
|
|
127
|
-
margin: 2px 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.status-success {
|
|
131
|
-
background: #d4edda;
|
|
132
|
-
color: #155724;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.status-error {
|
|
136
|
-
background: #f8d7da;
|
|
137
|
-
color: #721c24;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.status-loading {
|
|
141
|
-
background: #fff3cd;
|
|
142
|
-
color: #856404;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* Column 2: Frames Grid */
|
|
146
|
-
.frames-header-info {
|
|
147
|
-
padding: 4px 8px;
|
|
148
|
-
background: #f8f9fa;
|
|
149
|
-
border-bottom: 1px solid #ddd;
|
|
150
|
-
font-size: 10px;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.frames-grid {
|
|
154
|
-
display: grid;
|
|
155
|
-
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
156
|
-
gap: 4px;
|
|
157
|
-
padding: 4px;
|
|
158
|
-
overflow-y: auto;
|
|
159
|
-
max-height: calc(100vh - 55px);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.frame-card {
|
|
163
|
-
border: 1px solid #ddd;
|
|
164
|
-
border-radius: 2px;
|
|
165
|
-
overflow: hidden;
|
|
166
|
-
background: white;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.frame-img {
|
|
170
|
-
width: 100%;
|
|
171
|
-
height: 80px;
|
|
172
|
-
object-fit: contain;
|
|
173
|
-
cursor: pointer;
|
|
174
|
-
display: block;
|
|
175
|
-
background: #f5f5f5;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.frame-meta {
|
|
179
|
-
padding: 2px 4px;
|
|
180
|
-
font-size: 8px;
|
|
181
|
-
line-height: 1.1;
|
|
182
|
-
background: #f8f9fa;
|
|
183
|
-
border-top: 1px solid #eee;
|
|
184
|
-
font-weight: bold;
|
|
185
|
-
color: #000;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.frame-meta .label {
|
|
189
|
-
font-weight: bold;
|
|
190
|
-
color: #000;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.frame-meta .similarity {
|
|
194
|
-
color: #e74c3c;
|
|
195
|
-
font-weight: bold;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.frame-filename {
|
|
199
|
-
font-size: 7px;
|
|
200
|
-
color: #888;
|
|
201
|
-
word-break: break-all;
|
|
202
|
-
margin-top: 1px;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.frame-size {
|
|
206
|
-
font-size: 7px;
|
|
207
|
-
color: #666;
|
|
208
|
-
margin-top: 1px;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* Column 3: Results */
|
|
212
|
-
.result-section {
|
|
213
|
-
margin-bottom: 6px;
|
|
214
|
-
border: 1px solid #eee;
|
|
215
|
-
border-radius: 2px;
|
|
216
|
-
overflow: hidden;
|
|
217
|
-
display: flex;
|
|
218
|
-
flex-direction: column;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.result-section.flexible {
|
|
222
|
-
flex: 1;
|
|
223
|
-
margin-bottom: 6px;
|
|
224
|
-
min-height: 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.result-header {
|
|
228
|
-
background: #f1f3f4;
|
|
229
|
-
padding: 2px 4px;
|
|
230
|
-
font-size: 9px;
|
|
231
|
-
font-weight: bold;
|
|
232
|
-
border-bottom: 1px solid #eee;
|
|
233
|
-
flex-shrink: 0;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.result-content {
|
|
237
|
-
overflow-y: auto;
|
|
238
|
-
padding: 4px;
|
|
239
|
-
background: #fafafa;
|
|
240
|
-
flex: 1;
|
|
241
|
-
min-height: 0;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.result-content.fixed-height {
|
|
245
|
-
max-height: 200px;
|
|
246
|
-
flex: none;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.json-content {
|
|
250
|
-
font-family: 'Monaco', 'Menlo', monospace;
|
|
251
|
-
font-size: 9px;
|
|
252
|
-
line-height: 1.3;
|
|
253
|
-
white-space: pre-wrap;
|
|
254
|
-
word-break: break-all;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.analysis-content {
|
|
258
|
-
font-size: 10px;
|
|
259
|
-
line-height: 1.4;
|
|
260
|
-
white-space: pre-wrap;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.interleaved-content {
|
|
264
|
-
font-family: 'Monaco', 'Menlo', monospace;
|
|
265
|
-
font-size: 9px;
|
|
266
|
-
line-height: 1.4;
|
|
267
|
-
height: 50vh;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.frame-entry {
|
|
271
|
-
margin-bottom: 12px;
|
|
272
|
-
padding: 6px;
|
|
273
|
-
border: 1px solid #e0e0e0;
|
|
274
|
-
border-radius: 3px;
|
|
275
|
-
background: #fafafa;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.frame-header {
|
|
279
|
-
font-weight: bold;
|
|
280
|
-
color: #2c3e50;
|
|
281
|
-
margin-bottom: 4px;
|
|
282
|
-
font-size: 10px;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.frame-description {
|
|
286
|
-
margin-bottom: 4px;
|
|
287
|
-
color: #333;
|
|
288
|
-
word-wrap: break-word;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.frame-image {
|
|
292
|
-
width: 100%;
|
|
293
|
-
max-width: 100%;
|
|
294
|
-
height: 120px;
|
|
295
|
-
object-fit: contain;
|
|
296
|
-
border: 1px solid #ddd;
|
|
297
|
-
border-radius: 2px;
|
|
298
|
-
cursor: pointer;
|
|
299
|
-
background: #f5f5f5;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.frame-url {
|
|
303
|
-
font-size: 8px;
|
|
304
|
-
color: #666;
|
|
305
|
-
word-break: break-all;
|
|
306
|
-
margin-top: 2px;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/* Modal */
|
|
310
|
-
.frame-modal {
|
|
311
|
-
display: none;
|
|
312
|
-
position: fixed;
|
|
313
|
-
z-index: 1000;
|
|
314
|
-
left: 0;
|
|
315
|
-
top: 0;
|
|
316
|
-
width: 100%;
|
|
317
|
-
height: 100%;
|
|
318
|
-
background-color: rgba(0,0,0,0.9);
|
|
319
|
-
outline: none;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.frame-modal-content {
|
|
323
|
-
display: flex;
|
|
324
|
-
justify-content: center;
|
|
325
|
-
align-items: center;
|
|
326
|
-
height: 100%;
|
|
327
|
-
padding: 20px;
|
|
328
|
-
position: relative;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.frame-modal img {
|
|
332
|
-
max-width: 90%;
|
|
333
|
-
max-height: 70%;
|
|
334
|
-
object-fit: contain;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.frame-modal-close {
|
|
338
|
-
position: absolute;
|
|
339
|
-
top: 15px;
|
|
340
|
-
right: 25px;
|
|
341
|
-
color: white;
|
|
342
|
-
font-size: 30px;
|
|
343
|
-
font-weight: bold;
|
|
344
|
-
cursor: pointer;
|
|
345
|
-
z-index: 1001;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.frame-modal-nav {
|
|
349
|
-
position: absolute;
|
|
350
|
-
top: 50%;
|
|
351
|
-
transform: translateY(-50%);
|
|
352
|
-
background: rgba(255, 255, 255, 0.5);
|
|
353
|
-
border: none;
|
|
354
|
-
border-radius: 50%;
|
|
355
|
-
width: 40px;
|
|
356
|
-
height: 40px;
|
|
357
|
-
cursor: pointer;
|
|
358
|
-
font-size: 18px;
|
|
359
|
-
font-weight: bold;
|
|
360
|
-
color: #000;
|
|
361
|
-
display: flex;
|
|
362
|
-
align-items: center;
|
|
363
|
-
justify-content: center;
|
|
364
|
-
z-index: 1001;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
.frame-modal-nav:hover {
|
|
368
|
-
background: rgba(255, 255, 255, 0.75);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.frame-modal-nav-prev {
|
|
372
|
-
left: 20px;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
.frame-modal-nav-next {
|
|
376
|
-
right: 20px;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
.frame-modal-info {
|
|
380
|
-
position: absolute;
|
|
381
|
-
top: 15px;
|
|
382
|
-
left: 50%;
|
|
383
|
-
transform: translateX(-50%);
|
|
384
|
-
background: rgba(0, 0, 0, 0.9);
|
|
385
|
-
color: white;
|
|
386
|
-
padding: 16px 24px;
|
|
387
|
-
border-radius: 8px;
|
|
388
|
-
font-size: 11px;
|
|
389
|
-
font-weight: bold;
|
|
390
|
-
z-index: 1001;
|
|
391
|
-
text-align: center;
|
|
392
|
-
line-height: 1.4;
|
|
393
|
-
backdrop-filter: blur(8px);
|
|
394
|
-
max-width: 600px;
|
|
395
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
.frame-modal-title {
|
|
399
|
-
font-size: 13px;
|
|
400
|
-
margin-bottom: 6px;
|
|
401
|
-
color: #ffffff;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
.frame-modal-meta {
|
|
405
|
-
display: grid;
|
|
406
|
-
grid-template-columns: 1fr 1fr;
|
|
407
|
-
gap: 8px 16px;
|
|
408
|
-
font-size: 10px;
|
|
409
|
-
color: #e0e0e0;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
.frame-modal-description {
|
|
413
|
-
margin-top: 8px;
|
|
414
|
-
padding-top: 8px;
|
|
415
|
-
border-top: 1px solid #444;
|
|
416
|
-
font-size: 9px;
|
|
417
|
-
color: #ccc;
|
|
418
|
-
font-weight: normal;
|
|
419
|
-
line-height: 1.3;
|
|
420
|
-
text-align: left;
|
|
421
|
-
max-height: 60px;
|
|
422
|
-
overflow-y: auto;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.frame-modal-meta-item {
|
|
426
|
-
text-align: left;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.frame-modal-meta-label {
|
|
430
|
-
color: #999;
|
|
431
|
-
margin-right: 4px;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
.frame-modal-meta-value {
|
|
435
|
-
color: #ffffff;
|
|
436
|
-
font-weight: normal;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
.frame-modal-meta .similarity-value {
|
|
440
|
-
color: #ff6b6b;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.frame-modal-filmstrip {
|
|
444
|
-
position: absolute;
|
|
445
|
-
bottom: 20px;
|
|
446
|
-
left: 50%;
|
|
447
|
-
transform: translateX(-50%);
|
|
448
|
-
display: flex;
|
|
449
|
-
gap: 4px;
|
|
450
|
-
background: rgba(0, 0, 0, 0.7);
|
|
451
|
-
padding: 8px;
|
|
452
|
-
border-radius: 8px;
|
|
453
|
-
max-width: 80%;
|
|
454
|
-
overflow-x: auto;
|
|
455
|
-
z-index: 1001;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
.frame-modal-filmstrip::-webkit-scrollbar {
|
|
459
|
-
height: 4px;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
.frame-modal-filmstrip::-webkit-scrollbar-track {
|
|
463
|
-
background: rgba(255, 255, 255, 0.2);
|
|
464
|
-
border-radius: 2px;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
.frame-modal-filmstrip::-webkit-scrollbar-thumb {
|
|
468
|
-
background: rgba(255, 255, 255, 0.5);
|
|
469
|
-
border-radius: 2px;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.filmstrip-frame {
|
|
473
|
-
width: 60px;
|
|
474
|
-
height: 34px;
|
|
475
|
-
border: 2px solid transparent;
|
|
476
|
-
border-radius: 3px;
|
|
477
|
-
cursor: pointer;
|
|
478
|
-
object-fit: cover;
|
|
479
|
-
opacity: 0.6;
|
|
480
|
-
transition: all 0.2s;
|
|
481
|
-
flex-shrink: 0;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.filmstrip-frame.active {
|
|
485
|
-
border-color: #007bff;
|
|
486
|
-
opacity: 1;
|
|
487
|
-
transform: scale(1.1);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
.filmstrip-frame:hover {
|
|
491
|
-
opacity: 0.8;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
.hidden {
|
|
495
|
-
display: none;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/* Analysis list styles */
|
|
499
|
-
.analysis-item {
|
|
500
|
-
padding: 6px 4px;
|
|
501
|
-
margin-bottom: 4px;
|
|
502
|
-
border: 1px solid #e0e0e0;
|
|
503
|
-
border-radius: 2px;
|
|
504
|
-
cursor: pointer;
|
|
505
|
-
background: white;
|
|
506
|
-
transition: background-color 0.2s;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
.analysis-item:hover {
|
|
510
|
-
background: #f0f8ff;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
.analysis-item.active {
|
|
514
|
-
background: #e3f2fd;
|
|
515
|
-
border-color: #2196f3;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
.analysis-item-name {
|
|
519
|
-
font-weight: bold;
|
|
520
|
-
font-size: 9px;
|
|
521
|
-
color: #2c3e50;
|
|
522
|
-
margin-bottom: 2px;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.analysis-item-info {
|
|
526
|
-
font-size: 8px;
|
|
527
|
-
color: #666;
|
|
528
|
-
line-height: 1.2;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
.analysis-item-date {
|
|
532
|
-
font-size: 7px;
|
|
533
|
-
color: #999;
|
|
534
|
-
margin-top: 2px;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/* Analyses section specific styles */
|
|
538
|
-
.analyses-section {
|
|
539
|
-
flex: 1;
|
|
540
|
-
display: flex;
|
|
541
|
-
flex-direction: column;
|
|
542
|
-
min-height: 0;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
.analyses-section .control-content {
|
|
546
|
-
flex: 1;
|
|
547
|
-
display: flex;
|
|
548
|
-
flex-direction: column;
|
|
549
|
-
min-height: 0;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
.analyses-list {
|
|
553
|
-
flex: 1;
|
|
554
|
-
overflow-y: auto;
|
|
555
|
-
min-height: 0;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/* Scrollbar styling */
|
|
559
|
-
::-webkit-scrollbar {
|
|
560
|
-
width: 6px;
|
|
561
|
-
height: 6px;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
::-webkit-scrollbar-track {
|
|
565
|
-
background: #f1f1f1;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
::-webkit-scrollbar-thumb {
|
|
569
|
-
background: #888;
|
|
570
|
-
border-radius: 3px;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
::-webkit-scrollbar-thumb:hover {
|
|
574
|
-
background: #555;
|
|
575
|
-
}
|
|
576
|
-
</style>
|
|
577
|
-
</head>
|
|
578
|
-
<body>
|
|
579
|
-
<div class="main-container">
|
|
580
|
-
<!-- Column 1: Controls -->
|
|
581
|
-
<div class="column">
|
|
582
|
-
<div class="column-header">Video Analysis</div>
|
|
583
|
-
<div class="column-content">
|
|
584
|
-
<div class="control-section">
|
|
585
|
-
<div class="control-header">LOAD DATA</div>
|
|
586
|
-
<div class="control-content">
|
|
587
|
-
<div class="input-group">
|
|
588
|
-
<label class="input-label">Analysis URL</label>
|
|
589
|
-
<input
|
|
590
|
-
type="text"
|
|
591
|
-
id="videoUrl"
|
|
592
|
-
class="input-field"
|
|
593
|
-
placeholder="Paste analysis JSON URL..."
|
|
594
|
-
>
|
|
595
|
-
</div>
|
|
596
|
-
<div class="input-group">
|
|
597
|
-
<label class="input-label">File</label>
|
|
598
|
-
<input type="file" id="fileInput" accept=".json" style="display: none;">
|
|
599
|
-
<button id="loadJsonButton" class="btn btn-secondary">Load JSON</button>
|
|
600
|
-
</div>
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
|
|
604
|
-
<div class="control-section">
|
|
605
|
-
<div class="control-header">RUN NEW ANALYSIS</div>
|
|
606
|
-
<div class="control-content">
|
|
607
|
-
<div class="input-group">
|
|
608
|
-
<label class="input-label">Video URL</label>
|
|
609
|
-
<input
|
|
610
|
-
type="text"
|
|
611
|
-
id="analysisUrl"
|
|
612
|
-
class="input-field"
|
|
613
|
-
placeholder="Enter video URL to analyze..."
|
|
614
|
-
>
|
|
615
|
-
</div>
|
|
616
|
-
<div class="input-group">
|
|
617
|
-
<label class="input-label">FPS</label>
|
|
618
|
-
<input type="text" id="fpsInput" class="input-field" placeholder="30" value="30">
|
|
619
|
-
</div>
|
|
620
|
-
<div class="input-group">
|
|
621
|
-
<label class="input-label">Threshold</label>
|
|
622
|
-
<input
|
|
623
|
-
type="text"
|
|
624
|
-
id="thresholdInput"
|
|
625
|
-
class="input-field"
|
|
626
|
-
placeholder="0.01"
|
|
627
|
-
value="0.01"
|
|
628
|
-
>
|
|
629
|
-
</div>
|
|
630
|
-
<button id="runAnalysisButton" class="btn btn-primary">Run Analysis</button>
|
|
631
|
-
</div>
|
|
632
|
-
</div>
|
|
633
|
-
|
|
634
|
-
<div class="control-section analyses-section">
|
|
635
|
-
<div class="control-header">SAVED ANALYSES</div>
|
|
636
|
-
<div class="control-content">
|
|
637
|
-
<div id="analysesList" class="analyses-list">
|
|
638
|
-
<div style="text-align: center; color: #666; font-size: 10px; padding: 10px;">
|
|
639
|
-
Loading analyses...
|
|
640
|
-
</div>
|
|
641
|
-
</div>
|
|
642
|
-
</div>
|
|
643
|
-
</div>
|
|
644
|
-
|
|
645
|
-
<div id="statusSection" class="control-section hidden">
|
|
646
|
-
<div class="control-header">Status</div>
|
|
647
|
-
<div class="control-content">
|
|
648
|
-
<div id="statusMessage" class="status-indicator"></div>
|
|
649
|
-
</div>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
|
|
654
|
-
<!-- Column 2: Frames -->
|
|
655
|
-
<div class="column">
|
|
656
|
-
<div class="column-header">Total Unique Frames</div>
|
|
657
|
-
<div id="framesHeaderInfo" class="frames-header-info">
|
|
658
|
-
Count: <span id="frameCount">0</span>(max is 100) | Total Size: <span id="totalSize"
|
|
659
|
-
>0</span
|
|
660
|
-
>(max is 32MB)
|
|
661
|
-
</div>
|
|
662
|
-
<div class="column-content">
|
|
663
|
-
<div id="framesGrid" class="frames-grid">
|
|
664
|
-
<div
|
|
665
|
-
style="grid-column: 1/-1; text-align: center; color: #666; font-size: 10px; padding: 20px;"
|
|
666
|
-
>
|
|
667
|
-
No result
|
|
668
|
-
</div>
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
</div>
|
|
672
|
-
|
|
673
|
-
<!-- Column 3: Results -->
|
|
674
|
-
<div class="column results-column">
|
|
675
|
-
<div class="column-header">Final Result</div>
|
|
676
|
-
<div class="column-content">
|
|
677
|
-
<div class="result-section">
|
|
678
|
-
<div class="result-header">Analysis</div>
|
|
679
|
-
<div class="result-content fixed-height">
|
|
680
|
-
<div id="analysisResult" class="analysis-content">No result</div>
|
|
681
|
-
</div>
|
|
682
|
-
</div>
|
|
683
|
-
|
|
684
|
-
<div class="result-section flexible">
|
|
685
|
-
<div class="result-header">Interleaved Result</div>
|
|
686
|
-
<div class="result-content">
|
|
687
|
-
<div id="interleavedResult" class="json-content">No result</div>
|
|
688
|
-
</div>
|
|
689
|
-
</div>
|
|
690
|
-
|
|
691
|
-
<div class="result-section">
|
|
692
|
-
<div class="result-header">Video Info</div>
|
|
693
|
-
<div class="result-content fixed-height">
|
|
694
|
-
<div id="videoInfo" class="json-content">No result</div>
|
|
695
|
-
</div>
|
|
696
|
-
</div>
|
|
697
|
-
</div>
|
|
698
|
-
</div>
|
|
699
|
-
</div>
|
|
700
|
-
|
|
701
|
-
<!-- Frame Modal -->
|
|
702
|
-
<div id="frameModal" class="frame-modal" tabindex="0">
|
|
703
|
-
<span class="frame-modal-close">×</span>
|
|
704
|
-
<div class="frame-modal-info">
|
|
705
|
-
<div class="frame-modal-title" id="modalFrameTitle">Frame 1 of 10</div>
|
|
706
|
-
<div class="frame-modal-meta">
|
|
707
|
-
<div class="frame-modal-meta-item">
|
|
708
|
-
<span class="frame-modal-meta-label">T:</span>
|
|
709
|
-
<span class="frame-modal-meta-value" id="modalFrameTimestamp">0m00s</span>
|
|
710
|
-
</div>
|
|
711
|
-
<div class="frame-modal-meta-item">
|
|
712
|
-
<span class="frame-modal-meta-label">Diff:</span>
|
|
713
|
-
<span class="frame-modal-meta-value similarity-value" id="modalFrameDiff">First</span>
|
|
714
|
-
</div>
|
|
715
|
-
<div class="frame-modal-meta-item">
|
|
716
|
-
<span class="frame-modal-meta-label">Size:</span>
|
|
717
|
-
<span class="frame-modal-meta-value" id="modalFrameSize">0 KB</span>
|
|
718
|
-
</div>
|
|
719
|
-
<div class="frame-modal-meta-item">
|
|
720
|
-
<span class="frame-modal-meta-label">File:</span>
|
|
721
|
-
<span class="frame-modal-meta-value" id="modalFrameFile">frame.png</span>
|
|
722
|
-
</div>
|
|
723
|
-
<div class="frame-modal-meta-item">
|
|
724
|
-
<span class="frame-modal-meta-label">Dimensions:</span>
|
|
725
|
-
<span class="frame-modal-meta-value" id="modalFrameDimensions">Loading...</span>
|
|
726
|
-
</div>
|
|
727
|
-
<div class="frame-modal-meta-item">
|
|
728
|
-
<span class="frame-modal-meta-label">URL:</span>
|
|
729
|
-
<span
|
|
730
|
-
class="frame-modal-meta-value"
|
|
731
|
-
id="modalFrameUrl"
|
|
732
|
-
style="font-size: 8px; word-break: break-all;"
|
|
733
|
-
>...</span
|
|
734
|
-
>
|
|
735
|
-
</div>
|
|
736
|
-
</div>
|
|
737
|
-
<div
|
|
738
|
-
class="frame-modal-description"
|
|
739
|
-
id="modalFrameDescription"
|
|
740
|
-
style="display: none;"
|
|
741
|
-
></div>
|
|
742
|
-
</div>
|
|
743
|
-
<button class="frame-modal-nav frame-modal-nav-prev" id="modalPrevBtn">‹</button>
|
|
744
|
-
<button class="frame-modal-nav frame-modal-nav-next" id="modalNextBtn">›</button>
|
|
745
|
-
<div class="frame-modal-content">
|
|
746
|
-
<img id="modalImage" src="" alt="Frame">
|
|
747
|
-
</div>
|
|
748
|
-
<div class="frame-modal-filmstrip" id="modalFilmstrip">
|
|
749
|
-
<!-- Filmstrip frames will be populated by JavaScript -->
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
|
|
753
|
-
<script>
|
|
754
|
-
let currentData = null;
|
|
755
|
-
|
|
756
|
-
// LocalStorage keys
|
|
757
|
-
const STORAGE_KEYS = {
|
|
758
|
-
VIDEO_URL: 'video-analysis-url',
|
|
759
|
-
ANALYSIS_PARAMS: 'video-analysis-params'
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
// Load saved values from localStorage
|
|
763
|
-
function loadSavedValues() {
|
|
764
|
-
const savedUrl = localStorage.getItem(STORAGE_KEYS.VIDEO_URL);
|
|
765
|
-
const savedParams = localStorage.getItem(STORAGE_KEYS.ANALYSIS_PARAMS);
|
|
766
|
-
|
|
767
|
-
if (savedUrl) {
|
|
768
|
-
document.getElementById('videoUrl').value = savedUrl;
|
|
769
|
-
document.getElementById('analysisUrl').value = savedUrl;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (savedParams) {
|
|
773
|
-
const params = new URLSearchParams(savedParams);
|
|
774
|
-
const fps = params.get('fps');
|
|
775
|
-
const threshold = params.get('threshold');
|
|
776
|
-
if (fps) document.getElementById('fpsInput').value = fps;
|
|
777
|
-
if (threshold) document.getElementById('thresholdInput').value = threshold;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Save values to localStorage
|
|
782
|
-
function saveValues(url, params) {
|
|
783
|
-
if (url) localStorage.setItem(STORAGE_KEYS.VIDEO_URL, url);
|
|
784
|
-
if (params) localStorage.setItem(STORAGE_KEYS.ANALYSIS_PARAMS, params);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function showStatus(message, type = 'loading') {
|
|
788
|
-
const statusSection = document.getElementById('statusSection');
|
|
789
|
-
const statusMessage = document.getElementById('statusMessage');
|
|
790
|
-
|
|
791
|
-
statusMessage.textContent = message;
|
|
792
|
-
statusMessage.className = `status-indicator status-${type}`;
|
|
793
|
-
statusSection.classList.remove('hidden');
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function hideStatus() {
|
|
797
|
-
document.getElementById('statusSection').classList.add('hidden');
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
async function loadSimilarityData(frameIndex, currentFrame, previousFrame) {
|
|
801
|
-
try {
|
|
802
|
-
const response = await fetch(`/api/similarity?frame1=${encodeURIComponent(previousFrame)}&frame2=${encodeURIComponent(currentFrame)}`);
|
|
803
|
-
if (response.ok) {
|
|
804
|
-
const data = await response.json();
|
|
805
|
-
const diffElement = document.getElementById(`diff-${frameIndex}`);
|
|
806
|
-
if (diffElement && data.similarity !== null && data.similarity !== undefined) {
|
|
807
|
-
const percentage = (data.similarity * 100).toFixed(1);
|
|
808
|
-
diffElement.textContent = `Diff: ${percentage}%`;
|
|
809
|
-
|
|
810
|
-
// Also update the frame data for modal usage
|
|
811
|
-
if (currentData && currentData.unique_frames && currentData.unique_frames[frameIndex]) {
|
|
812
|
-
currentData.unique_frames[frameIndex].similarityPercentage = data.similarity;
|
|
813
|
-
}
|
|
814
|
-
} else if (diffElement) {
|
|
815
|
-
diffElement.textContent = 'Diff: N/A';
|
|
816
|
-
}
|
|
817
|
-
} else {
|
|
818
|
-
const diffElement = document.getElementById(`diff-${frameIndex}`);
|
|
819
|
-
if (diffElement) {
|
|
820
|
-
diffElement.textContent = 'Diff: Error';
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
} catch (error) {
|
|
824
|
-
console.log(`Error loading similarity for frame ${frameIndex}:`, error);
|
|
825
|
-
const diffElement = document.getElementById(`diff-${frameIndex}`);
|
|
826
|
-
if (diffElement) {
|
|
827
|
-
diffElement.textContent = 'Diff: Error';
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function showNoFramesMessage(isError) {
|
|
833
|
-
const framesGrid = document.getElementById('framesGrid');
|
|
834
|
-
|
|
835
|
-
if (isError) {
|
|
836
|
-
framesGrid.innerHTML = `
|
|
837
|
-
<div style="grid-column: 1/-1; text-align: center; padding: 20px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; color: #856404;">
|
|
838
|
-
<strong>⚠️ Error Scenario - No Frames Available</strong><br><br>
|
|
839
|
-
This JSON file contains an error analysis with no frame data.<br>
|
|
840
|
-
To view local frames from the unique_frames directory:<br><br>
|
|
841
|
-
<code>startAnalysisServer("path/to/analysis-result.json")</code><br><br>
|
|
842
|
-
This will load frames from the same directory as the JSON file.
|
|
843
|
-
</div>
|
|
844
|
-
`;
|
|
845
|
-
} else {
|
|
846
|
-
framesGrid.innerHTML = `
|
|
847
|
-
<div style="grid-column: 1/-1; text-align: center; color: #666; font-size: 10px; padding: 20px;">
|
|
848
|
-
No result
|
|
849
|
-
</div>
|
|
850
|
-
`;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
async function getActualFileSizes(frames) {
|
|
855
|
-
const frameSizes = {};
|
|
856
|
-
let totalSize = 0;
|
|
857
|
-
|
|
858
|
-
for (let i = 0; i < frames.length; i++) {
|
|
859
|
-
const frame = frames[i];
|
|
860
|
-
try {
|
|
861
|
-
const response = await fetch(frame.url || frame.image_url, { method: 'HEAD' });
|
|
862
|
-
const contentLength = response.headers.get('Content-Length');
|
|
863
|
-
|
|
864
|
-
if (contentLength) {
|
|
865
|
-
const sizeKB = parseInt(contentLength) / 1024;
|
|
866
|
-
frameSizes[i] = sizeKB;
|
|
867
|
-
totalSize += sizeKB;
|
|
868
|
-
} else {
|
|
869
|
-
// Fallback: estimate based on typical PNG sizes
|
|
870
|
-
const estimatedSize = 45 + Math.random() * 30; // 45-75KB range
|
|
871
|
-
frameSizes[i] = estimatedSize;
|
|
872
|
-
totalSize += estimatedSize;
|
|
873
|
-
}
|
|
874
|
-
} catch (error) {
|
|
875
|
-
// Fallback for CORS or network issues
|
|
876
|
-
const estimatedSize = 45 + Math.random() * 30; // 45-75KB range
|
|
877
|
-
frameSizes[i] = estimatedSize;
|
|
878
|
-
totalSize += estimatedSize;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return { totalSize, frameSizes };
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
let currentAnalysisId = null;
|
|
886
|
-
|
|
887
|
-
function displayAnalysesList(analyses) {
|
|
888
|
-
const analysesList = document.getElementById('analysesList');
|
|
889
|
-
|
|
890
|
-
if (!analyses || analyses.length === 0) {
|
|
891
|
-
analysesList.innerHTML = `
|
|
892
|
-
<div style="text-align: center; color: #666; font-size: 10px; padding: 10px;">
|
|
893
|
-
No saved analyses found
|
|
894
|
-
</div>
|
|
895
|
-
`;
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
analysesList.innerHTML = '';
|
|
900
|
-
|
|
901
|
-
analyses.forEach(analysis => {
|
|
902
|
-
const analysisItem = document.createElement('div');
|
|
903
|
-
analysisItem.className = 'analysis-item';
|
|
904
|
-
analysisItem.onclick = () => loadAnalysis(analysis.id);
|
|
905
|
-
|
|
906
|
-
// Format date
|
|
907
|
-
const date = new Date(analysis.modifiedTime);
|
|
908
|
-
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
909
|
-
|
|
910
|
-
// Truncate video URL for display
|
|
911
|
-
const videoUrl = analysis.video_url || 'Unknown';
|
|
912
|
-
const displayUrl = videoUrl.length > 30 ? videoUrl.substring(0, 30) + '...' : videoUrl;
|
|
913
|
-
|
|
914
|
-
analysisItem.innerHTML = `
|
|
915
|
-
<div class="analysis-item-name">${analysis.name}</div>
|
|
916
|
-
<div class="analysis-item-info">${analysis.unique_frames_count} frames</div>
|
|
917
|
-
<div class="analysis-item-info">${displayUrl}</div>
|
|
918
|
-
<div class="analysis-item-date">${dateStr}</div>
|
|
919
|
-
`;
|
|
920
|
-
|
|
921
|
-
analysesList.appendChild(analysisItem);
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
function setActiveAnalysis(analysisId) {
|
|
926
|
-
currentAnalysisId = analysisId;
|
|
927
|
-
|
|
928
|
-
// Update UI to show active analysis
|
|
929
|
-
const analysisItems = document.querySelectorAll('.analysis-item');
|
|
930
|
-
analysisItems.forEach((item, index) => {
|
|
931
|
-
const analyses = window.currentAnalyses || [];
|
|
932
|
-
if (analyses[index] && analyses[index].id === analysisId) {
|
|
933
|
-
item.classList.add('active');
|
|
934
|
-
} else {
|
|
935
|
-
item.classList.remove('active');
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// Helper function to convert external URLs to local ones
|
|
941
|
-
function convertToLocalUrl(externalUrl) {
|
|
942
|
-
if (!externalUrl) return null;
|
|
943
|
-
|
|
944
|
-
// Extract filename from external URL
|
|
945
|
-
// e.g., https://video-analysis.empirical.run/e08b01dade99170e/frame_000000.png -> frame_000000.png
|
|
946
|
-
const match = externalUrl.match(/\/([^\/]+\.png)$/);
|
|
947
|
-
if (match) {
|
|
948
|
-
const filename = match[1];
|
|
949
|
-
return `/api/frame/${encodeURIComponent(filename)}`;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Fallback to original URL if we can't parse it
|
|
953
|
-
return externalUrl;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Make these available globally for the injected script
|
|
957
|
-
window.displayAnalysesList = displayAnalysesList;
|
|
958
|
-
window.setActiveAnalysis = setActiveAnalysis;
|
|
959
|
-
|
|
960
|
-
async function displayData(data) {
|
|
961
|
-
currentData = data;
|
|
962
|
-
hideStatus();
|
|
963
|
-
|
|
964
|
-
// Always try to get local frames from API first, then fallback to JSON data
|
|
965
|
-
console.log('🔍 Checking for local frames via server API...');
|
|
966
|
-
try {
|
|
967
|
-
const response = await fetch('/api/unique-frames');
|
|
968
|
-
|
|
969
|
-
if (response.ok) {
|
|
970
|
-
const localFrames = await response.json();
|
|
971
|
-
|
|
972
|
-
if (localFrames && localFrames.length > 0) {
|
|
973
|
-
console.log(`📁 Found ${localFrames.length} local frames from server API`);
|
|
974
|
-
data.unique_frames = localFrames; // Use local frames
|
|
975
|
-
} else if (!data.unique_frames || data.unique_frames.length === 0) {
|
|
976
|
-
console.log('📋 No local frames and no frames in JSON data');
|
|
977
|
-
showNoFramesMessage(data.analysis && data.analysis.includes('Error'));
|
|
978
|
-
} else {
|
|
979
|
-
console.log('📋 No local frames, using JSON frame data with external URLs');
|
|
980
|
-
}
|
|
981
|
-
} else if (!data.unique_frames || data.unique_frames.length === 0) {
|
|
982
|
-
console.log('📋 API call failed and no frames in JSON data');
|
|
983
|
-
showNoFramesMessage(data.analysis && data.analysis.includes('Error'));
|
|
984
|
-
} else {
|
|
985
|
-
console.log('📋 API call failed, using JSON frame data with external URLs');
|
|
986
|
-
}
|
|
987
|
-
} catch (error) {
|
|
988
|
-
console.log('📋 API call error:', error.message);
|
|
989
|
-
if (!data.unique_frames || data.unique_frames.length === 0) {
|
|
990
|
-
showNoFramesMessage(data.analysis && data.analysis.includes('Error'));
|
|
991
|
-
} else {
|
|
992
|
-
console.log('📋 Using JSON frame data with external URLs');
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Update frame count and calculate sizes
|
|
997
|
-
const frameCount = data.unique_frames ? data.unique_frames.length : 0;
|
|
998
|
-
document.getElementById('frameCount').textContent = frameCount;
|
|
999
|
-
|
|
1000
|
-
// Calculate total size from local data if available
|
|
1001
|
-
let totalSizeMB = 0;
|
|
1002
|
-
if (data.unique_frames && data.unique_frames.length > 0 && data.unique_frames[0].size) {
|
|
1003
|
-
totalSizeMB = data.unique_frames.reduce((sum, frame) => sum + (frame.size || 0), 0) / (1024 * 1024);
|
|
1004
|
-
document.getElementById('totalSize').textContent = totalSizeMB.toFixed(2) + ' MB';
|
|
1005
|
-
} else {
|
|
1006
|
-
document.getElementById('totalSize').textContent = 'Calculating...';
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// Display frames
|
|
1010
|
-
const framesGrid = document.getElementById('framesGrid');
|
|
1011
|
-
framesGrid.innerHTML = '';
|
|
1012
|
-
|
|
1013
|
-
if (data.unique_frames && data.unique_frames.length > 0) {
|
|
1014
|
-
data.unique_frames.forEach((frame, index) => {
|
|
1015
|
-
const frameCard = document.createElement('div');
|
|
1016
|
-
frameCard.className = 'frame-card';
|
|
1017
|
-
frameCard.id = `frame-${index}`;
|
|
1018
|
-
|
|
1019
|
-
const imgSrc = frame.url || frame.image_url || '';
|
|
1020
|
-
const filename = frame.fileName || frame.path || `frame_${index.toString().padStart(6, '0')}.png`;
|
|
1021
|
-
const frameSize = frame.size ? (frame.size / 1024).toFixed(1) : 'Loading...';
|
|
1022
|
-
|
|
1023
|
-
frameCard.innerHTML = `
|
|
1024
|
-
<img src="${imgSrc}" alt="Frame ${index + 1}" class="frame-img"
|
|
1025
|
-
onerror="this.style.display='none'"
|
|
1026
|
-
onclick="openFrameModal(${index})">
|
|
1027
|
-
<div class="frame-meta">
|
|
1028
|
-
<div class="label">#${index + 1}</div>
|
|
1029
|
-
<div>T: ${frame.timestamp || 'N/A'}</div>
|
|
1030
|
-
<div class="similarity" id="diff-${index}">Diff: ${index === 0 ? 'First' : 'Loading...'}</div>
|
|
1031
|
-
<div class="frame-size">${frameSize} KB</div>
|
|
1032
|
-
<div class="frame-filename">${filename}</div>
|
|
1033
|
-
${frame.description ? `<div>${frame.description.substring(0, 25)}...</div>` : ''}
|
|
1034
|
-
</div>
|
|
1035
|
-
`;
|
|
1036
|
-
framesGrid.appendChild(frameCard);
|
|
1037
|
-
|
|
1038
|
-
// Load similarity data asynchronously for non-first frames
|
|
1039
|
-
if (index > 0) {
|
|
1040
|
-
loadSimilarityData(index, filename, data.unique_frames[index - 1].fileName);
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
// Only calculate remote sizes if we don't have local size data
|
|
1045
|
-
if (!data.unique_frames[0].size) {
|
|
1046
|
-
getActualFileSizes(data.unique_frames).then(({ totalSize, frameSizes }) => {
|
|
1047
|
-
const totalSizeMB = (totalSize / 1024).toFixed(2);
|
|
1048
|
-
document.getElementById('totalSize').textContent = totalSizeMB + ' MB';
|
|
1049
|
-
|
|
1050
|
-
// Update individual frame sizes
|
|
1051
|
-
Object.keys(frameSizes).forEach(index => {
|
|
1052
|
-
const sizeElement = document.querySelector(`#frame-${index} .frame-size`);
|
|
1053
|
-
if (sizeElement) {
|
|
1054
|
-
sizeElement.textContent = frameSizes[index].toFixed(1) + ' KB';
|
|
1055
|
-
}
|
|
1056
|
-
});
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
} else {
|
|
1060
|
-
framesGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #666; font-size: 10px; padding: 20px;">No result</div>';
|
|
1061
|
-
document.getElementById('totalSize').textContent = '0 MB';
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// Display analysis (prioritize analysis over interleaved_tool_result for the analysis section)
|
|
1065
|
-
const analysisContent = data.analysis || 'No result';
|
|
1066
|
-
document.getElementById('analysisResult').textContent = analysisContent;
|
|
1067
|
-
|
|
1068
|
-
// Display interleaved results
|
|
1069
|
-
const interleavedResultDiv = document.getElementById('interleavedResult');
|
|
1070
|
-
if (data.interleaved_tool_result) {
|
|
1071
|
-
interleavedResultDiv.innerHTML = '';
|
|
1072
|
-
interleavedResultDiv.className = 'interleaved-content';
|
|
1073
|
-
|
|
1074
|
-
try {
|
|
1075
|
-
let parsedResult = data.interleaved_tool_result;
|
|
1076
|
-
if (typeof parsedResult === 'string') {
|
|
1077
|
-
parsedResult = JSON.parse(parsedResult);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (Array.isArray(parsedResult)) {
|
|
1081
|
-
if (parsedResult.length === 0) {
|
|
1082
|
-
interleavedResultDiv.textContent = 'No result';
|
|
1083
|
-
} else {
|
|
1084
|
-
parsedResult.forEach((item, index) => {
|
|
1085
|
-
if (item.type === 'text' && item.text) {
|
|
1086
|
-
let frameData;
|
|
1087
|
-
try {
|
|
1088
|
-
frameData = JSON.parse(item.text);
|
|
1089
|
-
} catch {
|
|
1090
|
-
frameData = { description: item.text };
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// Find the corresponding image in the next item
|
|
1094
|
-
const nextItem = parsedResult[index + 1];
|
|
1095
|
-
const externalImageUrl = nextItem && nextItem.type && nextItem.type.startsWith('image') ? nextItem.url : null;
|
|
1096
|
-
const imageUrl = convertToLocalUrl(externalImageUrl);
|
|
1097
|
-
|
|
1098
|
-
const frameEntry = document.createElement('div');
|
|
1099
|
-
frameEntry.className = 'frame-entry';
|
|
1100
|
-
|
|
1101
|
-
const frameIndex = Math.floor(index/2);
|
|
1102
|
-
frameEntry.innerHTML = `
|
|
1103
|
-
<div class="frame-header">${frameData.key_frame || `Frame ${frameIndex + 1}`}</div>
|
|
1104
|
-
<div class="frame-description">${frameData.description || item.text}</div>
|
|
1105
|
-
${imageUrl ? `
|
|
1106
|
-
<img src="${imageUrl}" alt="Frame" class="frame-image" onclick="openFrameModal(${frameIndex})">
|
|
1107
|
-
` : ''}
|
|
1108
|
-
`;
|
|
1109
|
-
|
|
1110
|
-
interleavedResultDiv.appendChild(frameEntry);
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
} else {
|
|
1115
|
-
interleavedResultDiv.textContent = JSON.stringify(parsedResult, null, 2);
|
|
1116
|
-
}
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
interleavedResultDiv.textContent = typeof data.interleaved_tool_result === 'string' ?
|
|
1119
|
-
data.interleaved_tool_result : JSON.stringify(data.interleaved_tool_result, null, 2);
|
|
1120
|
-
}
|
|
1121
|
-
} else {
|
|
1122
|
-
interleavedResultDiv.textContent = 'No result';
|
|
1123
|
-
interleavedResultDiv.className = 'json-content';
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Display video info
|
|
1127
|
-
const videoInfo = {
|
|
1128
|
-
url: data.video_url || 'N/A',
|
|
1129
|
-
id: data.analysis_id || 'N/A',
|
|
1130
|
-
model: data.params?.model || 'N/A',
|
|
1131
|
-
fps: data.params?.fps || 'N/A',
|
|
1132
|
-
threshold: data.params?.threshold || 'N/A'
|
|
1133
|
-
};
|
|
1134
|
-
document.getElementById('videoInfo').textContent = JSON.stringify(videoInfo, null, 2);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
async function runAnalysis() {
|
|
1138
|
-
const videoUrl = document.getElementById('analysisUrl').value.trim();
|
|
1139
|
-
const fps = document.getElementById('fpsInput').value.trim();
|
|
1140
|
-
const threshold = document.getElementById('thresholdInput').value.trim();
|
|
1141
|
-
|
|
1142
|
-
if (!videoUrl) {
|
|
1143
|
-
showStatus('Please enter a video URL', 'error');
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const params = `fps=${fps};threshold=${threshold}`;
|
|
1148
|
-
saveValues(videoUrl, params);
|
|
1149
|
-
|
|
1150
|
-
showStatus('Running video analysis...', 'loading');
|
|
1151
|
-
|
|
1152
|
-
try {
|
|
1153
|
-
const response = await fetch('/api/analyze', {
|
|
1154
|
-
method: 'POST',
|
|
1155
|
-
headers: {
|
|
1156
|
-
'Content-Type': 'application/json',
|
|
1157
|
-
},
|
|
1158
|
-
body: JSON.stringify({
|
|
1159
|
-
url: videoUrl,
|
|
1160
|
-
params: params
|
|
1161
|
-
})
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
if (!response.ok) {
|
|
1165
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const data = await response.json();
|
|
1169
|
-
displayData(data);
|
|
1170
|
-
showStatus('Analysis completed successfully', 'success');
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
showStatus(`Error: ${error.message}`, 'error');
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function loadFromFile() {
|
|
1177
|
-
document.getElementById('fileInput').click();
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
document.getElementById('fileInput').addEventListener('change', function(e) {
|
|
1181
|
-
const file = e.target.files[0];
|
|
1182
|
-
if (!file) return;
|
|
1183
|
-
|
|
1184
|
-
showStatus('Loading JSON file...', 'loading');
|
|
1185
|
-
const reader = new FileReader();
|
|
1186
|
-
reader.onload = function(e) {
|
|
1187
|
-
try {
|
|
1188
|
-
const data = JSON.parse(e.target.result);
|
|
1189
|
-
displayData(data);
|
|
1190
|
-
showStatus('JSON file loaded successfully', 'success');
|
|
1191
|
-
} catch (error) {
|
|
1192
|
-
showStatus(`Error parsing JSON: ${error.message}`, 'error');
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
reader.readAsText(file);
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
// Event listeners
|
|
1199
|
-
document.getElementById('runAnalysisButton').addEventListener('click', runAnalysis);
|
|
1200
|
-
document.getElementById('loadJsonButton').addEventListener('click', loadFromFile);
|
|
1201
|
-
|
|
1202
|
-
// Allow Enter key to trigger analysis
|
|
1203
|
-
document.getElementById('analysisUrl').addEventListener('keypress', function(e) {
|
|
1204
|
-
if (e.key === 'Enter') runAnalysis();
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
document.getElementById('fpsInput').addEventListener('keypress', function(e) {
|
|
1208
|
-
if (e.key === 'Enter') runAnalysis();
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
document.getElementById('thresholdInput').addEventListener('keypress', function(e) {
|
|
1212
|
-
if (e.key === 'Enter') runAnalysis();
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
let currentModalIndex = 0;
|
|
1216
|
-
let modalFrames = [];
|
|
1217
|
-
|
|
1218
|
-
// Frame modal functionality
|
|
1219
|
-
function openFrameModal(index) {
|
|
1220
|
-
if (!currentData || !currentData.unique_frames || currentData.unique_frames.length === 0) return;
|
|
1221
|
-
|
|
1222
|
-
modalFrames = currentData.unique_frames;
|
|
1223
|
-
currentModalIndex = index;
|
|
1224
|
-
|
|
1225
|
-
updateModalContent();
|
|
1226
|
-
createFilmstrip();
|
|
1227
|
-
|
|
1228
|
-
const modal = document.getElementById('frameModal');
|
|
1229
|
-
modal.style.display = 'block';
|
|
1230
|
-
modal.focus();
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function updateModalContent() {
|
|
1234
|
-
if (modalFrames.length === 0) return;
|
|
1235
|
-
|
|
1236
|
-
const frame = modalFrames[currentModalIndex];
|
|
1237
|
-
const modalImage = document.getElementById('modalImage');
|
|
1238
|
-
|
|
1239
|
-
// Update image
|
|
1240
|
-
const imageUrl = frame.url || frame.image_url || '';
|
|
1241
|
-
modalImage.src = imageUrl;
|
|
1242
|
-
modalImage.alt = `Frame ${currentModalIndex + 1}`;
|
|
1243
|
-
|
|
1244
|
-
// Update metadata
|
|
1245
|
-
const filename = frame.fileName || frame.path || `frame_${currentModalIndex}`;
|
|
1246
|
-
const frameSize = frame.size ? (frame.size / 1024).toFixed(1) : 'Loading...';
|
|
1247
|
-
const similarityPercentage = frame.similarityPercentage !== null && frame.similarityPercentage !== undefined
|
|
1248
|
-
? (frame.similarityPercentage * 100).toFixed(1) + '%'
|
|
1249
|
-
: (currentModalIndex === 0 ? 'First' : 'N/A');
|
|
1250
|
-
|
|
1251
|
-
// Update title
|
|
1252
|
-
document.getElementById('modalFrameTitle').textContent = `Frame ${currentModalIndex + 1} of ${modalFrames.length}`;
|
|
1253
|
-
|
|
1254
|
-
// Update metadata fields
|
|
1255
|
-
document.getElementById('modalFrameTimestamp').textContent = frame.timestamp || 'N/A';
|
|
1256
|
-
document.getElementById('modalFrameDiff').textContent = similarityPercentage;
|
|
1257
|
-
document.getElementById('modalFrameSize').textContent = frameSize + ' KB';
|
|
1258
|
-
document.getElementById('modalFrameFile').textContent = filename;
|
|
1259
|
-
document.getElementById('modalFrameUrl').textContent = imageUrl || 'N/A';
|
|
1260
|
-
|
|
1261
|
-
// Update dimensions when image loads
|
|
1262
|
-
modalImage.onload = function() {
|
|
1263
|
-
document.getElementById('modalFrameDimensions').textContent = `${this.naturalWidth} × ${this.naturalHeight}`;
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
// Handle case where image fails to load
|
|
1267
|
-
modalImage.onerror = function() {
|
|
1268
|
-
document.getElementById('modalFrameDimensions').textContent = 'N/A';
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
// Show description if available
|
|
1272
|
-
const descriptionElement = document.getElementById('modalFrameDescription');
|
|
1273
|
-
if (frame.description) {
|
|
1274
|
-
descriptionElement.textContent = frame.description;
|
|
1275
|
-
descriptionElement.style.display = 'block';
|
|
1276
|
-
} else {
|
|
1277
|
-
descriptionElement.style.display = 'none';
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// Update filmstrip active state
|
|
1281
|
-
updateFilmstripActive();
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function createFilmstrip() {
|
|
1285
|
-
const filmstrip = document.getElementById('modalFilmstrip');
|
|
1286
|
-
filmstrip.innerHTML = '';
|
|
1287
|
-
|
|
1288
|
-
modalFrames.forEach((frame, index) => {
|
|
1289
|
-
const img = document.createElement('img');
|
|
1290
|
-
img.src = frame.url || frame.image_url || '';
|
|
1291
|
-
img.className = 'filmstrip-frame';
|
|
1292
|
-
img.alt = `Frame ${index + 1}`;
|
|
1293
|
-
img.onclick = (e) => {
|
|
1294
|
-
e.stopPropagation();
|
|
1295
|
-
currentModalIndex = index;
|
|
1296
|
-
updateModalContent();
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
filmstrip.appendChild(img);
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function updateFilmstripActive() {
|
|
1304
|
-
const filmstripFrames = document.querySelectorAll('.filmstrip-frame');
|
|
1305
|
-
filmstripFrames.forEach((frame, index) => {
|
|
1306
|
-
if (index === currentModalIndex) {
|
|
1307
|
-
frame.classList.add('active');
|
|
1308
|
-
// Scroll active frame into view
|
|
1309
|
-
frame.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
1310
|
-
} else {
|
|
1311
|
-
frame.classList.remove('active');
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function navigateModal(direction) {
|
|
1317
|
-
if (modalFrames.length === 0) return;
|
|
1318
|
-
|
|
1319
|
-
if (direction === 'next') {
|
|
1320
|
-
currentModalIndex = (currentModalIndex + 1) % modalFrames.length;
|
|
1321
|
-
} else if (direction === 'prev') {
|
|
1322
|
-
currentModalIndex = (currentModalIndex - 1 + modalFrames.length) % modalFrames.length;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
updateModalContent();
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function closeFrameModal() {
|
|
1329
|
-
document.getElementById('frameModal').style.display = 'none';
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// Event listeners for modal
|
|
1333
|
-
document.querySelector('.frame-modal-close').addEventListener('click', closeFrameModal);
|
|
1334
|
-
|
|
1335
|
-
document.getElementById('modalPrevBtn').addEventListener('click', (e) => {
|
|
1336
|
-
e.stopPropagation();
|
|
1337
|
-
navigateModal('prev');
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
document.getElementById('modalNextBtn').addEventListener('click', (e) => {
|
|
1341
|
-
e.stopPropagation();
|
|
1342
|
-
navigateModal('next');
|
|
1343
|
-
});
|
|
1344
|
-
|
|
1345
|
-
document.getElementById('frameModal').addEventListener('click', function(e) {
|
|
1346
|
-
if (e.target === this) closeFrameModal();
|
|
1347
|
-
});
|
|
1348
|
-
|
|
1349
|
-
document.getElementById('frameModal').addEventListener('keydown', function(e) {
|
|
1350
|
-
switch(e.key) {
|
|
1351
|
-
case 'ArrowLeft':
|
|
1352
|
-
e.preventDefault();
|
|
1353
|
-
navigateModal('prev');
|
|
1354
|
-
break;
|
|
1355
|
-
case 'ArrowRight':
|
|
1356
|
-
e.preventDefault();
|
|
1357
|
-
navigateModal('next');
|
|
1358
|
-
break;
|
|
1359
|
-
case 'Escape':
|
|
1360
|
-
e.preventDefault();
|
|
1361
|
-
closeFrameModal();
|
|
1362
|
-
break;
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
|
|
1366
|
-
// Initialize page
|
|
1367
|
-
if (document.readyState === 'loading') {
|
|
1368
|
-
document.addEventListener('DOMContentLoaded', loadSavedValues);
|
|
1369
|
-
} else {
|
|
1370
|
-
loadSavedValues();
|
|
1371
|
-
}
|
|
1372
|
-
</script>
|
|
1373
|
-
</body>
|
|
1374
|
-
</html>
|