@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/agent/base/index.d.ts +3 -3
  3. package/dist/agent/base/index.d.ts.map +1 -1
  4. package/dist/agent/base/index.js +5 -3
  5. package/dist/agent/chat/exports.d.ts +1 -1
  6. package/dist/agent/chat/exports.d.ts.map +1 -1
  7. package/dist/agent/chat/exports.js +1 -3
  8. package/dist/agent/chat/models.d.ts.map +1 -1
  9. package/dist/agent/chat/models.js +1 -1
  10. package/dist/agent/chat/state.d.ts +1 -8
  11. package/dist/agent/chat/state.d.ts.map +1 -1
  12. package/dist/agent/chat/state.js +0 -15
  13. package/dist/agent/cli.d.ts.map +1 -1
  14. package/dist/agent/cli.js +12 -25
  15. package/dist/agent/code-review/index.d.ts +1 -1
  16. package/dist/agent/code-review/index.d.ts.map +1 -1
  17. package/dist/agent/code-review/index.js +3 -0
  18. package/dist/agent/code-review/types.d.ts +9 -9
  19. package/dist/agent/code-review/types.d.ts.map +1 -1
  20. package/dist/agent/triage/index.d.ts +1 -1
  21. package/dist/agent/triage/index.d.ts.map +1 -1
  22. package/dist/agent/triage/index.js +9 -14
  23. package/dist/bin/index.js +0 -55
  24. package/dist/tools/analyse-video/index.d.ts.map +1 -1
  25. package/dist/tools/analyse-video/index.js +8 -2
  26. package/dist/tools/definitions/analyse-video.d.ts +4 -4
  27. package/dist/tools/definitions/analyse-video.js +2 -2
  28. package/dist/tools/executor/base.d.ts +1 -1
  29. package/dist/tools/executor/base.d.ts.map +1 -1
  30. package/dist/tools/executor/base.js +19 -2
  31. package/dist/tools/executor/utils/index.d.ts +5 -3
  32. package/dist/tools/executor/utils/index.d.ts.map +1 -1
  33. package/dist/tools/executor/utils/index.js +22 -1
  34. package/dist/tools/file-operations/replace.d.ts.map +1 -1
  35. package/dist/tools/file-operations/replace.js +20 -21
  36. package/dist/tools/file-operations/shared/helpers.d.ts +3 -5
  37. package/dist/tools/file-operations/shared/helpers.d.ts.map +1 -1
  38. package/dist/tools/file-operations/shared/helpers.js +1 -5
  39. package/dist/tools/review-pull-request/index.d.ts.map +1 -1
  40. package/dist/tools/review-pull-request/index.js +4 -11
  41. package/dist/tools/upgrade-packages/index.d.ts.map +1 -1
  42. package/dist/tools/upgrade-packages/index.js +4 -0
  43. package/dist/tools/upgrade-packages/utils.d.ts +1 -0
  44. package/dist/tools/upgrade-packages/utils.d.ts.map +1 -1
  45. package/dist/tools/upgrade-packages/utils.js +1 -0
  46. package/dist/trace-utils/index.d.ts +1 -1
  47. package/dist/trace-utils/index.d.ts.map +1 -1
  48. package/dist/trace-utils/index.js +1 -1
  49. package/dist/utils/dedup/dedup-image.d.ts +22 -0
  50. package/dist/utils/dedup/dedup-image.d.ts.map +1 -0
  51. package/dist/utils/dedup/dedup-image.js +26 -0
  52. package/dist/utils/dedup/find-threshold.d.ts +2 -0
  53. package/dist/utils/dedup/find-threshold.d.ts.map +1 -0
  54. package/dist/utils/{find-threshold.js → dedup/find-threshold.js} +0 -13
  55. package/dist/video-core/agent-orchestrator.d.ts +1 -2
  56. package/dist/video-core/agent-orchestrator.d.ts.map +1 -1
  57. package/dist/video-core/agent-orchestrator.js +11 -30
  58. package/dist/video-core/index.d.ts +11 -16
  59. package/dist/video-core/index.d.ts.map +1 -1
  60. package/dist/video-core/index.js +110 -180
  61. package/dist/video-core/model-limits.d.ts.map +1 -1
  62. package/dist/video-core/model-limits.js +8 -2
  63. package/dist/video-core/storage-manager.d.ts.map +1 -1
  64. package/dist/video-core/storage-manager.js +13 -6
  65. package/dist/video-core/utils.d.ts +0 -10
  66. package/dist/video-core/utils.d.ts.map +1 -1
  67. package/dist/video-core/utils.js +1 -18
  68. package/package.json +5 -4
  69. package/tsconfig.tsbuildinfo +1 -1
  70. package/dist/utils/artifact-paths.d.ts +0 -20
  71. package/dist/utils/artifact-paths.d.ts.map +0 -1
  72. package/dist/utils/artifact-paths.js +0 -16
  73. package/dist/utils/dedup-image-fs.d.ts +0 -13
  74. package/dist/utils/dedup-image-fs.d.ts.map +0 -1
  75. package/dist/utils/dedup-image-fs.js +0 -84
  76. package/dist/utils/dedup-image.d.ts +0 -12
  77. package/dist/utils/dedup-image.d.ts.map +0 -1
  78. package/dist/utils/dedup-image.js +0 -25
  79. package/dist/utils/ffmpeg/index.d.ts +0 -26
  80. package/dist/utils/ffmpeg/index.d.ts.map +0 -1
  81. package/dist/utils/ffmpeg/index.js +0 -415
  82. package/dist/utils/find-threshold.d.ts +0 -8
  83. package/dist/utils/find-threshold.d.ts.map +0 -1
  84. package/dist/video-core/analysis-server.d.ts +0 -24
  85. package/dist/video-core/analysis-server.d.ts.map +0 -1
  86. package/dist/video-core/analysis-server.js +0 -398
  87. 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">&times;</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>