@probelabs/probe-chat 0.6.0-rc56

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/index.html ADDED
@@ -0,0 +1,3686 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Probe - AI-Native Code Understanding</title>
8
+ <!-- Add Marked.js for Markdown rendering -->
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <!-- Add Highlight.js for syntax highlighting -->
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
13
+ <!-- Add Mermaid.js for diagram rendering -->
14
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
15
+ <style>
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18
+ margin: 0;
19
+ padding: 0;
20
+ line-height: 1.5;
21
+ color: #333;
22
+ min-height: 100vh;
23
+ overflow-x: hidden;
24
+ overflow-y: auto;
25
+ }
26
+
27
+ #chat-container {
28
+ max-width: 868px;
29
+ /* 900px - 16px padding on each side */
30
+ margin: 0 auto;
31
+ display: flex;
32
+ flex-direction: column;
33
+ min-height: 100vh;
34
+ /* Changed from height to min-height to allow expansion */
35
+ position: relative;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ .header {
40
+ padding: 10px 0;
41
+ border-bottom: 1px solid #eee;
42
+ display: block; /* Ensure header is visible by default */
43
+ }
44
+
45
+ .header-container {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ align-items: center;
49
+ }
50
+
51
+ .header-left {
52
+ display: flex;
53
+ align-items: center;
54
+ }
55
+
56
+ .header-logo {
57
+ height: 30px;
58
+ margin-right: 10px;
59
+ }
60
+
61
+ .header-left a {
62
+ text-decoration: none;
63
+ display: inline-block;
64
+ }
65
+
66
+ .header-left a:hover .header-logo {
67
+ opacity: 0.8;
68
+ transition: opacity 0.2s ease;
69
+ }
70
+
71
+ .new-chat-link {
72
+ font-size: 15px;
73
+ color: #555;
74
+ text-decoration: none;
75
+ font-weight: 500;
76
+ padding: 5px 10px;
77
+ border: 1px solid #ddd;
78
+ border-radius: 4px;
79
+ margin-left: 5px;
80
+ transition: all 0.2s ease;
81
+ }
82
+
83
+ .new-chat-link:hover {
84
+ background-color: #f5f5f5;
85
+ border-color: #ccc;
86
+ }
87
+
88
+ /* History dropdown styles */
89
+ .history-dropdown {
90
+ position: relative;
91
+ display: inline-block;
92
+ margin-right: 5px;
93
+ }
94
+
95
+ .history-button {
96
+ font-size: 15px;
97
+ color: #555;
98
+ background: none;
99
+ border: 1px solid #ddd;
100
+ border-radius: 4px;
101
+ padding: 5px 10px;
102
+ cursor: pointer;
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 5px;
106
+ font-weight: 500;
107
+ transition: all 0.2s ease;
108
+ }
109
+
110
+ .history-button:hover {
111
+ background-color: #f5f5f5;
112
+ border-color: #ccc;
113
+ }
114
+
115
+ .history-button svg {
116
+ width: 16px;
117
+ height: 16px;
118
+ }
119
+
120
+ .history-dropdown-menu {
121
+ position: absolute;
122
+ top: 100%;
123
+ left: 0;
124
+ background: white;
125
+ border: 1px solid #ddd;
126
+ border-radius: 6px;
127
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
128
+ z-index: 1000;
129
+ min-width: 320px;
130
+ max-width: 400px;
131
+ max-height: 400px;
132
+ overflow-y: auto;
133
+ display: none;
134
+ margin-top: 2px;
135
+ }
136
+
137
+ .history-dropdown-menu.show {
138
+ display: block;
139
+ }
140
+
141
+ .history-dropdown-header {
142
+ padding: 12px 16px 8px;
143
+ font-size: 14px;
144
+ font-weight: 600;
145
+ color: #333;
146
+ border-bottom: 1px solid #eee;
147
+ }
148
+
149
+ .history-loading {
150
+ padding: 16px;
151
+ text-align: center;
152
+ color: #666;
153
+ font-size: 14px;
154
+ }
155
+
156
+ .history-empty {
157
+ padding: 16px;
158
+ text-align: center;
159
+ color: #666;
160
+ font-size: 14px;
161
+ }
162
+
163
+ .history-list {
164
+ padding: 8px 0;
165
+ }
166
+
167
+ .history-item {
168
+ padding: 12px 16px;
169
+ cursor: pointer;
170
+ border-bottom: 1px solid #f5f5f5;
171
+ transition: background-color 0.2s ease;
172
+ }
173
+
174
+ .history-item:last-child {
175
+ border-bottom: none;
176
+ }
177
+
178
+ .history-item:hover {
179
+ background-color: #f8f9fa;
180
+ }
181
+
182
+ .history-item-preview {
183
+ font-size: 14px;
184
+ color: #333;
185
+ margin-bottom: 4px;
186
+ line-height: 1.4;
187
+ display: -webkit-box;
188
+ -webkit-line-clamp: 2;
189
+ -webkit-box-orient: vertical;
190
+ overflow: hidden;
191
+ }
192
+
193
+ .history-item-meta {
194
+ font-size: 12px;
195
+ color: #666;
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ }
200
+
201
+ .history-item-time {
202
+ font-size: 11px;
203
+ }
204
+
205
+ .history-item-count {
206
+ font-size: 11px;
207
+ background: #e9ecef;
208
+ padding: 2px 6px;
209
+ border-radius: 10px;
210
+ }
211
+
212
+ #messages {
213
+ flex: 1;
214
+ background-color: #fff;
215
+ /* Removed overflow-y: auto to let the page handle scrolling */
216
+ margin-bottom: 80px;
217
+ /* Keep margin for fixed input form */
218
+ margin-top: 20px;
219
+ /* Space for fixed input form */
220
+ }
221
+
222
+ .tool-call {
223
+ margin: 10px 0;
224
+ border: 1px solid #ddd;
225
+ border-radius: 8px;
226
+ background-color: #f8f9fa;
227
+ overflow: hidden;
228
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
229
+ transition: all 0.2s ease;
230
+ }
231
+
232
+ .tool-call:hover {
233
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
234
+ }
235
+
236
+ .tool-call-header {
237
+ background-color: #e9ecef;
238
+ padding: 10px 14px;
239
+ font-weight: bold;
240
+ border-bottom: 1px solid #ddd;
241
+ display: flex;
242
+ justify-content: space-between;
243
+ align-items: center;
244
+ }
245
+
246
+ .tool-call-name {
247
+ color: #0066cc;
248
+ font-size: 1.05em;
249
+ }
250
+
251
+ .tool-call-timestamp {
252
+ font-size: 0.8em;
253
+ color: #666;
254
+ font-style: italic;
255
+ }
256
+
257
+ .tool-call-content {
258
+ padding: 12px;
259
+ }
260
+
261
+ .tool-call-description {
262
+ background-color: #ffffff;
263
+ padding: 10px 12px;
264
+ border-radius: 6px;
265
+ border-left: 3px solid #44CDF3;
266
+ margin-bottom: 10px;
267
+ font-size: 0.95em;
268
+ color: #333;
269
+ }
270
+
271
+ .tool-call-args {
272
+ background-color: #ffffff;
273
+ padding: 10px;
274
+ border-radius: 6px;
275
+ border: 1px solid #eee;
276
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
277
+ font-size: 0.9em;
278
+ margin-bottom: 10px;
279
+ white-space: pre-wrap;
280
+ }
281
+
282
+ .tool-call-result {
283
+ background-color: #f0f8ff;
284
+ padding: 10px;
285
+ border-radius: 6px;
286
+ border: 1px solid #e0e8ff;
287
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
288
+ font-size: 0.9em;
289
+ white-space: pre-wrap;
290
+ max-height: 300px;
291
+ overflow-y: auto;
292
+ }
293
+
294
+
295
+ #input-form {
296
+ position: fixed;
297
+ display: flex;
298
+ padding: 16px 0;
299
+ background-color: white;
300
+ border-top: 1px solid #ddd;
301
+ z-index: 100;
302
+ max-width: 868px;
303
+ width: calc(100% - 32px);
304
+ margin: 0 auto;
305
+ left: 50%;
306
+ transform: translateX(-50%);
307
+ box-sizing: border-box;
308
+ align-items: flex-end;
309
+ /* Align items to the bottom */
310
+ }
311
+
312
+ #input-form.centered {
313
+ top: 50%;
314
+ transform: translate(-50%, -50%);
315
+ border-top: none;
316
+ }
317
+
318
+ .centered-logo-container {
319
+ text-align: center;
320
+ margin-bottom: 20px;
321
+ position: fixed;
322
+ top: 35%;
323
+ left: 50%;
324
+ transform: translate(-50%, -100%);
325
+ z-index: 99;
326
+ }
327
+
328
+ .api-setup-mode .centered-logo-container {
329
+ position: static;
330
+ margin: 40px auto 20px;
331
+ width: 100%;
332
+ max-width: 800px;
333
+ transform: none;
334
+ }
335
+
336
+ .centered-logo-container h1 {
337
+ font-weight: 300;
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ margin: 0;
342
+ }
343
+
344
+ .centered-logo-container h1 img {
345
+ height: 80px;
346
+ margin-right: 16px;
347
+ }
348
+
349
+ .centered-logo-container h1 {
350
+ font-size: 38px;
351
+ }
352
+
353
+ #input-form.bottom {
354
+ bottom: 0;
355
+ }
356
+
357
+ .search-suggestions {
358
+ color: #999;
359
+ font-size: 0.85em;
360
+ display: none;
361
+ text-align: left;
362
+ padding: 0;
363
+ position: fixed;
364
+ max-width: 868px;
365
+ width: calc(100% - 32px);
366
+ margin: 0 auto;
367
+ background-color: white;
368
+ left: 50%;
369
+ transform: translateX(-50%);
370
+ z-index: 99;
371
+ }
372
+
373
+ .search-suggestions ul {
374
+ list-style: none;
375
+ padding: 0;
376
+ margin: 0;
377
+ display: flex;
378
+ flex-wrap: wrap;
379
+ }
380
+
381
+ .search-suggestions li {
382
+ padding: 6px 16px 6px 0;
383
+ white-space: nowrap;
384
+ cursor: pointer;
385
+ }
386
+
387
+ .search-suggestions li:hover {
388
+ color: #44CDF3;
389
+ }
390
+
391
+ #input-form.centered .search-suggestions {
392
+ display: block;
393
+ }
394
+
395
+ .folder-info {
396
+ color: #666;
397
+ font-size: 0.9em;
398
+ margin-top: 10px;
399
+ padding-top: 10px;
400
+ border-top: 1px solid #e0e0e0;
401
+ font-style: italic;
402
+ font-weight: 500;
403
+ }
404
+
405
+ #input-form.centered .folder-info {
406
+ display: block;
407
+ }
408
+
409
+
410
+ #message-input {
411
+ resize: none;
412
+ flex: 1;
413
+ padding: 12px 40px 12px 16px;
414
+ border: 1px solid #ddd;
415
+ border-radius: 8px;
416
+ font-size: 14px;
417
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
418
+ resize: none;
419
+ overflow-y: auto;
420
+ height: 1.5em;
421
+ width: 100%;
422
+ box-sizing: border-box;
423
+ /* Changed from 44px to auto */
424
+ min-height: 44px;
425
+ /* Ensures 1 row minimum */
426
+ max-height: 200px;
427
+ /* Limits to ~10 rows */
428
+ line-height: 1.5em;
429
+
430
+ box-sizing: border-box;
431
+ }
432
+
433
+ button {
434
+ padding: 12px 24px;
435
+ margin-left: 10px;
436
+ background-color: #44CDF3;
437
+ color: white;
438
+ border: none;
439
+ border-radius: 8px;
440
+ cursor: pointer;
441
+ font-weight: bold;
442
+ font-size: 18px;
443
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
444
+ transition: all 0.2s ease;
445
+ align-self: flex-end;
446
+ /* Ensure button aligns to bottom */
447
+ height: 44px;
448
+ /* Match the height of the single-line textarea */
449
+ }
450
+
451
+ button:hover {
452
+ background-color: #2bb5db;
453
+ }
454
+
455
+ #search-button {
456
+ padding: 12px 20px;
457
+ background-color: #44CDF3;
458
+ color: white;
459
+ border: none;
460
+ border-radius: 8px;
461
+ cursor: pointer;
462
+ font-weight: 500;
463
+ transition: all 0.2s ease;
464
+ white-space: nowrap;
465
+ flex-shrink: 0;
466
+ align-self: flex-start;
467
+ margin-left: 0;
468
+ height: auto;
469
+ }
470
+
471
+
472
+ #folder-list ul {
473
+ margin: 4px 0;
474
+ padding-left: 20px;
475
+ }
476
+
477
+ #folder-list li {
478
+ padding: 2px 0;
479
+ }
480
+
481
+ #folder-list strong {
482
+ color: #333;
483
+ font-weight: 600;
484
+ }
485
+
486
+ .footer {
487
+ text-align: center;
488
+ padding: 10px;
489
+ font-size: 14px;
490
+ color: #666;
491
+ position: fixed;
492
+ bottom: 0;
493
+ left: 0;
494
+ right: 0;
495
+ background-color: white;
496
+ border-top: 1px solid #eee;
497
+ z-index: 50;
498
+ }
499
+
500
+ .footer a {
501
+ color: #2196F3;
502
+ text-decoration: none;
503
+ }
504
+
505
+ .footer a:hover {
506
+ text-decoration: underline;
507
+ }
508
+
509
+ .header-container {
510
+ display: flex;
511
+ align-items: center;
512
+ justify-content: space-between;
513
+ }
514
+
515
+ .example {
516
+ font-style: italic;
517
+ color: #666;
518
+ margin: 8px 0;
519
+ }
520
+
521
+ /* Markdown styling */
522
+ .markdown-content {
523
+ line-height: 1.6;
524
+ }
525
+
526
+ .markdown-content h1,
527
+ .markdown-content h2,
528
+ .markdown-content h3 {
529
+ margin-top: 24px;
530
+ margin-bottom: 16px;
531
+ font-weight: 600;
532
+ line-height: 1.25;
533
+ }
534
+
535
+ .markdown-content h1 {
536
+ font-size: 2em;
537
+ }
538
+
539
+ .markdown-content h2 {
540
+ font-size: 1.5em;
541
+ }
542
+
543
+ .markdown-content h3 {
544
+ font-size: 1.25em;
545
+ }
546
+
547
+ .markdown-content p,
548
+ .markdown-content ul,
549
+ .markdown-content ol {
550
+ margin-top: 0;
551
+ margin-bottom: 16px;
552
+ }
553
+
554
+ .markdown-content code {
555
+ padding: 0.2em 0.4em;
556
+ margin: 0;
557
+ font-size: 85%;
558
+ background-color: rgba(27, 31, 35, 0.05);
559
+ border-radius: 3px;
560
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
561
+ }
562
+
563
+ .markdown-content pre {
564
+ padding: 16px;
565
+ overflow: auto;
566
+ font-size: 85%;
567
+ line-height: 1.45;
568
+ background-color: #f6f8fa;
569
+ border-radius: 3px;
570
+ margin-top: 0;
571
+ margin-bottom: 16px;
572
+ }
573
+
574
+ .markdown-content pre code {
575
+ padding: 0;
576
+ margin: 0;
577
+ font-size: 100%;
578
+ background-color: transparent;
579
+ border: 0;
580
+ }
581
+
582
+ .user-message {
583
+ background-color: #f1f1f1;
584
+ padding: 10px 14px;
585
+ border-radius: 18px;
586
+ margin-bottom: 6px;
587
+ max-width: 80%;
588
+ align-self: flex-end;
589
+ font-weight: 500;
590
+ }
591
+
592
+ .ai-message {
593
+ background-color: transparent;
594
+ padding: 10px 14px;
595
+ margin-bottom: 4px;
596
+ max-width: 90%;
597
+ align-self: flex-start;
598
+ }
599
+
600
+ .message-container {
601
+ display: flex;
602
+ flex-direction: column;
603
+ }
604
+
605
+ .copy-button-container {
606
+ align-self: flex-start;
607
+ margin-bottom: 12px;
608
+ margin-top: -20px;
609
+ }
610
+
611
+ .copy-button {
612
+ background-color: white;
613
+ border-radius: 4px;
614
+ padding: 4px 8px;
615
+ cursor: pointer;
616
+ font-size: 12px;
617
+ color: #666;
618
+ display: flex;
619
+ align-items: center;
620
+ border: none;
621
+ }
622
+
623
+ .copy-button:hover {
624
+ background-color: #d0d0d0;
625
+ }
626
+
627
+ .copy-button svg {
628
+ width: 16px;
629
+ height: 16px;
630
+ margin-right: 4px;
631
+ }
632
+
633
+ .message-container {
634
+ display: flex;
635
+ flex-direction: column;
636
+ }
637
+
638
+ /* Mermaid diagram styling */
639
+ .mermaid {
640
+ background-color: #f8f9fa;
641
+ padding: 16px;
642
+ border-radius: 8px;
643
+ margin: 16px 0;
644
+ overflow-x: auto;
645
+ text-align: center;
646
+ position: relative;
647
+ }
648
+
649
+ .mermaid svg,
650
+ .mermaid-png {
651
+ max-width: 100%;
652
+ height: auto;
653
+ transition: transform 0.2s ease;
654
+ }
655
+
656
+ /* Zoom icon overlay */
657
+ .mermaid-container {
658
+ position: relative;
659
+ display: inline-block;
660
+ }
661
+
662
+ .zoom-icon {
663
+ position: absolute;
664
+ top: 10px;
665
+ right: 10px;
666
+ background-color: rgba(255, 255, 255, 0.8);
667
+ border-radius: 50%;
668
+ width: 32px;
669
+ height: 32px;
670
+ display: flex;
671
+ align-items: center;
672
+ justify-content: center;
673
+ cursor: pointer;
674
+ opacity: 0;
675
+ transition: opacity 0.2s ease;
676
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
677
+ z-index: 10;
678
+ }
679
+
680
+ .mermaid-container:hover .zoom-icon {
681
+ opacity: 1;
682
+ }
683
+
684
+ .zoom-icon svg {
685
+ width: 18px;
686
+ height: 18px;
687
+ }
688
+
689
+ /* Fullscreen dialog */
690
+ .diagram-dialog {
691
+ position: fixed;
692
+ top: 0;
693
+ left: 0;
694
+ width: 100%;
695
+ height: 100%;
696
+ background-color: rgba(0, 0, 0, 0.85);
697
+ display: flex;
698
+ align-items: center;
699
+ justify-content: center;
700
+ z-index: 1000;
701
+ padding: 40px;
702
+ box-sizing: border-box;
703
+ opacity: 0;
704
+ pointer-events: none;
705
+ transition: opacity 0.3s ease;
706
+ overflow: hidden;
707
+ /* Prevent any scrolling */
708
+ }
709
+
710
+ .diagram-dialog.active {
711
+ opacity: 1;
712
+ pointer-events: auto;
713
+ }
714
+
715
+ .diagram-dialog-content {
716
+ max-width: 90%;
717
+ max-height: 90%;
718
+ background-color: white;
719
+ border-radius: 8px;
720
+ padding: 20px;
721
+ position: relative;
722
+ display: flex;
723
+ align-items: center;
724
+ justify-content: center;
725
+ overflow: hidden;
726
+ /* Prevent scrollbars */
727
+ }
728
+
729
+ .diagram-dialog img,
730
+ .diagram-dialog svg {
731
+ max-width: 100%;
732
+ max-height: 100%;
733
+ object-fit: contain;
734
+ /* Ensure image fits while maintaining aspect ratio */
735
+ display: block;
736
+ }
737
+
738
+ .close-dialog {
739
+ position: absolute;
740
+ top: 10px;
741
+ right: 10px;
742
+ background-color: rgba(255, 255, 255, 0.8);
743
+ border-radius: 50%;
744
+ width: 36px;
745
+ height: 36px;
746
+ display: flex;
747
+ align-items: center;
748
+ justify-content: center;
749
+ cursor: pointer;
750
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
751
+ z-index: 1001;
752
+ }
753
+
754
+ .close-dialog svg {
755
+ width: 20px;
756
+ height: 20px;
757
+ }
758
+
759
+ /* Token usage display styles */
760
+ .token-usage {
761
+ position: fixed;
762
+ bottom: 80px;
763
+ right: 20px;
764
+ background-color: rgba(255, 255, 255, 0.95);
765
+ border: 1px solid #ddd;
766
+ border-radius: 8px;
767
+ padding: 10px 14px;
768
+ font-size: 12px;
769
+ color: #666;
770
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
771
+ z-index: 100;
772
+ display: none;
773
+ /* Hidden by default, shown after first message */
774
+ }
775
+
776
+ .token-usage-content {
777
+ display: flex;
778
+ flex-direction: column;
779
+ gap: 6px;
780
+ }
781
+
782
+ .token-usage-table {
783
+ display: table;
784
+ width: 100%;
785
+ border-collapse: collapse;
786
+ }
787
+
788
+ .token-usage-row {
789
+ display: table-row;
790
+ }
791
+
792
+ .token-label {
793
+ display: table-cell;
794
+ font-weight: bold;
795
+ color: #444;
796
+ padding-right: 10px;
797
+ text-align: left;
798
+ white-space: nowrap;
799
+ }
800
+
801
+ .token-value {
802
+ display: table-cell;
803
+ text-align: right;
804
+ white-space: nowrap;
805
+ }
806
+
807
+ .cache-info {
808
+ color: #888;
809
+ font-size: 11px;
810
+ margin-left: 5px;
811
+ }
812
+
813
+ /* New minimal image upload styles */
814
+ .input-wrapper {
815
+ display: flex;
816
+ align-items: flex-start;
817
+ gap: 8px;
818
+ width: 100%;
819
+ }
820
+
821
+ .textarea-container {
822
+ position: relative;
823
+ flex: 1;
824
+ display: flex;
825
+ align-items: flex-start;
826
+ }
827
+
828
+ #message-input {
829
+ flex: 1;
830
+ min-height: 40px;
831
+ padding-right: 40px; /* Make space for upload icon */
832
+ }
833
+
834
+ .image-upload-icon {
835
+ position: absolute;
836
+ right: 12px;
837
+ top: 50%;
838
+ transform: translateY(-50%);
839
+ width: 24px;
840
+ height: 24px;
841
+ background: none !important;
842
+ border: none !important;
843
+ cursor: pointer;
844
+ color: #999 !important;
845
+ padding: 4px !important;
846
+ border-radius: 4px !important;
847
+ transition: all 0.2s ease;
848
+ display: flex;
849
+ align-items: center;
850
+ justify-content: center;
851
+ z-index: 10;
852
+ margin: 0 !important;
853
+ font-size: inherit !important;
854
+ font-weight: normal !important;
855
+ box-shadow: none !important;
856
+ }
857
+
858
+ .image-upload-icon:hover {
859
+ color: #666 !important;
860
+ background-color: rgba(0, 0, 0, 0.05) !important;
861
+ }
862
+
863
+ .image-upload-icon svg {
864
+ width: 16px;
865
+ height: 16px;
866
+ }
867
+
868
+ /* Floating thumbnails */
869
+ .floating-thumbnails {
870
+ display: flex;
871
+ flex-wrap: wrap;
872
+ gap: 8px;
873
+ margin-bottom: 8px;
874
+ padding: 0;
875
+ position: absolute;
876
+ bottom: 100%;
877
+ left: 0;
878
+ right: 0;
879
+ z-index: 101;
880
+ justify-content: flex-start;
881
+ }
882
+
883
+ .floating-thumbnail {
884
+ position: relative;
885
+ width: 60px;
886
+ height: 60px;
887
+ }
888
+
889
+ .floating-thumbnail img {
890
+ width: 100%;
891
+ height: 100%;
892
+ object-fit: cover;
893
+ border-radius: 6px;
894
+ border: 1px solid #e0e0e0;
895
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
896
+ }
897
+
898
+ .floating-thumbnail-remove {
899
+ position: absolute;
900
+ top: 0px;
901
+ right: -16px;
902
+ width: 16px;
903
+ height: 16px;
904
+ background: none !important;
905
+ color: #000;
906
+ border: none;
907
+ cursor: pointer;
908
+ font-size: 12px;
909
+ font-weight: bold;
910
+ display: flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ line-height: 1;
914
+ transition: opacity 0.2s ease;
915
+ opacity: 0.6;
916
+ text-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
917
+ }
918
+
919
+ .floating-thumbnail-remove:hover {
920
+ opacity: 1;
921
+ }
922
+
923
+ /* Drag and drop styles */
924
+ .textarea-container.drag-over textarea {
925
+ background-color: #e3f2fd;
926
+ border-color: #2196f3;
927
+ }
928
+
929
+ /* Image display in messages */
930
+ .user-message img,
931
+ .ai-message img {
932
+ max-width: 100%;
933
+ max-height: 300px;
934
+ border-radius: 8px;
935
+ margin: 8px 0;
936
+ border: 1px solid #e0e0e0;
937
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
938
+ cursor: pointer;
939
+ transition: transform 0.2s ease;
940
+ }
941
+
942
+ .user-message img:hover,
943
+ .ai-message img:hover {
944
+ transform: scale(1.02);
945
+ }
946
+
947
+ /* Mobile responsive adjustments */
948
+ @media (max-width: 768px) {
949
+ .floating-thumbnails {
950
+ gap: 6px;
951
+ }
952
+
953
+ .floating-thumbnail {
954
+ width: 50px;
955
+ height: 50px;
956
+ }
957
+
958
+ .user-message img,
959
+ .ai-message img {
960
+ max-height: 200px;
961
+ }
962
+
963
+ .image-upload-icon {
964
+ right: 10px;
965
+ }
966
+ #message-input {
967
+ padding: 12px 36px 12px 16px;
968
+ }
969
+ }
970
+ </style>
971
+ <style>
972
+ /* Styles for the API key setup message */
973
+ #api-key-setup {
974
+ display: none;
975
+ background-color: #f8f9fa;
976
+ border: 1px solid #ddd;
977
+ border-radius: 8px;
978
+ padding: 20px;
979
+ margin: 20px auto;
980
+ max-width: 800px;
981
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
982
+ }
983
+
984
+ #api-key-setup h2 {
985
+ color: #333;
986
+ margin-top: 0;
987
+ border-bottom: 1px solid #eee;
988
+ padding-bottom: 10px;
989
+ }
990
+
991
+ #api-key-setup code {
992
+ background-color: #f1f1f1;
993
+ padding: 2px 5px;
994
+ border-radius: 3px;
995
+ font-family: monospace;
996
+ }
997
+
998
+ /* API Key Form Styles */
999
+ #api-key-form {
1000
+ background-color: #fff;
1001
+ border: 1px solid #ddd;
1002
+ border-radius: 8px;
1003
+ padding: 20px;
1004
+ margin-top: 20px;
1005
+ }
1006
+
1007
+ #api-key-form h3 {
1008
+ margin-top: 0;
1009
+ color: #44CDF3;
1010
+ }
1011
+
1012
+ #api-key-form .form-group {
1013
+ margin-bottom: 15px;
1014
+ }
1015
+
1016
+ #api-key-form label {
1017
+ display: block;
1018
+ margin-bottom: 5px;
1019
+ font-weight: 500;
1020
+ }
1021
+
1022
+ #api-key-form select,
1023
+ #api-key-form input {
1024
+ width: 100%;
1025
+ padding: 10px;
1026
+ border: 1px solid #ddd;
1027
+ border-radius: 4px;
1028
+ font-size: 14px;
1029
+ }
1030
+
1031
+ #api-key-form .buttons {
1032
+ display: flex;
1033
+ justify-content: space-between;
1034
+ margin-top: 20px;
1035
+ }
1036
+
1037
+ #api-key-form button {
1038
+ padding: 10px 15px;
1039
+ background-color: #44CDF3;
1040
+ color: white;
1041
+ border: none;
1042
+ border-radius: 4px;
1043
+ cursor: pointer;
1044
+ font-weight: bold;
1045
+ }
1046
+
1047
+ #api-key-form button:hover {
1048
+ background-color: #2bb5db;
1049
+ }
1050
+
1051
+ #reset-api-key {
1052
+ background-color: #f44336 !important;
1053
+ }
1054
+
1055
+ #reset-api-key:hover {
1056
+ background-color: #d32f2f !important;
1057
+ }
1058
+
1059
+ .api-key-status {
1060
+ margin-top: 10px;
1061
+ padding: 10px;
1062
+ border-radius: 4px;
1063
+ font-size: 14px;
1064
+ }
1065
+
1066
+ .api-key-status.success {
1067
+ background-color: #e8f5e9;
1068
+ color: #2e7d32;
1069
+ border-left: 4px solid #4caf50;
1070
+ }
1071
+
1072
+ .api-key-status.error {
1073
+ background-color: #ffebee;
1074
+ color: #c62828;
1075
+ border-left: 4px solid #f44336;
1076
+ }
1077
+
1078
+ h1 a:hover {
1079
+ text-decoration: underline;
1080
+ }
1081
+ </style>
1082
+ </head>
1083
+
1084
+ <body>
1085
+ <div id="chat-container">
1086
+ <div class="header">
1087
+ <div class="header-container">
1088
+ <div class="header-left">
1089
+ <a href="/" title="Go to Home Page">
1090
+ <img src="/logo.png" alt="Probe Logo" class="header-logo">
1091
+ </a>
1092
+ <div class="history-dropdown">
1093
+ <button id="history-button" class="history-button" title="Chat History">
1094
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1095
+ <path d="M12 1v6l4-4"></path>
1096
+ <path d="M21 12c0 5-4 9-9 9s-9-4-9-9 4-9 9-9c2.5 0 4.8 1 6.5 2.8"></path>
1097
+ </svg>
1098
+ History
1099
+ </button>
1100
+ <div id="history-dropdown-menu" class="history-dropdown-menu">
1101
+ <div class="history-dropdown-header">Recent Chats</div>
1102
+ <div id="history-loading" class="history-loading">Loading...</div>
1103
+ <div id="history-list" class="history-list"></div>
1104
+ <div id="history-empty" class="history-empty" style="display: none;">No recent chats</div>
1105
+ </div>
1106
+ </div>
1107
+ <a href="#" class="new-chat-link">New chat</a>
1108
+ </div>
1109
+ <div id="api-settings">
1110
+ <a href="#" id="header-reset-api-key"
1111
+ style="display: none; font-size: 12px; color: #f44336; text-decoration: none;">Reset API Key</a>
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+
1116
+ <div id="empty-state-logo" class="centered-logo-container">
1117
+ <h1><img src="/logo.png" alt="Probe Logo"><a href="https://probeai.dev/"
1118
+ style="color: inherit; text-decoration: none;">Probe </a>&nbsp;- AI-Native Code Understanding</h1>
1119
+ </div>
1120
+
1121
+ <!-- API Key Setup Instructions -->
1122
+ <div id="api-key-setup">
1123
+ <h2>API Key Setup Required</h2>
1124
+ <p>To use the Probe AI chat interface, you need to configure at least one API key. You have two options:</p>
1125
+
1126
+ <!-- API Key Web Form -->
1127
+ <div id="api-key-form">
1128
+ <h3>Option 1: Configure API Key in Browser</h3>
1129
+ <p>Enter your API key details below to start using the chat interface immediately:</p>
1130
+
1131
+ <div class="form-group">
1132
+ <label for="api-provider">API Provider:</label>
1133
+ <select id="api-provider">
1134
+ <option value="anthropic">Anthropic Claude</option>
1135
+ <option value="openai">OpenAI</option>
1136
+ <option value="google">Google AI</option>
1137
+ </select>
1138
+ </div>
1139
+
1140
+ <div class="form-group">
1141
+ <label for="api-key">API Key:</label>
1142
+ <input type="password" id="api-key" placeholder="Enter your API key">
1143
+ </div>
1144
+
1145
+ <div class="form-group">
1146
+ <label for="api-url">Custom API URL (Optional):</label>
1147
+ <input type="text" id="api-url" placeholder="Leave blank for default API URL">
1148
+ </div>
1149
+
1150
+ <div class="api-key-status" id="api-key-status" style="display: none;"></div>
1151
+
1152
+ <div class="buttons">
1153
+ <button type="button" id="save-api-key">Save API Key</button>
1154
+ </div>
1155
+
1156
+ <p style="margin-top: 10px; font-size: 0.9em; color: #666;">
1157
+ Your API key will be stored in your browser's local storage and sent with each request.
1158
+ No data is stored on our servers.
1159
+ </p>
1160
+ </div>
1161
+
1162
+ <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;">
1163
+ <h3>Option 2: Using Server-Side Configuration</h3>
1164
+ <p>Alternatively, you can configure API keys on the server:</p>
1165
+
1166
+ <div style="background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 10px;">
1167
+ <p><strong>Using a .env file:</strong></p>
1168
+ <ol style="margin-left: 20px;">
1169
+ <li>Create a <code>.env</code> file in the current directory by copying <code>.env.example</code></li>
1170
+ <li>Add your API key to the <code>.env</code> file (uncomment and replace with your key)</li>
1171
+ <li>Restart the application</li>
1172
+ </ol>
1173
+
1174
+ <p style="margin-top: 15px;"><strong>Using environment variables:</strong></p>
1175
+ <ul style="margin-left: 20px;">
1176
+ <li>Anthropic: <code>ANTHROPIC_API_KEY=your_anthropic_api_key</code></li>
1177
+ <li>OpenAI: <code>OPENAI_API_KEY=your_openai_api_key</code></li>
1178
+ <li>Google AI: <code>GOOGLE_API_KEY=your_google_api_key</code></li>
1179
+ </ul>
1180
+ </div>
1181
+ </div>
1182
+ </div>
1183
+ <div id="messages" class="message-container"></div>
1184
+ </div>
1185
+
1186
+ <form id="input-form" onsubmit="return false;">
1187
+ <div class="input-wrapper">
1188
+ <div class="textarea-container">
1189
+ <!-- Floating image thumbnails - positioned above textarea -->
1190
+ <div id="floating-thumbnails" class="floating-thumbnails" style="display: none;"></div>
1191
+ <textarea id="message-input" placeholder="Ask about code..." required rows="1"></textarea>
1192
+ <input type="file" id="image-upload" accept="image/*" multiple style="display: none;">
1193
+ <button type="button" id="image-upload-button" class="image-upload-icon" title="Upload images">
1194
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1195
+ <path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.64 16.2a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
1196
+ </svg>
1197
+ </button>
1198
+ </div>
1199
+ <button type="button" id="search-button">Search</button>
1200
+ </div>
1201
+ </form>
1202
+ <div class="search-suggestions">
1203
+ <ul>
1204
+ <li>Find functions that handle user authentication</li>
1205
+ <li>Search for database connection implementations</li>
1206
+ <li>Show error handling patterns in the codebase</li>
1207
+ <li>List all API endpoints in the project</li>
1208
+ <li>Find code that processes user input</li>
1209
+ <li>Show how configuration is loaded</li>
1210
+ <li>Find file parsing implementations</li>
1211
+ </ul>
1212
+ <div id="folder-info" class="folder-info"></div>
1213
+ </div>
1214
+ <div id="token-usage" class="token-usage">
1215
+ <div class="token-usage-content">
1216
+ <div class="token-usage-table">
1217
+ <div class="token-usage-row">
1218
+ <div class="token-label">Current:</div>
1219
+ <div class="token-value">
1220
+ <span id="current-request">0</span> req / <span id="current-response">0</span> resp
1221
+ </div>
1222
+ </div>
1223
+ <div class="token-usage-row">
1224
+ <div class="token-label">Cache:</div>
1225
+ <div class="token-value">
1226
+ <span id="current-cache-read">0</span> read / <span id="current-cache-write">0</span> write
1227
+ </div>
1228
+ </div>
1229
+ <div class="token-usage-row">
1230
+ <div class="token-label">Context:</div>
1231
+ <div class="token-value">
1232
+ <span id="context-window">0</span> tokens
1233
+ </div>
1234
+ </div>
1235
+ <div class="token-usage-row">
1236
+ <div class="token-label">Total Cache:</div>
1237
+ <div class="token-value">
1238
+ <span id="total-cache-read">0</span> read / <span id="total-cache-write">0</span> write
1239
+ </div>
1240
+ </div>
1241
+ <div class="token-usage-row">
1242
+ <div class="token-label">Total:</div>
1243
+ <div class="token-value">
1244
+ <span id="total-request">0</span> req / <span id="total-response">0</span> resp
1245
+ </div>
1246
+ </div>
1247
+ </div>
1248
+ </div>
1249
+ </div>
1250
+ <div class="footer">
1251
+ Powered by <a href="https://probeai.dev/" target="_blank">Probe</a>
1252
+ </div>
1253
+ </div>
1254
+ <script>
1255
+ // Token Usage Display Functionality
1256
+ // Function to update token usage display
1257
+ function updateTokenUsageDisplay(tokenUsage) {
1258
+ if (!tokenUsage) return;
1259
+
1260
+ console.log('[TokenUsage] Updating display with:', tokenUsage);
1261
+
1262
+ // Update current token usage
1263
+ if (tokenUsage.current) {
1264
+ document.getElementById('current-request').textContent = tokenUsage.current.request || 0;
1265
+ document.getElementById('current-response').textContent = tokenUsage.current.response || 0;
1266
+
1267
+ // Update cache information in separate row
1268
+ const cacheRead = tokenUsage.current.cacheRead || 0;
1269
+ const cacheWrite = tokenUsage.current.cacheWrite || 0;
1270
+ document.getElementById('current-cache-read').textContent = cacheRead;
1271
+ document.getElementById('current-cache-write').textContent = cacheWrite;
1272
+ }
1273
+
1274
+ // Update context window - force display even if 0
1275
+ const contextWindow = tokenUsage.contextWindow || 0;
1276
+ document.getElementById('context-window').textContent = contextWindow;
1277
+
1278
+ // Update total cache information
1279
+ if (tokenUsage.total && tokenUsage.total.cache) {
1280
+ const totalCacheRead = tokenUsage.total.cache.read || 0;
1281
+ const totalCacheWrite = tokenUsage.total.cache.write || 0;
1282
+ document.getElementById('total-cache-read').textContent = totalCacheRead;
1283
+ document.getElementById('total-cache-write').textContent = totalCacheWrite;
1284
+ } else if (tokenUsage.total) {
1285
+ // Fallback to direct properties if cache object is not available
1286
+ const totalCacheRead = tokenUsage.total.cacheRead || 0;
1287
+ const totalCacheWrite = tokenUsage.total.cacheWrite || 0;
1288
+ document.getElementById('total-cache-read').textContent = totalCacheRead;
1289
+ document.getElementById('total-cache-write').textContent = totalCacheWrite;
1290
+ }
1291
+
1292
+ // Update total token usage
1293
+ if (tokenUsage.total) {
1294
+ document.getElementById('total-request').textContent = tokenUsage.total.request || 0;
1295
+ document.getElementById('total-response').textContent = tokenUsage.total.response || 0;
1296
+ }
1297
+
1298
+ // Show token usage display
1299
+ const tokenUsageElement = document.getElementById('token-usage');
1300
+ if (tokenUsageElement) {
1301
+ tokenUsageElement.style.display = 'block';
1302
+ }
1303
+ }
1304
+
1305
+ // Make token usage functions available globally
1306
+ window.tokenUsageDisplay = {
1307
+ update: updateTokenUsageDisplay,
1308
+ fetch: function (sessionId) {
1309
+ if (!sessionId) {
1310
+ console.log('[TokenUsage] No session ID provided for fetchTokenUsage');
1311
+ // Try to get session ID from window object
1312
+ if (window.sessionId) {
1313
+ console.log(`[TokenUsage] Using session ID from window object: ${window.sessionId}`);
1314
+ sessionId = window.sessionId;
1315
+ } else {
1316
+ console.log('[TokenUsage] No session ID available, cannot fetch token usage');
1317
+ return;
1318
+ }
1319
+ }
1320
+
1321
+ // Check if this session is still the current one
1322
+ if (sessionId !== window.sessionId) {
1323
+ console.log(`[TokenUsage] Session ID mismatch: ${sessionId} vs current ${window.sessionId}, skipping fetch`);
1324
+ return;
1325
+ }
1326
+
1327
+ console.log(`[TokenUsage] Fetching token usage for session: ${sessionId}`);
1328
+ // Use a more reliable fetch with keepalive for Firefox
1329
+ fetch(`/api/token-usage?sessionId=${sessionId}`, {
1330
+ method: 'GET',
1331
+ cache: 'no-store',
1332
+ keepalive: true,
1333
+ headers: {
1334
+ 'Cache-Control': 'no-cache',
1335
+ 'Pragma': 'no-cache'
1336
+ }
1337
+ })
1338
+ .then(response => {
1339
+ console.log(`[TokenUsage] Response status: ${response.status}`);
1340
+
1341
+ if (response.ok) {
1342
+ return response.json();
1343
+ }
1344
+ throw new Error('Failed to fetch token usage');
1345
+ })
1346
+ .then(data => {
1347
+ console.log('[TokenUsage] Received token usage data:', data);
1348
+ updateTokenUsageDisplay(data);
1349
+ })
1350
+ .catch(error => {
1351
+ console.error('[TokenUsage] Error fetching token usage:', error);
1352
+ // Display a small error indicator in the token usage display
1353
+ const tokenUsageElement = document.getElementById('token-usage');
1354
+ if (tokenUsageElement && tokenUsageElement.style.display !== 'none') {
1355
+ const errorIndicator = document.createElement('div');
1356
+ errorIndicator.style.color = '#f44336';
1357
+ errorIndicator.style.fontSize = '10px';
1358
+ errorIndicator.style.marginTop = '5px';
1359
+ errorIndicator.textContent = 'Error updating token usage';
1360
+
1361
+ // Remove any existing error indicators
1362
+ const existingIndicators = tokenUsageElement.querySelectorAll('[data-error-indicator]');
1363
+ existingIndicators.forEach(el => el.remove());
1364
+
1365
+ // Add the data attribute for future reference
1366
+ errorIndicator.setAttribute('data-error-indicator', 'true');
1367
+
1368
+ // Add to the token usage display
1369
+ tokenUsageElement.querySelector('.token-usage-content').appendChild(errorIndicator);
1370
+
1371
+ // Remove after 5 seconds
1372
+ setTimeout(() => {
1373
+ if (errorIndicator.parentNode) {
1374
+ errorIndicator.parentNode.removeChild(errorIndicator);
1375
+ }
1376
+ }, 5000);
1377
+ }
1378
+ });
1379
+ }
1380
+ };
1381
+
1382
+ function convertSvgToPng(svgElement, containerDiv, index) {
1383
+ if (!svgElement) {
1384
+ console.error('No SVG found!');
1385
+ return;
1386
+ }
1387
+
1388
+ try {
1389
+ // Get the actual rendered size of the SVG
1390
+ const rect = svgElement.getBoundingClientRect();
1391
+ const svgWidth = rect.width;
1392
+ const svgHeight = rect.height;
1393
+ console.log(`SVG rendered size: ${svgWidth}x${svgHeight}`);
1394
+
1395
+ // Define scale factor for higher resolution (increase for more clarity)
1396
+ const scale = 6; // Try 3 or 4 if still blurry
1397
+
1398
+ // Create canvas with scaled dimensions
1399
+ const canvasWidth = svgWidth * scale;
1400
+ const canvasHeight = svgHeight * scale;
1401
+ const canvas = document.createElement('canvas');
1402
+ canvas.width = canvasWidth;
1403
+ canvas.height = canvasHeight;
1404
+ const ctx = canvas.getContext('2d');
1405
+
1406
+ // Enable image smoothing for better quality
1407
+ ctx.imageSmoothingEnabled = true;
1408
+ ctx.imageSmoothingQuality = 'high';
1409
+
1410
+ // Serialize SVG to string
1411
+ const svgString = new XMLSerializer().serializeToString(svgElement);
1412
+ const svgDataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgString);
1413
+
1414
+ const img = new Image();
1415
+ img.onload = function () {
1416
+ // Draw the image onto the canvas at scaled size
1417
+ ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
1418
+
1419
+ // Convert canvas to PNG data URL
1420
+ const pngDataUrl = canvas.toDataURL('image/png');
1421
+
1422
+ // Create the PNG image element
1423
+ const pngImage = document.createElement('img');
1424
+ pngImage.src = pngDataUrl;
1425
+ pngImage.width = svgWidth; // Display at original size
1426
+ pngImage.height = svgHeight;
1427
+ pngImage.alt = 'Diagram as PNG';
1428
+ pngImage.className = 'mermaid-png';
1429
+ pngImage.setAttribute('data-full-size', pngDataUrl); // Store full-size image URL for zoom
1430
+
1431
+ // Log PNG natural size to verify resolution
1432
+ pngImage.onload = function () {
1433
+ console.log(`PNG natural size: ${this.naturalWidth}x${this.naturalHeight}`);
1434
+ };
1435
+
1436
+ // Create a container for the image with zoom functionality
1437
+ const container = document.createElement('div');
1438
+ container.className = 'mermaid-container';
1439
+
1440
+ // Create zoom icon
1441
+ const zoomIcon = document.createElement('div');
1442
+ zoomIcon.className = 'zoom-icon';
1443
+ zoomIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>';
1444
+
1445
+ // Add click event to zoom icon
1446
+ zoomIcon.addEventListener('click', function (e) {
1447
+ e.stopPropagation();
1448
+ showDiagramDialog(pngDataUrl);
1449
+ });
1450
+
1451
+ // Replace the SVG with the PNG image in the container
1452
+ svgElement.style.display = 'none';
1453
+ container.appendChild(pngImage);
1454
+ container.appendChild(zoomIcon);
1455
+ svgElement.insertAdjacentElement('afterend', container);
1456
+
1457
+ console.log(`Replaced SVG with PNG image (index: ${index || 0})`);
1458
+ };
1459
+ img.onerror = function () {
1460
+ console.error('Error loading SVG image for conversion');
1461
+ };
1462
+ img.src = svgDataUrl;
1463
+ } catch (error) {
1464
+ console.error('Error in SVG to PNG conversion process:', error);
1465
+ }
1466
+ }
1467
+
1468
+ // Function to show diagram in fullscreen dialog
1469
+ function showDiagramDialog(imageUrl) {
1470
+ // Create dialog if it doesn't exist
1471
+ let dialog = document.getElementById('diagram-dialog');
1472
+ if (!dialog) {
1473
+ dialog = document.createElement('div');
1474
+ dialog.id = 'diagram-dialog';
1475
+ dialog.className = 'diagram-dialog';
1476
+
1477
+ const dialogContent = document.createElement('div');
1478
+ dialogContent.className = 'diagram-dialog-content';
1479
+
1480
+ const closeButton = document.createElement('div');
1481
+ closeButton.className = 'close-dialog';
1482
+ closeButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
1483
+ closeButton.addEventListener('click', function () {
1484
+ dialog.classList.remove('active');
1485
+ });
1486
+
1487
+ dialog.appendChild(dialogContent);
1488
+ dialog.appendChild(closeButton);
1489
+ document.body.appendChild(dialog);
1490
+
1491
+ // Close dialog when clicking outside content
1492
+ dialog.addEventListener('click', function (e) {
1493
+ if (e.target === dialog) {
1494
+ dialog.classList.remove('active');
1495
+ }
1496
+ });
1497
+
1498
+ // Close dialog with Escape key
1499
+ document.addEventListener('keydown', function (e) {
1500
+ if (e.key === 'Escape' && dialog.classList.contains('active')) {
1501
+ dialog.classList.remove('active');
1502
+ }
1503
+ });
1504
+ }
1505
+
1506
+ // Update dialog content with the image
1507
+ const dialogContent = dialog.querySelector('.diagram-dialog-content');
1508
+ dialogContent.innerHTML = '';
1509
+
1510
+ const img = document.createElement('img');
1511
+ img.src = imageUrl;
1512
+ img.alt = 'Diagram (Full Size)';
1513
+
1514
+ // Ensure image fits screen by checking dimensions after loading
1515
+ img.onload = function () {
1516
+ // Image is now loaded, ensure it fits within the dialog content
1517
+ const viewportWidth = window.innerWidth * 0.9 - 40; // 90% of viewport minus padding
1518
+ const viewportHeight = window.innerHeight * 0.9 - 40; // 90% of viewport minus padding
1519
+
1520
+ console.log(`Image natural size: ${img.naturalWidth}x${img.naturalHeight}`);
1521
+ console.log(`Available viewport space: ${viewportWidth}x${viewportHeight}`);
1522
+
1523
+ // Image will be constrained by CSS max-width/max-height and object-fit
1524
+ };
1525
+
1526
+ dialogContent.appendChild(img);
1527
+
1528
+ // Show dialog
1529
+ dialog.classList.add('active');
1530
+ }
1531
+ // Initialize session ID - either from URL or generate new one
1532
+ let sessionId;
1533
+
1534
+ // Check if session ID is provided via URL (from server-side injection)
1535
+ const sessionIdFromUrl = document.body.getAttribute('data-session-id');
1536
+ if (sessionIdFromUrl) {
1537
+ sessionId = sessionIdFromUrl;
1538
+ console.log(`Using session ID from URL: ${sessionId}`);
1539
+ // Restore session history for existing sessions
1540
+ restoreSessionHistory(sessionId);
1541
+ } else {
1542
+ // Generate new session ID for root path visits
1543
+ sessionId = crypto.randomUUID();
1544
+ console.log(`Generated new session ID: ${sessionId}`);
1545
+ }
1546
+
1547
+ // Make session ID available to other scripts
1548
+ // We use both direct property assignment and event dispatch for compatibility
1549
+ // Direct property is used by internal functions like tokenUsageDisplay.fetch
1550
+ window.sessionId = sessionId;
1551
+
1552
+ // Dispatch an event with the session ID for any external scripts that may be listening
1553
+ window.dispatchEvent(new MessageEvent('message', {
1554
+ data: { sessionId: sessionId }
1555
+ }));
1556
+
1557
+ // Helper function to extract content from XML-wrapped messages
1558
+ function extractContentFromXML(content) {
1559
+ // Handle different XML patterns used by the assistant
1560
+ const patterns = [
1561
+ /<task>([\s\S]*?)<\/task>/,
1562
+ /<attempt_completion>\s*<result>([\s\S]*?)<\/result>\s*<\/attempt_completion>/,
1563
+ /<result>([\s\S]*?)<\/result>/
1564
+ ];
1565
+
1566
+ for (const pattern of patterns) {
1567
+ const match = content.match(pattern);
1568
+ if (match) {
1569
+ return match[1].trim();
1570
+ }
1571
+ }
1572
+
1573
+ // Remove all internal reasoning and tool call XML tags
1574
+ let cleanContent = content;
1575
+
1576
+ // Remove thinking tags (internal reasoning) - these are never shown to users
1577
+ cleanContent = cleanContent.replace(/<thinking>[\s\S]*?<\/thinking>/gs, '');
1578
+
1579
+ // Remove all tool calls - these are rendered separately as tool call boxes
1580
+ cleanContent = cleanContent.replace(/<search>\s*<query>.*?<\/query>\s*(?:<path>.*?<\/path>)?\s*(?:<allow_tests>.*?<\/allow_tests>)?\s*<\/search>/gs, '');
1581
+ cleanContent = cleanContent.replace(/<extract>\s*<file_path>.*?<\/file_path>\s*(?:<line>.*?<\/line>)?\s*(?:<end_line>.*?<\/end_line>)?\s*<\/extract>/gs, '');
1582
+ cleanContent = cleanContent.replace(/<query>\s*<pattern>.*?<\/pattern>\s*(?:<path>.*?<\/path>)?\s*(?:<language>.*?<\/language>)?\s*<\/query>/gs, '');
1583
+ cleanContent = cleanContent.replace(/<listFiles>\s*<directory>.*?<\/directory>\s*(?:<pattern>.*?<\/pattern>)?\s*<\/listFiles>/gs, '');
1584
+ cleanContent = cleanContent.replace(/<searchFiles>\s*<pattern>.*?<\/pattern>\s*(?:<directory>.*?<\/directory>)?\s*<\/searchFiles>/gs, '');
1585
+
1586
+ // Clean up extra whitespace and empty lines
1587
+ cleanContent = cleanContent.replace(/\n\s*\n\s*\n/g, '\n\n').replace(/^\s+|\s+$/g, '');
1588
+
1589
+ // If after cleaning there's no meaningful content, return empty string
1590
+ if (!cleanContent || cleanContent.trim().length < 3) {
1591
+ return '';
1592
+ }
1593
+
1594
+ return cleanContent;
1595
+ }
1596
+
1597
+ // Function to add a user message to the chat display
1598
+ function addUserMessage(content, images = []) {
1599
+ const userMsgDiv = document.createElement('div');
1600
+ userMsgDiv.className = 'user-message markdown-content';
1601
+
1602
+ // Extract content from XML if wrapped
1603
+ const cleanContent = extractContentFromXML(content);
1604
+
1605
+ // Render the text message
1606
+ userMsgDiv.innerHTML = renderMarkdown(cleanContent);
1607
+
1608
+ // Handle images if present
1609
+ if (images && images.length > 0) {
1610
+ images.forEach(imageData => {
1611
+ const img = document.createElement('img');
1612
+ img.src = imageData.url || imageData;
1613
+ img.style.maxWidth = '100%';
1614
+ img.style.marginTop = '10px';
1615
+ userMsgDiv.appendChild(img);
1616
+ });
1617
+ }
1618
+
1619
+ messagesDiv.appendChild(userMsgDiv);
1620
+
1621
+ // Apply syntax highlighting to code blocks
1622
+ userMsgDiv.querySelectorAll('pre code').forEach((block) => {
1623
+ hljs.highlightElement(block);
1624
+ });
1625
+ }
1626
+
1627
+ // Function to parse and extract tool calls from assistant message content
1628
+ function parseToolCallsFromContent(content) {
1629
+ const toolCalls = [];
1630
+
1631
+ // Pattern to match actual tool call formats that are sent via SSE
1632
+ // These are the tool calls that users see in real-time, not internal reasoning
1633
+ const toolPatterns = [
1634
+ { name: 'search', pattern: /<search>\s*<query>(.*?)<\/query>\s*(?:<path>(.*?)<\/path>)?\s*(?:<allow_tests>(.*?)<\/allow_tests>)?\s*<\/search>/s },
1635
+ { name: 'extract', pattern: /<extract>\s*<file_path>(.*?)<\/file_path>\s*(?:<line>(.*?)<\/line>)?\s*(?:<end_line>(.*?)<\/end_line>)?\s*<\/extract>/s },
1636
+ { name: 'query', pattern: /<query>\s*<pattern>(.*?)<\/pattern>\s*(?:<path>(.*?)<\/path>)?\s*(?:<language>(.*?)<\/language>)?\s*<\/query>/s },
1637
+ { name: 'listFiles', pattern: /<listFiles>\s*<directory>(.*?)<\/directory>\s*(?:<pattern>(.*?)<\/pattern>)?\s*<\/listFiles>/s },
1638
+ { name: 'searchFiles', pattern: /<searchFiles>\s*<pattern>(.*?)<\/pattern>\s*(?:<directory>(.*?)<\/directory>)?\s*<\/searchFiles>/s },
1639
+ ];
1640
+
1641
+ toolPatterns.forEach(({ name, pattern }) => {
1642
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags + 'g'));
1643
+ for (const match of matches) {
1644
+ const toolCall = {
1645
+ name: name,
1646
+ timestamp: new Date().toISOString(),
1647
+ status: 'completed',
1648
+ args: {}
1649
+ };
1650
+
1651
+ // Map captured groups to appropriate argument names
1652
+ if (name === 'search') {
1653
+ toolCall.args.query = match[1]?.trim() || '';
1654
+ toolCall.args.path = match[2]?.trim() || '.';
1655
+ toolCall.args.allow_tests = match[3]?.trim() === 'true';
1656
+ } else if (name === 'extract') {
1657
+ toolCall.args.file_path = match[1]?.trim() || '';
1658
+ toolCall.args.line = match[2] ? parseInt(match[2].trim()) : undefined;
1659
+ toolCall.args.end_line = match[3] ? parseInt(match[3].trim()) : undefined;
1660
+ } else if (name === 'query') {
1661
+ toolCall.args.pattern = match[1]?.trim() || '';
1662
+ toolCall.args.path = match[2]?.trim() || '.';
1663
+ toolCall.args.language = match[3]?.trim() || '';
1664
+ } else if (name === 'listFiles') {
1665
+ toolCall.args.directory = match[1]?.trim() || '.';
1666
+ toolCall.args.pattern = match[2]?.trim() || '';
1667
+ } else if (name === 'searchFiles') {
1668
+ toolCall.args.pattern = match[1]?.trim() || '';
1669
+ toolCall.args.directory = match[2]?.trim() || '.';
1670
+ }
1671
+
1672
+ toolCalls.push(toolCall);
1673
+ }
1674
+ });
1675
+
1676
+ return toolCalls;
1677
+ }
1678
+
1679
+ // Function to add an assistant message to the chat display
1680
+ function addAssistantMessage(content) {
1681
+ const aiMsgDiv = document.createElement('div');
1682
+ aiMsgDiv.className = 'ai-message markdown-content';
1683
+
1684
+ // Parse tool calls from the content first
1685
+ const toolCalls = parseToolCallsFromContent(content);
1686
+
1687
+ // Add tool calls to the message if any exist
1688
+ toolCalls.forEach(toolCall => {
1689
+ addToolCallToMessage(aiMsgDiv, toolCall);
1690
+ });
1691
+
1692
+ // Extract final result content (skip tool call XML tags)
1693
+ const cleanContent = extractContentFromXML(content);
1694
+
1695
+ // Only add text content if there's actual result content
1696
+ if (cleanContent && cleanContent.trim()) {
1697
+ // Store the original message for copying
1698
+ aiMsgDiv.setAttribute('data-original-markdown', cleanContent);
1699
+
1700
+ // Process and render the content
1701
+ const processedContent = processMessageForDisplay(cleanContent);
1702
+ const contentDiv = document.createElement('div');
1703
+ contentDiv.innerHTML = renderMarkdown(processedContent);
1704
+ aiMsgDiv.appendChild(contentDiv);
1705
+
1706
+ // Apply syntax highlighting to code blocks
1707
+ contentDiv.querySelectorAll('pre code').forEach((block) => {
1708
+ hljs.highlightElement(block);
1709
+ });
1710
+
1711
+ // Apply Mermaid rendering to diagrams
1712
+ const mermaidElements = contentDiv.querySelectorAll('.mermaid, .language-mermaid');
1713
+ if (mermaidElements.length > 0) {
1714
+ console.log(`Found ${mermaidElements.length} mermaid diagrams in restored message`);
1715
+ try {
1716
+ if (typeof mermaid.run === 'function') {
1717
+ mermaid.run({ nodes: mermaidElements });
1718
+ } else if (typeof mermaid.init === 'function') {
1719
+ mermaid.init(undefined, mermaidElements);
1720
+ }
1721
+ } catch (error) {
1722
+ console.error('Error rendering mermaid in restored message:', error);
1723
+ }
1724
+ }
1725
+ }
1726
+
1727
+ // Add to messages container
1728
+ messagesDiv.appendChild(aiMsgDiv);
1729
+ }
1730
+
1731
+ // Function to restore session history from server
1732
+ async function restoreSessionHistory(sessionId) {
1733
+ try {
1734
+ console.log(`Restoring session history for: ${sessionId}`);
1735
+ const response = await fetch(`/api/session/${sessionId}/history`);
1736
+ const data = await response.json();
1737
+
1738
+ if (data.exists && data.history && data.history.length > 0) {
1739
+ console.log(`Restored ${data.history.length} messages for session: ${sessionId}`);
1740
+
1741
+ // Render restored messages
1742
+ data.history.forEach(message => {
1743
+ if (message.role === 'user') {
1744
+ addUserMessage(message.content, message.images || []);
1745
+ } else if (message.role === 'assistant') {
1746
+ addAssistantMessage(message.content);
1747
+ }
1748
+ });
1749
+
1750
+ // Update token usage if available
1751
+ if (data.tokenUsage && window.tokenUsageDisplay) {
1752
+ window.tokenUsageDisplay.update(data.tokenUsage);
1753
+ }
1754
+
1755
+ // Update UI to reflect restored chat state
1756
+ positionInputForm();
1757
+
1758
+ // Hide search suggestions when loading from history
1759
+ const searchSuggestions = document.querySelector('.search-suggestions');
1760
+ if (searchSuggestions) {
1761
+ searchSuggestions.style.display = 'none';
1762
+ }
1763
+
1764
+ // Ensure all Mermaid diagrams are rendered after restoration
1765
+ setTimeout(() => {
1766
+ const allMermaidElements = document.querySelectorAll('.mermaid:not([data-processed]), .language-mermaid:not([data-processed])');
1767
+ if (allMermaidElements.length > 0) {
1768
+ console.log(`Rendering ${allMermaidElements.length} unprocessed mermaid diagrams after session restoration`);
1769
+ try {
1770
+ if (typeof mermaid.run === 'function') {
1771
+ mermaid.run({ nodes: allMermaidElements });
1772
+ } else if (typeof mermaid.init === 'function') {
1773
+ mermaid.init(undefined, allMermaidElements);
1774
+ }
1775
+ } catch (error) {
1776
+ console.error('Error rendering mermaid after session restoration:', error);
1777
+ }
1778
+ }
1779
+ }, 100); // Small delay to ensure DOM is fully updated
1780
+ } else {
1781
+ console.log(`No history found for session: ${sessionId}`);
1782
+ // Show user-friendly message for session not found
1783
+ showSessionNotFoundMessage(sessionId);
1784
+ }
1785
+ } catch (error) {
1786
+ console.error('Error restoring session history:', error);
1787
+ // Show error message for network or other errors
1788
+ showSessionNotFoundMessage(sessionId);
1789
+ }
1790
+ }
1791
+
1792
+ // Function to show session not found message
1793
+ function showSessionNotFoundMessage(sessionId) {
1794
+ const messageDiv = document.createElement('div');
1795
+ messageDiv.className = 'ai-message markdown-content';
1796
+ messageDiv.style.borderLeft = '4px solid #ff6b6b';
1797
+ messageDiv.style.backgroundColor = '#fff5f5';
1798
+ messageDiv.innerHTML = `
1799
+ <div style="padding: 15px;">
1800
+ <h3 style="margin-top: 0; color: #c92a2a;">Session Not Found</h3>
1801
+ <p>The chat session with ID <code>${sessionId}</code> was not found or has expired.</p>
1802
+ <p>This could happen if:</p>
1803
+ <ul>
1804
+ <li>The session has been inactive for more than 2 hours</li>
1805
+ <li>The server was restarted</li>
1806
+ <li>The session ID is invalid</li>
1807
+ </ul>
1808
+ <p>You can start a new conversation by <a href="/" style="color: #1976d2;">returning to the home page</a>.</p>
1809
+ </div>
1810
+ `;
1811
+ messagesDiv.appendChild(messageDiv);
1812
+
1813
+ // Update UI to show the message
1814
+ positionInputForm();
1815
+ }
1816
+
1817
+ // Function to update URL when session changes (for new chats)
1818
+ function updateUrlForSession(sessionId) {
1819
+ const newUrl = `/chat/${sessionId}`;
1820
+ if (window.location.pathname !== newUrl) {
1821
+ window.history.pushState({ sessionId }, '', newUrl);
1822
+ console.log(`Updated URL to: ${newUrl}`);
1823
+ }
1824
+ }
1825
+
1826
+ // Handle browser back/forward navigation
1827
+ window.addEventListener('popstate', (event) => {
1828
+ if (event.state && event.state.sessionId) {
1829
+ // User navigated to a different session
1830
+ console.log(`Navigating to session: ${event.state.sessionId}`);
1831
+ window.location.reload(); // Reload to restore the session
1832
+ }
1833
+ });
1834
+
1835
+ // History dropdown functionality
1836
+ class HistoryDropdown {
1837
+ constructor() {
1838
+ this.button = document.getElementById('history-button');
1839
+ this.menu = document.getElementById('history-dropdown-menu');
1840
+ this.loading = document.getElementById('history-loading');
1841
+ this.list = document.getElementById('history-list');
1842
+ this.empty = document.getElementById('history-empty');
1843
+ this.isOpen = false;
1844
+
1845
+ this.init();
1846
+ }
1847
+
1848
+ init() {
1849
+ // Button click handler
1850
+ this.button.addEventListener('click', (e) => {
1851
+ e.preventDefault();
1852
+ e.stopPropagation();
1853
+ this.toggle();
1854
+ });
1855
+
1856
+ // Close dropdown when clicking outside
1857
+ document.addEventListener('click', (e) => {
1858
+ if (!this.menu.contains(e.target) && !this.button.contains(e.target)) {
1859
+ this.close();
1860
+ }
1861
+ });
1862
+
1863
+ // Prevent dropdown from closing when clicking inside
1864
+ this.menu.addEventListener('click', (e) => {
1865
+ e.stopPropagation();
1866
+ });
1867
+ }
1868
+
1869
+ async toggle() {
1870
+ if (this.isOpen) {
1871
+ this.close();
1872
+ } else {
1873
+ await this.open();
1874
+ }
1875
+ }
1876
+
1877
+ async open() {
1878
+ this.isOpen = true;
1879
+ this.menu.classList.add('show');
1880
+ this.showLoading();
1881
+
1882
+ try {
1883
+ await this.loadSessions();
1884
+ } catch (error) {
1885
+ console.error('Error loading sessions:', error);
1886
+ this.showError();
1887
+ }
1888
+ }
1889
+
1890
+ close() {
1891
+ this.isOpen = false;
1892
+ this.menu.classList.remove('show');
1893
+ }
1894
+
1895
+ showLoading() {
1896
+ this.loading.style.display = 'block';
1897
+ this.list.style.display = 'none';
1898
+ this.empty.style.display = 'none';
1899
+ }
1900
+
1901
+ showList() {
1902
+ this.loading.style.display = 'none';
1903
+ this.list.style.display = 'block';
1904
+ this.empty.style.display = 'none';
1905
+ }
1906
+
1907
+ showEmpty() {
1908
+ this.loading.style.display = 'none';
1909
+ this.list.style.display = 'none';
1910
+ this.empty.style.display = 'block';
1911
+ }
1912
+
1913
+ showError() {
1914
+ this.loading.textContent = 'Error loading history';
1915
+ this.loading.style.color = '#f44336';
1916
+ }
1917
+
1918
+ async loadSessions() {
1919
+ try {
1920
+ const response = await fetch('/api/sessions');
1921
+ const data = await response.json();
1922
+
1923
+ if (data.sessions && data.sessions.length > 0) {
1924
+ this.renderSessions(data.sessions);
1925
+ this.showList();
1926
+ } else {
1927
+ this.showEmpty();
1928
+ }
1929
+ } catch (error) {
1930
+ console.error('Error fetching sessions:', error);
1931
+ throw error;
1932
+ }
1933
+ }
1934
+
1935
+ renderSessions(sessions) {
1936
+ this.list.innerHTML = '';
1937
+
1938
+ sessions.forEach(session => {
1939
+ const item = document.createElement('div');
1940
+ item.className = 'history-item';
1941
+ item.dataset.sessionId = session.sessionId;
1942
+
1943
+ // Check if this is the current session
1944
+ const isCurrent = session.sessionId === window.sessionId;
1945
+ if (isCurrent) {
1946
+ item.style.backgroundColor = '#e3f2fd';
1947
+ }
1948
+
1949
+ item.innerHTML = `
1950
+ <div class="history-item-preview">${this.escapeHtml(session.preview)}</div>
1951
+ <div class="history-item-meta">
1952
+ <span class="history-item-time">${session.relativeTime}</span>
1953
+ <span class="history-item-count">${session.messageCount} messages</span>
1954
+ </div>
1955
+ `;
1956
+
1957
+ item.addEventListener('click', () => {
1958
+ if (!isCurrent) {
1959
+ this.navigateToSession(session.sessionId);
1960
+ }
1961
+ this.close();
1962
+ });
1963
+
1964
+ this.list.appendChild(item);
1965
+ });
1966
+ }
1967
+
1968
+ navigateToSession(sessionId) {
1969
+ const url = `/chat/${sessionId}`;
1970
+ console.log(`Navigating to session: ${sessionId}`);
1971
+ window.location.href = url;
1972
+ }
1973
+
1974
+ escapeHtml(text) {
1975
+ const div = document.createElement('div');
1976
+ div.textContent = text;
1977
+ return div.innerHTML;
1978
+ }
1979
+ }
1980
+
1981
+ // Initialize history dropdown
1982
+ document.addEventListener('DOMContentLoaded', () => {
1983
+ new HistoryDropdown();
1984
+ });
1985
+ const messagesDiv = document.getElementById('messages');
1986
+ const form = document.getElementById('input-form');
1987
+ const searchSuggestionsDiv = document.querySelector('.search-suggestions');
1988
+ const input = document.getElementById('message-input');
1989
+ const folderListDiv = document.getElementById('folder-list');
1990
+
1991
+ // Position the input form in the center initially and handle UI elements visibility
1992
+ function positionInputForm() {
1993
+ const footer = document.querySelector('.footer');
1994
+ const header = document.querySelector('.header');
1995
+ const emptyStateLogo = document.getElementById('empty-state-logo');
1996
+
1997
+ if (messagesDiv.children.length === 0) {
1998
+ form.classList.add('centered');
1999
+ form.classList.remove('bottom');
2000
+ // Show footer when no messages
2001
+ if (footer) {
2002
+ footer.style.display = 'block';
2003
+ }
2004
+ // Always show the header (it contains the history dropdown)
2005
+ if (header) {
2006
+ header.style.display = 'block';
2007
+ }
2008
+ if (emptyStateLogo) {
2009
+ emptyStateLogo.style.display = 'block';
2010
+ }
2011
+ } else {
2012
+ form.classList.remove('centered');
2013
+ form.classList.add('bottom');
2014
+ // Hide footer when chat is started
2015
+ if (footer) {
2016
+ footer.style.display = 'none';
2017
+ }
2018
+ // Show the top header and hide the centered logo
2019
+ if (header) {
2020
+ header.style.display = 'block';
2021
+ }
2022
+ if (emptyStateLogo) {
2023
+ emptyStateLogo.style.display = 'none';
2024
+ }
2025
+ }
2026
+ }
2027
+
2028
+ // Make search suggestions clickable
2029
+ function setupSearchSuggestions() {
2030
+ document.querySelectorAll('.search-suggestions li').forEach(item => {
2031
+ item.addEventListener('click', () => {
2032
+ input.value = item.textContent;
2033
+ input.focus();
2034
+ });
2035
+ });
2036
+ }
2037
+
2038
+ // Initialize on page load
2039
+ window.addEventListener('load', () => {
2040
+ setupSearchSuggestions();
2041
+ positionInputForm();
2042
+ positionSearchSuggestions();
2043
+
2044
+ // Focus the input field on page load
2045
+ setTimeout(() => {
2046
+ const inputField = document.getElementById('message-input');
2047
+ if (inputField) {
2048
+ inputField.focus();
2049
+ }
2050
+ }, 100);
2051
+ });
2052
+
2053
+
2054
+ // Position search suggestions relative to input form
2055
+ function positionSearchSuggestions() {
2056
+ const formRect = form.getBoundingClientRect();
2057
+
2058
+ if (form.classList.contains('centered')) {
2059
+ // Position directly below the form
2060
+ searchSuggestionsDiv.style.top = formRect.bottom + 'px';
2061
+ searchSuggestionsDiv.style.display = 'block';
2062
+ } else {
2063
+ searchSuggestionsDiv.style.display = 'none';
2064
+ }
2065
+ }
2066
+
2067
+ // Update search suggestions position when window is resized
2068
+ window.addEventListener('resize', positionSearchSuggestions);
2069
+
2070
+ // Check if Mermaid is properly loaded
2071
+ function checkMermaidLoaded() {
2072
+ if (typeof mermaid === 'undefined') {
2073
+ console.error('Mermaid is not loaded properly');
2074
+ return false;
2075
+ }
2076
+ console.log('Mermaid version:', mermaid.version);
2077
+ return true;
2078
+ }
2079
+
2080
+ // Initialize mermaid
2081
+ if (checkMermaidLoaded()) {
2082
+ mermaid.initialize({
2083
+ startOnLoad: false,
2084
+ theme: 'default',
2085
+ securityLevel: 'loose',
2086
+ flowchart: { htmlLabels: true },
2087
+ logLevel: 3, // Add logging for debugging (1: error, 2: warn, 3: info, 4: debug, 5: trace)
2088
+ fontFamily: 'monospace'
2089
+ });
2090
+
2091
+ // Run mermaid on page load to render the test diagram
2092
+ window.addEventListener('DOMContentLoaded', () => {
2093
+ setTimeout(() => {
2094
+ try {
2095
+ console.log('Running mermaid on page load');
2096
+ mermaid.run();
2097
+ } catch (error) {
2098
+ console.error('Error initializing mermaid:', error);
2099
+ }
2100
+ }, 500);
2101
+ });
2102
+ }
2103
+
2104
+ // Configure marked.js
2105
+ // Configure Marked.js with logging
2106
+ marked.setOptions({
2107
+ highlight: function (code, lang) {
2108
+ console.log(`Highlighting code with language: ${lang}`);
2109
+ if (lang === 'mermaid') {
2110
+ console.log('Returning mermaid div');
2111
+ return `<div class="mermaid">${code}</div>`;
2112
+ }
2113
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
2114
+ return hljs.highlight(code, { language }).value;
2115
+ },
2116
+ langPrefix: 'hljs language-',
2117
+ gfm: true,
2118
+ breaks: true
2119
+ });
2120
+ // Fetch API key status and check for no API keys mode on page load
2121
+ window.addEventListener('DOMContentLoaded', async () => {
2122
+ // First check if we have an API key in local storage
2123
+ const storedApiKey = localStorage.getItem('probeApiKey');
2124
+ if (storedApiKey) {
2125
+ // Show the reset button in the header
2126
+ const headerResetButton = document.getElementById('header-reset-api-key');
2127
+ if (headerResetButton) {
2128
+ headerResetButton.style.display = 'inline-block';
2129
+ }
2130
+ }
2131
+ // Check if we're in API key setup mode
2132
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
2133
+ const inputForm = document.getElementById('input-form');
2134
+ const searchSuggestions = document.querySelector('.search-suggestions');
2135
+
2136
+ // If API key setup is visible, we're in API setup mode
2137
+ if (apiKeySetupDiv && window.getComputedStyle(apiKeySetupDiv).display !== 'none') {
2138
+ // Add class to body for API setup mode styling
2139
+ document.body.classList.add('api-setup-mode');
2140
+
2141
+ // Hide search suggestions and input form
2142
+ if (inputForm) inputForm.style.display = 'none';
2143
+ if (searchSuggestions) searchSuggestions.style.display = 'none';
2144
+ } else {
2145
+ // Remove API setup mode class if not in setup mode
2146
+ document.body.classList.remove('api-setup-mode');
2147
+ }
2148
+
2149
+ try {
2150
+ const response = await fetch('/folders');
2151
+ const data = await response.json();
2152
+
2153
+ // Check if we're in no API keys mode
2154
+ if (data.noApiKeysMode) {
2155
+ handleNoApiKeysMode();
2156
+ }
2157
+
2158
+ // Display folder information
2159
+ displayFolderInfo(data.folders);
2160
+ } catch (error) {
2161
+ console.error('Error fetching API status:', error);
2162
+ }
2163
+ });
2164
+
2165
+ // Function to display folder information
2166
+ function displayFolderInfo(folders) {
2167
+ const folderInfoDiv = document.getElementById('folder-info');
2168
+
2169
+ if (!folderInfoDiv) return;
2170
+
2171
+ // Clear any existing content
2172
+ folderInfoDiv.innerHTML = '';
2173
+
2174
+ // Set a loading message
2175
+ folderInfoDiv.textContent = 'Determining search location...';
2176
+
2177
+ // Fetch the current directory from the server's /folders endpoint
2178
+ fetch('/folders')
2179
+ .then(response => response.json())
2180
+ .then(data => {
2181
+ // Use the currentDir property which contains the absolute path
2182
+ if (data.currentDir) {
2183
+ // Display the absolute path from the server
2184
+ folderInfoDiv.textContent = `Searching in: ${data.currentDir}`;
2185
+
2186
+ // If there are multiple folders, show that info
2187
+ if (data.folders && data.folders.length > 1) {
2188
+ folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`;
2189
+ }
2190
+ }
2191
+ // Fallback to folders if currentDir is not available
2192
+ else if (data.folders && data.folders.length > 0) {
2193
+ folderInfoDiv.textContent = `Searching in: ${data.folders[0]}`;
2194
+
2195
+ if (data.folders.length > 1) {
2196
+ folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`;
2197
+ }
2198
+ }
2199
+ // Last resort fallback
2200
+ else {
2201
+ folderInfoDiv.textContent = `Searching in: . (current directory)`;
2202
+ }
2203
+ })
2204
+ .catch(error => {
2205
+ console.error('Error fetching folder info:', error);
2206
+ folderInfoDiv.textContent = `Searching in: . (current directory)`;
2207
+ });
2208
+ }
2209
+
2210
+ // Handle no API keys mode
2211
+ function handleNoApiKeysMode() {
2212
+ // Check if body has the data-no-api-keys attribute
2213
+ const noApiKeys = document.body.getAttribute('data-no-api-keys') === 'true';
2214
+
2215
+ // Check if API key is already stored in local storage
2216
+ const storedApiKey = localStorage.getItem('probeApiKey');
2217
+
2218
+ // Add or remove api-setup-mode class based on whether we need to show the API key setup
2219
+ if (noApiKeys && !storedApiKey) {
2220
+ document.body.classList.add('api-setup-mode');
2221
+ } else {
2222
+ document.body.classList.remove('api-setup-mode');
2223
+ }
2224
+
2225
+ // Get UI elements
2226
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
2227
+ const inputForm = document.getElementById('input-form');
2228
+ const searchSuggestions = document.querySelector('.search-suggestions');
2229
+
2230
+ if (noApiKeys && !storedApiKey) {
2231
+ console.log('No API keys detected and no local storage key - showing setup instructions');
2232
+
2233
+ // Show the API key setup div
2234
+ if (apiKeySetupDiv) {
2235
+ apiKeySetupDiv.style.display = 'block';
2236
+ }
2237
+
2238
+ // Hide the chat interface elements
2239
+ if (inputForm) {
2240
+ inputForm.style.display = 'none';
2241
+ }
2242
+
2243
+ if (searchSuggestions) {
2244
+ searchSuggestions.style.display = 'none';
2245
+ }
2246
+ } else if (noApiKeys && storedApiKey) {
2247
+ console.log('No server API keys but local storage key found - enabling chat interface');
2248
+
2249
+ // Hide the API key setup div
2250
+ if (apiKeySetupDiv) {
2251
+ apiKeySetupDiv.style.display = 'none';
2252
+ }
2253
+
2254
+ // Show the chat interface elements
2255
+ if (inputForm) {
2256
+ inputForm.style.display = 'flex';
2257
+ }
2258
+
2259
+ // Remove API setup mode class
2260
+ document.body.classList.remove('api-setup-mode');
2261
+ }
2262
+ }
2263
+
2264
+
2265
+ // Render markdown content
2266
+ function renderMarkdown(text) {
2267
+ // Just parse the markdown and return the HTML
2268
+ return marked.parse(text);
2269
+ }
2270
+
2271
+ // Test function to manually render a Mermaid diagram
2272
+ function testMermaidRendering() {
2273
+ console.log('Testing Mermaid rendering...');
2274
+ try {
2275
+ // Create a simple test diagram directly
2276
+ const testDiv = document.createElement('div');
2277
+ testDiv.className = 'mermaid';
2278
+ testDiv.textContent = 'graph TD;\nA-->B;';
2279
+ document.body.appendChild(testDiv);
2280
+
2281
+ console.log('Created test diagram with content:', testDiv.textContent);
2282
+
2283
+ // Render the direct mermaid div
2284
+ setTimeout(() => {
2285
+ try {
2286
+ console.log('Running mermaid on test div');
2287
+ if (typeof mermaid.run === 'function') {
2288
+ console.log('Using mermaid.run() for test');
2289
+ mermaid.run({
2290
+ nodes: [testDiv]
2291
+ });
2292
+ } else if (typeof mermaid.init === 'function') {
2293
+ console.log('Using mermaid.init() for test');
2294
+ mermaid.init(undefined, [testDiv]);
2295
+ }
2296
+
2297
+ // Verify if rendering worked
2298
+ setTimeout(() => {
2299
+ const svg = testDiv.querySelector('svg');
2300
+ if (svg) {
2301
+ console.log('Test diagram rendered successfully!');
2302
+ } else {
2303
+ console.error('Test diagram did not render to SVG');
2304
+ }
2305
+
2306
+ // Remove test div after verification
2307
+ document.body.removeChild(testDiv);
2308
+ }, 100);
2309
+ } catch (error) {
2310
+ console.error('Error rendering test mermaid diagram:', error);
2311
+ console.error('Error details:', error.message);
2312
+
2313
+ // Remove test div on error
2314
+ document.body.removeChild(testDiv);
2315
+ }
2316
+ }, 200);
2317
+ } catch (error) {
2318
+ console.error('Unexpected error in test function:', error);
2319
+ }
2320
+ }
2321
+
2322
+ // Run test on page load
2323
+ window.addEventListener('DOMContentLoaded', () => {
2324
+ setTimeout(testMermaidRendering, 1000);
2325
+ });
2326
+
2327
+ // Connect to SSE endpoint for tool calls
2328
+ let eventSource;
2329
+ let currentAiMessageDiv = null;
2330
+
2331
+ function connectToToolEvents() {
2332
+ // Close existing connection if any
2333
+ if (eventSource) {
2334
+ console.log('Closing existing SSE connection');
2335
+ eventSource.close();
2336
+ }
2337
+
2338
+ // Clear any existing displayed tool calls when connecting with a new session ID
2339
+ if (window.displayedToolCalls) {
2340
+ window.displayedToolCalls.clear();
2341
+ console.log('Cleared displayed tool calls for new session');
2342
+ }
2343
+
2344
+ console.log(`%c Connecting to SSE endpoint with session ID: ${sessionId}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2345
+ // Connect to SSE endpoint with session ID
2346
+ const sseUrl = `/api/tool-events?sessionId=${sessionId}`;
2347
+ console.log('SSE URL:', sseUrl);
2348
+
2349
+ // Add a timestamp to prevent caching in Firefox
2350
+ const nocacheUrl = `${sseUrl}&_nocache=${Date.now()}`;
2351
+ eventSource = new EventSource(nocacheUrl);
2352
+
2353
+ // Handle connection event
2354
+ eventSource.addEventListener('connection', (event) => {
2355
+ console.log('%c Connected to tool events stream', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event.data);
2356
+ try {
2357
+ const connectionData = JSON.parse(event.data);
2358
+ console.log('Connection data:', connectionData);
2359
+ } catch (error) {
2360
+ console.error('Error parsing connection data:', error, event.data);
2361
+ }
2362
+ });
2363
+
2364
+ // Handle test events
2365
+ eventSource.addEventListener('test', (event) => {
2366
+ console.log('%c Received test event:', 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;', event.data);
2367
+ try {
2368
+ const testData = JSON.parse(event.data);
2369
+ console.log('%c Test data:', 'background: #673AB7; color: white; padding: 2px 5px; border-radius: 2px;', testData);
2370
+
2371
+ // Log specific test data properties
2372
+ console.log('Test message:', testData.message);
2373
+ console.log('Test timestamp:', testData.timestamp);
2374
+ console.log('Test session ID:', testData.sessionId);
2375
+
2376
+ if (testData.status) {
2377
+ console.log('Test status:', testData.status);
2378
+ }
2379
+
2380
+ if (testData.connectionInfo) {
2381
+ console.log('Connection info:', testData.connectionInfo);
2382
+ }
2383
+
2384
+ if (testData.sequence === 2) {
2385
+ console.log('%c SSE connection fully verified with follow-up test', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2386
+ }
2387
+
2388
+ // Add a visual indicator that the SSE connection is working
2389
+ const connectionIndicator = document.createElement('div');
2390
+ connectionIndicator.style.position = 'fixed';
2391
+ connectionIndicator.style.bottom = '10px';
2392
+ connectionIndicator.style.right = '10px';
2393
+ connectionIndicator.style.backgroundColor = '#4CAF50';
2394
+ connectionIndicator.style.color = 'white';
2395
+ connectionIndicator.style.padding = '5px 10px';
2396
+ connectionIndicator.style.borderRadius = '4px';
2397
+ connectionIndicator.style.fontSize = '12px';
2398
+ connectionIndicator.style.zIndex = '1000';
2399
+ connectionIndicator.style.opacity = '0.8';
2400
+ connectionIndicator.textContent = 'SSE Connected';
2401
+
2402
+ // Remove after 3 seconds
2403
+ setTimeout(() => {
2404
+ if (document.body.contains(connectionIndicator)) {
2405
+ document.body.removeChild(connectionIndicator);
2406
+ }
2407
+ }, 3000);
2408
+
2409
+ document.body.appendChild(connectionIndicator);
2410
+
2411
+ } catch (error) {
2412
+ console.error('Error parsing test event data:', error, event.data);
2413
+ }
2414
+ });
2415
+
2416
+ // Initialize a Set to track displayed tool calls
2417
+ if (!window.displayedToolCalls) {
2418
+ window.displayedToolCalls = new Set();
2419
+ }
2420
+
2421
+ // Handle tool call events
2422
+ eventSource.addEventListener('toolCall', (event) => {
2423
+ // If no request is in progress, ignore the tool call
2424
+ if (!isRequestInProgress) {
2425
+ console.log('Tool call received but no request in progress, ignoring');
2426
+ return;
2427
+ }
2428
+ console.log('%c Received tool call event:', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event);
2429
+ try {
2430
+ const toolCall = JSON.parse(event.data);
2431
+ console.log('%c Tool call data:', 'background: #2196F3; color: white; padding: 2px 5px; border-radius: 2px;', toolCall);
2432
+
2433
+ // Skip events with status "started" - only process "completed" events
2434
+ if (toolCall.status === "started") {
2435
+ console.log('%c Skipping "started" event, waiting for "completed"', 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2436
+ return;
2437
+ }
2438
+
2439
+ // Create a unique identifier for this tool call
2440
+ const query = toolCall.args.query || toolCall.args.keywords || toolCall.args.pattern || '';
2441
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2442
+
2443
+ // Create a simpler fingerprint that doesn't include timestamp
2444
+ // This helps catch duplicate events with different timestamps
2445
+ const toolCallFingerprint = `${toolCall.name}-${query}-${path}`;
2446
+
2447
+ // Check if we've already displayed this exact tool call
2448
+ if (window.displayedToolCalls.has(toolCallFingerprint)) {
2449
+ console.log(`%c Skipping duplicate tool call: ${toolCallFingerprint}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2450
+ return;
2451
+ }
2452
+
2453
+ // Add this tool call to our set of displayed tool calls
2454
+ window.displayedToolCalls.add(toolCallFingerprint);
2455
+ console.log(`%c Added tool call to displayed set: ${toolCallFingerprint}`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;');
2456
+
2457
+ // Format the tool call description for display
2458
+ let toolDescription = '';
2459
+ if (toolCall.name === 'searchCode' || toolCall.name === 'search') {
2460
+ const language = toolCall.args.language;
2461
+ const exact = toolCall.args.exact;
2462
+
2463
+ let locationInfo = path !== '.' ? ` in ${path}` : '';
2464
+ let languageInfo = language ? ` (language: ${language})` : '';
2465
+ let exactInfo = exact === true ? ` (exact match)` : '';
2466
+
2467
+ toolDescription = `Searching code with "${query}"${locationInfo}${languageInfo}${exactInfo}`;
2468
+ } else if (toolCall.name === 'queryCode' || toolCall.name === 'query') {
2469
+ toolDescription = `Querying code with pattern "${query}"${path === '.' ? '' : ` in ${path}`}`;
2470
+ } else if (toolCall.name === 'extractCode' || toolCall.name === 'extract') {
2471
+ const filePath = toolCall.args.file_path || '';
2472
+ const line = toolCall.args.line;
2473
+ const endLine = toolCall.args.end_line;
2474
+
2475
+ let lineInfo = '';
2476
+ if (line && endLine) {
2477
+ lineInfo = ` (lines ${line}-${endLine})`;
2478
+ } else if (line) {
2479
+ lineInfo = ` (from line ${line})`;
2480
+ }
2481
+
2482
+ toolDescription = `Extracting code from ${filePath}${lineInfo}`;
2483
+ } else {
2484
+ toolDescription = `Using ${toolCall.name} tool`;
2485
+ }
2486
+
2487
+ // Log the tool call being processed
2488
+ console.log(`%c Processing tool call: "${toolDescription}"`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;');
2489
+
2490
+ // Add tool call to the current AI message if it exists
2491
+ if (currentAiMessageDiv) {
2492
+ addToolCallToMessage(currentAiMessageDiv, toolCall);
2493
+ } else {
2494
+ console.warn('No current AI message div to add tool call to');
2495
+ // Create a temporary div to display the tool call
2496
+ const tempDiv = document.createElement('div');
2497
+ tempDiv.className = 'ai-message';
2498
+ tempDiv.innerHTML = '<div class="tool-call-header">Tool call received but no message context</div>';
2499
+ messagesDiv.appendChild(tempDiv);
2500
+ addToolCallToMessage(tempDiv, toolCall);
2501
+ }
2502
+ } catch (error) {
2503
+ console.error('Error parsing tool call data:', error, event.data);
2504
+ }
2505
+ });
2506
+
2507
+ // Handle errors
2508
+ eventSource.onerror = (error) => {
2509
+ console.error('%c SSE Error:', 'background: #F44336; color: white; padding: 2px 5px; border-radius: 2px;', error);
2510
+
2511
+ // Log detailed readyState information
2512
+ const readyStateMap = {
2513
+ 0: 'CONNECTING',
2514
+ 1: 'OPEN',
2515
+ 2: 'CLOSED'
2516
+ };
2517
+ const readyState = eventSource.readyState;
2518
+ console.log(`EventSource readyState: ${readyState} (${readyStateMap[readyState] || 'UNKNOWN'})`);
2519
+
2520
+ // Check if the connection was established before the error
2521
+ if (readyState === 2) {
2522
+ console.log('Connection was closed. Attempting to reconnect...');
2523
+ } else if (readyState === 0) {
2524
+ console.log('Connection is still trying to connect. Will retry if it fails.');
2525
+ }
2526
+
2527
+ // Try to reconnect after a delay
2528
+ console.log('Will attempt to reconnect in 5 seconds...');
2529
+ setTimeout(connectToToolEvents, 5000);
2530
+ };
2531
+
2532
+ // Add open event handler
2533
+ eventSource.onopen = () => {
2534
+ console.log('%c SSE connection opened successfully', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2535
+ console.log('Ready to receive tool call events for session:', sessionId);
2536
+ };
2537
+ }
2538
+
2539
+ // Add tool call to the AI message
2540
+ function addToolCallToMessage(messageDiv, toolCall) {
2541
+ console.log('%c Adding tool call to message:', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', toolCall);
2542
+
2543
+ try {
2544
+ // Format the tool call description for display
2545
+ let toolDescription = '';
2546
+ if (toolCall.name === 'searchCode' || toolCall.name === 'search') {
2547
+ const query = toolCall.args.query || toolCall.args.keywords || '';
2548
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2549
+ const language = toolCall.args.language;
2550
+ const exact = toolCall.args.exact;
2551
+
2552
+ let locationInfo = path !== '.' ? ` in ${path}` : '';
2553
+ let languageInfo = language ? ` (language: ${language})` : '';
2554
+ let exactInfo = exact === true ? ` (exact match)` : '';
2555
+
2556
+ toolDescription = `Searching code with "${query}"${locationInfo}${languageInfo}${exactInfo}`;
2557
+ } else if (toolCall.name === 'queryCode' || toolCall.name === 'query') {
2558
+ const query = toolCall.args.query || toolCall.args.pattern || '';
2559
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2560
+ toolDescription = `Querying code with pattern "${query}"${path === '.' ? '' : ` in ${path}`}`;
2561
+ } else if (toolCall.name === 'extractCode' || toolCall.name === 'extract') {
2562
+ const filePath = toolCall.args.file_path || '';
2563
+ const line = toolCall.args.line;
2564
+ const endLine = toolCall.args.end_line;
2565
+
2566
+ let lineInfo = '';
2567
+ if (line && endLine) {
2568
+ lineInfo = ` (lines ${line}-${endLine})`;
2569
+ } else if (line) {
2570
+ lineInfo = ` (from line ${line})`;
2571
+ }
2572
+
2573
+ toolDescription = `Extracting code from ${filePath}${lineInfo}`;
2574
+ } else {
2575
+ toolDescription = `Using ${toolCall.name} tool`;
2576
+ }
2577
+
2578
+ // Create a simple paragraph element with the formatted description
2579
+ const paragraph = document.createElement('p');
2580
+ paragraph.textContent = toolDescription;
2581
+ paragraph.style.fontStyle = 'italic';
2582
+ paragraph.style.color = '#555';
2583
+ paragraph.style.margin = '8px 0';
2584
+
2585
+ // Add the paragraph to the message div
2586
+ messageDiv.appendChild(paragraph);
2587
+
2588
+ // Scroll to the bottom
2589
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
2590
+
2591
+ console.log('%c Tool call added successfully', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2592
+ } catch (error) {
2593
+ console.error('Error adding tool call to message:', error);
2594
+ }
2595
+ }
2596
+ // Connect to tool events on page load
2597
+ window.addEventListener('DOMContentLoaded', () => {
2598
+ // Check if we're in no API keys mode
2599
+ const noApiKeys = document.body.getAttribute('data-no-api-keys') === 'true';
2600
+
2601
+ if (!noApiKeys) {
2602
+ connectToToolEvents();
2603
+ }
2604
+ });
2605
+
2606
+ // Handle "New chat" button click
2607
+ document.addEventListener('DOMContentLoaded', () => {
2608
+ const newChatLink = document.querySelector('.new-chat-link');
2609
+ if (newChatLink) {
2610
+ newChatLink.addEventListener('click', (e) => {
2611
+ e.preventDefault();
2612
+
2613
+ // Cancel any ongoing requests for the current session
2614
+ cancelRequest(sessionId).catch(err => console.error('Error cancelling session on new chat:', err));
2615
+
2616
+ // Generate a new session ID
2617
+ sessionId = crypto.randomUUID();
2618
+ console.log(`New chat started in current window. New session ID: ${sessionId}`);
2619
+
2620
+ // Update URL to reflect new session
2621
+ updateUrlForSession(sessionId);
2622
+
2623
+ // Make session ID available to other scripts
2624
+ // We use both direct property assignment and event dispatch for compatibility
2625
+ window.sessionId = sessionId;
2626
+
2627
+ // Dispatch an event with the session ID for any external scripts that may be listening
2628
+ window.dispatchEvent(new MessageEvent('message', {
2629
+ data: { sessionId: sessionId }
2630
+ }));
2631
+
2632
+ // Clear the messages
2633
+ messagesDiv.innerHTML = '';
2634
+
2635
+ // Reset the UI
2636
+ positionInputForm();
2637
+ searchSuggestionsDiv.style.display = 'block';
2638
+
2639
+ // Hide and reset token usage display
2640
+ const tokenUsageElement = document.getElementById('token-usage');
2641
+ if (tokenUsageElement) {
2642
+ tokenUsageElement.style.display = 'none';
2643
+
2644
+ // Reset token usage counters
2645
+ document.getElementById('current-request').textContent = '0';
2646
+ document.getElementById('current-response').textContent = '0';
2647
+ document.getElementById('total-request').textContent = '0';
2648
+ document.getElementById('total-response').textContent = '0';
2649
+ }
2650
+
2651
+ // Close existing SSE connection and reconnect with new session ID
2652
+ if (eventSource) {
2653
+ eventSource.close();
2654
+ }
2655
+ connectToToolEvents();
2656
+
2657
+ // Send a request to the server to clear the chat history for this session
2658
+ fetch('/chat', {
2659
+ method: 'POST',
2660
+ headers: { 'Content-Type': 'application/json' },
2661
+ body: JSON.stringify({
2662
+ message: '__clear_history__',
2663
+ sessionId,
2664
+ clearHistory: true
2665
+ })
2666
+ }).catch(err => console.error('Error clearing chat history:', err));
2667
+ });
2668
+ }
2669
+ });
2670
+
2671
+ // Add event listener for page unload to cancel the current session
2672
+ window.addEventListener('beforeunload', () => {
2673
+ // If we have a sessionId that's about to become invalid, cancel it
2674
+ if (sessionId) {
2675
+ // Use navigator.sendBeacon for more reliable delivery during page unload
2676
+ const data = JSON.stringify({ sessionId });
2677
+ if (navigator.sendBeacon) {
2678
+ navigator.sendBeacon('/cancel-request', data);
2679
+ } else {
2680
+ // Fallback to fetch for older browsers
2681
+ fetch('/cancel-request', {
2682
+ method: 'POST',
2683
+ headers: { 'Content-Type': 'application/json' },
2684
+ body: data,
2685
+ // Use keepalive to ensure the request completes even if the page is unloading
2686
+ keepalive: true
2687
+ }).catch((err) => console.error('Error cancelling session on unload:', err));
2688
+ }
2689
+ }
2690
+ });
2691
+
2692
+ // Controller for aborting fetch requests
2693
+ let currentController = null;
2694
+ // Flag to track if a request is in progress
2695
+ let isRequestInProgress = false;
2696
+
2697
+ // Function to cancel the current request on the server
2698
+ async function cancelRequest(sessionId) {
2699
+ try {
2700
+ const response = await fetch('/cancel-request', {
2701
+ method: 'POST',
2702
+ headers: { 'Content-Type': 'application/json' },
2703
+ body: JSON.stringify({ sessionId })
2704
+ });
2705
+
2706
+ if (response.ok) {
2707
+ console.log('Request cancelled successfully on server');
2708
+ } else {
2709
+ console.error('Failed to cancel request on server');
2710
+ }
2711
+ } catch (error) {
2712
+ console.error('Error cancelling request:', error);
2713
+ }
2714
+ }
2715
+
2716
+ // Handle form submission
2717
+ // Use the button click event instead of form submit to avoid potential form submission issues
2718
+ const searchButton = document.getElementById('search-button');
2719
+ searchButton.addEventListener('click', async (e) => {
2720
+ e.preventDefault();
2721
+
2722
+ // If this is a stop action
2723
+ if (searchButton.textContent === 'Stop') {
2724
+ // Abort the current fetch request
2725
+ if (currentController) {
2726
+ currentController.abort();
2727
+ currentController = null;
2728
+ }
2729
+
2730
+ // Send cancellation request to the server
2731
+ if (isRequestInProgress) {
2732
+ await cancelRequest(sessionId);
2733
+ isRequestInProgress = false;
2734
+
2735
+ // Stop token usage polling when request is cancelled
2736
+ stopTokenUsagePolling();
2737
+ }
2738
+
2739
+ // Reset the button to "Search" and enable input
2740
+ searchButton.textContent = 'Search';
2741
+ searchButton.style.backgroundColor = '#44CDF3';
2742
+ input.disabled = false;
2743
+ return;
2744
+ }
2745
+
2746
+ const message = input.value.trim();
2747
+ if (!message) return;
2748
+
2749
+ // Check if this is the first message
2750
+ const isFirstMessage = messagesDiv.children.length === 0;
2751
+
2752
+ // Update URL for first message if we're on root path
2753
+ if (isFirstMessage && window.location.pathname === '/') {
2754
+ updateUrlForSession(sessionId);
2755
+ }
2756
+
2757
+ // Display user message with proper image handling
2758
+ const userMsgDiv = document.createElement('div');
2759
+ userMsgDiv.className = 'user-message markdown-content'; // Add markdown-content class
2760
+
2761
+ // Render the text message
2762
+ userMsgDiv.innerHTML = renderMarkdown(message);
2763
+
2764
+ // Get uploaded images for display in user message
2765
+ const userMessageImages = window.getUploadedImagesForChat ? window.getUploadedImagesForChat() : [];
2766
+
2767
+ // Add uploaded images if any
2768
+ if (userMessageImages.length > 0) {
2769
+ userMessageImages.forEach(imageUrl => {
2770
+ const imgElement = document.createElement('img');
2771
+ imgElement.src = imageUrl;
2772
+ imgElement.alt = 'Uploaded image';
2773
+ imgElement.style.maxWidth = '100%';
2774
+ imgElement.style.maxHeight = '300px';
2775
+ imgElement.style.borderRadius = '8px';
2776
+ imgElement.style.margin = '8px 0';
2777
+ imgElement.style.border = '1px solid #e0e0e0';
2778
+ imgElement.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
2779
+ imgElement.style.cursor = 'pointer';
2780
+ imgElement.style.transition = 'transform 0.2s ease';
2781
+ userMsgDiv.appendChild(imgElement);
2782
+ });
2783
+ }
2784
+
2785
+ messagesDiv.appendChild(userMsgDiv);
2786
+
2787
+ // Apply syntax highlighting to code blocks in user message
2788
+ userMsgDiv.querySelectorAll('pre code').forEach((block) => {
2789
+ hljs.highlightElement(block);
2790
+ });
2791
+
2792
+ // Render Mermaid diagrams in user message
2793
+ const userMermaidDivs = userMsgDiv.querySelectorAll('.mermaid');
2794
+ if (userMermaidDivs.length > 0) {
2795
+ console.log(`Found ${userMermaidDivs.length} mermaid diagrams in user message`);
2796
+ try {
2797
+ if (typeof mermaid.run === 'function') {
2798
+ mermaid.run({ nodes: userMermaidDivs });
2799
+ } else if (typeof mermaid.init === 'function') {
2800
+ mermaid.init(undefined, userMermaidDivs);
2801
+ }
2802
+ // Convert rendered SVGs to PNGs
2803
+ setTimeout(() => {
2804
+ const renderedSvgs = userMsgDiv.querySelectorAll('.mermaid svg');
2805
+ if (renderedSvgs.length > 0) {
2806
+ renderedSvgs.forEach((svg, index) => {
2807
+ convertSvgToPng(svg, userMsgDiv, index);
2808
+ });
2809
+ }
2810
+ }, 100);
2811
+ } catch (error) {
2812
+ console.error('Error rendering mermaid in user message:', error);
2813
+ }
2814
+ }
2815
+ input.value = '';
2816
+ autoResizeTextarea(); // Reset textarea height after clearing content
2817
+
2818
+ // If this is the first message, move the input form to the bottom and hide UI elements
2819
+ if (isFirstMessage) {
2820
+ positionInputForm();
2821
+ searchSuggestionsDiv.style.display = 'none';
2822
+
2823
+ // Ensure footer is hidden when chat starts
2824
+ const footer = document.querySelector('.footer');
2825
+ if (footer) {
2826
+ footer.style.display = 'none';
2827
+ }
2828
+
2829
+ // Show token usage display
2830
+ document.getElementById('token-usage').style.display = 'block';
2831
+
2832
+ // Show token usage display
2833
+ const tokenUsageElement = document.getElementById('token-usage');
2834
+ if (tokenUsageElement) {
2835
+ tokenUsageElement.style.display = 'block';
2836
+ }
2837
+
2838
+ // Keep the allowed folders section visible during chat
2839
+ // This is the key change - we don't hide the folder information anymore
2840
+ }
2841
+
2842
+ // Create AI message container
2843
+ const aiMsgDiv = document.createElement('div');
2844
+ aiMsgDiv.className = 'ai-message markdown-content';
2845
+
2846
+ // Store the original message for copying
2847
+ aiMsgDiv.setAttribute('data-original-markdown', '');
2848
+
2849
+ // Add the AI message to the DOM
2850
+ messagesDiv.appendChild(aiMsgDiv);
2851
+
2852
+ // Set as current AI message for tool calls
2853
+ currentAiMessageDiv = aiMsgDiv;
2854
+
2855
+ // Disable input and change button to "Stop"
2856
+ input.disabled = true;
2857
+ searchButton.textContent = 'Stop';
2858
+ searchButton.style.backgroundColor = '#f44336';
2859
+
2860
+ // Set request in progress flag
2861
+ isRequestInProgress = true;
2862
+
2863
+ // Start token usage polling for long-running requests
2864
+ startTokenUsagePolling();
2865
+
2866
+ // Send message to server
2867
+ try {
2868
+ // Log the session ID being used
2869
+ console.log(`%c Using session ID for chat request: ${sessionId}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2870
+
2871
+ // Get API key from local storage if available
2872
+ const storedApiProvider = localStorage.getItem('probeApiProvider');
2873
+ const storedApiKey = localStorage.getItem('probeApiKey');
2874
+ const storedApiUrl = localStorage.getItem('probeApiUrl');
2875
+
2876
+ // Get uploaded images as base64 data URLs
2877
+ const uploadedImageUrls = window.getUploadedImagesForChat ? window.getUploadedImagesForChat() : [];
2878
+
2879
+ const requestData = {
2880
+ message: message, // Keep text separate from images
2881
+ images: uploadedImageUrls, // Send images separately
2882
+ sessionId, // Include session ID with the request
2883
+ apiProvider: storedApiProvider,
2884
+ apiKey: storedApiKey,
2885
+ apiUrl: storedApiUrl
2886
+ };
2887
+
2888
+ if (uploadedImageUrls.length > 0) {
2889
+ console.log(`Including ${uploadedImageUrls.length} uploaded image(s) with message`);
2890
+ }
2891
+ console.log('Sending chat request with data:', requestData);
2892
+
2893
+ // Add a visual indicator that we're using this session ID
2894
+ const sessionIndicator = document.createElement('div');
2895
+ sessionIndicator.style.position = 'fixed';
2896
+ sessionIndicator.style.top = '10px';
2897
+ sessionIndicator.style.right = '10px';
2898
+ sessionIndicator.style.backgroundColor = '#FF9800';
2899
+ sessionIndicator.style.color = 'white';
2900
+ sessionIndicator.style.padding = '5px 10px';
2901
+ sessionIndicator.style.borderRadius = '4px';
2902
+ sessionIndicator.style.fontSize = '12px';
2903
+ sessionIndicator.style.zIndex = '1000';
2904
+ sessionIndicator.style.opacity = '0.8';
2905
+ sessionIndicator.textContent = `Session ID: ${sessionId.substring(0, 8)}...`;
2906
+
2907
+ // Remove after 3 seconds
2908
+ setTimeout(() => {
2909
+ if (document.body.contains(sessionIndicator)) {
2910
+ document.body.removeChild(sessionIndicator);
2911
+ }
2912
+ }, 3000);
2913
+
2914
+ document.body.appendChild(sessionIndicator);
2915
+
2916
+ // Create a new AbortController for this request
2917
+ currentController = new AbortController();
2918
+ const signal = currentController.signal;
2919
+
2920
+ const response = await fetch('/chat', {
2921
+ method: 'POST',
2922
+ headers: {
2923
+ 'Content-Type': 'application/json',
2924
+ 'Cache-Control': 'no-cache',
2925
+ 'Pragma': 'no-cache'
2926
+ },
2927
+ cache: 'no-store',
2928
+ body: JSON.stringify(requestData),
2929
+ signal: signal
2930
+ }).catch(error => {
2931
+ if (error.name === 'AbortError') {
2932
+ console.log('Fetch aborted');
2933
+ aiMsgDiv.innerHTML += '<p><em>Search was stopped by user.</em></p>';
2934
+ return null;
2935
+ }
2936
+ throw error;
2937
+ });
2938
+
2939
+ // If response is null (aborted), reset UI and return
2940
+ if (!response) {
2941
+ // Reset button to "Search" and enable input
2942
+ form.querySelector('button').textContent = 'Search';
2943
+ form.querySelector('button').style.backgroundColor = '#44CDF3';
2944
+ input.disabled = false;
2945
+ return;
2946
+ }
2947
+
2948
+ // We'll rely on polling and final fetch for token usage updates
2949
+ // No need to extract from headers as it's redundant
2950
+
2951
+ const reader = response.body.getReader();
2952
+ const decoder = new TextDecoder();
2953
+ let aiResponse = '';
2954
+
2955
+ while (true) {
2956
+ const { done, value } = await reader.read();
2957
+ if (done) break;
2958
+ const chunk = decoder.decode(value, { stream: true });
2959
+ aiResponse += chunk;
2960
+
2961
+ try {
2962
+ // Parse the JSON response to extract the "response" field
2963
+ const jsonResponse = JSON.parse(aiResponse);
2964
+ const markdownContent = jsonResponse.response;
2965
+
2966
+ // Update the original markdown attribute with just the markdown content
2967
+ aiMsgDiv.setAttribute('data-original-markdown', markdownContent);
2968
+
2969
+ // Render markdown content
2970
+ aiMsgDiv.innerHTML = renderMarkdown(markdownContent);
2971
+
2972
+ // Apply syntax highlighting to code blocks
2973
+ aiMsgDiv.querySelectorAll('pre code').forEach((block) => {
2974
+ hljs.highlightElement(block);
2975
+ });
2976
+
2977
+ // Don't render mermaid diagrams during streaming - will render once at the end
2978
+ // This prevents premature rendering attempts that might fail
2979
+
2980
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
2981
+ } catch (error) {
2982
+ console.error('Error processing response chunk:', error);
2983
+
2984
+ // Check if it's a JSON parsing error or a markdown rendering error
2985
+ if (error instanceof SyntaxError) {
2986
+ // If it's a JSON parsing error, show a message about incomplete response
2987
+ aiMsgDiv.innerHTML = '<p><em>Receiving response...</em></p>';
2988
+ } else {
2989
+ // If it's a markdown rendering error, try to parse JSON but show raw content
2990
+ try {
2991
+ const jsonResponse = JSON.parse(aiResponse);
2992
+ aiMsgDiv.textContent = jsonResponse.response || aiResponse;
2993
+ } catch (jsonError) {
2994
+ // If JSON parsing fails, show the raw text
2995
+ aiMsgDiv.textContent = aiResponse;
2996
+ }
2997
+ }
2998
+ }
2999
+ }
3000
+
3001
+ // Final render after all content is received
3002
+ setTimeout(() => {
3003
+ try {
3004
+ // Parse the complete JSON response
3005
+ const jsonResponse = JSON.parse(aiResponse);
3006
+ const markdownContent = jsonResponse.response;
3007
+
3008
+ // Update token usage if available
3009
+ if (jsonResponse.tokenUsage && window.tokenUsageDisplay) {
3010
+ window.tokenUsageDisplay.update(jsonResponse.tokenUsage);
3011
+ }
3012
+
3013
+ // Make sure the final content is set correctly
3014
+ aiMsgDiv.setAttribute('data-original-markdown', markdownContent);
3015
+ aiMsgDiv.innerHTML = renderMarkdown(markdownContent);
3016
+
3017
+ // Apply syntax highlighting to code blocks
3018
+ aiMsgDiv.querySelectorAll('pre code').forEach((block) => {
3019
+ hljs.highlightElement(block);
3020
+ });
3021
+
3022
+ // Specifically target mermaid diagrams in the current message
3023
+ const finalMermaidDivs = aiMsgDiv.querySelectorAll('.language-mermaid');
3024
+
3025
+ if (finalMermaidDivs.length > 0) {
3026
+ console.log(`Final render: Found ${finalMermaidDivs.length} mermaid diagrams in current message`);
3027
+
3028
+ // Log the content of the first diagram for debugging
3029
+ if (finalMermaidDivs[0]) {
3030
+ console.log('First diagram content:', finalMermaidDivs[0].textContent.substring(0, 100) + '...');
3031
+ }
3032
+
3033
+ // Try direct rendering with specific nodes from current message
3034
+ if (typeof mermaid.run === 'function') {
3035
+ console.log('Using mermaid.run() for rendering');
3036
+ mermaid.run({
3037
+ nodes: finalMermaidDivs
3038
+ });
3039
+ } else if (typeof mermaid.init === 'function') {
3040
+ // Fallback to older mermaid versions
3041
+ console.log('Using mermaid.init() for rendering');
3042
+ mermaid.init(undefined, finalMermaidDivs);
3043
+ } else {
3044
+ console.error('No suitable mermaid rendering method found');
3045
+ }
3046
+
3047
+ // Verify rendering success
3048
+ setTimeout(() => {
3049
+ // Update selector to find SVGs inside code.language-mermaid elements
3050
+ const renderedSvgs = aiMsgDiv.querySelectorAll('.language-mermaid svg, .mermaid svg');
3051
+ console.log(`Rendering verification: Found ${renderedSvgs.length} rendered SVGs`);
3052
+
3053
+ // Convert SVGs to PNGs if any were rendered
3054
+ if (renderedSvgs.length > 0) {
3055
+ console.log('Converting SVGs to PNGs...');
3056
+ renderedSvgs.forEach((svg, index) => {
3057
+ convertSvgToPng(svg, aiMsgDiv, index);
3058
+ });
3059
+ }
3060
+
3061
+ // Also add zoom functionality to any existing PNG images
3062
+ setTimeout(() => {
3063
+ const existingPngs = aiMsgDiv.querySelectorAll('.mermaid-png:not(.zoom-enabled)');
3064
+ if (existingPngs.length > 0) {
3065
+ console.log(`Adding zoom functionality to ${existingPngs.length} existing PNG images`);
3066
+ existingPngs.forEach((png) => {
3067
+ if (!png.parentElement.classList.contains('mermaid-container')) {
3068
+ const container = document.createElement('div');
3069
+ container.className = 'mermaid-container';
3070
+
3071
+ const zoomIcon = document.createElement('div');
3072
+ zoomIcon.className = 'zoom-icon';
3073
+ zoomIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>';
3074
+
3075
+ zoomIcon.addEventListener('click', function (e) {
3076
+ e.stopPropagation();
3077
+ showDiagramDialog(png.src);
3078
+ });
3079
+
3080
+ png.parentNode.insertBefore(container, png);
3081
+ container.appendChild(png);
3082
+ container.appendChild(zoomIcon);
3083
+ png.classList.add('zoom-enabled');
3084
+ }
3085
+ });
3086
+ }
3087
+ }, 200);
3088
+ }, 100);
3089
+ } else {
3090
+ console.log('No mermaid diagrams found in current message');
3091
+ }
3092
+ } catch (error) {
3093
+ console.warn('Final mermaid rendering error:', error);
3094
+ console.error('Error details:', error.message);
3095
+ }
3096
+
3097
+ // Add copy button below the message after rendering is complete
3098
+ if (!aiMsgDiv.nextElementSibling || !aiMsgDiv.nextElementSibling.classList.contains('copy-button-container')) {
3099
+ // Create copy button container
3100
+ const copyButtonContainer = document.createElement('div');
3101
+ copyButtonContainer.className = 'copy-button-container';
3102
+
3103
+ // Create copy button
3104
+ const copyButton = document.createElement('button');
3105
+ copyButton.className = 'copy-button';
3106
+ copyButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z" fill="#666"></path></svg>Copy`;
3107
+
3108
+ // Add click event to copy button
3109
+ copyButton.addEventListener('click', function () {
3110
+ const markdown = aiMsgDiv.getAttribute('data-original-markdown');
3111
+ if (markdown) {
3112
+ // Copy just the markdown content, not the raw JSON
3113
+ navigator.clipboard.writeText(markdown).then(() => {
3114
+ // Visual feedback
3115
+ copyButton.textContent = 'Copied!';
3116
+
3117
+ setTimeout(() => {
3118
+ copyButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z" fill="#666"></path></svg>Copy`;
3119
+ }, 2000);
3120
+ }).catch(err => {
3121
+ console.error('Failed to copy text: ', err);
3122
+ copyButton.textContent = 'Failed to copy';
3123
+
3124
+ setTimeout(() => {
3125
+ copyButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z" fill="#666"></path></svg>Copy`;
3126
+ }, 2000);
3127
+ });
3128
+ }
3129
+ });
3130
+
3131
+ // Add elements to the DOM
3132
+ copyButtonContainer.appendChild(copyButton);
3133
+
3134
+ // Insert after the AI message
3135
+ if (aiMsgDiv.nextSibling) {
3136
+ messagesDiv.insertBefore(copyButtonContainer, aiMsgDiv.nextSibling);
3137
+ } else {
3138
+ messagesDiv.appendChild(copyButtonContainer);
3139
+ }
3140
+ }
3141
+ }, 500); // Increased timeout to ensure DOM is fully updated
3142
+ } catch (error) {
3143
+ console.error('Error:', error);
3144
+ const errorMsg = document.createElement('div');
3145
+ errorMsg.className = 'ai-message';
3146
+ errorMsg.textContent = 'Error occurred while processing your request.';
3147
+ messagesDiv.appendChild(errorMsg);
3148
+ } finally {
3149
+ // Fetch and update token usage after each chat interaction
3150
+ // Only if this is still the current session
3151
+ if (window.sessionId === sessionId && window.tokenUsageDisplay && typeof window.tokenUsageDisplay.fetch === 'function') {
3152
+ console.log('[TokenUsage] Fetching final token usage after request completion');
3153
+ window.tokenUsageDisplay.fetch(sessionId);
3154
+ }
3155
+
3156
+ // Clear uploaded images after successful send
3157
+ if (window.clearUploadedImagesAfterSend) {
3158
+ window.clearUploadedImagesAfterSend();
3159
+ }
3160
+
3161
+ // Reset button to "Search" and enable input
3162
+ searchButton.textContent = 'Search';
3163
+ searchButton.style.backgroundColor = '#44CDF3';
3164
+ input.disabled = false;
3165
+ currentController = null;
3166
+ isRequestInProgress = false;
3167
+
3168
+ // Stop token usage polling when request is completed
3169
+ stopTokenUsagePolling();
3170
+ }
3171
+
3172
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
3173
+ });
3174
+
3175
+ // Use the updateTokenUsageDisplay function defined at the beginning of the script
3176
+
3177
+ // Fetch token usage manually only for long-running requests
3178
+ // This helps avoid redundant polling for quick responses
3179
+ let tokenUsagePollingTimer = null;
3180
+ let pollingAttempts = 0;
3181
+ const MAX_POLLING_ATTEMPTS = 10;
3182
+
3183
+ // Function to start polling for token usage updates
3184
+ function startTokenUsagePolling() {
3185
+ // Reset attempts counter
3186
+ pollingAttempts = 0;
3187
+ // Clear any existing timer
3188
+ if (tokenUsagePollingTimer) {
3189
+ clearInterval(tokenUsagePollingTimer);
3190
+ }
3191
+
3192
+ // Start polling immediately to show token usage as soon as possible
3193
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3194
+ console.log('[TokenUsage] Starting token usage polling for request...');
3195
+
3196
+ // Do an initial fetch right away
3197
+ window.tokenUsageDisplay.fetch(sessionId);
3198
+
3199
+ // Poll every 3 seconds (reduced from 5 seconds for more responsive updates)
3200
+ tokenUsagePollingTimer = setInterval(() => {
3201
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3202
+ console.log('[TokenUsage] Polling for token usage updates...');
3203
+ window.tokenUsageDisplay.fetch(sessionId);
3204
+
3205
+ // Increment attempts counter
3206
+ pollingAttempts++;
3207
+
3208
+ // If we've reached the maximum number of attempts, slow down polling
3209
+ if (pollingAttempts >= MAX_POLLING_ATTEMPTS) {
3210
+ console.log('[TokenUsage] Reached maximum polling attempts, slowing down polling');
3211
+ clearInterval(tokenUsagePollingTimer);
3212
+ tokenUsagePollingTimer = setInterval(() => {
3213
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3214
+ console.log('[TokenUsage] Slow polling for token usage updates...');
3215
+ window.tokenUsageDisplay.fetch(sessionId);
3216
+ } else {
3217
+ clearInterval(tokenUsagePollingTimer);
3218
+ tokenUsagePollingTimer = null;
3219
+ console.log('[TokenUsage] Stopped slow polling - request completed');
3220
+ }
3221
+ }, 10000); // Slow down to every 10 seconds
3222
+ }
3223
+ } else {
3224
+ // Stop polling if request is no longer in progress
3225
+ clearInterval(tokenUsagePollingTimer);
3226
+ tokenUsagePollingTimer = null;
3227
+ console.log('[TokenUsage] Stopped polling - request completed');
3228
+ }
3229
+ }, 3000);
3230
+ }
3231
+ }
3232
+
3233
+ // Function to stop polling
3234
+ function stopTokenUsagePolling() {
3235
+ if (tokenUsagePollingTimer) {
3236
+ clearInterval(tokenUsagePollingTimer);
3237
+ tokenUsagePollingTimer = null;
3238
+ console.log('[TokenUsage] Stopped token usage polling');
3239
+ }
3240
+ }
3241
+
3242
+ const messageInput = document.getElementById('message-input');
3243
+
3244
+ function autoResizeTextarea() {
3245
+ messageInput.style.height = 'auto'; // Reset to natural height
3246
+ const scrollHeight = messageInput.scrollHeight;
3247
+ messageInput.style.height = Math.min(scrollHeight, 200) + 'px'; // Set height, capped at 200px
3248
+ }
3249
+
3250
+ // Initialize height on page load
3251
+ window.addEventListener('load', () => {
3252
+ autoResizeTextarea(); // Set initial height based on content (empty = min-height)
3253
+ });
3254
+
3255
+ // Auto-resize as user types
3256
+ messageInput.addEventListener('input', autoResizeTextarea);
3257
+
3258
+ // Handle Shift+Enter for new line and Enter for form submission
3259
+ messageInput.addEventListener('keydown', function (e) {
3260
+ if (e.key === 'Enter') {
3261
+ if (e.shiftKey) {
3262
+ // Allow new line with Shift+Enter and resize
3263
+ setTimeout(autoResizeTextarea, 0);
3264
+ } else {
3265
+ // Trigger search button click on Enter without Shift
3266
+ e.preventDefault();
3267
+ searchButton.click();
3268
+ }
3269
+ }
3270
+ });
3271
+
3272
+ // API Key Form Functionality
3273
+ document.addEventListener('DOMContentLoaded', function () {
3274
+ // Check if API key is already stored
3275
+ const storedApiProvider = localStorage.getItem('probeApiProvider');
3276
+ const storedApiKey = localStorage.getItem('probeApiKey');
3277
+ const storedApiUrl = localStorage.getItem('probeApiUrl');
3278
+
3279
+ const apiProviderSelect = document.getElementById('api-provider');
3280
+ const apiKeyInput = document.getElementById('api-key');
3281
+ const apiUrlInput = document.getElementById('api-url');
3282
+ const saveButton = document.getElementById('save-api-key');
3283
+ const headerResetButton = document.getElementById('header-reset-api-key');
3284
+ const statusDiv = document.getElementById('api-key-status');
3285
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
3286
+ const inputForm = document.getElementById('input-form');
3287
+
3288
+ // If API key is stored, show a success message and show the header reset button
3289
+ if (storedApiKey) {
3290
+ // Show the reset button in the header
3291
+ headerResetButton.style.display = 'inline-block';
3292
+ statusDiv.textContent = `API key for ${storedApiProvider} is configured`;
3293
+ statusDiv.className = 'api-key-status success';
3294
+ statusDiv.style.display = 'block';
3295
+
3296
+ // Fill the form with stored values
3297
+ if (storedApiProvider) {
3298
+ apiProviderSelect.value = storedApiProvider;
3299
+ }
3300
+ if (storedApiKey) {
3301
+ apiKeyInput.value = '••••••••••••••••••••••••••';
3302
+ }
3303
+ if (storedApiUrl) {
3304
+ apiUrlInput.value = storedApiUrl;
3305
+ }
3306
+
3307
+ // If we have an API key in local storage, always enable the chat interface
3308
+ // regardless of no API keys mode
3309
+ // Hide API key setup and show input form
3310
+ apiKeySetupDiv.style.display = 'none';
3311
+ inputForm.style.display = 'flex';
3312
+
3313
+ // Remove API setup mode class
3314
+ document.body.classList.remove('api-setup-mode');
3315
+ }
3316
+
3317
+ // Save API key to local storage
3318
+ saveButton.addEventListener('click', function () {
3319
+ const provider = apiProviderSelect.value;
3320
+ const key = apiKeyInput.value;
3321
+ const url = apiUrlInput.value;
3322
+
3323
+ // Don't save if the key is masked
3324
+ if (key === '••••••••••••••••••••••••••') {
3325
+ statusDiv.textContent = 'No changes made to API key';
3326
+ statusDiv.className = 'api-key-status';
3327
+ statusDiv.style.display = 'block';
3328
+ return;
3329
+ }
3330
+
3331
+ // Validate inputs
3332
+ if (!key) {
3333
+ statusDiv.textContent = 'Please enter an API key';
3334
+ statusDiv.className = 'api-key-status error';
3335
+ statusDiv.style.display = 'block';
3336
+ return;
3337
+ }
3338
+
3339
+ // Save to local storage
3340
+ localStorage.setItem('probeApiProvider', provider);
3341
+ localStorage.setItem('probeApiKey', key);
3342
+ if (url) {
3343
+ localStorage.setItem('probeApiUrl', url);
3344
+ } else {
3345
+ localStorage.removeItem('probeApiUrl');
3346
+ }
3347
+
3348
+ // Show success message
3349
+ statusDiv.textContent = `API key for ${provider} saved successfully`;
3350
+ statusDiv.className = 'api-key-status success';
3351
+ statusDiv.style.display = 'block';
3352
+
3353
+ // Mask the API key for security
3354
+ apiKeyInput.value = '••••••••••••••••••••••••••';
3355
+
3356
+ // If we're in no API keys mode, enable the chat interface
3357
+ if (document.body.getAttribute('data-no-api-keys') === 'true') {
3358
+ // Hide API key setup and show input form
3359
+ apiKeySetupDiv.style.display = 'none';
3360
+ inputForm.style.display = 'flex';
3361
+
3362
+ // Refresh the page to apply changes
3363
+ setTimeout(() => {
3364
+ window.location.reload();
3365
+ }, 1000);
3366
+ }
3367
+ });
3368
+
3369
+ // Reset API key from header button
3370
+ headerResetButton.addEventListener('click', function (e) {
3371
+ e.preventDefault();
3372
+
3373
+ // Confirm before resetting
3374
+ if (confirm('Are you sure you want to reset your API key configuration?')) {
3375
+ // Remove from local storage
3376
+ localStorage.removeItem('probeApiProvider');
3377
+ localStorage.removeItem('probeApiKey');
3378
+ localStorage.removeItem('probeApiUrl');
3379
+
3380
+ // Hide the reset button
3381
+ headerResetButton.style.display = 'none';
3382
+
3383
+ // Show message
3384
+ alert('API key configuration has been reset.');
3385
+
3386
+ // Reload the page
3387
+ window.location.reload();
3388
+ }
3389
+ });
3390
+ });
3391
+
3392
+ // Image Upload Functionality
3393
+ (function() {
3394
+ // Global state to track uploaded images
3395
+ window.uploadedImages = window.uploadedImages || [];
3396
+
3397
+ // Get DOM elements
3398
+ const imageUploadButton = document.getElementById('image-upload-button');
3399
+ const imageUploadInput = document.getElementById('image-upload');
3400
+ const messageInput = document.getElementById('message-input');
3401
+ const textareaContainer = document.querySelector('.textarea-container');
3402
+ const floatingThumbnails = document.getElementById('floating-thumbnails');
3403
+
3404
+ // Image upload button click handler
3405
+ imageUploadButton.addEventListener('click', function() {
3406
+ imageUploadInput.click();
3407
+ });
3408
+
3409
+ // File input change handler
3410
+ imageUploadInput.addEventListener('change', function(e) {
3411
+ const files = Array.from(e.target.files);
3412
+ handleImageFiles(files);
3413
+ });
3414
+
3415
+ // Drag and drop functionality
3416
+ textareaContainer.addEventListener('dragover', function(e) {
3417
+ e.preventDefault();
3418
+ textareaContainer.classList.add('drag-over');
3419
+ });
3420
+
3421
+ textareaContainer.addEventListener('dragleave', function(e) {
3422
+ e.preventDefault();
3423
+ textareaContainer.classList.remove('drag-over');
3424
+ });
3425
+
3426
+ textareaContainer.addEventListener('drop', function(e) {
3427
+ e.preventDefault();
3428
+ textareaContainer.classList.remove('drag-over');
3429
+
3430
+ const files = Array.from(e.dataTransfer.files).filter(file =>
3431
+ file.type.startsWith('image/')
3432
+ );
3433
+
3434
+ if (files.length > 0) {
3435
+ handleImageFiles(files);
3436
+ }
3437
+ });
3438
+
3439
+ // Clipboard paste functionality
3440
+ messageInput.addEventListener('paste', function(e) {
3441
+ const clipboardItems = e.clipboardData.items;
3442
+ const imageFiles = [];
3443
+
3444
+ for (let i = 0; i < clipboardItems.length; i++) {
3445
+ const item = clipboardItems[i];
3446
+ if (item.type.startsWith('image/')) {
3447
+ const file = item.getAsFile();
3448
+ if (file) {
3449
+ imageFiles.push(file);
3450
+ }
3451
+ }
3452
+ }
3453
+
3454
+ if (imageFiles.length > 0) {
3455
+ e.preventDefault(); // Prevent default paste behavior
3456
+ handleImageFiles(imageFiles);
3457
+ }
3458
+ });
3459
+
3460
+ // Handle image files
3461
+ function handleImageFiles(files) {
3462
+ files.forEach(file => {
3463
+ // Validate file size (10MB limit)
3464
+ if (file.size > 10 * 1024 * 1024) {
3465
+ showImageError(`File "${file.name}" is too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Maximum size is 10MB.`);
3466
+ return;
3467
+ }
3468
+
3469
+ // Validate file type
3470
+ if (!file.type.startsWith('image/')) {
3471
+ showImageError(`File "${file.name}" is not a valid image file.`);
3472
+ return;
3473
+ }
3474
+
3475
+ // Convert to base64 and add to uploaded images
3476
+ const reader = new FileReader();
3477
+ reader.onload = function(e) {
3478
+ const base64Data = e.target.result;
3479
+ const imageInfo = {
3480
+ id: Date.now() + Math.random(),
3481
+ name: file.name,
3482
+ size: file.size,
3483
+ type: file.type,
3484
+ base64: base64Data
3485
+ };
3486
+
3487
+ window.uploadedImages.push(imageInfo);
3488
+ addImagePreview(imageInfo);
3489
+ updateThumbnailsVisibility();
3490
+ };
3491
+
3492
+ reader.onerror = function() {
3493
+ showImageError(`Failed to read file "${file.name}".`);
3494
+ };
3495
+
3496
+ reader.readAsDataURL(file);
3497
+ });
3498
+ }
3499
+
3500
+ // Add image preview
3501
+ function addImagePreview(imageInfo) {
3502
+ const thumbnailItem = document.createElement('div');
3503
+ thumbnailItem.className = 'floating-thumbnail';
3504
+ thumbnailItem.dataset.imageId = imageInfo.id;
3505
+
3506
+ thumbnailItem.innerHTML = `
3507
+ <img src="${imageInfo.base64}" alt="${imageInfo.name}">
3508
+ <button type="button" class="floating-thumbnail-remove" onclick="removeImagePreview('${imageInfo.id}')">×</button>
3509
+ `;
3510
+
3511
+ floatingThumbnails.appendChild(thumbnailItem);
3512
+ }
3513
+
3514
+ // Remove image preview
3515
+ window.removeImagePreview = function(imageId) {
3516
+ // Remove from uploaded images array
3517
+ window.uploadedImages = window.uploadedImages.filter(img => img.id != imageId);
3518
+
3519
+ // Remove from DOM
3520
+ const thumbnailItem = document.querySelector(`[data-image-id="${imageId}"]`);
3521
+ if (thumbnailItem) {
3522
+ thumbnailItem.remove();
3523
+ }
3524
+
3525
+ updateThumbnailsVisibility();
3526
+ };
3527
+
3528
+ // Clear all images (internal function)
3529
+ function clearAllImages() {
3530
+ window.uploadedImages = [];
3531
+ floatingThumbnails.innerHTML = '';
3532
+ updateThumbnailsVisibility();
3533
+ }
3534
+
3535
+ // Update thumbnails container visibility
3536
+ function updateThumbnailsVisibility() {
3537
+ if (window.uploadedImages.length > 0) {
3538
+ floatingThumbnails.style.display = 'flex';
3539
+ } else {
3540
+ floatingThumbnails.style.display = 'none';
3541
+ }
3542
+ }
3543
+
3544
+ // Show image error
3545
+ function showImageError(message) {
3546
+ console.error('[Image Upload]', message);
3547
+
3548
+ // Create error notification
3549
+ const errorDiv = document.createElement('div');
3550
+ errorDiv.style.position = 'fixed';
3551
+ errorDiv.style.top = '20px';
3552
+ errorDiv.style.right = '20px';
3553
+ errorDiv.style.backgroundColor = '#dc3545';
3554
+ errorDiv.style.color = 'white';
3555
+ errorDiv.style.padding = '12px 16px';
3556
+ errorDiv.style.borderRadius = '6px';
3557
+ errorDiv.style.zIndex = '9999';
3558
+ errorDiv.style.maxWidth = '300px';
3559
+ errorDiv.style.fontSize = '14px';
3560
+ errorDiv.textContent = message;
3561
+
3562
+ document.body.appendChild(errorDiv);
3563
+
3564
+ // Auto-remove after 5 seconds
3565
+ setTimeout(() => {
3566
+ if (errorDiv.parentNode) {
3567
+ errorDiv.parentNode.removeChild(errorDiv);
3568
+ }
3569
+ }, 5000);
3570
+ }
3571
+
3572
+ // Function to get images as base64 data URLs for chat
3573
+ window.getUploadedImagesForChat = function() {
3574
+ return window.uploadedImages.map(img => img.base64);
3575
+ };
3576
+
3577
+ // Function to clear images after successful send
3578
+ window.clearUploadedImagesAfterSend = function() {
3579
+ clearAllImages();
3580
+ };
3581
+ })();
3582
+
3583
+ // Function to process message for display (handle base64 images)
3584
+ function processMessageForDisplay(message) {
3585
+ // Pattern to match base64 data URLs
3586
+ const base64ImagePattern = /data:image\/([a-zA-Z]*);base64,([A-Za-z0-9+/=]+)/g;
3587
+
3588
+ // Replace base64 data URLs with proper img tags
3589
+ const processedMessage = message.replace(base64ImagePattern, (match, imageType, base64Data) => {
3590
+ // Estimate file size for display
3591
+ const estimatedSize = (base64Data.length * 3) / 4;
3592
+ const sizeText = estimatedSize > 1024 * 1024
3593
+ ? `${(estimatedSize / 1024 / 1024).toFixed(1)}MB`
3594
+ : `${(estimatedSize / 1024).toFixed(1)}KB`;
3595
+
3596
+ // Create an image markdown with the base64 data
3597
+ return `![Uploaded image (${imageType}, ${sizeText})](${match})`;
3598
+ });
3599
+
3600
+ return processedMessage;
3601
+ }
3602
+
3603
+ // Make the function globally available
3604
+ window.processMessageForDisplay = processMessageForDisplay;
3605
+
3606
+ // Add click-to-zoom functionality for images in messages
3607
+ document.addEventListener('click', function(e) {
3608
+ if (e.target.tagName === 'IMG' && (e.target.closest('.user-message') || e.target.closest('.ai-message'))) {
3609
+ e.preventDefault();
3610
+ showImageDialog(e.target.src, e.target.alt || 'Image');
3611
+ }
3612
+ });
3613
+
3614
+ // Function to show image in full-screen dialog
3615
+ function showImageDialog(imageSrc, imageAlt) {
3616
+ // Create dialog overlay
3617
+ const overlay = document.createElement('div');
3618
+ overlay.style.position = 'fixed';
3619
+ overlay.style.top = '0';
3620
+ overlay.style.left = '0';
3621
+ overlay.style.width = '100%';
3622
+ overlay.style.height = '100%';
3623
+ overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
3624
+ overlay.style.zIndex = '10000';
3625
+ overlay.style.display = 'flex';
3626
+ overlay.style.alignItems = 'center';
3627
+ overlay.style.justifyContent = 'center';
3628
+ overlay.style.cursor = 'pointer';
3629
+
3630
+ // Create image container
3631
+ const imageContainer = document.createElement('div');
3632
+ imageContainer.style.position = 'relative';
3633
+ imageContainer.style.maxWidth = '90%';
3634
+ imageContainer.style.maxHeight = '90%';
3635
+
3636
+ // Create image element
3637
+ const img = document.createElement('img');
3638
+ img.src = imageSrc;
3639
+ img.alt = imageAlt;
3640
+ img.style.maxWidth = '100%';
3641
+ img.style.maxHeight = '100%';
3642
+ img.style.objectFit = 'contain';
3643
+ img.style.borderRadius = '8px';
3644
+
3645
+ // Create close button
3646
+ const closeButton = document.createElement('button');
3647
+ closeButton.innerHTML = '×';
3648
+ closeButton.style.position = 'absolute';
3649
+ closeButton.style.top = '-10px';
3650
+ closeButton.style.right = '-10px';
3651
+ closeButton.style.width = '30px';
3652
+ closeButton.style.height = '30px';
3653
+ closeButton.style.borderRadius = '50%';
3654
+ closeButton.style.border = 'none';
3655
+ closeButton.style.backgroundColor = '#fff';
3656
+ closeButton.style.color = '#333';
3657
+ closeButton.style.fontSize = '18px';
3658
+ closeButton.style.cursor = 'pointer';
3659
+ closeButton.style.zIndex = '10001';
3660
+
3661
+ // Add elements to DOM
3662
+ imageContainer.appendChild(img);
3663
+ imageContainer.appendChild(closeButton);
3664
+ overlay.appendChild(imageContainer);
3665
+ document.body.appendChild(overlay);
3666
+
3667
+ // Close on overlay click or close button click
3668
+ overlay.addEventListener('click', function(e) {
3669
+ if (e.target === overlay || e.target === closeButton) {
3670
+ document.body.removeChild(overlay);
3671
+ }
3672
+ });
3673
+
3674
+ // Close on escape key
3675
+ const escapeHandler = function(e) {
3676
+ if (e.key === 'Escape') {
3677
+ document.body.removeChild(overlay);
3678
+ document.removeEventListener('keydown', escapeHandler);
3679
+ }
3680
+ };
3681
+ document.addEventListener('keydown', escapeHandler);
3682
+ }
3683
+ </script>
3684
+ </body>
3685
+
3686
+ </html>