@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/README.md +338 -0
- package/TRACING.md +226 -0
- package/appTracer.js +947 -0
- package/auth.js +76 -0
- package/bin/probe-chat.js +13 -0
- package/cancelRequest.js +84 -0
- package/fileSpanExporter.js +183 -0
- package/implement/README.md +228 -0
- package/implement/backends/AiderBackend.js +750 -0
- package/implement/backends/BaseBackend.js +276 -0
- package/implement/backends/ClaudeCodeBackend.js +767 -0
- package/implement/backends/MockBackend.js +237 -0
- package/implement/backends/registry.js +85 -0
- package/implement/core/BackendManager.js +567 -0
- package/implement/core/ImplementTool.js +354 -0
- package/implement/core/config.js +428 -0
- package/implement/core/timeouts.js +58 -0
- package/implement/core/utils.js +496 -0
- package/implement/types/BackendTypes.js +126 -0
- package/index.html +3751 -0
- package/index.js +582 -0
- package/logo.png +0 -0
- package/package.json +101 -0
- package/probeChat.js +269 -0
- package/probeTool.js +714 -0
- package/storage/JsonChatStorage.js +476 -0
- package/telemetry.js +287 -0
- package/test/integration/chatFlows.test.js +320 -0
- package/test/integration/toolCalling.test.js +471 -0
- package/test/mocks/mockLLMProvider.js +269 -0
- package/test/test-backends.js +90 -0
- package/test/testUtils.js +530 -0
- package/test/unit/backendTimeout.test.js +161 -0
- package/test/verify-tests.js +118 -0
- package/tokenCounter.js +419 -0
- package/tokenUsageDisplay.js +134 -0
- package/tools.js +186 -0
- package/webServer.js +1103 -0
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> - 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 ``;
|
|
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>
|