@probelabs/probe-chat 0.6.0-rc100

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,3751 @@
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
+ console.log(`[DEBUG] Session restoration data:`, {
1739
+ exists: data.exists,
1740
+ historyLength: data.history ? data.history.length : 'null/undefined',
1741
+ condition: data.exists && data.history && data.history.length > 0
1742
+ });
1743
+
1744
+ if (data.exists && data.history && data.history.length > 0) {
1745
+ console.log(`Restored ${data.history.length} messages for session: ${sessionId}`);
1746
+
1747
+ // Count message types for debugging
1748
+ const messageTypes = {};
1749
+ data.history.forEach(msg => {
1750
+ messageTypes[msg.role] = (messageTypes[msg.role] || 0) + 1;
1751
+ });
1752
+ console.log(`[DEBUG] Message types to restore:`, messageTypes);
1753
+
1754
+ // Render restored messages
1755
+ let currentAiMessage = null;
1756
+ data.history.forEach((message, index) => {
1757
+ console.log(`[DEBUG] Processing message ${index + 1}/${data.history.length}: role=${message.role}`);
1758
+ if (message.role === 'user') {
1759
+ // Ensure images is always an array
1760
+ const images = Array.isArray(message.images) ? message.images : [];
1761
+ addUserMessage(message.content, images);
1762
+ currentAiMessage = null; // Reset for new conversation turn
1763
+ } else if (message.role === 'assistant') {
1764
+ addAssistantMessage(message.content);
1765
+ // Get the most recent AI message for tool call rendering
1766
+ const messagesDiv = document.getElementById('messages');
1767
+ const aiMessages = messagesDiv.querySelectorAll('.ai-message');
1768
+ currentAiMessage = aiMessages[aiMessages.length - 1];
1769
+ } else if (message.role === 'toolCall') {
1770
+ console.log(`[DEBUG] Rendering toolCall message`);
1771
+ // Handle tool call messages during restoration
1772
+ try {
1773
+ // Parse the tool call from the stored message
1774
+ const toolCall = {
1775
+ name: message.metadata?.name || 'unknown',
1776
+ args: message.metadata?.args || {},
1777
+ timestamp: message.timestamp,
1778
+ status: 'completed'
1779
+ };
1780
+
1781
+ // If we have a current AI message, add the tool call to it
1782
+ if (currentAiMessage) {
1783
+ addToolCallToMessage(currentAiMessage, toolCall);
1784
+ } else {
1785
+ console.log(`[DEBUG] No current AI message for tool call, creating temporary message`);
1786
+ // Create a temporary AI message for the tool call
1787
+ const tempDiv = document.createElement('div');
1788
+ tempDiv.className = 'ai-message';
1789
+ const messagesDiv = document.getElementById('messages');
1790
+ messagesDiv.appendChild(tempDiv);
1791
+ addToolCallToMessage(tempDiv, toolCall);
1792
+ currentAiMessage = tempDiv;
1793
+ }
1794
+ } catch (error) {
1795
+ console.error('[DEBUG] Error rendering tool call:', error, message);
1796
+ }
1797
+ } else {
1798
+ console.log(`[DEBUG] Skipping message with role: ${message.role}`);
1799
+ }
1800
+ });
1801
+
1802
+ // Update token usage if available
1803
+ if (data.tokenUsage && window.tokenUsageDisplay) {
1804
+ window.tokenUsageDisplay.update(data.tokenUsage);
1805
+ }
1806
+
1807
+ // Update UI to reflect restored chat state
1808
+ positionInputForm();
1809
+
1810
+ // Hide search suggestions when loading from history
1811
+ const searchSuggestions = document.querySelector('.search-suggestions');
1812
+ if (searchSuggestions) {
1813
+ searchSuggestions.style.display = 'none';
1814
+ }
1815
+
1816
+ // Ensure all Mermaid diagrams are rendered after restoration
1817
+ setTimeout(() => {
1818
+ const allMermaidElements = document.querySelectorAll('.mermaid:not([data-processed]), .language-mermaid:not([data-processed])');
1819
+ if (allMermaidElements.length > 0) {
1820
+ console.log(`Rendering ${allMermaidElements.length} unprocessed mermaid diagrams after session restoration`);
1821
+ try {
1822
+ if (typeof mermaid.run === 'function') {
1823
+ mermaid.run({ nodes: allMermaidElements });
1824
+ } else if (typeof mermaid.init === 'function') {
1825
+ mermaid.init(undefined, allMermaidElements);
1826
+ }
1827
+ } catch (error) {
1828
+ console.error('Error rendering mermaid after session restoration:', error);
1829
+ }
1830
+ }
1831
+ }, 100); // Small delay to ensure DOM is fully updated
1832
+ } else {
1833
+ console.log(`[DEBUG] No history found for session: ${sessionId}`, {
1834
+ exists: data.exists,
1835
+ historyExists: !!data.history,
1836
+ historyLength: data.history ? data.history.length : 'N/A'
1837
+ });
1838
+ // Show user-friendly message for session not found
1839
+ showSessionNotFoundMessage(sessionId);
1840
+ }
1841
+ } catch (error) {
1842
+ console.error('[DEBUG] Error restoring session history:', error);
1843
+ console.error('[DEBUG] Error stack:', error.stack);
1844
+ // Show error message for network or other errors
1845
+ showSessionNotFoundMessage(sessionId);
1846
+ }
1847
+ }
1848
+
1849
+ // Function to show session not found message
1850
+ function showSessionNotFoundMessage(sessionId) {
1851
+ const messageDiv = document.createElement('div');
1852
+ messageDiv.className = 'ai-message markdown-content';
1853
+ messageDiv.style.borderLeft = '4px solid #ff6b6b';
1854
+ messageDiv.style.backgroundColor = '#fff5f5';
1855
+ messageDiv.innerHTML = `
1856
+ <div style="padding: 15px;">
1857
+ <h3 style="margin-top: 0; color: #c92a2a;">Session Not Found</h3>
1858
+ <p>The chat session with ID <code>${sessionId}</code> was not found or has expired.</p>
1859
+ <p>This could happen if:</p>
1860
+ <ul>
1861
+ <li>The session has been inactive for more than 2 hours</li>
1862
+ <li>The server was restarted</li>
1863
+ <li>The session ID is invalid</li>
1864
+ </ul>
1865
+ <p>You can start a new conversation by <a href="/" style="color: #1976d2;">returning to the home page</a>.</p>
1866
+ </div>
1867
+ `;
1868
+ messagesDiv.appendChild(messageDiv);
1869
+
1870
+ // Update UI to show the message
1871
+ positionInputForm();
1872
+ }
1873
+
1874
+ // Function to update URL when session changes (for new chats)
1875
+ function updateUrlForSession(sessionId) {
1876
+ const newUrl = `/chat/${sessionId}`;
1877
+ if (window.location.pathname !== newUrl) {
1878
+ window.history.pushState({ sessionId }, '', newUrl);
1879
+ console.log(`Updated URL to: ${newUrl}`);
1880
+ }
1881
+ }
1882
+
1883
+ // Handle browser back/forward navigation
1884
+ window.addEventListener('popstate', (event) => {
1885
+ if (event.state && event.state.sessionId) {
1886
+ // User navigated to a different session
1887
+ console.log(`Navigating to session: ${event.state.sessionId}`);
1888
+ window.location.reload(); // Reload to restore the session
1889
+ }
1890
+ });
1891
+
1892
+ // History dropdown functionality
1893
+ class HistoryDropdown {
1894
+ constructor() {
1895
+ this.button = document.getElementById('history-button');
1896
+ this.menu = document.getElementById('history-dropdown-menu');
1897
+ this.loading = document.getElementById('history-loading');
1898
+ this.list = document.getElementById('history-list');
1899
+ this.empty = document.getElementById('history-empty');
1900
+ this.isOpen = false;
1901
+
1902
+ this.init();
1903
+ }
1904
+
1905
+ init() {
1906
+ // Button click handler
1907
+ this.button.addEventListener('click', (e) => {
1908
+ e.preventDefault();
1909
+ e.stopPropagation();
1910
+ this.toggle();
1911
+ });
1912
+
1913
+ // Close dropdown when clicking outside
1914
+ document.addEventListener('click', (e) => {
1915
+ if (!this.menu.contains(e.target) && !this.button.contains(e.target)) {
1916
+ this.close();
1917
+ }
1918
+ });
1919
+
1920
+ // Prevent dropdown from closing when clicking inside
1921
+ this.menu.addEventListener('click', (e) => {
1922
+ e.stopPropagation();
1923
+ });
1924
+ }
1925
+
1926
+ async toggle() {
1927
+ if (this.isOpen) {
1928
+ this.close();
1929
+ } else {
1930
+ await this.open();
1931
+ }
1932
+ }
1933
+
1934
+ async open() {
1935
+ this.isOpen = true;
1936
+ this.menu.classList.add('show');
1937
+ this.showLoading();
1938
+
1939
+ try {
1940
+ await this.loadSessions();
1941
+ } catch (error) {
1942
+ console.error('Error loading sessions:', error);
1943
+ this.showError();
1944
+ }
1945
+ }
1946
+
1947
+ close() {
1948
+ this.isOpen = false;
1949
+ this.menu.classList.remove('show');
1950
+ }
1951
+
1952
+ showLoading() {
1953
+ this.loading.style.display = 'block';
1954
+ this.list.style.display = 'none';
1955
+ this.empty.style.display = 'none';
1956
+ }
1957
+
1958
+ showList() {
1959
+ this.loading.style.display = 'none';
1960
+ this.list.style.display = 'block';
1961
+ this.empty.style.display = 'none';
1962
+ }
1963
+
1964
+ showEmpty() {
1965
+ this.loading.style.display = 'none';
1966
+ this.list.style.display = 'none';
1967
+ this.empty.style.display = 'block';
1968
+ }
1969
+
1970
+ showError() {
1971
+ this.loading.textContent = 'Error loading history';
1972
+ this.loading.style.color = '#f44336';
1973
+ }
1974
+
1975
+ async loadSessions() {
1976
+ try {
1977
+ const response = await fetch('/api/sessions');
1978
+ const data = await response.json();
1979
+
1980
+ if (data.sessions && data.sessions.length > 0) {
1981
+ this.renderSessions(data.sessions);
1982
+ this.showList();
1983
+ } else {
1984
+ this.showEmpty();
1985
+ }
1986
+ } catch (error) {
1987
+ console.error('Error fetching sessions:', error);
1988
+ throw error;
1989
+ }
1990
+ }
1991
+
1992
+ renderSessions(sessions) {
1993
+ this.list.innerHTML = '';
1994
+
1995
+ sessions.forEach(session => {
1996
+ const item = document.createElement('div');
1997
+ item.className = 'history-item';
1998
+ item.dataset.sessionId = session.sessionId;
1999
+
2000
+ // Check if this is the current session
2001
+ const isCurrent = session.sessionId === window.sessionId;
2002
+ if (isCurrent) {
2003
+ item.style.backgroundColor = '#e3f2fd';
2004
+ }
2005
+
2006
+ item.innerHTML = `
2007
+ <div class="history-item-preview">${this.escapeHtml(session.preview)}</div>
2008
+ <div class="history-item-meta">
2009
+ <span class="history-item-time">${session.relativeTime}</span>
2010
+ <span class="history-item-count">${session.messageCount} messages</span>
2011
+ </div>
2012
+ `;
2013
+
2014
+ item.addEventListener('click', () => {
2015
+ if (!isCurrent) {
2016
+ this.navigateToSession(session.sessionId);
2017
+ }
2018
+ this.close();
2019
+ });
2020
+
2021
+ this.list.appendChild(item);
2022
+ });
2023
+ }
2024
+
2025
+ navigateToSession(sessionId) {
2026
+ const url = `/chat/${sessionId}`;
2027
+ console.log(`Navigating to session: ${sessionId}`);
2028
+ window.location.href = url;
2029
+ }
2030
+
2031
+ escapeHtml(text) {
2032
+ const div = document.createElement('div');
2033
+ div.textContent = text;
2034
+ return div.innerHTML;
2035
+ }
2036
+ }
2037
+
2038
+ // Initialize history dropdown
2039
+ document.addEventListener('DOMContentLoaded', () => {
2040
+ new HistoryDropdown();
2041
+ });
2042
+ const messagesDiv = document.getElementById('messages');
2043
+ const form = document.getElementById('input-form');
2044
+ const searchSuggestionsDiv = document.querySelector('.search-suggestions');
2045
+ const input = document.getElementById('message-input');
2046
+ const folderListDiv = document.getElementById('folder-list');
2047
+
2048
+ // Position the input form in the center initially and handle UI elements visibility
2049
+ function positionInputForm() {
2050
+ const footer = document.querySelector('.footer');
2051
+ const header = document.querySelector('.header');
2052
+ const emptyStateLogo = document.getElementById('empty-state-logo');
2053
+
2054
+ if (messagesDiv.children.length === 0) {
2055
+ form.classList.add('centered');
2056
+ form.classList.remove('bottom');
2057
+ // Show footer when no messages
2058
+ if (footer) {
2059
+ footer.style.display = 'block';
2060
+ }
2061
+ // Always show the header (it contains the history dropdown)
2062
+ if (header) {
2063
+ header.style.display = 'block';
2064
+ }
2065
+ if (emptyStateLogo) {
2066
+ emptyStateLogo.style.display = 'block';
2067
+ }
2068
+ } else {
2069
+ form.classList.remove('centered');
2070
+ form.classList.add('bottom');
2071
+ // Hide footer when chat is started
2072
+ if (footer) {
2073
+ footer.style.display = 'none';
2074
+ }
2075
+ // Show the top header and hide the centered logo
2076
+ if (header) {
2077
+ header.style.display = 'block';
2078
+ }
2079
+ if (emptyStateLogo) {
2080
+ emptyStateLogo.style.display = 'none';
2081
+ }
2082
+ }
2083
+ }
2084
+
2085
+ // Make search suggestions clickable
2086
+ function setupSearchSuggestions() {
2087
+ document.querySelectorAll('.search-suggestions li').forEach(item => {
2088
+ item.addEventListener('click', () => {
2089
+ input.value = item.textContent;
2090
+ input.focus();
2091
+ });
2092
+ });
2093
+ }
2094
+
2095
+ // Initialize on page load
2096
+ window.addEventListener('load', () => {
2097
+ setupSearchSuggestions();
2098
+ positionInputForm();
2099
+ positionSearchSuggestions();
2100
+
2101
+ // Focus the input field on page load
2102
+ setTimeout(() => {
2103
+ const inputField = document.getElementById('message-input');
2104
+ if (inputField) {
2105
+ inputField.focus();
2106
+ }
2107
+ }, 100);
2108
+ });
2109
+
2110
+
2111
+ // Position search suggestions relative to input form
2112
+ function positionSearchSuggestions() {
2113
+ const formRect = form.getBoundingClientRect();
2114
+
2115
+ if (form.classList.contains('centered')) {
2116
+ // Position directly below the form
2117
+ searchSuggestionsDiv.style.top = formRect.bottom + 'px';
2118
+ searchSuggestionsDiv.style.display = 'block';
2119
+ } else {
2120
+ searchSuggestionsDiv.style.display = 'none';
2121
+ }
2122
+ }
2123
+
2124
+ // Update search suggestions position when window is resized
2125
+ window.addEventListener('resize', positionSearchSuggestions);
2126
+
2127
+ // Check if Mermaid is properly loaded
2128
+ function checkMermaidLoaded() {
2129
+ if (typeof mermaid === 'undefined') {
2130
+ console.error('Mermaid is not loaded properly');
2131
+ return false;
2132
+ }
2133
+ console.log('Mermaid version:', mermaid.version);
2134
+ return true;
2135
+ }
2136
+
2137
+ // Initialize mermaid
2138
+ if (checkMermaidLoaded()) {
2139
+ mermaid.initialize({
2140
+ startOnLoad: false,
2141
+ theme: 'default',
2142
+ securityLevel: 'loose',
2143
+ flowchart: { htmlLabels: true },
2144
+ logLevel: 3, // Add logging for debugging (1: error, 2: warn, 3: info, 4: debug, 5: trace)
2145
+ fontFamily: 'monospace'
2146
+ });
2147
+
2148
+ // Run mermaid on page load to render the test diagram
2149
+ window.addEventListener('DOMContentLoaded', () => {
2150
+ setTimeout(() => {
2151
+ try {
2152
+ console.log('Running mermaid on page load');
2153
+ mermaid.run();
2154
+ } catch (error) {
2155
+ console.error('Error initializing mermaid:', error);
2156
+ }
2157
+ }, 500);
2158
+ });
2159
+ }
2160
+
2161
+ // Configure marked.js
2162
+ // Configure Marked.js with logging
2163
+ marked.setOptions({
2164
+ highlight: function (code, lang) {
2165
+ console.log(`Highlighting code with language: ${lang}`);
2166
+ if (lang === 'mermaid') {
2167
+ console.log('Returning mermaid div');
2168
+ return `<div class="mermaid">${code}</div>`;
2169
+ }
2170
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
2171
+ return hljs.highlight(code, { language }).value;
2172
+ },
2173
+ langPrefix: 'hljs language-',
2174
+ gfm: true,
2175
+ breaks: true
2176
+ });
2177
+ // Fetch API key status and check for no API keys mode on page load
2178
+ window.addEventListener('DOMContentLoaded', async () => {
2179
+ // First check if we have an API key in local storage
2180
+ const storedApiKey = localStorage.getItem('probeApiKey');
2181
+ if (storedApiKey) {
2182
+ // Show the reset button in the header
2183
+ const headerResetButton = document.getElementById('header-reset-api-key');
2184
+ if (headerResetButton) {
2185
+ headerResetButton.style.display = 'inline-block';
2186
+ }
2187
+ }
2188
+ // Check if we're in API key setup mode
2189
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
2190
+ const inputForm = document.getElementById('input-form');
2191
+ const searchSuggestions = document.querySelector('.search-suggestions');
2192
+
2193
+ // If API key setup is visible, we're in API setup mode
2194
+ if (apiKeySetupDiv && window.getComputedStyle(apiKeySetupDiv).display !== 'none') {
2195
+ // Add class to body for API setup mode styling
2196
+ document.body.classList.add('api-setup-mode');
2197
+
2198
+ // Hide search suggestions and input form
2199
+ if (inputForm) inputForm.style.display = 'none';
2200
+ if (searchSuggestions) searchSuggestions.style.display = 'none';
2201
+ } else {
2202
+ // Remove API setup mode class if not in setup mode
2203
+ document.body.classList.remove('api-setup-mode');
2204
+ }
2205
+
2206
+ try {
2207
+ const response = await fetch('/folders');
2208
+ const data = await response.json();
2209
+
2210
+ // Check if we're in no API keys mode
2211
+ if (data.noApiKeysMode) {
2212
+ handleNoApiKeysMode();
2213
+ }
2214
+
2215
+ // Display folder information
2216
+ displayFolderInfo(data.folders);
2217
+ } catch (error) {
2218
+ console.error('Error fetching API status:', error);
2219
+ }
2220
+ });
2221
+
2222
+ // Function to display folder information
2223
+ function displayFolderInfo(folders) {
2224
+ const folderInfoDiv = document.getElementById('folder-info');
2225
+
2226
+ if (!folderInfoDiv) return;
2227
+
2228
+ // Clear any existing content
2229
+ folderInfoDiv.innerHTML = '';
2230
+
2231
+ // Set a loading message
2232
+ folderInfoDiv.textContent = 'Determining search location...';
2233
+
2234
+ // Fetch the current directory from the server's /folders endpoint
2235
+ fetch('/folders')
2236
+ .then(response => response.json())
2237
+ .then(data => {
2238
+ // Use the currentDir property which contains the absolute path
2239
+ if (data.currentDir) {
2240
+ // Display the absolute path from the server
2241
+ folderInfoDiv.textContent = `Searching in: ${data.currentDir}`;
2242
+
2243
+ // If there are multiple folders, show that info
2244
+ if (data.folders && data.folders.length > 1) {
2245
+ folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`;
2246
+ }
2247
+ }
2248
+ // Fallback to folders if currentDir is not available
2249
+ else if (data.folders && data.folders.length > 0) {
2250
+ folderInfoDiv.textContent = `Searching in: ${data.folders[0]}`;
2251
+
2252
+ if (data.folders.length > 1) {
2253
+ folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`;
2254
+ }
2255
+ }
2256
+ // Last resort fallback
2257
+ else {
2258
+ folderInfoDiv.textContent = `Searching in: . (current directory)`;
2259
+ }
2260
+ })
2261
+ .catch(error => {
2262
+ console.error('Error fetching folder info:', error);
2263
+ folderInfoDiv.textContent = `Searching in: . (current directory)`;
2264
+ });
2265
+ }
2266
+
2267
+ // Handle no API keys mode
2268
+ function handleNoApiKeysMode() {
2269
+ // Check if body has the data-no-api-keys attribute
2270
+ const noApiKeys = document.body.getAttribute('data-no-api-keys') === 'true';
2271
+
2272
+ // Check if API key is already stored in local storage
2273
+ const storedApiKey = localStorage.getItem('probeApiKey');
2274
+
2275
+ // Add or remove api-setup-mode class based on whether we need to show the API key setup
2276
+ if (noApiKeys && !storedApiKey) {
2277
+ document.body.classList.add('api-setup-mode');
2278
+ } else {
2279
+ document.body.classList.remove('api-setup-mode');
2280
+ }
2281
+
2282
+ // Get UI elements
2283
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
2284
+ const inputForm = document.getElementById('input-form');
2285
+ const searchSuggestions = document.querySelector('.search-suggestions');
2286
+
2287
+ if (noApiKeys && !storedApiKey) {
2288
+ console.log('No API keys detected and no local storage key - showing setup instructions');
2289
+
2290
+ // Show the API key setup div
2291
+ if (apiKeySetupDiv) {
2292
+ apiKeySetupDiv.style.display = 'block';
2293
+ }
2294
+
2295
+ // Hide the chat interface elements
2296
+ if (inputForm) {
2297
+ inputForm.style.display = 'none';
2298
+ }
2299
+
2300
+ if (searchSuggestions) {
2301
+ searchSuggestions.style.display = 'none';
2302
+ }
2303
+ } else if (noApiKeys && storedApiKey) {
2304
+ console.log('No server API keys but local storage key found - enabling chat interface');
2305
+
2306
+ // Hide the API key setup div
2307
+ if (apiKeySetupDiv) {
2308
+ apiKeySetupDiv.style.display = 'none';
2309
+ }
2310
+
2311
+ // Show the chat interface elements
2312
+ if (inputForm) {
2313
+ inputForm.style.display = 'flex';
2314
+ }
2315
+
2316
+ // Remove API setup mode class
2317
+ document.body.classList.remove('api-setup-mode');
2318
+ }
2319
+ }
2320
+
2321
+
2322
+ // Render markdown content
2323
+ function renderMarkdown(text) {
2324
+ // Just parse the markdown and return the HTML
2325
+ return marked.parse(text);
2326
+ }
2327
+
2328
+ // Test function to manually render a Mermaid diagram
2329
+ function testMermaidRendering() {
2330
+ console.log('Testing Mermaid rendering...');
2331
+ try {
2332
+ // Create a simple test diagram directly
2333
+ const testDiv = document.createElement('div');
2334
+ testDiv.className = 'mermaid';
2335
+ testDiv.textContent = 'graph TD;\nA-->B;';
2336
+ document.body.appendChild(testDiv);
2337
+
2338
+ console.log('Created test diagram with content:', testDiv.textContent);
2339
+
2340
+ // Render the direct mermaid div
2341
+ setTimeout(() => {
2342
+ try {
2343
+ console.log('Running mermaid on test div');
2344
+ if (typeof mermaid.run === 'function') {
2345
+ console.log('Using mermaid.run() for test');
2346
+ mermaid.run({
2347
+ nodes: [testDiv]
2348
+ });
2349
+ } else if (typeof mermaid.init === 'function') {
2350
+ console.log('Using mermaid.init() for test');
2351
+ mermaid.init(undefined, [testDiv]);
2352
+ }
2353
+
2354
+ // Verify if rendering worked
2355
+ setTimeout(() => {
2356
+ const svg = testDiv.querySelector('svg');
2357
+ if (svg) {
2358
+ console.log('Test diagram rendered successfully!');
2359
+ } else {
2360
+ console.error('Test diagram did not render to SVG');
2361
+ }
2362
+
2363
+ // Remove test div after verification
2364
+ document.body.removeChild(testDiv);
2365
+ }, 100);
2366
+ } catch (error) {
2367
+ console.error('Error rendering test mermaid diagram:', error);
2368
+ console.error('Error details:', error.message);
2369
+
2370
+ // Remove test div on error
2371
+ document.body.removeChild(testDiv);
2372
+ }
2373
+ }, 200);
2374
+ } catch (error) {
2375
+ console.error('Unexpected error in test function:', error);
2376
+ }
2377
+ }
2378
+
2379
+ // Run test on page load
2380
+ window.addEventListener('DOMContentLoaded', () => {
2381
+ setTimeout(testMermaidRendering, 1000);
2382
+ });
2383
+
2384
+ // Connect to SSE endpoint for tool calls
2385
+ let eventSource;
2386
+ let currentAiMessageDiv = null;
2387
+
2388
+ function connectToToolEvents() {
2389
+ // Close existing connection if any
2390
+ if (eventSource) {
2391
+ console.log('Closing existing SSE connection');
2392
+ eventSource.close();
2393
+ }
2394
+
2395
+ // Clear any existing displayed tool calls when connecting with a new session ID
2396
+ if (window.displayedToolCalls) {
2397
+ window.displayedToolCalls.clear();
2398
+ console.log('Cleared displayed tool calls for new session');
2399
+ }
2400
+
2401
+ console.log(`%c Connecting to SSE endpoint with session ID: ${sessionId}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2402
+ // Connect to SSE endpoint with session ID
2403
+ const sseUrl = `/api/tool-events?sessionId=${sessionId}`;
2404
+ console.log('SSE URL:', sseUrl);
2405
+
2406
+ // Add a timestamp to prevent caching in Firefox
2407
+ const nocacheUrl = `${sseUrl}&_nocache=${Date.now()}`;
2408
+ eventSource = new EventSource(nocacheUrl);
2409
+
2410
+ // Handle connection event
2411
+ eventSource.addEventListener('connection', (event) => {
2412
+ console.log('%c Connected to tool events stream', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event.data);
2413
+ try {
2414
+ const connectionData = JSON.parse(event.data);
2415
+ console.log('Connection data:', connectionData);
2416
+ } catch (error) {
2417
+ console.error('Error parsing connection data:', error, event.data);
2418
+ }
2419
+ });
2420
+
2421
+ // Handle test events
2422
+ eventSource.addEventListener('test', (event) => {
2423
+ console.log('%c Received test event:', 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;', event.data);
2424
+ try {
2425
+ const testData = JSON.parse(event.data);
2426
+ console.log('%c Test data:', 'background: #673AB7; color: white; padding: 2px 5px; border-radius: 2px;', testData);
2427
+
2428
+ // Log specific test data properties
2429
+ console.log('Test message:', testData.message);
2430
+ console.log('Test timestamp:', testData.timestamp);
2431
+ console.log('Test session ID:', testData.sessionId);
2432
+
2433
+ if (testData.status) {
2434
+ console.log('Test status:', testData.status);
2435
+ }
2436
+
2437
+ if (testData.connectionInfo) {
2438
+ console.log('Connection info:', testData.connectionInfo);
2439
+ }
2440
+
2441
+ if (testData.sequence === 2) {
2442
+ console.log('%c SSE connection fully verified with follow-up test', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2443
+ }
2444
+
2445
+ // Add a visual indicator that the SSE connection is working
2446
+ const connectionIndicator = document.createElement('div');
2447
+ connectionIndicator.style.position = 'fixed';
2448
+ connectionIndicator.style.bottom = '10px';
2449
+ connectionIndicator.style.right = '10px';
2450
+ connectionIndicator.style.backgroundColor = '#4CAF50';
2451
+ connectionIndicator.style.color = 'white';
2452
+ connectionIndicator.style.padding = '5px 10px';
2453
+ connectionIndicator.style.borderRadius = '4px';
2454
+ connectionIndicator.style.fontSize = '12px';
2455
+ connectionIndicator.style.zIndex = '1000';
2456
+ connectionIndicator.style.opacity = '0.8';
2457
+ connectionIndicator.textContent = 'SSE Connected';
2458
+
2459
+ // Remove after 3 seconds
2460
+ setTimeout(() => {
2461
+ if (document.body.contains(connectionIndicator)) {
2462
+ document.body.removeChild(connectionIndicator);
2463
+ }
2464
+ }, 3000);
2465
+
2466
+ document.body.appendChild(connectionIndicator);
2467
+
2468
+ } catch (error) {
2469
+ console.error('Error parsing test event data:', error, event.data);
2470
+ }
2471
+ });
2472
+
2473
+ // Initialize a Set to track displayed tool calls
2474
+ if (!window.displayedToolCalls) {
2475
+ window.displayedToolCalls = new Set();
2476
+ }
2477
+
2478
+ // Handle tool call events
2479
+ eventSource.addEventListener('toolCall', (event) => {
2480
+ // If no request is in progress, ignore the tool call
2481
+ if (!isRequestInProgress) {
2482
+ console.log('Tool call received but no request in progress, ignoring');
2483
+ return;
2484
+ }
2485
+ console.log('%c Received tool call event:', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event);
2486
+ try {
2487
+ const toolCall = JSON.parse(event.data);
2488
+ console.log('%c Tool call data:', 'background: #2196F3; color: white; padding: 2px 5px; border-radius: 2px;', toolCall);
2489
+
2490
+ // Skip events with status "started" - only process "completed" events
2491
+ if (toolCall.status === "started") {
2492
+ console.log('%c Skipping "started" event, waiting for "completed"', 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2493
+ return;
2494
+ }
2495
+
2496
+ // Create a unique identifier for this tool call
2497
+ const query = toolCall.args.query || toolCall.args.keywords || toolCall.args.pattern || '';
2498
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2499
+
2500
+ // Create a simpler fingerprint that doesn't include timestamp
2501
+ // This helps catch duplicate events with different timestamps
2502
+ const toolCallFingerprint = `${toolCall.name}-${query}-${path}`;
2503
+
2504
+ // Check if we've already displayed this exact tool call
2505
+ if (window.displayedToolCalls.has(toolCallFingerprint)) {
2506
+ console.log(`%c Skipping duplicate tool call: ${toolCallFingerprint}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2507
+ return;
2508
+ }
2509
+
2510
+ // Add this tool call to our set of displayed tool calls
2511
+ window.displayedToolCalls.add(toolCallFingerprint);
2512
+ console.log(`%c Added tool call to displayed set: ${toolCallFingerprint}`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;');
2513
+
2514
+ // Format the tool call description for display
2515
+ let toolDescription = '';
2516
+ if (toolCall.name === 'searchCode' || toolCall.name === 'search') {
2517
+ const language = toolCall.args.language;
2518
+ const exact = toolCall.args.exact;
2519
+
2520
+ let locationInfo = path !== '.' ? ` in ${path}` : '';
2521
+ let languageInfo = language ? ` (language: ${language})` : '';
2522
+ let exactInfo = exact === true ? ` (exact match)` : '';
2523
+
2524
+ toolDescription = `Searching code with "${query}"${locationInfo}${languageInfo}${exactInfo}`;
2525
+ } else if (toolCall.name === 'queryCode' || toolCall.name === 'query') {
2526
+ toolDescription = `Querying code with pattern "${query}"${path === '.' ? '' : ` in ${path}`}`;
2527
+ } else if (toolCall.name === 'extractCode' || toolCall.name === 'extract') {
2528
+ const filePath = toolCall.args.file_path || '';
2529
+ const line = toolCall.args.line;
2530
+ const endLine = toolCall.args.end_line;
2531
+
2532
+ let lineInfo = '';
2533
+ if (line && endLine) {
2534
+ lineInfo = ` (lines ${line}-${endLine})`;
2535
+ } else if (line) {
2536
+ lineInfo = ` (from line ${line})`;
2537
+ }
2538
+
2539
+ toolDescription = `Extracting code from ${filePath}${lineInfo}`;
2540
+ } else {
2541
+ toolDescription = `Using ${toolCall.name} tool`;
2542
+ }
2543
+
2544
+ // Log the tool call being processed
2545
+ console.log(`%c Processing tool call: "${toolDescription}"`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;');
2546
+
2547
+ // Add tool call to the current AI message if it exists
2548
+ if (currentAiMessageDiv) {
2549
+ addToolCallToMessage(currentAiMessageDiv, toolCall);
2550
+ } else {
2551
+ console.warn('No current AI message div to add tool call to');
2552
+ // Create a temporary div to display the tool call
2553
+ const tempDiv = document.createElement('div');
2554
+ tempDiv.className = 'ai-message';
2555
+ tempDiv.innerHTML = '<div class="tool-call-header">Tool call received but no message context</div>';
2556
+ messagesDiv.appendChild(tempDiv);
2557
+ addToolCallToMessage(tempDiv, toolCall);
2558
+ }
2559
+ } catch (error) {
2560
+ console.error('Error parsing tool call data:', error, event.data);
2561
+ }
2562
+ });
2563
+
2564
+ // Handle errors
2565
+ eventSource.onerror = (error) => {
2566
+ console.error('%c SSE Error:', 'background: #F44336; color: white; padding: 2px 5px; border-radius: 2px;', error);
2567
+
2568
+ // Log detailed readyState information
2569
+ const readyStateMap = {
2570
+ 0: 'CONNECTING',
2571
+ 1: 'OPEN',
2572
+ 2: 'CLOSED'
2573
+ };
2574
+ const readyState = eventSource.readyState;
2575
+ console.log(`EventSource readyState: ${readyState} (${readyStateMap[readyState] || 'UNKNOWN'})`);
2576
+
2577
+ // Check if the connection was established before the error
2578
+ if (readyState === 2) {
2579
+ console.log('Connection was closed. Attempting to reconnect...');
2580
+ } else if (readyState === 0) {
2581
+ console.log('Connection is still trying to connect. Will retry if it fails.');
2582
+ }
2583
+
2584
+ // Try to reconnect after a delay
2585
+ console.log('Will attempt to reconnect in 5 seconds...');
2586
+ setTimeout(connectToToolEvents, 5000);
2587
+ };
2588
+
2589
+ // Add open event handler
2590
+ eventSource.onopen = () => {
2591
+ console.log('%c SSE connection opened successfully', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2592
+ console.log('Ready to receive tool call events for session:', sessionId);
2593
+ };
2594
+ }
2595
+
2596
+ // Add tool call to the AI message
2597
+ function addToolCallToMessage(messageDiv, toolCall) {
2598
+ console.log('%c Adding tool call to message:', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', toolCall);
2599
+
2600
+ try {
2601
+ // Format the tool call description for display
2602
+ let toolDescription = '';
2603
+ if (toolCall.name === 'searchCode' || toolCall.name === 'search') {
2604
+ const query = toolCall.args.query || toolCall.args.keywords || '';
2605
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2606
+ const language = toolCall.args.language;
2607
+ const exact = toolCall.args.exact;
2608
+
2609
+ let locationInfo = path !== '.' ? ` in ${path}` : '';
2610
+ let languageInfo = language ? ` (language: ${language})` : '';
2611
+ let exactInfo = exact === true ? ` (exact match)` : '';
2612
+
2613
+ toolDescription = `Searching code with "${query}"${locationInfo}${languageInfo}${exactInfo}`;
2614
+ } else if (toolCall.name === 'queryCode' || toolCall.name === 'query') {
2615
+ const query = toolCall.args.query || toolCall.args.pattern || '';
2616
+ const path = toolCall.args.path || toolCall.args.folder || '.';
2617
+ toolDescription = `Querying code with pattern "${query}"${path === '.' ? '' : ` in ${path}`}`;
2618
+ } else if (toolCall.name === 'extractCode' || toolCall.name === 'extract') {
2619
+ const filePath = toolCall.args.file_path || '';
2620
+ const line = toolCall.args.line;
2621
+ const endLine = toolCall.args.end_line;
2622
+
2623
+ let lineInfo = '';
2624
+ if (line && endLine) {
2625
+ lineInfo = ` (lines ${line}-${endLine})`;
2626
+ } else if (line) {
2627
+ lineInfo = ` (from line ${line})`;
2628
+ }
2629
+
2630
+ toolDescription = `Extracting code from ${filePath}${lineInfo}`;
2631
+ } else if (toolCall.name === 'searchFiles') {
2632
+ const pattern = toolCall.args.pattern || '';
2633
+ const directory = toolCall.args.directory || '.';
2634
+ toolDescription = `Searching for files matching "${pattern}"${directory !== '.' ? ` in ${directory}` : ''}`;
2635
+ } else if (toolCall.name === 'listFiles') {
2636
+ const directory = toolCall.args.directory || '.';
2637
+ const pattern = toolCall.args.pattern || '';
2638
+ toolDescription = `Listing files${pattern ? ` matching "${pattern}"` : ''}${directory !== '.' ? ` in ${directory}` : ''}`;
2639
+ } else {
2640
+ toolDescription = `Using ${toolCall.name} tool`;
2641
+ }
2642
+
2643
+ // Create a simple paragraph element with the formatted description
2644
+ const paragraph = document.createElement('p');
2645
+ paragraph.textContent = toolDescription;
2646
+ paragraph.style.fontStyle = 'italic';
2647
+ paragraph.style.color = '#555';
2648
+ paragraph.style.margin = '8px 0';
2649
+
2650
+ // Add the paragraph to the message div
2651
+ messageDiv.appendChild(paragraph);
2652
+
2653
+ // Scroll to the bottom
2654
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
2655
+
2656
+ console.log('%c Tool call added successfully', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;');
2657
+ } catch (error) {
2658
+ console.error('Error adding tool call to message:', error);
2659
+ }
2660
+ }
2661
+ // Connect to tool events on page load
2662
+ window.addEventListener('DOMContentLoaded', () => {
2663
+ // Check if we're in no API keys mode
2664
+ const noApiKeys = document.body.getAttribute('data-no-api-keys') === 'true';
2665
+
2666
+ if (!noApiKeys) {
2667
+ connectToToolEvents();
2668
+ }
2669
+ });
2670
+
2671
+ // Handle "New chat" button click
2672
+ document.addEventListener('DOMContentLoaded', () => {
2673
+ const newChatLink = document.querySelector('.new-chat-link');
2674
+ if (newChatLink) {
2675
+ newChatLink.addEventListener('click', (e) => {
2676
+ e.preventDefault();
2677
+
2678
+ // Cancel any ongoing requests for the current session
2679
+ cancelRequest(sessionId).catch(err => console.error('Error cancelling session on new chat:', err));
2680
+
2681
+ // Generate a new session ID
2682
+ sessionId = crypto.randomUUID();
2683
+ console.log(`New chat started in current window. New session ID: ${sessionId}`);
2684
+
2685
+ // Update URL to reflect new session
2686
+ updateUrlForSession(sessionId);
2687
+
2688
+ // Make session ID available to other scripts
2689
+ // We use both direct property assignment and event dispatch for compatibility
2690
+ window.sessionId = sessionId;
2691
+
2692
+ // Dispatch an event with the session ID for any external scripts that may be listening
2693
+ window.dispatchEvent(new MessageEvent('message', {
2694
+ data: { sessionId: sessionId }
2695
+ }));
2696
+
2697
+ // Clear the messages
2698
+ messagesDiv.innerHTML = '';
2699
+
2700
+ // Reset the UI
2701
+ positionInputForm();
2702
+ searchSuggestionsDiv.style.display = 'block';
2703
+
2704
+ // Hide and reset token usage display
2705
+ const tokenUsageElement = document.getElementById('token-usage');
2706
+ if (tokenUsageElement) {
2707
+ tokenUsageElement.style.display = 'none';
2708
+
2709
+ // Reset token usage counters
2710
+ document.getElementById('current-request').textContent = '0';
2711
+ document.getElementById('current-response').textContent = '0';
2712
+ document.getElementById('total-request').textContent = '0';
2713
+ document.getElementById('total-response').textContent = '0';
2714
+ }
2715
+
2716
+ // Close existing SSE connection and reconnect with new session ID
2717
+ if (eventSource) {
2718
+ eventSource.close();
2719
+ }
2720
+ connectToToolEvents();
2721
+
2722
+ // Send a request to the server to clear the chat history for this session
2723
+ fetch('/chat', {
2724
+ method: 'POST',
2725
+ headers: { 'Content-Type': 'application/json' },
2726
+ body: JSON.stringify({
2727
+ message: '__clear_history__',
2728
+ sessionId,
2729
+ clearHistory: true
2730
+ })
2731
+ }).catch(err => console.error('Error clearing chat history:', err));
2732
+ });
2733
+ }
2734
+ });
2735
+
2736
+ // Add event listener for page unload to cancel the current session
2737
+ window.addEventListener('beforeunload', () => {
2738
+ // If we have a sessionId that's about to become invalid, cancel it
2739
+ if (sessionId) {
2740
+ // Use navigator.sendBeacon for more reliable delivery during page unload
2741
+ const data = JSON.stringify({ sessionId });
2742
+ if (navigator.sendBeacon) {
2743
+ navigator.sendBeacon('/cancel-request', data);
2744
+ } else {
2745
+ // Fallback to fetch for older browsers
2746
+ fetch('/cancel-request', {
2747
+ method: 'POST',
2748
+ headers: { 'Content-Type': 'application/json' },
2749
+ body: data,
2750
+ // Use keepalive to ensure the request completes even if the page is unloading
2751
+ keepalive: true
2752
+ }).catch((err) => console.error('Error cancelling session on unload:', err));
2753
+ }
2754
+ }
2755
+ });
2756
+
2757
+ // Controller for aborting fetch requests
2758
+ let currentController = null;
2759
+ // Flag to track if a request is in progress
2760
+ let isRequestInProgress = false;
2761
+
2762
+ // Function to cancel the current request on the server
2763
+ async function cancelRequest(sessionId) {
2764
+ try {
2765
+ const response = await fetch('/cancel-request', {
2766
+ method: 'POST',
2767
+ headers: { 'Content-Type': 'application/json' },
2768
+ body: JSON.stringify({ sessionId })
2769
+ });
2770
+
2771
+ if (response.ok) {
2772
+ console.log('Request cancelled successfully on server');
2773
+ } else {
2774
+ console.error('Failed to cancel request on server');
2775
+ }
2776
+ } catch (error) {
2777
+ console.error('Error cancelling request:', error);
2778
+ }
2779
+ }
2780
+
2781
+ // Handle form submission
2782
+ // Use the button click event instead of form submit to avoid potential form submission issues
2783
+ const searchButton = document.getElementById('search-button');
2784
+ searchButton.addEventListener('click', async (e) => {
2785
+ e.preventDefault();
2786
+
2787
+ // If this is a stop action
2788
+ if (searchButton.textContent === 'Stop') {
2789
+ // Abort the current fetch request
2790
+ if (currentController) {
2791
+ currentController.abort();
2792
+ currentController = null;
2793
+ }
2794
+
2795
+ // Send cancellation request to the server
2796
+ if (isRequestInProgress) {
2797
+ await cancelRequest(sessionId);
2798
+ isRequestInProgress = false;
2799
+
2800
+ // Stop token usage polling when request is cancelled
2801
+ stopTokenUsagePolling();
2802
+ }
2803
+
2804
+ // Reset the button to "Search" and enable input
2805
+ searchButton.textContent = 'Search';
2806
+ searchButton.style.backgroundColor = '#44CDF3';
2807
+ input.disabled = false;
2808
+ return;
2809
+ }
2810
+
2811
+ const message = input.value.trim();
2812
+ if (!message) return;
2813
+
2814
+ // Check if this is the first message
2815
+ const isFirstMessage = messagesDiv.children.length === 0;
2816
+
2817
+ // Update URL for first message if we're on root path
2818
+ if (isFirstMessage && window.location.pathname === '/') {
2819
+ updateUrlForSession(sessionId);
2820
+ }
2821
+
2822
+ // Display user message with proper image handling
2823
+ const userMsgDiv = document.createElement('div');
2824
+ userMsgDiv.className = 'user-message markdown-content'; // Add markdown-content class
2825
+
2826
+ // Render the text message
2827
+ userMsgDiv.innerHTML = renderMarkdown(message);
2828
+
2829
+ // Get uploaded images for display in user message
2830
+ const userMessageImages = window.getUploadedImagesForChat ? window.getUploadedImagesForChat() : [];
2831
+
2832
+ // Add uploaded images if any
2833
+ if (userMessageImages.length > 0) {
2834
+ userMessageImages.forEach(imageUrl => {
2835
+ const imgElement = document.createElement('img');
2836
+ imgElement.src = imageUrl;
2837
+ imgElement.alt = 'Uploaded image';
2838
+ imgElement.style.maxWidth = '100%';
2839
+ imgElement.style.maxHeight = '300px';
2840
+ imgElement.style.borderRadius = '8px';
2841
+ imgElement.style.margin = '8px 0';
2842
+ imgElement.style.border = '1px solid #e0e0e0';
2843
+ imgElement.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
2844
+ imgElement.style.cursor = 'pointer';
2845
+ imgElement.style.transition = 'transform 0.2s ease';
2846
+ userMsgDiv.appendChild(imgElement);
2847
+ });
2848
+ }
2849
+
2850
+ messagesDiv.appendChild(userMsgDiv);
2851
+
2852
+ // Apply syntax highlighting to code blocks in user message
2853
+ userMsgDiv.querySelectorAll('pre code').forEach((block) => {
2854
+ hljs.highlightElement(block);
2855
+ });
2856
+
2857
+ // Render Mermaid diagrams in user message
2858
+ const userMermaidDivs = userMsgDiv.querySelectorAll('.mermaid');
2859
+ if (userMermaidDivs.length > 0) {
2860
+ console.log(`Found ${userMermaidDivs.length} mermaid diagrams in user message`);
2861
+ try {
2862
+ if (typeof mermaid.run === 'function') {
2863
+ mermaid.run({ nodes: userMermaidDivs });
2864
+ } else if (typeof mermaid.init === 'function') {
2865
+ mermaid.init(undefined, userMermaidDivs);
2866
+ }
2867
+ // Convert rendered SVGs to PNGs
2868
+ setTimeout(() => {
2869
+ const renderedSvgs = userMsgDiv.querySelectorAll('.mermaid svg');
2870
+ if (renderedSvgs.length > 0) {
2871
+ renderedSvgs.forEach((svg, index) => {
2872
+ convertSvgToPng(svg, userMsgDiv, index);
2873
+ });
2874
+ }
2875
+ }, 100);
2876
+ } catch (error) {
2877
+ console.error('Error rendering mermaid in user message:', error);
2878
+ }
2879
+ }
2880
+ input.value = '';
2881
+ autoResizeTextarea(); // Reset textarea height after clearing content
2882
+
2883
+ // If this is the first message, move the input form to the bottom and hide UI elements
2884
+ if (isFirstMessage) {
2885
+ positionInputForm();
2886
+ searchSuggestionsDiv.style.display = 'none';
2887
+
2888
+ // Ensure footer is hidden when chat starts
2889
+ const footer = document.querySelector('.footer');
2890
+ if (footer) {
2891
+ footer.style.display = 'none';
2892
+ }
2893
+
2894
+ // Show token usage display
2895
+ document.getElementById('token-usage').style.display = 'block';
2896
+
2897
+ // Show token usage display
2898
+ const tokenUsageElement = document.getElementById('token-usage');
2899
+ if (tokenUsageElement) {
2900
+ tokenUsageElement.style.display = 'block';
2901
+ }
2902
+
2903
+ // Keep the allowed folders section visible during chat
2904
+ // This is the key change - we don't hide the folder information anymore
2905
+ }
2906
+
2907
+ // Create AI message container
2908
+ const aiMsgDiv = document.createElement('div');
2909
+ aiMsgDiv.className = 'ai-message markdown-content';
2910
+
2911
+ // Store the original message for copying
2912
+ aiMsgDiv.setAttribute('data-original-markdown', '');
2913
+
2914
+ // Add the AI message to the DOM
2915
+ messagesDiv.appendChild(aiMsgDiv);
2916
+
2917
+ // Set as current AI message for tool calls
2918
+ currentAiMessageDiv = aiMsgDiv;
2919
+
2920
+ // Disable input and change button to "Stop"
2921
+ input.disabled = true;
2922
+ searchButton.textContent = 'Stop';
2923
+ searchButton.style.backgroundColor = '#f44336';
2924
+
2925
+ // Set request in progress flag
2926
+ isRequestInProgress = true;
2927
+
2928
+ // Start token usage polling for long-running requests
2929
+ startTokenUsagePolling();
2930
+
2931
+ // Send message to server
2932
+ try {
2933
+ // Log the session ID being used
2934
+ console.log(`%c Using session ID for chat request: ${sessionId}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;');
2935
+
2936
+ // Get API key from local storage if available
2937
+ const storedApiProvider = localStorage.getItem('probeApiProvider');
2938
+ const storedApiKey = localStorage.getItem('probeApiKey');
2939
+ const storedApiUrl = localStorage.getItem('probeApiUrl');
2940
+
2941
+ // Get uploaded images as base64 data URLs
2942
+ const uploadedImageUrls = window.getUploadedImagesForChat ? window.getUploadedImagesForChat() : [];
2943
+
2944
+ const requestData = {
2945
+ message: message, // Keep text separate from images
2946
+ images: uploadedImageUrls, // Send images separately
2947
+ sessionId, // Include session ID with the request
2948
+ apiProvider: storedApiProvider,
2949
+ apiKey: storedApiKey,
2950
+ apiUrl: storedApiUrl
2951
+ };
2952
+
2953
+ if (uploadedImageUrls.length > 0) {
2954
+ console.log(`Including ${uploadedImageUrls.length} uploaded image(s) with message`);
2955
+ }
2956
+ console.log('Sending chat request with data:', requestData);
2957
+
2958
+ // Add a visual indicator that we're using this session ID
2959
+ const sessionIndicator = document.createElement('div');
2960
+ sessionIndicator.style.position = 'fixed';
2961
+ sessionIndicator.style.top = '10px';
2962
+ sessionIndicator.style.right = '10px';
2963
+ sessionIndicator.style.backgroundColor = '#FF9800';
2964
+ sessionIndicator.style.color = 'white';
2965
+ sessionIndicator.style.padding = '5px 10px';
2966
+ sessionIndicator.style.borderRadius = '4px';
2967
+ sessionIndicator.style.fontSize = '12px';
2968
+ sessionIndicator.style.zIndex = '1000';
2969
+ sessionIndicator.style.opacity = '0.8';
2970
+ sessionIndicator.textContent = `Session ID: ${sessionId.substring(0, 8)}...`;
2971
+
2972
+ // Remove after 3 seconds
2973
+ setTimeout(() => {
2974
+ if (document.body.contains(sessionIndicator)) {
2975
+ document.body.removeChild(sessionIndicator);
2976
+ }
2977
+ }, 3000);
2978
+
2979
+ document.body.appendChild(sessionIndicator);
2980
+
2981
+ // Create a new AbortController for this request
2982
+ currentController = new AbortController();
2983
+ const signal = currentController.signal;
2984
+
2985
+ const response = await fetch('/chat', {
2986
+ method: 'POST',
2987
+ headers: {
2988
+ 'Content-Type': 'application/json',
2989
+ 'Cache-Control': 'no-cache',
2990
+ 'Pragma': 'no-cache'
2991
+ },
2992
+ cache: 'no-store',
2993
+ body: JSON.stringify(requestData),
2994
+ signal: signal
2995
+ }).catch(error => {
2996
+ if (error.name === 'AbortError') {
2997
+ console.log('Fetch aborted');
2998
+ aiMsgDiv.innerHTML += '<p><em>Search was stopped by user.</em></p>';
2999
+ return null;
3000
+ }
3001
+ throw error;
3002
+ });
3003
+
3004
+ // If response is null (aborted), reset UI and return
3005
+ if (!response) {
3006
+ // Reset button to "Search" and enable input
3007
+ form.querySelector('button').textContent = 'Search';
3008
+ form.querySelector('button').style.backgroundColor = '#44CDF3';
3009
+ input.disabled = false;
3010
+ return;
3011
+ }
3012
+
3013
+ // We'll rely on polling and final fetch for token usage updates
3014
+ // No need to extract from headers as it's redundant
3015
+
3016
+ const reader = response.body.getReader();
3017
+ const decoder = new TextDecoder();
3018
+ let aiResponse = '';
3019
+
3020
+ while (true) {
3021
+ const { done, value } = await reader.read();
3022
+ if (done) break;
3023
+ const chunk = decoder.decode(value, { stream: true });
3024
+ aiResponse += chunk;
3025
+
3026
+ try {
3027
+ // Parse the JSON response to extract the "response" field
3028
+ const jsonResponse = JSON.parse(aiResponse);
3029
+ const markdownContent = jsonResponse.response;
3030
+
3031
+ // Update the original markdown attribute with just the markdown content
3032
+ aiMsgDiv.setAttribute('data-original-markdown', markdownContent);
3033
+
3034
+ // Render markdown content
3035
+ aiMsgDiv.innerHTML = renderMarkdown(markdownContent);
3036
+
3037
+ // Apply syntax highlighting to code blocks
3038
+ aiMsgDiv.querySelectorAll('pre code').forEach((block) => {
3039
+ hljs.highlightElement(block);
3040
+ });
3041
+
3042
+ // Don't render mermaid diagrams during streaming - will render once at the end
3043
+ // This prevents premature rendering attempts that might fail
3044
+
3045
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
3046
+ } catch (error) {
3047
+ console.error('Error processing response chunk:', error);
3048
+
3049
+ // Check if it's a JSON parsing error or a markdown rendering error
3050
+ if (error instanceof SyntaxError) {
3051
+ // If it's a JSON parsing error, show a message about incomplete response
3052
+ aiMsgDiv.innerHTML = '<p><em>Receiving response...</em></p>';
3053
+ } else {
3054
+ // If it's a markdown rendering error, try to parse JSON but show raw content
3055
+ try {
3056
+ const jsonResponse = JSON.parse(aiResponse);
3057
+ aiMsgDiv.textContent = jsonResponse.response || aiResponse;
3058
+ } catch (jsonError) {
3059
+ // If JSON parsing fails, show the raw text
3060
+ aiMsgDiv.textContent = aiResponse;
3061
+ }
3062
+ }
3063
+ }
3064
+ }
3065
+
3066
+ // Final render after all content is received
3067
+ setTimeout(() => {
3068
+ try {
3069
+ // Parse the complete JSON response
3070
+ const jsonResponse = JSON.parse(aiResponse);
3071
+ const markdownContent = jsonResponse.response;
3072
+
3073
+ // Update token usage if available
3074
+ if (jsonResponse.tokenUsage && window.tokenUsageDisplay) {
3075
+ window.tokenUsageDisplay.update(jsonResponse.tokenUsage);
3076
+ }
3077
+
3078
+ // Make sure the final content is set correctly
3079
+ aiMsgDiv.setAttribute('data-original-markdown', markdownContent);
3080
+ aiMsgDiv.innerHTML = renderMarkdown(markdownContent);
3081
+
3082
+ // Apply syntax highlighting to code blocks
3083
+ aiMsgDiv.querySelectorAll('pre code').forEach((block) => {
3084
+ hljs.highlightElement(block);
3085
+ });
3086
+
3087
+ // Specifically target mermaid diagrams in the current message
3088
+ const finalMermaidDivs = aiMsgDiv.querySelectorAll('.language-mermaid');
3089
+
3090
+ if (finalMermaidDivs.length > 0) {
3091
+ console.log(`Final render: Found ${finalMermaidDivs.length} mermaid diagrams in current message`);
3092
+
3093
+ // Log the content of the first diagram for debugging
3094
+ if (finalMermaidDivs[0]) {
3095
+ console.log('First diagram content:', finalMermaidDivs[0].textContent.substring(0, 100) + '...');
3096
+ }
3097
+
3098
+ // Try direct rendering with specific nodes from current message
3099
+ if (typeof mermaid.run === 'function') {
3100
+ console.log('Using mermaid.run() for rendering');
3101
+ mermaid.run({
3102
+ nodes: finalMermaidDivs
3103
+ });
3104
+ } else if (typeof mermaid.init === 'function') {
3105
+ // Fallback to older mermaid versions
3106
+ console.log('Using mermaid.init() for rendering');
3107
+ mermaid.init(undefined, finalMermaidDivs);
3108
+ } else {
3109
+ console.error('No suitable mermaid rendering method found');
3110
+ }
3111
+
3112
+ // Verify rendering success
3113
+ setTimeout(() => {
3114
+ // Update selector to find SVGs inside code.language-mermaid elements
3115
+ const renderedSvgs = aiMsgDiv.querySelectorAll('.language-mermaid svg, .mermaid svg');
3116
+ console.log(`Rendering verification: Found ${renderedSvgs.length} rendered SVGs`);
3117
+
3118
+ // Convert SVGs to PNGs if any were rendered
3119
+ if (renderedSvgs.length > 0) {
3120
+ console.log('Converting SVGs to PNGs...');
3121
+ renderedSvgs.forEach((svg, index) => {
3122
+ convertSvgToPng(svg, aiMsgDiv, index);
3123
+ });
3124
+ }
3125
+
3126
+ // Also add zoom functionality to any existing PNG images
3127
+ setTimeout(() => {
3128
+ const existingPngs = aiMsgDiv.querySelectorAll('.mermaid-png:not(.zoom-enabled)');
3129
+ if (existingPngs.length > 0) {
3130
+ console.log(`Adding zoom functionality to ${existingPngs.length} existing PNG images`);
3131
+ existingPngs.forEach((png) => {
3132
+ if (!png.parentElement.classList.contains('mermaid-container')) {
3133
+ const container = document.createElement('div');
3134
+ container.className = 'mermaid-container';
3135
+
3136
+ const zoomIcon = document.createElement('div');
3137
+ zoomIcon.className = 'zoom-icon';
3138
+ 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>';
3139
+
3140
+ zoomIcon.addEventListener('click', function (e) {
3141
+ e.stopPropagation();
3142
+ showDiagramDialog(png.src);
3143
+ });
3144
+
3145
+ png.parentNode.insertBefore(container, png);
3146
+ container.appendChild(png);
3147
+ container.appendChild(zoomIcon);
3148
+ png.classList.add('zoom-enabled');
3149
+ }
3150
+ });
3151
+ }
3152
+ }, 200);
3153
+ }, 100);
3154
+ } else {
3155
+ console.log('No mermaid diagrams found in current message');
3156
+ }
3157
+ } catch (error) {
3158
+ console.warn('Final mermaid rendering error:', error);
3159
+ console.error('Error details:', error.message);
3160
+ }
3161
+
3162
+ // Add copy button below the message after rendering is complete
3163
+ if (!aiMsgDiv.nextElementSibling || !aiMsgDiv.nextElementSibling.classList.contains('copy-button-container')) {
3164
+ // Create copy button container
3165
+ const copyButtonContainer = document.createElement('div');
3166
+ copyButtonContainer.className = 'copy-button-container';
3167
+
3168
+ // Create copy button
3169
+ const copyButton = document.createElement('button');
3170
+ copyButton.className = 'copy-button';
3171
+ 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`;
3172
+
3173
+ // Add click event to copy button
3174
+ copyButton.addEventListener('click', function () {
3175
+ const markdown = aiMsgDiv.getAttribute('data-original-markdown');
3176
+ if (markdown) {
3177
+ // Copy just the markdown content, not the raw JSON
3178
+ navigator.clipboard.writeText(markdown).then(() => {
3179
+ // Visual feedback
3180
+ copyButton.textContent = 'Copied!';
3181
+
3182
+ setTimeout(() => {
3183
+ 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`;
3184
+ }, 2000);
3185
+ }).catch(err => {
3186
+ console.error('Failed to copy text: ', err);
3187
+ copyButton.textContent = 'Failed to copy';
3188
+
3189
+ setTimeout(() => {
3190
+ 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`;
3191
+ }, 2000);
3192
+ });
3193
+ }
3194
+ });
3195
+
3196
+ // Add elements to the DOM
3197
+ copyButtonContainer.appendChild(copyButton);
3198
+
3199
+ // Insert after the AI message
3200
+ if (aiMsgDiv.nextSibling) {
3201
+ messagesDiv.insertBefore(copyButtonContainer, aiMsgDiv.nextSibling);
3202
+ } else {
3203
+ messagesDiv.appendChild(copyButtonContainer);
3204
+ }
3205
+ }
3206
+ }, 500); // Increased timeout to ensure DOM is fully updated
3207
+ } catch (error) {
3208
+ console.error('Error:', error);
3209
+ const errorMsg = document.createElement('div');
3210
+ errorMsg.className = 'ai-message';
3211
+ errorMsg.textContent = 'Error occurred while processing your request.';
3212
+ messagesDiv.appendChild(errorMsg);
3213
+ } finally {
3214
+ // Fetch and update token usage after each chat interaction
3215
+ // Only if this is still the current session
3216
+ if (window.sessionId === sessionId && window.tokenUsageDisplay && typeof window.tokenUsageDisplay.fetch === 'function') {
3217
+ console.log('[TokenUsage] Fetching final token usage after request completion');
3218
+ window.tokenUsageDisplay.fetch(sessionId);
3219
+ }
3220
+
3221
+ // Clear uploaded images after successful send
3222
+ if (window.clearUploadedImagesAfterSend) {
3223
+ window.clearUploadedImagesAfterSend();
3224
+ }
3225
+
3226
+ // Reset button to "Search" and enable input
3227
+ searchButton.textContent = 'Search';
3228
+ searchButton.style.backgroundColor = '#44CDF3';
3229
+ input.disabled = false;
3230
+ currentController = null;
3231
+ isRequestInProgress = false;
3232
+
3233
+ // Stop token usage polling when request is completed
3234
+ stopTokenUsagePolling();
3235
+ }
3236
+
3237
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
3238
+ });
3239
+
3240
+ // Use the updateTokenUsageDisplay function defined at the beginning of the script
3241
+
3242
+ // Fetch token usage manually only for long-running requests
3243
+ // This helps avoid redundant polling for quick responses
3244
+ let tokenUsagePollingTimer = null;
3245
+ let pollingAttempts = 0;
3246
+ const MAX_POLLING_ATTEMPTS = 10;
3247
+
3248
+ // Function to start polling for token usage updates
3249
+ function startTokenUsagePolling() {
3250
+ // Reset attempts counter
3251
+ pollingAttempts = 0;
3252
+ // Clear any existing timer
3253
+ if (tokenUsagePollingTimer) {
3254
+ clearInterval(tokenUsagePollingTimer);
3255
+ }
3256
+
3257
+ // Start polling immediately to show token usage as soon as possible
3258
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3259
+ console.log('[TokenUsage] Starting token usage polling for request...');
3260
+
3261
+ // Do an initial fetch right away
3262
+ window.tokenUsageDisplay.fetch(sessionId);
3263
+
3264
+ // Poll every 3 seconds (reduced from 5 seconds for more responsive updates)
3265
+ tokenUsagePollingTimer = setInterval(() => {
3266
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3267
+ console.log('[TokenUsage] Polling for token usage updates...');
3268
+ window.tokenUsageDisplay.fetch(sessionId);
3269
+
3270
+ // Increment attempts counter
3271
+ pollingAttempts++;
3272
+
3273
+ // If we've reached the maximum number of attempts, slow down polling
3274
+ if (pollingAttempts >= MAX_POLLING_ATTEMPTS) {
3275
+ console.log('[TokenUsage] Reached maximum polling attempts, slowing down polling');
3276
+ clearInterval(tokenUsagePollingTimer);
3277
+ tokenUsagePollingTimer = setInterval(() => {
3278
+ if (isRequestInProgress && sessionId && window.tokenUsageDisplay) {
3279
+ console.log('[TokenUsage] Slow polling for token usage updates...');
3280
+ window.tokenUsageDisplay.fetch(sessionId);
3281
+ } else {
3282
+ clearInterval(tokenUsagePollingTimer);
3283
+ tokenUsagePollingTimer = null;
3284
+ console.log('[TokenUsage] Stopped slow polling - request completed');
3285
+ }
3286
+ }, 10000); // Slow down to every 10 seconds
3287
+ }
3288
+ } else {
3289
+ // Stop polling if request is no longer in progress
3290
+ clearInterval(tokenUsagePollingTimer);
3291
+ tokenUsagePollingTimer = null;
3292
+ console.log('[TokenUsage] Stopped polling - request completed');
3293
+ }
3294
+ }, 3000);
3295
+ }
3296
+ }
3297
+
3298
+ // Function to stop polling
3299
+ function stopTokenUsagePolling() {
3300
+ if (tokenUsagePollingTimer) {
3301
+ clearInterval(tokenUsagePollingTimer);
3302
+ tokenUsagePollingTimer = null;
3303
+ console.log('[TokenUsage] Stopped token usage polling');
3304
+ }
3305
+ }
3306
+
3307
+ const messageInput = document.getElementById('message-input');
3308
+
3309
+ function autoResizeTextarea() {
3310
+ messageInput.style.height = 'auto'; // Reset to natural height
3311
+ const scrollHeight = messageInput.scrollHeight;
3312
+ messageInput.style.height = Math.min(scrollHeight, 200) + 'px'; // Set height, capped at 200px
3313
+ }
3314
+
3315
+ // Initialize height on page load
3316
+ window.addEventListener('load', () => {
3317
+ autoResizeTextarea(); // Set initial height based on content (empty = min-height)
3318
+ });
3319
+
3320
+ // Auto-resize as user types
3321
+ messageInput.addEventListener('input', autoResizeTextarea);
3322
+
3323
+ // Handle Shift+Enter for new line and Enter for form submission
3324
+ messageInput.addEventListener('keydown', function (e) {
3325
+ if (e.key === 'Enter') {
3326
+ if (e.shiftKey) {
3327
+ // Allow new line with Shift+Enter and resize
3328
+ setTimeout(autoResizeTextarea, 0);
3329
+ } else {
3330
+ // Trigger search button click on Enter without Shift
3331
+ e.preventDefault();
3332
+ searchButton.click();
3333
+ }
3334
+ }
3335
+ });
3336
+
3337
+ // API Key Form Functionality
3338
+ document.addEventListener('DOMContentLoaded', function () {
3339
+ // Check if API key is already stored
3340
+ const storedApiProvider = localStorage.getItem('probeApiProvider');
3341
+ const storedApiKey = localStorage.getItem('probeApiKey');
3342
+ const storedApiUrl = localStorage.getItem('probeApiUrl');
3343
+
3344
+ const apiProviderSelect = document.getElementById('api-provider');
3345
+ const apiKeyInput = document.getElementById('api-key');
3346
+ const apiUrlInput = document.getElementById('api-url');
3347
+ const saveButton = document.getElementById('save-api-key');
3348
+ const headerResetButton = document.getElementById('header-reset-api-key');
3349
+ const statusDiv = document.getElementById('api-key-status');
3350
+ const apiKeySetupDiv = document.getElementById('api-key-setup');
3351
+ const inputForm = document.getElementById('input-form');
3352
+
3353
+ // If API key is stored, show a success message and show the header reset button
3354
+ if (storedApiKey) {
3355
+ // Show the reset button in the header
3356
+ headerResetButton.style.display = 'inline-block';
3357
+ statusDiv.textContent = `API key for ${storedApiProvider} is configured`;
3358
+ statusDiv.className = 'api-key-status success';
3359
+ statusDiv.style.display = 'block';
3360
+
3361
+ // Fill the form with stored values
3362
+ if (storedApiProvider) {
3363
+ apiProviderSelect.value = storedApiProvider;
3364
+ }
3365
+ if (storedApiKey) {
3366
+ apiKeyInput.value = '••••••••••••••••••••••••••';
3367
+ }
3368
+ if (storedApiUrl) {
3369
+ apiUrlInput.value = storedApiUrl;
3370
+ }
3371
+
3372
+ // If we have an API key in local storage, always enable the chat interface
3373
+ // regardless of no API keys mode
3374
+ // Hide API key setup and show input form
3375
+ apiKeySetupDiv.style.display = 'none';
3376
+ inputForm.style.display = 'flex';
3377
+
3378
+ // Remove API setup mode class
3379
+ document.body.classList.remove('api-setup-mode');
3380
+ }
3381
+
3382
+ // Save API key to local storage
3383
+ saveButton.addEventListener('click', function () {
3384
+ const provider = apiProviderSelect.value;
3385
+ const key = apiKeyInput.value;
3386
+ const url = apiUrlInput.value;
3387
+
3388
+ // Don't save if the key is masked
3389
+ if (key === '••••••••••••••••••••••••••') {
3390
+ statusDiv.textContent = 'No changes made to API key';
3391
+ statusDiv.className = 'api-key-status';
3392
+ statusDiv.style.display = 'block';
3393
+ return;
3394
+ }
3395
+
3396
+ // Validate inputs
3397
+ if (!key) {
3398
+ statusDiv.textContent = 'Please enter an API key';
3399
+ statusDiv.className = 'api-key-status error';
3400
+ statusDiv.style.display = 'block';
3401
+ return;
3402
+ }
3403
+
3404
+ // Save to local storage
3405
+ localStorage.setItem('probeApiProvider', provider);
3406
+ localStorage.setItem('probeApiKey', key);
3407
+ if (url) {
3408
+ localStorage.setItem('probeApiUrl', url);
3409
+ } else {
3410
+ localStorage.removeItem('probeApiUrl');
3411
+ }
3412
+
3413
+ // Show success message
3414
+ statusDiv.textContent = `API key for ${provider} saved successfully`;
3415
+ statusDiv.className = 'api-key-status success';
3416
+ statusDiv.style.display = 'block';
3417
+
3418
+ // Mask the API key for security
3419
+ apiKeyInput.value = '••••••••••••••••••••••••••';
3420
+
3421
+ // If we're in no API keys mode, enable the chat interface
3422
+ if (document.body.getAttribute('data-no-api-keys') === 'true') {
3423
+ // Hide API key setup and show input form
3424
+ apiKeySetupDiv.style.display = 'none';
3425
+ inputForm.style.display = 'flex';
3426
+
3427
+ // Refresh the page to apply changes
3428
+ setTimeout(() => {
3429
+ window.location.reload();
3430
+ }, 1000);
3431
+ }
3432
+ });
3433
+
3434
+ // Reset API key from header button
3435
+ headerResetButton.addEventListener('click', function (e) {
3436
+ e.preventDefault();
3437
+
3438
+ // Confirm before resetting
3439
+ if (confirm('Are you sure you want to reset your API key configuration?')) {
3440
+ // Remove from local storage
3441
+ localStorage.removeItem('probeApiProvider');
3442
+ localStorage.removeItem('probeApiKey');
3443
+ localStorage.removeItem('probeApiUrl');
3444
+
3445
+ // Hide the reset button
3446
+ headerResetButton.style.display = 'none';
3447
+
3448
+ // Show message
3449
+ alert('API key configuration has been reset.');
3450
+
3451
+ // Reload the page
3452
+ window.location.reload();
3453
+ }
3454
+ });
3455
+ });
3456
+
3457
+ // Image Upload Functionality
3458
+ (function() {
3459
+ // Global state to track uploaded images
3460
+ window.uploadedImages = window.uploadedImages || [];
3461
+
3462
+ // Get DOM elements
3463
+ const imageUploadButton = document.getElementById('image-upload-button');
3464
+ const imageUploadInput = document.getElementById('image-upload');
3465
+ const messageInput = document.getElementById('message-input');
3466
+ const textareaContainer = document.querySelector('.textarea-container');
3467
+ const floatingThumbnails = document.getElementById('floating-thumbnails');
3468
+
3469
+ // Image upload button click handler
3470
+ imageUploadButton.addEventListener('click', function() {
3471
+ imageUploadInput.click();
3472
+ });
3473
+
3474
+ // File input change handler
3475
+ imageUploadInput.addEventListener('change', function(e) {
3476
+ const files = Array.from(e.target.files);
3477
+ handleImageFiles(files);
3478
+ });
3479
+
3480
+ // Drag and drop functionality
3481
+ textareaContainer.addEventListener('dragover', function(e) {
3482
+ e.preventDefault();
3483
+ textareaContainer.classList.add('drag-over');
3484
+ });
3485
+
3486
+ textareaContainer.addEventListener('dragleave', function(e) {
3487
+ e.preventDefault();
3488
+ textareaContainer.classList.remove('drag-over');
3489
+ });
3490
+
3491
+ textareaContainer.addEventListener('drop', function(e) {
3492
+ e.preventDefault();
3493
+ textareaContainer.classList.remove('drag-over');
3494
+
3495
+ const files = Array.from(e.dataTransfer.files).filter(file =>
3496
+ file.type.startsWith('image/')
3497
+ );
3498
+
3499
+ if (files.length > 0) {
3500
+ handleImageFiles(files);
3501
+ }
3502
+ });
3503
+
3504
+ // Clipboard paste functionality
3505
+ messageInput.addEventListener('paste', function(e) {
3506
+ const clipboardItems = e.clipboardData.items;
3507
+ const imageFiles = [];
3508
+
3509
+ for (let i = 0; i < clipboardItems.length; i++) {
3510
+ const item = clipboardItems[i];
3511
+ if (item.type.startsWith('image/')) {
3512
+ const file = item.getAsFile();
3513
+ if (file) {
3514
+ imageFiles.push(file);
3515
+ }
3516
+ }
3517
+ }
3518
+
3519
+ if (imageFiles.length > 0) {
3520
+ e.preventDefault(); // Prevent default paste behavior
3521
+ handleImageFiles(imageFiles);
3522
+ }
3523
+ });
3524
+
3525
+ // Handle image files
3526
+ function handleImageFiles(files) {
3527
+ files.forEach(file => {
3528
+ // Validate file size (10MB limit)
3529
+ if (file.size > 10 * 1024 * 1024) {
3530
+ showImageError(`File "${file.name}" is too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Maximum size is 10MB.`);
3531
+ return;
3532
+ }
3533
+
3534
+ // Validate file type
3535
+ if (!file.type.startsWith('image/')) {
3536
+ showImageError(`File "${file.name}" is not a valid image file.`);
3537
+ return;
3538
+ }
3539
+
3540
+ // Convert to base64 and add to uploaded images
3541
+ const reader = new FileReader();
3542
+ reader.onload = function(e) {
3543
+ const base64Data = e.target.result;
3544
+ const imageInfo = {
3545
+ id: Date.now() + Math.random(),
3546
+ name: file.name,
3547
+ size: file.size,
3548
+ type: file.type,
3549
+ base64: base64Data
3550
+ };
3551
+
3552
+ window.uploadedImages.push(imageInfo);
3553
+ addImagePreview(imageInfo);
3554
+ updateThumbnailsVisibility();
3555
+ };
3556
+
3557
+ reader.onerror = function() {
3558
+ showImageError(`Failed to read file "${file.name}".`);
3559
+ };
3560
+
3561
+ reader.readAsDataURL(file);
3562
+ });
3563
+ }
3564
+
3565
+ // Add image preview
3566
+ function addImagePreview(imageInfo) {
3567
+ const thumbnailItem = document.createElement('div');
3568
+ thumbnailItem.className = 'floating-thumbnail';
3569
+ thumbnailItem.dataset.imageId = imageInfo.id;
3570
+
3571
+ thumbnailItem.innerHTML = `
3572
+ <img src="${imageInfo.base64}" alt="${imageInfo.name}">
3573
+ <button type="button" class="floating-thumbnail-remove" onclick="removeImagePreview('${imageInfo.id}')">×</button>
3574
+ `;
3575
+
3576
+ floatingThumbnails.appendChild(thumbnailItem);
3577
+ }
3578
+
3579
+ // Remove image preview
3580
+ window.removeImagePreview = function(imageId) {
3581
+ // Remove from uploaded images array
3582
+ window.uploadedImages = window.uploadedImages.filter(img => img.id != imageId);
3583
+
3584
+ // Remove from DOM
3585
+ const thumbnailItem = document.querySelector(`[data-image-id="${imageId}"]`);
3586
+ if (thumbnailItem) {
3587
+ thumbnailItem.remove();
3588
+ }
3589
+
3590
+ updateThumbnailsVisibility();
3591
+ };
3592
+
3593
+ // Clear all images (internal function)
3594
+ function clearAllImages() {
3595
+ window.uploadedImages = [];
3596
+ floatingThumbnails.innerHTML = '';
3597
+ updateThumbnailsVisibility();
3598
+ }
3599
+
3600
+ // Update thumbnails container visibility
3601
+ function updateThumbnailsVisibility() {
3602
+ if (window.uploadedImages.length > 0) {
3603
+ floatingThumbnails.style.display = 'flex';
3604
+ } else {
3605
+ floatingThumbnails.style.display = 'none';
3606
+ }
3607
+ }
3608
+
3609
+ // Show image error
3610
+ function showImageError(message) {
3611
+ console.error('[Image Upload]', message);
3612
+
3613
+ // Create error notification
3614
+ const errorDiv = document.createElement('div');
3615
+ errorDiv.style.position = 'fixed';
3616
+ errorDiv.style.top = '20px';
3617
+ errorDiv.style.right = '20px';
3618
+ errorDiv.style.backgroundColor = '#dc3545';
3619
+ errorDiv.style.color = 'white';
3620
+ errorDiv.style.padding = '12px 16px';
3621
+ errorDiv.style.borderRadius = '6px';
3622
+ errorDiv.style.zIndex = '9999';
3623
+ errorDiv.style.maxWidth = '300px';
3624
+ errorDiv.style.fontSize = '14px';
3625
+ errorDiv.textContent = message;
3626
+
3627
+ document.body.appendChild(errorDiv);
3628
+
3629
+ // Auto-remove after 5 seconds
3630
+ setTimeout(() => {
3631
+ if (errorDiv.parentNode) {
3632
+ errorDiv.parentNode.removeChild(errorDiv);
3633
+ }
3634
+ }, 5000);
3635
+ }
3636
+
3637
+ // Function to get images as base64 data URLs for chat
3638
+ window.getUploadedImagesForChat = function() {
3639
+ return window.uploadedImages.map(img => img.base64);
3640
+ };
3641
+
3642
+ // Function to clear images after successful send
3643
+ window.clearUploadedImagesAfterSend = function() {
3644
+ clearAllImages();
3645
+ };
3646
+ })();
3647
+
3648
+ // Function to process message for display (handle base64 images)
3649
+ function processMessageForDisplay(message) {
3650
+ // Pattern to match base64 data URLs
3651
+ const base64ImagePattern = /data:image\/([a-zA-Z]*);base64,([A-Za-z0-9+/=]+)/g;
3652
+
3653
+ // Replace base64 data URLs with proper img tags
3654
+ const processedMessage = message.replace(base64ImagePattern, (match, imageType, base64Data) => {
3655
+ // Estimate file size for display
3656
+ const estimatedSize = (base64Data.length * 3) / 4;
3657
+ const sizeText = estimatedSize > 1024 * 1024
3658
+ ? `${(estimatedSize / 1024 / 1024).toFixed(1)}MB`
3659
+ : `${(estimatedSize / 1024).toFixed(1)}KB`;
3660
+
3661
+ // Create an image markdown with the base64 data
3662
+ return `![Uploaded image (${imageType}, ${sizeText})](${match})`;
3663
+ });
3664
+
3665
+ return processedMessage;
3666
+ }
3667
+
3668
+ // Make the function globally available
3669
+ window.processMessageForDisplay = processMessageForDisplay;
3670
+
3671
+ // Add click-to-zoom functionality for images in messages
3672
+ document.addEventListener('click', function(e) {
3673
+ if (e.target.tagName === 'IMG' && (e.target.closest('.user-message') || e.target.closest('.ai-message'))) {
3674
+ e.preventDefault();
3675
+ showImageDialog(e.target.src, e.target.alt || 'Image');
3676
+ }
3677
+ });
3678
+
3679
+ // Function to show image in full-screen dialog
3680
+ function showImageDialog(imageSrc, imageAlt) {
3681
+ // Create dialog overlay
3682
+ const overlay = document.createElement('div');
3683
+ overlay.style.position = 'fixed';
3684
+ overlay.style.top = '0';
3685
+ overlay.style.left = '0';
3686
+ overlay.style.width = '100%';
3687
+ overlay.style.height = '100%';
3688
+ overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
3689
+ overlay.style.zIndex = '10000';
3690
+ overlay.style.display = 'flex';
3691
+ overlay.style.alignItems = 'center';
3692
+ overlay.style.justifyContent = 'center';
3693
+ overlay.style.cursor = 'pointer';
3694
+
3695
+ // Create image container
3696
+ const imageContainer = document.createElement('div');
3697
+ imageContainer.style.position = 'relative';
3698
+ imageContainer.style.maxWidth = '90%';
3699
+ imageContainer.style.maxHeight = '90%';
3700
+
3701
+ // Create image element
3702
+ const img = document.createElement('img');
3703
+ img.src = imageSrc;
3704
+ img.alt = imageAlt;
3705
+ img.style.maxWidth = '100%';
3706
+ img.style.maxHeight = '100%';
3707
+ img.style.objectFit = 'contain';
3708
+ img.style.borderRadius = '8px';
3709
+
3710
+ // Create close button
3711
+ const closeButton = document.createElement('button');
3712
+ closeButton.innerHTML = '×';
3713
+ closeButton.style.position = 'absolute';
3714
+ closeButton.style.top = '-10px';
3715
+ closeButton.style.right = '-10px';
3716
+ closeButton.style.width = '30px';
3717
+ closeButton.style.height = '30px';
3718
+ closeButton.style.borderRadius = '50%';
3719
+ closeButton.style.border = 'none';
3720
+ closeButton.style.backgroundColor = '#fff';
3721
+ closeButton.style.color = '#333';
3722
+ closeButton.style.fontSize = '18px';
3723
+ closeButton.style.cursor = 'pointer';
3724
+ closeButton.style.zIndex = '10001';
3725
+
3726
+ // Add elements to DOM
3727
+ imageContainer.appendChild(img);
3728
+ imageContainer.appendChild(closeButton);
3729
+ overlay.appendChild(imageContainer);
3730
+ document.body.appendChild(overlay);
3731
+
3732
+ // Close on overlay click or close button click
3733
+ overlay.addEventListener('click', function(e) {
3734
+ if (e.target === overlay || e.target === closeButton) {
3735
+ document.body.removeChild(overlay);
3736
+ }
3737
+ });
3738
+
3739
+ // Close on escape key
3740
+ const escapeHandler = function(e) {
3741
+ if (e.key === 'Escape') {
3742
+ document.body.removeChild(overlay);
3743
+ document.removeEventListener('keydown', escapeHandler);
3744
+ }
3745
+ };
3746
+ document.addEventListener('keydown', escapeHandler);
3747
+ }
3748
+ </script>
3749
+ </body>
3750
+
3751
+ </html>