llama_bot_rails 0.1.7 → 0.1.8
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.
- checksums.yaml +4 -4
- data/README.md +22 -22
- data/app/channels/llama_bot_rails/chat_channel.rb +28 -0
- data/app/controllers/llama_bot_rails/agent_controller.rb +78 -5
- data/app/views/llama_bot_rails/agent/chat.html.erb +250 -127
- data/app/views/llama_bot_rails/agent/chat_ws.html.erb +1178 -0
- data/config/routes.rb +3 -0
- data/lib/generators/llama_bot_rails/install/install_generator.rb +10 -0
- data/lib/llama_bot_rails/agent_state_builder.rb +12 -7
- data/lib/llama_bot_rails/llama_bot.rb +47 -0
- data/lib/llama_bot_rails/version.rb +1 -1
- data/lib/llama_bot_rails.rb +5 -0
- metadata +2 -1
@@ -0,0 +1,1178 @@
|
|
1
|
+
<%# Why support both a websocket connection, (chat_channel.rb), and a non-websocket SSE connection?
|
2
|
+
|
3
|
+
Rails 6 wasn’t working with our ActionCable websocket connection, so I wanted to implement SSE as well.
|
4
|
+
|
5
|
+
We want to support a generic HTML interface that isn’t dependent on rails. (In case the Rails server goes down for whatever reason, we don’t lose access to LlamaBot).
|
6
|
+
|
7
|
+
Why have chat_channel.rb at all?
|
8
|
+
|
9
|
+
Because Ruby on Rails lacks good tooling to handle real-time interaction, that isn’t through ActionCable.
|
10
|
+
For “cancel” requests. Websocket is a 2 way connection, so we can send a ‘cancel’ in.
|
11
|
+
To support legacy LlamaPress stuff.
|
12
|
+
We chose to implement it with ActionCable plus Async Websockets.
|
13
|
+
But, it’s Ruby on Rails specific, and is best for UI/UX experiences.
|
14
|
+
|
15
|
+
SSE is better for other clients that aren’t Ruby on Rails specific, and if you want to handle just a simple SSE approach.
|
16
|
+
|
17
|
+
This does add some complexity though.
|
18
|
+
|
19
|
+
You now have 2 different paradigms of front-end JavaScript consuming from LlamaBot
|
20
|
+
ActionCable consumption
|
21
|
+
StreamedResponse consumption.
|
22
|
+
|
23
|
+
You also have 2 new middleware layers:
|
24
|
+
ActionCable <-> chat_channel.rb <-> /ws <-> request_handler.py
|
25
|
+
HTTPS <-> agent_controller.rb <-> LlamaBot.rb <-> FastAPI HTTPS
|
26
|
+
|
27
|
+
So this increases our overall surface area for the application.
|
28
|
+
|
29
|
+
This deprecated and will be removed over time.
|
30
|
+
%>
|
31
|
+
|
32
|
+
<!DOCTYPE html>
|
33
|
+
<html>
|
34
|
+
<head>
|
35
|
+
<title>LlamaBot Chat</title>
|
36
|
+
<style>
|
37
|
+
:root {
|
38
|
+
--bg-primary: #1a1a1a;
|
39
|
+
--bg-secondary: #2d2d2d;
|
40
|
+
--text-primary: #ffffff;
|
41
|
+
--text-secondary: #b3b3b3;
|
42
|
+
--accent-color: #2196f3;
|
43
|
+
--error-color: #f44336;
|
44
|
+
--success-color: #4caf50;
|
45
|
+
--sidebar-width: 250px;
|
46
|
+
--sidebar-collapsed-width: 60px;
|
47
|
+
--header-height: 80px;
|
48
|
+
}
|
49
|
+
|
50
|
+
body {
|
51
|
+
background-color: var(--bg-primary);
|
52
|
+
color: var(--text-primary);
|
53
|
+
margin: 0;
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
55
|
+
}
|
56
|
+
|
57
|
+
.app-container {
|
58
|
+
display: flex;
|
59
|
+
height: 100vh;
|
60
|
+
position: relative;
|
61
|
+
overflow: hidden; /* Prevent content from causing horizontal scroll */
|
62
|
+
}
|
63
|
+
|
64
|
+
.threads-sidebar {
|
65
|
+
width: var(--sidebar-width);
|
66
|
+
background-color: var(--bg-secondary);
|
67
|
+
padding: 20px;
|
68
|
+
border-right: 1px solid #404040;
|
69
|
+
overflow-y: auto;
|
70
|
+
transition: width 0.3s ease;
|
71
|
+
position: relative;
|
72
|
+
flex-shrink: 0; /* Prevent sidebar from shrinking */
|
73
|
+
min-width: var(--sidebar-width); /* Ensure minimum width */
|
74
|
+
}
|
75
|
+
|
76
|
+
.threads-sidebar.collapsed {
|
77
|
+
width: var(--sidebar-collapsed-width);
|
78
|
+
min-width: var(--sidebar-collapsed-width); /* Update min-width when collapsed */
|
79
|
+
padding: 20px 10px;
|
80
|
+
}
|
81
|
+
|
82
|
+
.threads-sidebar.collapsed .thread-item {
|
83
|
+
display: none;
|
84
|
+
}
|
85
|
+
|
86
|
+
.threads-sidebar.collapsed h2 {
|
87
|
+
display: none;
|
88
|
+
}
|
89
|
+
|
90
|
+
.thread-item {
|
91
|
+
padding: 10px;
|
92
|
+
margin-bottom: 8px;
|
93
|
+
border-radius: 4px;
|
94
|
+
cursor: pointer;
|
95
|
+
transition: background-color 0.2s;
|
96
|
+
white-space: nowrap;
|
97
|
+
overflow: hidden;
|
98
|
+
text-overflow: ellipsis;
|
99
|
+
}
|
100
|
+
|
101
|
+
.thread-item:hover {
|
102
|
+
background-color: #404040;
|
103
|
+
}
|
104
|
+
|
105
|
+
.thread-item.active {
|
106
|
+
background-color: var(--accent-color);
|
107
|
+
}
|
108
|
+
|
109
|
+
.chat-container {
|
110
|
+
flex-grow: 1;
|
111
|
+
display: flex;
|
112
|
+
flex-direction: column;
|
113
|
+
padding: 20px;
|
114
|
+
transition: margin-left 0.3s ease;
|
115
|
+
min-width: 0; /* Allow container to shrink below its content size */
|
116
|
+
overflow: hidden; /* Prevent content from causing horizontal scroll */
|
117
|
+
}
|
118
|
+
|
119
|
+
.chat-header {
|
120
|
+
display: flex;
|
121
|
+
align-items: center;
|
122
|
+
justify-content: space-between;
|
123
|
+
margin-bottom: 20px;
|
124
|
+
height: var(--header-height);
|
125
|
+
}
|
126
|
+
|
127
|
+
.header-left {
|
128
|
+
display: flex;
|
129
|
+
align-items: center;
|
130
|
+
}
|
131
|
+
|
132
|
+
.compose-button {
|
133
|
+
background-color: var(--accent-color);
|
134
|
+
color: white;
|
135
|
+
border: none;
|
136
|
+
border-radius: 6px;
|
137
|
+
padding: 8px 16px;
|
138
|
+
cursor: pointer;
|
139
|
+
font-size: 14px;
|
140
|
+
display: flex;
|
141
|
+
align-items: center;
|
142
|
+
gap: 6px;
|
143
|
+
transition: background-color 0.2s;
|
144
|
+
}
|
145
|
+
|
146
|
+
.compose-button:hover {
|
147
|
+
background-color: #1976d2;
|
148
|
+
}
|
149
|
+
|
150
|
+
.welcome-message {
|
151
|
+
text-align: center;
|
152
|
+
padding: 40px 20px;
|
153
|
+
color: var(--text-secondary);
|
154
|
+
}
|
155
|
+
|
156
|
+
.welcome-message h2 {
|
157
|
+
color: var(--text-primary);
|
158
|
+
margin-bottom: 10px;
|
159
|
+
font-size: 24px;
|
160
|
+
}
|
161
|
+
|
162
|
+
.welcome-message p {
|
163
|
+
font-size: 16px;
|
164
|
+
margin: 0;
|
165
|
+
}
|
166
|
+
|
167
|
+
.logo-container {
|
168
|
+
position: relative;
|
169
|
+
display: inline-block;
|
170
|
+
margin-right: 10px;
|
171
|
+
}
|
172
|
+
|
173
|
+
.logo {
|
174
|
+
width: 40px;
|
175
|
+
height: 40px;
|
176
|
+
display: block;
|
177
|
+
}
|
178
|
+
|
179
|
+
.connection-status {
|
180
|
+
position: absolute;
|
181
|
+
bottom: -2px;
|
182
|
+
right: -2px;
|
183
|
+
width: 12px;
|
184
|
+
height: 12px;
|
185
|
+
border-radius: 50%;
|
186
|
+
border: 2px solid var(--bg-primary);
|
187
|
+
transition: background-color 0.3s ease;
|
188
|
+
z-index: 10;
|
189
|
+
pointer-events: none;
|
190
|
+
}
|
191
|
+
|
192
|
+
.status-green {
|
193
|
+
background-color: #22c55e !important;
|
194
|
+
}
|
195
|
+
|
196
|
+
.status-yellow {
|
197
|
+
background-color: #eab308 !important;
|
198
|
+
}
|
199
|
+
|
200
|
+
.status-red {
|
201
|
+
background-color: #ef4444 !important;
|
202
|
+
}
|
203
|
+
|
204
|
+
.error-modal {
|
205
|
+
display: none;
|
206
|
+
position: fixed;
|
207
|
+
top: 50%;
|
208
|
+
left: 50%;
|
209
|
+
transform: translate(-50%, -50%);
|
210
|
+
background-color: var(--bg-secondary);
|
211
|
+
padding: 20px;
|
212
|
+
border-radius: 8px;
|
213
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
214
|
+
z-index: 1000;
|
215
|
+
}
|
216
|
+
|
217
|
+
.error-modal.visible {
|
218
|
+
display: block;
|
219
|
+
}
|
220
|
+
|
221
|
+
.modal-overlay {
|
222
|
+
display: none;
|
223
|
+
position: fixed;
|
224
|
+
top: 0;
|
225
|
+
left: 0;
|
226
|
+
right: 0;
|
227
|
+
bottom: 0;
|
228
|
+
background-color: rgba(0, 0, 0, 0.5);
|
229
|
+
z-index: 999;
|
230
|
+
}
|
231
|
+
|
232
|
+
.modal-overlay.visible {
|
233
|
+
display: block;
|
234
|
+
}
|
235
|
+
|
236
|
+
.heart-animation {
|
237
|
+
font-size: 24px;
|
238
|
+
color: #e91e63;
|
239
|
+
margin: 0 10px;
|
240
|
+
opacity: 0;
|
241
|
+
transition: opacity 0.3s ease;
|
242
|
+
}
|
243
|
+
|
244
|
+
.heart-animation.visible {
|
245
|
+
opacity: 1;
|
246
|
+
}
|
247
|
+
|
248
|
+
.toggle-sidebar {
|
249
|
+
background: none;
|
250
|
+
border: none;
|
251
|
+
color: var(--text-primary);
|
252
|
+
cursor: pointer;
|
253
|
+
padding: 8px;
|
254
|
+
margin-right: 10px;
|
255
|
+
display: flex;
|
256
|
+
align-items: center;
|
257
|
+
justify-content: center;
|
258
|
+
transition: transform 0.3s ease;
|
259
|
+
}
|
260
|
+
|
261
|
+
.toggle-sidebar:hover {
|
262
|
+
background-color: var(--bg-secondary);
|
263
|
+
border-radius: 4px;
|
264
|
+
}
|
265
|
+
|
266
|
+
.toggle-sidebar.collapsed {
|
267
|
+
transform: rotate(180deg);
|
268
|
+
}
|
269
|
+
|
270
|
+
.chat-messages {
|
271
|
+
flex-grow: 1;
|
272
|
+
border: 1px solid #404040;
|
273
|
+
border-radius: 8px;
|
274
|
+
padding: 20px;
|
275
|
+
overflow-y: auto;
|
276
|
+
margin-bottom: 20px;
|
277
|
+
background-color: var(--bg-secondary);
|
278
|
+
}
|
279
|
+
|
280
|
+
.message {
|
281
|
+
margin-bottom: 10px;
|
282
|
+
padding: 8px;
|
283
|
+
border-radius: 4px;
|
284
|
+
max-width: 80%;
|
285
|
+
word-wrap: break-word;
|
286
|
+
line-height: 1.4;
|
287
|
+
}
|
288
|
+
|
289
|
+
.message code {
|
290
|
+
background-color: rgba(255, 255, 255, 0.1);
|
291
|
+
padding: 2px 4px;
|
292
|
+
border-radius: 3px;
|
293
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
294
|
+
font-size: 0.9em;
|
295
|
+
}
|
296
|
+
|
297
|
+
.message pre {
|
298
|
+
background-color: rgba(255, 255, 255, 0.1);
|
299
|
+
padding: 8px;
|
300
|
+
border-radius: 4px;
|
301
|
+
overflow-x: auto;
|
302
|
+
margin: 8px 0;
|
303
|
+
}
|
304
|
+
|
305
|
+
.message pre code {
|
306
|
+
background: none;
|
307
|
+
padding: 0;
|
308
|
+
}
|
309
|
+
|
310
|
+
.message ul, .message ol {
|
311
|
+
margin: 8px 0;
|
312
|
+
padding-left: 20px;
|
313
|
+
}
|
314
|
+
|
315
|
+
.message li {
|
316
|
+
margin: 4px 0;
|
317
|
+
}
|
318
|
+
|
319
|
+
.message strong {
|
320
|
+
font-weight: bold;
|
321
|
+
}
|
322
|
+
|
323
|
+
.message em {
|
324
|
+
font-style: italic;
|
325
|
+
}
|
326
|
+
|
327
|
+
.human-message {
|
328
|
+
background-color: var(--accent-color);
|
329
|
+
margin-left: auto;
|
330
|
+
}
|
331
|
+
|
332
|
+
.tool-message {
|
333
|
+
background-color: #404040;
|
334
|
+
margin-right: auto;
|
335
|
+
}
|
336
|
+
|
337
|
+
.ai-message {
|
338
|
+
background-color: #404040;
|
339
|
+
margin-right: auto;
|
340
|
+
}
|
341
|
+
|
342
|
+
.error-message {
|
343
|
+
background-color: var(--error-color);
|
344
|
+
color: white;
|
345
|
+
margin-right: auto;
|
346
|
+
border-left: 4px solid #d32f2f;
|
347
|
+
}
|
348
|
+
|
349
|
+
.pong-message {
|
350
|
+
text-align: center;
|
351
|
+
font-size: 24px;
|
352
|
+
color: #e91e63;
|
353
|
+
margin: 10px 0;
|
354
|
+
}
|
355
|
+
|
356
|
+
.input-container {
|
357
|
+
display: flex;
|
358
|
+
gap: 10px;
|
359
|
+
padding: 10px 0;
|
360
|
+
}
|
361
|
+
|
362
|
+
#message-input {
|
363
|
+
flex-grow: 1;
|
364
|
+
padding: 12px;
|
365
|
+
border: 1px solid #404040;
|
366
|
+
border-radius: 4px;
|
367
|
+
background-color: var(--bg-secondary);
|
368
|
+
color: var(--text-primary);
|
369
|
+
}
|
370
|
+
|
371
|
+
button {
|
372
|
+
padding: 12px 24px;
|
373
|
+
background-color: var(--accent-color);
|
374
|
+
color: white;
|
375
|
+
border: none;
|
376
|
+
border-radius: 4px;
|
377
|
+
cursor: pointer;
|
378
|
+
transition: background-color 0.2s;
|
379
|
+
}
|
380
|
+
|
381
|
+
button:hover {
|
382
|
+
background-color: #1976d2;
|
383
|
+
}
|
384
|
+
|
385
|
+
@media (max-width: 768px) {
|
386
|
+
.threads-sidebar {
|
387
|
+
position: fixed;
|
388
|
+
height: 100vh;
|
389
|
+
z-index: 1000;
|
390
|
+
transform: translateX(0);
|
391
|
+
transition: transform 0.3s ease;
|
392
|
+
}
|
393
|
+
|
394
|
+
.threads-sidebar.collapsed {
|
395
|
+
transform: translateX(-100%);
|
396
|
+
width: var(--sidebar-width);
|
397
|
+
}
|
398
|
+
|
399
|
+
.chat-container {
|
400
|
+
margin-left: 0;
|
401
|
+
}
|
402
|
+
|
403
|
+
.message {
|
404
|
+
max-width: 90%;
|
405
|
+
}
|
406
|
+
}
|
407
|
+
|
408
|
+
.message h1, .message h2, .message h3, .message h4, .message h5, .message h6 {
|
409
|
+
margin: 12px 0 8px 0;
|
410
|
+
color: var(--text-primary);
|
411
|
+
}
|
412
|
+
|
413
|
+
.message h1 {
|
414
|
+
font-size: 1.5em;
|
415
|
+
border-bottom: 1px solid #404040;
|
416
|
+
padding-bottom: 4px;
|
417
|
+
}
|
418
|
+
|
419
|
+
.message h2 {
|
420
|
+
font-size: 1.3em;
|
421
|
+
}
|
422
|
+
|
423
|
+
.message h3 {
|
424
|
+
font-size: 1.2em;
|
425
|
+
}
|
426
|
+
|
427
|
+
.message h4 {
|
428
|
+
font-size: 1.1em;
|
429
|
+
}
|
430
|
+
|
431
|
+
.message h5 {
|
432
|
+
font-size: 1.05em;
|
433
|
+
}
|
434
|
+
|
435
|
+
.message h6 {
|
436
|
+
font-size: 1em;
|
437
|
+
color: var(--text-secondary);
|
438
|
+
}
|
439
|
+
|
440
|
+
/* Clean loading indicator - just animated text */
|
441
|
+
.loading-indicator {
|
442
|
+
display: none;
|
443
|
+
align-items: center;
|
444
|
+
padding: 16px 20px;
|
445
|
+
color: var(--text-secondary);
|
446
|
+
font-size: 14px;
|
447
|
+
margin-bottom: 10px;
|
448
|
+
background: rgba(255, 255, 255, 0.02);
|
449
|
+
border-radius: 8px;
|
450
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
451
|
+
}
|
452
|
+
|
453
|
+
.loading-indicator.visible {
|
454
|
+
display: flex;
|
455
|
+
}
|
456
|
+
|
457
|
+
.loading-text {
|
458
|
+
font-style: italic;
|
459
|
+
}
|
460
|
+
|
461
|
+
.loading-dots::after {
|
462
|
+
content: '';
|
463
|
+
animation: dots 1.5s steps(4, end) infinite;
|
464
|
+
}
|
465
|
+
|
466
|
+
@keyframes dots {
|
467
|
+
0%, 20% { content: ''; }
|
468
|
+
40% { content: '.'; }
|
469
|
+
60% { content: '..'; }
|
470
|
+
80%, 100% { content: '...'; }
|
471
|
+
}
|
472
|
+
|
473
|
+
/* Suggested Prompts Styling - Always visible above input */
|
474
|
+
.suggested-prompts {
|
475
|
+
margin-bottom: 16px;
|
476
|
+
padding: 0 4px;
|
477
|
+
}
|
478
|
+
|
479
|
+
.prompts-label {
|
480
|
+
font-size: 13px;
|
481
|
+
color: var(--text-secondary);
|
482
|
+
margin-bottom: 8px;
|
483
|
+
font-weight: 500;
|
484
|
+
letter-spacing: 0.3px;
|
485
|
+
}
|
486
|
+
|
487
|
+
.prompts-container {
|
488
|
+
display: flex;
|
489
|
+
flex-direction: column;
|
490
|
+
gap: 6px;
|
491
|
+
}
|
492
|
+
|
493
|
+
.prompts-row {
|
494
|
+
display: flex;
|
495
|
+
gap: 8px;
|
496
|
+
overflow-x: auto;
|
497
|
+
padding: 2px;
|
498
|
+
scrollbar-width: none; /* Firefox */
|
499
|
+
-ms-overflow-style: none; /* IE and Edge */
|
500
|
+
}
|
501
|
+
|
502
|
+
.prompts-row::-webkit-scrollbar {
|
503
|
+
display: none; /* Chrome, Safari, Opera */
|
504
|
+
}
|
505
|
+
|
506
|
+
.prompt-button {
|
507
|
+
background: rgba(255, 255, 255, 0.03);
|
508
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
509
|
+
border-radius: 6px;
|
510
|
+
padding: 8px 12px;
|
511
|
+
color: var(--text-secondary);
|
512
|
+
font-size: 13px;
|
513
|
+
cursor: pointer;
|
514
|
+
transition: all 0.2s ease;
|
515
|
+
font-family: inherit;
|
516
|
+
white-space: nowrap;
|
517
|
+
flex-shrink: 0;
|
518
|
+
min-width: fit-content;
|
519
|
+
}
|
520
|
+
|
521
|
+
.prompt-button:hover {
|
522
|
+
background: rgba(33, 150, 243, 0.08);
|
523
|
+
border-color: rgba(33, 150, 243, 0.2);
|
524
|
+
color: var(--text-primary);
|
525
|
+
transform: translateY(-1px);
|
526
|
+
}
|
527
|
+
|
528
|
+
.prompt-button:active {
|
529
|
+
transform: translateY(0);
|
530
|
+
}
|
531
|
+
|
532
|
+
@media (max-width: 768px) {
|
533
|
+
.prompts-grid {
|
534
|
+
grid-template-columns: 1fr;
|
535
|
+
}
|
536
|
+
|
537
|
+
.prompt-button {
|
538
|
+
font-size: 13px;
|
539
|
+
padding: 10px 14px;
|
540
|
+
}
|
541
|
+
}
|
542
|
+
</style>
|
543
|
+
|
544
|
+
<% if defined?(javascript_importmap_tags) %> <!-- Rails 7+ -->
|
545
|
+
<%= javascript_importmap_tags %>
|
546
|
+
<% else %> <!-- Rails 6 -->
|
547
|
+
<%= javascript_include_tag "application" %>
|
548
|
+
<% end %>
|
549
|
+
|
550
|
+
<%= javascript_include_tag "llama_bot_rails/application" %>
|
551
|
+
<% if defined?(action_cable_meta_tag) %>
|
552
|
+
<%= action_cable_meta_tag %>
|
553
|
+
<% end %>
|
554
|
+
<!-- Add Snarkdown CDN -->
|
555
|
+
<script src="https://unpkg.com/snarkdown/dist/snarkdown.umd.js"></script>
|
556
|
+
</head>
|
557
|
+
<body>
|
558
|
+
<div class="app-container">
|
559
|
+
<div class="threads-sidebar" id="threads-sidebar">
|
560
|
+
<h2>Conversations</h2>
|
561
|
+
<div id="threads-list">
|
562
|
+
<!-- Threads will be added here dynamically -->
|
563
|
+
</div>
|
564
|
+
</div>
|
565
|
+
|
566
|
+
<div class="chat-container">
|
567
|
+
<div class="chat-header">
|
568
|
+
<div class="header-left">
|
569
|
+
<button class="toggle-sidebar" id="toggle-sidebar" title="Toggle sidebar">
|
570
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
571
|
+
<path d="M15 18l-6-6 6-6" />
|
572
|
+
</svg>
|
573
|
+
</button>
|
574
|
+
<div class="logo-container">
|
575
|
+
<img src="https://service-jobs-images.s3.us-east-2.amazonaws.com/7rl98t1weu387r43il97h6ipk1l7" alt="LlamaBot Logo" class="logo">
|
576
|
+
<div id="connectionStatusIconForLlamaBot" class="connection-status status-yellow"></div>
|
577
|
+
</div>
|
578
|
+
<h1>LlamaBot Chat</h1>
|
579
|
+
</div>
|
580
|
+
<button class="compose-button" onclick="startNewConversation()">
|
581
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
582
|
+
<path d="M12 5v14M5 12h14"/>
|
583
|
+
</svg>
|
584
|
+
New Chat
|
585
|
+
</button>
|
586
|
+
</div>
|
587
|
+
<div class="chat-messages" id="chat-messages">
|
588
|
+
<!-- Messages will be added here dynamically -->
|
589
|
+
</div>
|
590
|
+
|
591
|
+
<!-- Simple loading indicator with just animated text -->
|
592
|
+
<div class="loading-indicator" id="loading-indicator">
|
593
|
+
<span class="loading-text">LlamaBot is thinking<span class="loading-dots"></span></span>
|
594
|
+
</div>
|
595
|
+
|
596
|
+
<!-- Suggested Prompts - Always visible above input -->
|
597
|
+
<div class="suggested-prompts" id="suggested-prompts">
|
598
|
+
<div class="prompts-label">Quick actions:</div>
|
599
|
+
<div class="prompts-container">
|
600
|
+
<div class="prompts-row">
|
601
|
+
<button class="prompt-button" onclick="selectPrompt(this)">What models are defined in this app?</button>
|
602
|
+
<button class="prompt-button" onclick="selectPrompt(this)">What routes exist?</button>
|
603
|
+
<button class="prompt-button" onclick="selectPrompt(this)">How many users are in the database?</button>
|
604
|
+
<button class="prompt-button" onclick="selectPrompt(this)">Show me the schema for the User model</button>
|
605
|
+
</div>
|
606
|
+
<div class="prompts-row">
|
607
|
+
<button class="prompt-button" onclick="selectPrompt(this)">Send a text with Twilio</button>
|
608
|
+
<button class="prompt-button" onclick="selectPrompt(this)">Create a BlogPost with title and body fields</button>
|
609
|
+
<button class="prompt-button" onclick="selectPrompt(this)">Generate a scaffolded Page model</button>
|
610
|
+
</div>
|
611
|
+
</div>
|
612
|
+
</div>
|
613
|
+
|
614
|
+
<div class="input-container">
|
615
|
+
<input type="text" id="message-input" placeholder="Type your message...">
|
616
|
+
<button onclick="sendMessage()">Send</button>
|
617
|
+
</div>
|
618
|
+
</div>
|
619
|
+
</div>
|
620
|
+
|
621
|
+
<div class="modal-overlay" id="modalOverlay"></div>
|
622
|
+
<div class="error-modal" id="errorModal">
|
623
|
+
<h2>Connection Error</h2>
|
624
|
+
<p>Lost connection to LlamaBot. Is it running? Refresh the page.</p>
|
625
|
+
<button onclick="closeErrorModal()">Close</button>
|
626
|
+
</div>
|
627
|
+
|
628
|
+
<script>
|
629
|
+
let currentThreadId = null;
|
630
|
+
let isSidebarCollapsed = false;
|
631
|
+
let lastPongTime = Date.now();
|
632
|
+
let redStatusStartTime = null;
|
633
|
+
let errorModalShown = false;
|
634
|
+
let connectionCheckInterval;
|
635
|
+
let subscription = null;
|
636
|
+
|
637
|
+
function waitForCableConnection(callback) {
|
638
|
+
const interval = setInterval(() => {
|
639
|
+
if (window.LlamaBotRails && LlamaBotRails.cable) {
|
640
|
+
clearInterval(interval);
|
641
|
+
callback(LlamaBotRails.cable);
|
642
|
+
}
|
643
|
+
}, 50);
|
644
|
+
}
|
645
|
+
|
646
|
+
waitForCableConnection((consumer) => {
|
647
|
+
const sessionId = crypto.randomUUID();
|
648
|
+
|
649
|
+
subscription = consumer.subscriptions.create({channel: 'LlamaBotRails::ChatChannel', session_id: sessionId}, {
|
650
|
+
connected() {
|
651
|
+
console.log('Connected to chat channel');
|
652
|
+
lastPongTime = Date.now();
|
653
|
+
loadThreads();
|
654
|
+
startConnectionCheck();
|
655
|
+
},
|
656
|
+
disconnected() {
|
657
|
+
console.log('Disconnected from chat channel');
|
658
|
+
updateStatusIcon('status-red');
|
659
|
+
},
|
660
|
+
received(data) {
|
661
|
+
const parsedData = JSON.parse(data).message;
|
662
|
+
switch (parsedData.type) {
|
663
|
+
case "ai":
|
664
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
665
|
+
break;
|
666
|
+
case "tool":
|
667
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
668
|
+
break;
|
669
|
+
case "error":
|
670
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
671
|
+
break;
|
672
|
+
case "pong":
|
673
|
+
lastPongTime = Date.now();
|
674
|
+
break;
|
675
|
+
}
|
676
|
+
}
|
677
|
+
});
|
678
|
+
});
|
679
|
+
|
680
|
+
function startConnectionCheck() {
|
681
|
+
if (connectionCheckInterval) {
|
682
|
+
clearInterval(connectionCheckInterval);
|
683
|
+
}
|
684
|
+
connectionCheckInterval = setInterval(updateConnectionStatus, 1000);
|
685
|
+
}
|
686
|
+
|
687
|
+
function updateConnectionStatus() {
|
688
|
+
const timeSinceLastPong = Date.now() - lastPongTime;
|
689
|
+
|
690
|
+
if (timeSinceLastPong < 30000) { // Less than 30 seconds
|
691
|
+
updateStatusIcon('status-green');
|
692
|
+
redStatusStartTime = null;
|
693
|
+
errorModalShown = false;
|
694
|
+
} else if (timeSinceLastPong < 50000) { // Between 30-50 seconds
|
695
|
+
updateStatusIcon('status-yellow');
|
696
|
+
redStatusStartTime = null;
|
697
|
+
errorModalShown = false;
|
698
|
+
} else { // More than 50 seconds
|
699
|
+
updateStatusIcon('status-red');
|
700
|
+
if (!redStatusStartTime) {
|
701
|
+
redStatusStartTime = Date.now();
|
702
|
+
} else if (Date.now() - redStatusStartTime > 5000 && !errorModalShown) { // 5 seconds in red status
|
703
|
+
showErrorModal();
|
704
|
+
}
|
705
|
+
}
|
706
|
+
}
|
707
|
+
|
708
|
+
function updateStatusIcon(statusClass) {
|
709
|
+
const statusIndicator = document.getElementById('connectionStatusIconForLlamaBot');
|
710
|
+
statusIndicator.classList.remove('status-green', 'status-yellow', 'status-red');
|
711
|
+
statusIndicator.classList.add(statusClass);
|
712
|
+
}
|
713
|
+
|
714
|
+
function showErrorModal() {
|
715
|
+
const modal = document.getElementById('errorModal');
|
716
|
+
const overlay = document.getElementById('modalOverlay');
|
717
|
+
modal.classList.add('visible');
|
718
|
+
overlay.classList.add('visible');
|
719
|
+
errorModalShown = true;
|
720
|
+
}
|
721
|
+
|
722
|
+
function closeErrorModal() {
|
723
|
+
const modal = document.getElementById('errorModal');
|
724
|
+
const overlay = document.getElementById('modalOverlay');
|
725
|
+
modal.classList.remove('visible');
|
726
|
+
overlay.classList.remove('visible');
|
727
|
+
}
|
728
|
+
|
729
|
+
// Toggle sidebar
|
730
|
+
document.getElementById('toggle-sidebar').addEventListener('click', function() {
|
731
|
+
const sidebar = document.getElementById('threads-sidebar');
|
732
|
+
const toggleButton = this;
|
733
|
+
isSidebarCollapsed = !isSidebarCollapsed;
|
734
|
+
|
735
|
+
sidebar.classList.toggle('collapsed');
|
736
|
+
toggleButton.classList.toggle('collapsed');
|
737
|
+
});
|
738
|
+
|
739
|
+
async function loadThreads() {
|
740
|
+
try {
|
741
|
+
const response = await fetch('/llama_bot/agent/threads');
|
742
|
+
const threads = await response.json();
|
743
|
+
console.log('Loaded threads:', threads); // Debug log
|
744
|
+
|
745
|
+
const threadsList = document.getElementById('threads-list');
|
746
|
+
threadsList.innerHTML = '';
|
747
|
+
|
748
|
+
if (!threads || threads.length === 0) {
|
749
|
+
console.log('No threads available');
|
750
|
+
// Start with a blank conversation
|
751
|
+
startNewConversation();
|
752
|
+
return;
|
753
|
+
}
|
754
|
+
|
755
|
+
|
756
|
+
//sort conversation threads by creation date.
|
757
|
+
threads.sort((a, b) => { // checkpoint_id in LangGraph checkpoints are monotonically increasing, so we know their order based on checkpoint_id
|
758
|
+
const checkpoint_id_a = a.state[2].configurable.checkpoint_id; //langgraph checkpoint object structure, derived from a breakpoint and inspecting object shape.
|
759
|
+
const checkpoint_id_b = b.state[2].configurable.checkpoint_id;
|
760
|
+
if (checkpoint_id_a === checkpoint_id_b) {
|
761
|
+
return a.thread_id.localeCompare(b.thread_id);
|
762
|
+
} else {
|
763
|
+
return checkpoint_id_b.localeCompare(checkpoint_id_a);
|
764
|
+
}
|
765
|
+
});
|
766
|
+
|
767
|
+
threads.forEach(thread => {
|
768
|
+
const threadElement = createThreadElement(thread);
|
769
|
+
threadsList.appendChild(threadElement);
|
770
|
+
});
|
771
|
+
|
772
|
+
// Start with a blank conversation instead of loading the first thread
|
773
|
+
startNewConversation();
|
774
|
+
} catch (error) {
|
775
|
+
console.error('Error loading threads:', error);
|
776
|
+
// Start with a blank conversation on error
|
777
|
+
startNewConversation();
|
778
|
+
}
|
779
|
+
}
|
780
|
+
|
781
|
+
function createThreadElement(thread) {
|
782
|
+
const threadElement = document.createElement('div');
|
783
|
+
threadElement.className = 'thread-item';
|
784
|
+
const threadId = thread.thread_id || thread.id;
|
785
|
+
|
786
|
+
// Parse timestamp from thread ID and format it nicely
|
787
|
+
let displayText;
|
788
|
+
if (threadId && threadId.match(/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/)) {
|
789
|
+
// Parse the timestamp format: YYYY-MM-DD_HH-MM-SS
|
790
|
+
const [datePart, timePart] = threadId.split('_');
|
791
|
+
const [year, month, day] = datePart.split('-');
|
792
|
+
const [hour, minute, second] = timePart.split('-');
|
793
|
+
|
794
|
+
const date = new Date(year, month - 1, day, hour, minute, second);
|
795
|
+
displayText = date.toLocaleString('en-US', {
|
796
|
+
month: 'short',
|
797
|
+
day: 'numeric',
|
798
|
+
hour: 'numeric',
|
799
|
+
minute: '2-digit',
|
800
|
+
hour12: true
|
801
|
+
});
|
802
|
+
} else {
|
803
|
+
displayText = threadId || 'New Chat';
|
804
|
+
}
|
805
|
+
|
806
|
+
threadElement.textContent = displayText;
|
807
|
+
threadElement.dataset.threadId = threadId;
|
808
|
+
threadElement.onclick = () => {
|
809
|
+
console.log('Clicked thread with ID:', threadId); // Debug log
|
810
|
+
loadThread(threadId);
|
811
|
+
};
|
812
|
+
return threadElement;
|
813
|
+
}
|
814
|
+
|
815
|
+
async function loadThread(threadId) {
|
816
|
+
console.log('Loading thread:', threadId); // Debug log
|
817
|
+
|
818
|
+
if (!threadId) {
|
819
|
+
console.error('No thread ID provided');
|
820
|
+
return;
|
821
|
+
}
|
822
|
+
|
823
|
+
currentThreadId = threadId;
|
824
|
+
const messagesDiv = document.getElementById('chat-messages');
|
825
|
+
messagesDiv.innerHTML = '';
|
826
|
+
|
827
|
+
try {
|
828
|
+
const response = await fetch(`/llama_bot/agent/chat-history/${threadId}`);
|
829
|
+
const threadState = await response.json();
|
830
|
+
console.log('Loaded thread state:', threadState); // Debug log
|
831
|
+
|
832
|
+
if (Array.isArray(threadState) && threadState.length > 0) {
|
833
|
+
// Get the messages array from the first state object
|
834
|
+
const messages = threadState[0].messages || [];
|
835
|
+
console.log('Processing messages:', messages); // Debug log
|
836
|
+
messages.forEach(message => { //NOTE: this is where you can access
|
837
|
+
if (message) {
|
838
|
+
addMessage(message.content, message.type, message);
|
839
|
+
}
|
840
|
+
});
|
841
|
+
}
|
842
|
+
|
843
|
+
// Update active thread in sidebar
|
844
|
+
document.querySelectorAll('.thread-item').forEach(item => {
|
845
|
+
item.classList.remove('active');
|
846
|
+
if (item.dataset.threadId === threadId) {
|
847
|
+
item.classList.add('active');
|
848
|
+
}
|
849
|
+
});
|
850
|
+
} catch (error) {
|
851
|
+
console.error('Error loading chat history:', error);
|
852
|
+
addMessage('Error loading chat history', 'error');
|
853
|
+
}
|
854
|
+
}
|
855
|
+
|
856
|
+
function startNewConversation() {
|
857
|
+
currentThreadId = null;
|
858
|
+
const messagesDiv = document.getElementById('chat-messages');
|
859
|
+
messagesDiv.innerHTML = '';
|
860
|
+
|
861
|
+
// Show welcome message
|
862
|
+
showWelcomeMessage();
|
863
|
+
}
|
864
|
+
|
865
|
+
function showWelcomeMessage() {
|
866
|
+
const messagesDiv = document.getElementById('chat-messages');
|
867
|
+
const welcomeDiv = document.createElement('div');
|
868
|
+
welcomeDiv.className = 'welcome-message';
|
869
|
+
welcomeDiv.innerHTML = `
|
870
|
+
<h2>Welcome</h2>
|
871
|
+
<p>What's on the agenda?</p>
|
872
|
+
`;
|
873
|
+
messagesDiv.appendChild(welcomeDiv);
|
874
|
+
}
|
875
|
+
|
876
|
+
function showLoadingIndicator() {
|
877
|
+
const loadingIndicator = document.getElementById('loading-indicator');
|
878
|
+
loadingIndicator.classList.add('visible');
|
879
|
+
}
|
880
|
+
|
881
|
+
function hideLoadingIndicator() {
|
882
|
+
const loadingIndicator = document.getElementById('loading-indicator');
|
883
|
+
loadingIndicator.classList.remove('visible');
|
884
|
+
}
|
885
|
+
|
886
|
+
function selectPrompt(buttonElement) {
|
887
|
+
const promptText = buttonElement.textContent;
|
888
|
+
const messageInput = document.getElementById('message-input');
|
889
|
+
|
890
|
+
// Populate the input field
|
891
|
+
messageInput.value = promptText;
|
892
|
+
|
893
|
+
// Focus the input field for better UX
|
894
|
+
messageInput.focus();
|
895
|
+
|
896
|
+
// Add a subtle animation to show the prompt was selected
|
897
|
+
buttonElement.style.transform = 'scale(0.98)';
|
898
|
+
setTimeout(() => {
|
899
|
+
buttonElement.style.transform = '';
|
900
|
+
}, 150);
|
901
|
+
}
|
902
|
+
|
903
|
+
function sendMessage() {
|
904
|
+
const input = document.getElementById('message-input');
|
905
|
+
const message = input.value.trim();
|
906
|
+
|
907
|
+
if (message) {
|
908
|
+
// Check if subscription is available
|
909
|
+
if (!subscription) {
|
910
|
+
console.error('WebSocket connection not established yet');
|
911
|
+
addMessage('Connection not ready. Please wait...', 'error');
|
912
|
+
return;
|
913
|
+
}
|
914
|
+
|
915
|
+
// Clear welcome message if it exists
|
916
|
+
const welcomeMessage = document.querySelector('.welcome-message');
|
917
|
+
if (welcomeMessage) {
|
918
|
+
welcomeMessage.remove();
|
919
|
+
}
|
920
|
+
|
921
|
+
addMessage(message, 'human');
|
922
|
+
input.value = '';
|
923
|
+
|
924
|
+
// Show loading indicator
|
925
|
+
showLoadingIndicator();
|
926
|
+
|
927
|
+
// Generate timestamp-based thread ID if we don't have one
|
928
|
+
let threadId = currentThreadId;
|
929
|
+
if (!threadId || threadId === 'global_thread_id') {
|
930
|
+
// Create timestamp in format: YYYY-MM-DD_HH-MM-SS
|
931
|
+
const now = new Date();
|
932
|
+
threadId = now.getFullYear() + '-' +
|
933
|
+
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
934
|
+
String(now.getDate()).padStart(2, '0') + '_' +
|
935
|
+
String(now.getHours()).padStart(2, '0') + '-' +
|
936
|
+
String(now.getMinutes()).padStart(2, '0') + '-' +
|
937
|
+
String(now.getSeconds()).padStart(2, '0');
|
938
|
+
currentThreadId = threadId;
|
939
|
+
}
|
940
|
+
|
941
|
+
const messageData = {
|
942
|
+
message: message,
|
943
|
+
thread_id: threadId
|
944
|
+
};
|
945
|
+
|
946
|
+
console.log('Sending message with data:', messageData); // Debug log
|
947
|
+
subscription.send(messageData);
|
948
|
+
}
|
949
|
+
}
|
950
|
+
|
951
|
+
/**
|
952
|
+
* @param {string} text - The text content of the message
|
953
|
+
* @param {string} sender - The sender of the message. This matches LangGraph schema -- either 'ai', 'tool', or 'human'. 'error' if an error occurs somewhere in the stack.
|
954
|
+
* @param {object} base_message - The base message object. This is the object that is sent from LangGraph, and is used to parse the message.
|
955
|
+
* @returns {void}
|
956
|
+
*/
|
957
|
+
function addMessage(text, sender, base_message=null) {
|
958
|
+
console.log('🧠 Message from LlamaBot:', text, sender, base_message);
|
959
|
+
|
960
|
+
// Hide loading indicator when we receive an AI response
|
961
|
+
if (sender === 'ai') {
|
962
|
+
hideLoadingIndicator();
|
963
|
+
}
|
964
|
+
|
965
|
+
const messagesDiv = document.getElementById('chat-messages');
|
966
|
+
const messageDiv = document.createElement('div');
|
967
|
+
messageDiv.className = `message ${sender}-message`;
|
968
|
+
|
969
|
+
// Parse markdown for bot messages using Snarkdown, keep plain text for user messages
|
970
|
+
if (sender === 'ai') { //Arghh. We're having issues with difference in formats between when we're streaming from updates mode, and when pulling state from checkpoint.
|
971
|
+
if (text == ''){ //this is most likely a tool call.
|
972
|
+
let tool_call = base_message.additional_kwargs['tool_calls'][0];
|
973
|
+
|
974
|
+
// The below works for loading message history from checkpoint (persistence), AND when receiving messages from LangGraph streaming "updates" mode. This is a LangGraph BaseMessage object.
|
975
|
+
let function_name = tool_call.function.name;
|
976
|
+
let function_arguments = JSON.parse(tool_call.function.arguments);
|
977
|
+
|
978
|
+
if (function_name == 'run_rails_console_command') { //this is our standardized tool for running rails console commands. Matches the function name in llamabot/backend/agents/llamabot_v1/nodes.py:run_rails_console_command
|
979
|
+
let rails_console_command = function_arguments.rails_console_command;
|
980
|
+
let message_to_user = function_arguments.message_to_user;
|
981
|
+
let internal_thoughts = function_arguments.internal_thoughts;
|
982
|
+
|
983
|
+
messageDiv.innerHTML = `
|
984
|
+
<div class="tool-execution-block">
|
985
|
+
<!-- Main action message -->
|
986
|
+
<div class="tool-action-message">
|
987
|
+
<div class="tool-action-header">
|
988
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="tool-action-icon">
|
989
|
+
<path d="M9 12l2 2 4-4"/>
|
990
|
+
<circle cx="12" cy="12" r="10"/>
|
991
|
+
</svg>
|
992
|
+
<span class="tool-action-label">LlamaBot</span>
|
993
|
+
</div>
|
994
|
+
<div class="tool-action-content">${message_to_user}</div>
|
995
|
+
</div>
|
996
|
+
|
997
|
+
<!-- Internal reasoning -->
|
998
|
+
<div class="tool-reasoning">
|
999
|
+
<div class="tool-reasoning-header">
|
1000
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="tool-reasoning-icon">
|
1001
|
+
<circle cx="12" cy="12" r="5"/>
|
1002
|
+
<path d="M12 1v6"/>
|
1003
|
+
<path d="M12 17v6"/>
|
1004
|
+
<path d="M4.22 4.22l4.24 4.24"/>
|
1005
|
+
<path d="M15.54 15.54l4.24 4.24"/>
|
1006
|
+
<path d="M1 12h6"/>
|
1007
|
+
<path d="M17 12h6"/>
|
1008
|
+
<path d="M4.22 19.78l4.24-4.24"/>
|
1009
|
+
<path d="M15.54 8.46l4.24-4.24"/>
|
1010
|
+
</svg>
|
1011
|
+
<span class="tool-reasoning-label">Reasoning</span>
|
1012
|
+
</div>
|
1013
|
+
<div class="tool-reasoning-content">${internal_thoughts}</div>
|
1014
|
+
</div>
|
1015
|
+
|
1016
|
+
<!-- Command execution -->
|
1017
|
+
<div class="tool-command-block">
|
1018
|
+
<div class="tool-command-content">
|
1019
|
+
<span class="command-prompt">$</span> <code>${rails_console_command.replace(/;/g, ';<br>')}</code>
|
1020
|
+
</div>
|
1021
|
+
</div>
|
1022
|
+
</div>
|
1023
|
+
|
1024
|
+
<style>
|
1025
|
+
.tool-execution-block {
|
1026
|
+
background: rgba(255, 255, 255, 0.02);
|
1027
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
1028
|
+
border-radius: 12px;
|
1029
|
+
padding: 0;
|
1030
|
+
overflow: hidden;
|
1031
|
+
margin: 4px 0;
|
1032
|
+
}
|
1033
|
+
|
1034
|
+
.tool-action-message {
|
1035
|
+
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(33, 150, 243, 0.05) 100%);
|
1036
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
1037
|
+
padding: 16px 20px;
|
1038
|
+
}
|
1039
|
+
|
1040
|
+
.tool-action-header {
|
1041
|
+
display: flex;
|
1042
|
+
align-items: center;
|
1043
|
+
gap: 8px;
|
1044
|
+
margin-bottom: 8px;
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
.tool-action-icon {
|
1048
|
+
color: var(--accent-color);
|
1049
|
+
flex-shrink: 0;
|
1050
|
+
}
|
1051
|
+
|
1052
|
+
.tool-action-label {
|
1053
|
+
font-size: 13px;
|
1054
|
+
font-weight: 600;
|
1055
|
+
color: var(--accent-color);
|
1056
|
+
text-transform: uppercase;
|
1057
|
+
letter-spacing: 0.5px;
|
1058
|
+
}
|
1059
|
+
|
1060
|
+
.tool-action-content {
|
1061
|
+
color: var(--text-primary);
|
1062
|
+
font-size: 15px;
|
1063
|
+
line-height: 1.5;
|
1064
|
+
margin-left: 24px;
|
1065
|
+
}
|
1066
|
+
|
1067
|
+
.tool-reasoning {
|
1068
|
+
background: rgba(255, 255, 255, 0.02);
|
1069
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
1070
|
+
padding: 14px 20px;
|
1071
|
+
}
|
1072
|
+
|
1073
|
+
.tool-reasoning-header {
|
1074
|
+
display: flex;
|
1075
|
+
align-items: center;
|
1076
|
+
gap: 8px;
|
1077
|
+
margin-bottom: 6px;
|
1078
|
+
}
|
1079
|
+
|
1080
|
+
.tool-reasoning-icon {
|
1081
|
+
color: var(--text-secondary);
|
1082
|
+
flex-shrink: 0;
|
1083
|
+
}
|
1084
|
+
|
1085
|
+
.tool-reasoning-label {
|
1086
|
+
font-size: 12px;
|
1087
|
+
font-weight: 500;
|
1088
|
+
color: var(--text-secondary);
|
1089
|
+
text-transform: uppercase;
|
1090
|
+
letter-spacing: 0.5px;
|
1091
|
+
}
|
1092
|
+
|
1093
|
+
.tool-reasoning-content {
|
1094
|
+
color: var(--text-secondary);
|
1095
|
+
font-size: 14px;
|
1096
|
+
line-height: 1.4;
|
1097
|
+
font-style: italic;
|
1098
|
+
margin-left: 22px;
|
1099
|
+
opacity: 0.8;
|
1100
|
+
}
|
1101
|
+
|
1102
|
+
.tool-command-block {
|
1103
|
+
background: rgba(255, 255, 255, 0.02);
|
1104
|
+
padding: 16px 20px;
|
1105
|
+
}
|
1106
|
+
|
1107
|
+
.tool-command-content {
|
1108
|
+
background: rgba(0, 0, 0, 0.4);
|
1109
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1110
|
+
border-radius: 8px;
|
1111
|
+
padding: 14px 18px;
|
1112
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Source Code Pro', monospace;
|
1113
|
+
font-size: 13px;
|
1114
|
+
line-height: 1.6;
|
1115
|
+
color: #e5e7eb;
|
1116
|
+
overflow-x: auto;
|
1117
|
+
display: flex;
|
1118
|
+
align-items: flex-start;
|
1119
|
+
}
|
1120
|
+
|
1121
|
+
.command-prompt {
|
1122
|
+
color: #10b981;
|
1123
|
+
font-weight: 600;
|
1124
|
+
margin-right: 8px;
|
1125
|
+
flex-shrink: 0;
|
1126
|
+
}
|
1127
|
+
|
1128
|
+
.tool-command-content code {
|
1129
|
+
background: none;
|
1130
|
+
padding: 0;
|
1131
|
+
color: inherit;
|
1132
|
+
font-size: inherit;
|
1133
|
+
font-family: inherit;
|
1134
|
+
flex: 1;
|
1135
|
+
}
|
1136
|
+
|
1137
|
+
.tool-execution-block:hover {
|
1138
|
+
border-color: rgba(255, 255, 255, 0.12);
|
1139
|
+
}
|
1140
|
+
|
1141
|
+
.tool-execution-block:hover .tool-action-message {
|
1142
|
+
background: linear-gradient(135deg, rgba(33, 150, 243, 0.12) 0%, rgba(33, 150, 243, 0.06) 100%);
|
1143
|
+
}
|
1144
|
+
</style>
|
1145
|
+
`;
|
1146
|
+
}
|
1147
|
+
else {
|
1148
|
+
messageDiv.innerHTML = `🔨 - ${function_name}`;
|
1149
|
+
messageDiv.innerHTML += `<pre>${JSON.stringify(function_arguments, null, 2)}</pre>`;
|
1150
|
+
}
|
1151
|
+
|
1152
|
+
}
|
1153
|
+
else {
|
1154
|
+
messageDiv.innerHTML = snarkdown(text);
|
1155
|
+
}
|
1156
|
+
} else if (sender === 'tool') { //tool messages are not parsed as markdown
|
1157
|
+
if (base_message.name == 'run_rails_console_command') {
|
1158
|
+
command_result = JSON.parse(base_message.content)['result'];
|
1159
|
+
messageDiv.innerHTML = `🖥️ - ${command_result}`;
|
1160
|
+
}
|
1161
|
+
else {
|
1162
|
+
messageDiv.textContent = `🔨 - ${text}`;
|
1163
|
+
}
|
1164
|
+
} else {
|
1165
|
+
messageDiv.textContent = text;
|
1166
|
+
}
|
1167
|
+
messagesDiv.appendChild(messageDiv);
|
1168
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
1169
|
+
}
|
1170
|
+
|
1171
|
+
document.getElementById('message-input').addEventListener('keypress', function(e) {
|
1172
|
+
if (e.key === 'Enter') {
|
1173
|
+
sendMessage();
|
1174
|
+
}
|
1175
|
+
});
|
1176
|
+
</script>
|
1177
|
+
</body>
|
1178
|
+
</html>
|