llama_bot_rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +249 -0
- data/Rakefile +8 -0
- data/app/assets/config/llama_bot_rails_manifest.js +1 -0
- data/app/assets/javascripts/llama_bot_rails/application.js +7 -0
- data/app/assets/javascripts/llama_bot_rails/chat.js +13 -0
- data/app/assets/stylesheets/llama_bot_rails/application.css +15 -0
- data/app/channels/llama_bot_rails/application_cable/channel.rb +8 -0
- data/app/channels/llama_bot_rails/application_cable/connection.rb +13 -0
- data/app/channels/llama_bot_rails/chat_channel.rb +306 -0
- data/app/controllers/llama_bot_rails/agent_controller.rb +72 -0
- data/app/controllers/llama_bot_rails/application_controller.rb +4 -0
- data/app/helpers/llama_bot_rails/application_helper.rb +4 -0
- data/app/javascript/channels/consumer.js +4 -0
- data/app/jobs/llama_bot_rails/application_job.rb +4 -0
- data/app/models/llama_bot_rails/application_record.rb +5 -0
- data/app/views/layouts/llama_bot_rails/application.html.erb +17 -0
- data/app/views/llama_bot_rails/agent/chat.html.erb +962 -0
- data/bin/rails +26 -0
- data/bin/rubocop +8 -0
- data/config/initializers/llama_bot_rails.rb +2 -0
- data/config/routes.rb +6 -0
- data/lib/llama_bot_rails/agent_state_builder.rb +17 -0
- data/lib/llama_bot_rails/engine.rb +23 -0
- data/lib/llama_bot_rails/llama_bot.rb +25 -0
- data/lib/llama_bot_rails/tools/rails_console_tool.rb +20 -0
- data/lib/llama_bot_rails/version.rb +3 -0
- data/lib/llama_bot_rails.rb +10 -0
- data/lib/tasks/llama_bot_rails_tasks.rake +4 -0
- metadata +128 -0
@@ -0,0 +1,962 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>LlamaBot Chat</title>
|
5
|
+
<style>
|
6
|
+
:root {
|
7
|
+
--bg-primary: #1a1a1a;
|
8
|
+
--bg-secondary: #2d2d2d;
|
9
|
+
--text-primary: #ffffff;
|
10
|
+
--text-secondary: #b3b3b3;
|
11
|
+
--accent-color: #2196f3;
|
12
|
+
--error-color: #f44336;
|
13
|
+
--success-color: #4caf50;
|
14
|
+
--sidebar-width: 250px;
|
15
|
+
--sidebar-collapsed-width: 60px;
|
16
|
+
--header-height: 80px;
|
17
|
+
}
|
18
|
+
|
19
|
+
body {
|
20
|
+
background-color: var(--bg-primary);
|
21
|
+
color: var(--text-primary);
|
22
|
+
margin: 0;
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
24
|
+
}
|
25
|
+
|
26
|
+
.app-container {
|
27
|
+
display: flex;
|
28
|
+
height: 100vh;
|
29
|
+
position: relative;
|
30
|
+
overflow: hidden; /* Prevent content from causing horizontal scroll */
|
31
|
+
}
|
32
|
+
|
33
|
+
.threads-sidebar {
|
34
|
+
width: var(--sidebar-width);
|
35
|
+
background-color: var(--bg-secondary);
|
36
|
+
padding: 20px;
|
37
|
+
border-right: 1px solid #404040;
|
38
|
+
overflow-y: auto;
|
39
|
+
transition: width 0.3s ease;
|
40
|
+
position: relative;
|
41
|
+
flex-shrink: 0; /* Prevent sidebar from shrinking */
|
42
|
+
min-width: var(--sidebar-width); /* Ensure minimum width */
|
43
|
+
}
|
44
|
+
|
45
|
+
.threads-sidebar.collapsed {
|
46
|
+
width: var(--sidebar-collapsed-width);
|
47
|
+
min-width: var(--sidebar-collapsed-width); /* Update min-width when collapsed */
|
48
|
+
padding: 20px 10px;
|
49
|
+
}
|
50
|
+
|
51
|
+
.threads-sidebar.collapsed .thread-item {
|
52
|
+
display: none;
|
53
|
+
}
|
54
|
+
|
55
|
+
.threads-sidebar.collapsed h2 {
|
56
|
+
display: none;
|
57
|
+
}
|
58
|
+
|
59
|
+
.thread-item {
|
60
|
+
padding: 10px;
|
61
|
+
margin-bottom: 8px;
|
62
|
+
border-radius: 4px;
|
63
|
+
cursor: pointer;
|
64
|
+
transition: background-color 0.2s;
|
65
|
+
white-space: nowrap;
|
66
|
+
overflow: hidden;
|
67
|
+
text-overflow: ellipsis;
|
68
|
+
}
|
69
|
+
|
70
|
+
.thread-item:hover {
|
71
|
+
background-color: #404040;
|
72
|
+
}
|
73
|
+
|
74
|
+
.thread-item.active {
|
75
|
+
background-color: var(--accent-color);
|
76
|
+
}
|
77
|
+
|
78
|
+
.chat-container {
|
79
|
+
flex-grow: 1;
|
80
|
+
display: flex;
|
81
|
+
flex-direction: column;
|
82
|
+
padding: 20px;
|
83
|
+
transition: margin-left 0.3s ease;
|
84
|
+
min-width: 0; /* Allow container to shrink below its content size */
|
85
|
+
overflow: hidden; /* Prevent content from causing horizontal scroll */
|
86
|
+
}
|
87
|
+
|
88
|
+
.chat-header {
|
89
|
+
display: flex;
|
90
|
+
align-items: center;
|
91
|
+
justify-content: space-between;
|
92
|
+
margin-bottom: 20px;
|
93
|
+
height: var(--header-height);
|
94
|
+
}
|
95
|
+
|
96
|
+
.header-left {
|
97
|
+
display: flex;
|
98
|
+
align-items: center;
|
99
|
+
}
|
100
|
+
|
101
|
+
.compose-button {
|
102
|
+
background-color: var(--accent-color);
|
103
|
+
color: white;
|
104
|
+
border: none;
|
105
|
+
border-radius: 6px;
|
106
|
+
padding: 8px 16px;
|
107
|
+
cursor: pointer;
|
108
|
+
font-size: 14px;
|
109
|
+
display: flex;
|
110
|
+
align-items: center;
|
111
|
+
gap: 6px;
|
112
|
+
transition: background-color 0.2s;
|
113
|
+
}
|
114
|
+
|
115
|
+
.compose-button:hover {
|
116
|
+
background-color: #1976d2;
|
117
|
+
}
|
118
|
+
|
119
|
+
.welcome-message {
|
120
|
+
text-align: center;
|
121
|
+
padding: 40px 20px;
|
122
|
+
color: var(--text-secondary);
|
123
|
+
}
|
124
|
+
|
125
|
+
.welcome-message h2 {
|
126
|
+
color: var(--text-primary);
|
127
|
+
margin-bottom: 10px;
|
128
|
+
font-size: 24px;
|
129
|
+
}
|
130
|
+
|
131
|
+
.welcome-message p {
|
132
|
+
font-size: 16px;
|
133
|
+
margin: 0;
|
134
|
+
}
|
135
|
+
|
136
|
+
.logo-container {
|
137
|
+
position: relative;
|
138
|
+
display: inline-block;
|
139
|
+
margin-right: 10px;
|
140
|
+
}
|
141
|
+
|
142
|
+
.logo {
|
143
|
+
width: 40px;
|
144
|
+
height: 40px;
|
145
|
+
display: block;
|
146
|
+
}
|
147
|
+
|
148
|
+
.connection-status {
|
149
|
+
position: absolute;
|
150
|
+
bottom: -2px;
|
151
|
+
right: -2px;
|
152
|
+
width: 12px;
|
153
|
+
height: 12px;
|
154
|
+
border-radius: 50%;
|
155
|
+
border: 2px solid var(--bg-primary);
|
156
|
+
transition: background-color 0.3s ease;
|
157
|
+
z-index: 10;
|
158
|
+
pointer-events: none;
|
159
|
+
}
|
160
|
+
|
161
|
+
.status-green {
|
162
|
+
background-color: #22c55e !important;
|
163
|
+
}
|
164
|
+
|
165
|
+
.status-yellow {
|
166
|
+
background-color: #eab308 !important;
|
167
|
+
}
|
168
|
+
|
169
|
+
.status-red {
|
170
|
+
background-color: #ef4444 !important;
|
171
|
+
}
|
172
|
+
|
173
|
+
.error-modal {
|
174
|
+
display: none;
|
175
|
+
position: fixed;
|
176
|
+
top: 50%;
|
177
|
+
left: 50%;
|
178
|
+
transform: translate(-50%, -50%);
|
179
|
+
background-color: var(--bg-secondary);
|
180
|
+
padding: 20px;
|
181
|
+
border-radius: 8px;
|
182
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
183
|
+
z-index: 1000;
|
184
|
+
}
|
185
|
+
|
186
|
+
.error-modal.visible {
|
187
|
+
display: block;
|
188
|
+
}
|
189
|
+
|
190
|
+
.modal-overlay {
|
191
|
+
display: none;
|
192
|
+
position: fixed;
|
193
|
+
top: 0;
|
194
|
+
left: 0;
|
195
|
+
right: 0;
|
196
|
+
bottom: 0;
|
197
|
+
background-color: rgba(0, 0, 0, 0.5);
|
198
|
+
z-index: 999;
|
199
|
+
}
|
200
|
+
|
201
|
+
.modal-overlay.visible {
|
202
|
+
display: block;
|
203
|
+
}
|
204
|
+
|
205
|
+
.heart-animation {
|
206
|
+
font-size: 24px;
|
207
|
+
color: #e91e63;
|
208
|
+
margin: 0 10px;
|
209
|
+
opacity: 0;
|
210
|
+
transition: opacity 0.3s ease;
|
211
|
+
}
|
212
|
+
|
213
|
+
.heart-animation.visible {
|
214
|
+
opacity: 1;
|
215
|
+
}
|
216
|
+
|
217
|
+
.toggle-sidebar {
|
218
|
+
background: none;
|
219
|
+
border: none;
|
220
|
+
color: var(--text-primary);
|
221
|
+
cursor: pointer;
|
222
|
+
padding: 8px;
|
223
|
+
margin-right: 10px;
|
224
|
+
display: flex;
|
225
|
+
align-items: center;
|
226
|
+
justify-content: center;
|
227
|
+
transition: transform 0.3s ease;
|
228
|
+
}
|
229
|
+
|
230
|
+
.toggle-sidebar:hover {
|
231
|
+
background-color: var(--bg-secondary);
|
232
|
+
border-radius: 4px;
|
233
|
+
}
|
234
|
+
|
235
|
+
.toggle-sidebar.collapsed {
|
236
|
+
transform: rotate(180deg);
|
237
|
+
}
|
238
|
+
|
239
|
+
.chat-messages {
|
240
|
+
flex-grow: 1;
|
241
|
+
border: 1px solid #404040;
|
242
|
+
border-radius: 8px;
|
243
|
+
padding: 20px;
|
244
|
+
overflow-y: auto;
|
245
|
+
margin-bottom: 20px;
|
246
|
+
background-color: var(--bg-secondary);
|
247
|
+
}
|
248
|
+
|
249
|
+
.message {
|
250
|
+
margin-bottom: 10px;
|
251
|
+
padding: 8px;
|
252
|
+
border-radius: 4px;
|
253
|
+
max-width: 80%;
|
254
|
+
word-wrap: break-word;
|
255
|
+
line-height: 1.4;
|
256
|
+
}
|
257
|
+
|
258
|
+
.message code {
|
259
|
+
background-color: rgba(255, 255, 255, 0.1);
|
260
|
+
padding: 2px 4px;
|
261
|
+
border-radius: 3px;
|
262
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
263
|
+
font-size: 0.9em;
|
264
|
+
}
|
265
|
+
|
266
|
+
.message pre {
|
267
|
+
background-color: rgba(255, 255, 255, 0.1);
|
268
|
+
padding: 8px;
|
269
|
+
border-radius: 4px;
|
270
|
+
overflow-x: auto;
|
271
|
+
margin: 8px 0;
|
272
|
+
}
|
273
|
+
|
274
|
+
.message pre code {
|
275
|
+
background: none;
|
276
|
+
padding: 0;
|
277
|
+
}
|
278
|
+
|
279
|
+
.message ul, .message ol {
|
280
|
+
margin: 8px 0;
|
281
|
+
padding-left: 20px;
|
282
|
+
}
|
283
|
+
|
284
|
+
.message li {
|
285
|
+
margin: 4px 0;
|
286
|
+
}
|
287
|
+
|
288
|
+
.message strong {
|
289
|
+
font-weight: bold;
|
290
|
+
}
|
291
|
+
|
292
|
+
.message em {
|
293
|
+
font-style: italic;
|
294
|
+
}
|
295
|
+
|
296
|
+
.human-message {
|
297
|
+
background-color: var(--accent-color);
|
298
|
+
margin-left: auto;
|
299
|
+
}
|
300
|
+
|
301
|
+
.tool-message {
|
302
|
+
background-color: #404040;
|
303
|
+
margin-right: auto;
|
304
|
+
}
|
305
|
+
|
306
|
+
.ai-message {
|
307
|
+
background-color: #404040;
|
308
|
+
margin-right: auto;
|
309
|
+
}
|
310
|
+
|
311
|
+
.error-message {
|
312
|
+
background-color: var(--error-color);
|
313
|
+
color: white;
|
314
|
+
margin-right: auto;
|
315
|
+
border-left: 4px solid #d32f2f;
|
316
|
+
}
|
317
|
+
|
318
|
+
.pong-message {
|
319
|
+
text-align: center;
|
320
|
+
font-size: 24px;
|
321
|
+
color: #e91e63;
|
322
|
+
margin: 10px 0;
|
323
|
+
}
|
324
|
+
|
325
|
+
.input-container {
|
326
|
+
display: flex;
|
327
|
+
gap: 10px;
|
328
|
+
padding: 10px 0;
|
329
|
+
}
|
330
|
+
|
331
|
+
#message-input {
|
332
|
+
flex-grow: 1;
|
333
|
+
padding: 12px;
|
334
|
+
border: 1px solid #404040;
|
335
|
+
border-radius: 4px;
|
336
|
+
background-color: var(--bg-secondary);
|
337
|
+
color: var(--text-primary);
|
338
|
+
}
|
339
|
+
|
340
|
+
button {
|
341
|
+
padding: 12px 24px;
|
342
|
+
background-color: var(--accent-color);
|
343
|
+
color: white;
|
344
|
+
border: none;
|
345
|
+
border-radius: 4px;
|
346
|
+
cursor: pointer;
|
347
|
+
transition: background-color 0.2s;
|
348
|
+
}
|
349
|
+
|
350
|
+
button:hover {
|
351
|
+
background-color: #1976d2;
|
352
|
+
}
|
353
|
+
|
354
|
+
@media (max-width: 768px) {
|
355
|
+
.threads-sidebar {
|
356
|
+
position: fixed;
|
357
|
+
height: 100vh;
|
358
|
+
z-index: 1000;
|
359
|
+
transform: translateX(0);
|
360
|
+
transition: transform 0.3s ease;
|
361
|
+
}
|
362
|
+
|
363
|
+
.threads-sidebar.collapsed {
|
364
|
+
transform: translateX(-100%);
|
365
|
+
width: var(--sidebar-width);
|
366
|
+
}
|
367
|
+
|
368
|
+
.chat-container {
|
369
|
+
margin-left: 0;
|
370
|
+
}
|
371
|
+
|
372
|
+
.message {
|
373
|
+
max-width: 90%;
|
374
|
+
}
|
375
|
+
}
|
376
|
+
|
377
|
+
.message h1, .message h2, .message h3, .message h4, .message h5, .message h6 {
|
378
|
+
margin: 12px 0 8px 0;
|
379
|
+
color: var(--text-primary);
|
380
|
+
}
|
381
|
+
|
382
|
+
.message h1 {
|
383
|
+
font-size: 1.5em;
|
384
|
+
border-bottom: 1px solid #404040;
|
385
|
+
padding-bottom: 4px;
|
386
|
+
}
|
387
|
+
|
388
|
+
.message h2 {
|
389
|
+
font-size: 1.3em;
|
390
|
+
}
|
391
|
+
|
392
|
+
.message h3 {
|
393
|
+
font-size: 1.2em;
|
394
|
+
}
|
395
|
+
|
396
|
+
.message h4 {
|
397
|
+
font-size: 1.1em;
|
398
|
+
}
|
399
|
+
|
400
|
+
.message h5 {
|
401
|
+
font-size: 1.05em;
|
402
|
+
}
|
403
|
+
|
404
|
+
.message h6 {
|
405
|
+
font-size: 1em;
|
406
|
+
color: var(--text-secondary);
|
407
|
+
}
|
408
|
+
</style>
|
409
|
+
<%= javascript_include_tag "llama_bot_rails/application" %>
|
410
|
+
<%= action_cable_meta_tag %>
|
411
|
+
<!-- Add Snarkdown CDN -->
|
412
|
+
<script src="https://unpkg.com/snarkdown/dist/snarkdown.umd.js"></script>
|
413
|
+
</head>
|
414
|
+
<body>
|
415
|
+
<div class="app-container">
|
416
|
+
<div class="threads-sidebar" id="threads-sidebar">
|
417
|
+
<h2>Conversations</h2>
|
418
|
+
<div id="threads-list">
|
419
|
+
<!-- Threads will be added here dynamically -->
|
420
|
+
</div>
|
421
|
+
</div>
|
422
|
+
|
423
|
+
<div class="chat-container">
|
424
|
+
<div class="chat-header">
|
425
|
+
<div class="header-left">
|
426
|
+
<button class="toggle-sidebar" id="toggle-sidebar" title="Toggle sidebar">
|
427
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
428
|
+
<path d="M15 18l-6-6 6-6" />
|
429
|
+
</svg>
|
430
|
+
</button>
|
431
|
+
<div class="logo-container">
|
432
|
+
<img src="https://service-jobs-images.s3.us-east-2.amazonaws.com/7rl98t1weu387r43il97h6ipk1l7" alt="LlamaBot Logo" class="logo">
|
433
|
+
<div id="connectionStatusIconForLlamaBot" class="connection-status status-green"></div>
|
434
|
+
</div>
|
435
|
+
<h1>LlamaBot Chat</h1>
|
436
|
+
</div>
|
437
|
+
<button class="compose-button" onclick="startNewConversation()">
|
438
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
439
|
+
<path d="M12 5v14M5 12h14"/>
|
440
|
+
</svg>
|
441
|
+
New Chat
|
442
|
+
</button>
|
443
|
+
</div>
|
444
|
+
<div class="chat-messages" id="chat-messages">
|
445
|
+
<!-- Messages will be added here dynamically -->
|
446
|
+
</div>
|
447
|
+
<div class="input-container">
|
448
|
+
<input type="text" id="message-input" placeholder="Type your message...">
|
449
|
+
<button onclick="sendMessage()">Send</button>
|
450
|
+
</div>
|
451
|
+
</div>
|
452
|
+
</div>
|
453
|
+
|
454
|
+
<div class="modal-overlay" id="modalOverlay"></div>
|
455
|
+
<div class="error-modal" id="errorModal">
|
456
|
+
<h2>Connection Error</h2>
|
457
|
+
<p>Lost connection to LlamaBot. Is it running? Refresh the page.</p>
|
458
|
+
<button onclick="closeErrorModal()">Close</button>
|
459
|
+
</div>
|
460
|
+
|
461
|
+
<script>
|
462
|
+
let currentThreadId = null;
|
463
|
+
let isSidebarCollapsed = false;
|
464
|
+
let lastPongTime = Date.now();
|
465
|
+
let redStatusStartTime = null;
|
466
|
+
let errorModalShown = false;
|
467
|
+
let connectionCheckInterval;
|
468
|
+
|
469
|
+
// Initialize ActionCable connection
|
470
|
+
const consumer = LlamaBotRails.cable;
|
471
|
+
const subscription = consumer.subscriptions.create('LlamaBotRails::ChatChannel', {
|
472
|
+
connected() {
|
473
|
+
console.log('Connected to chat channel');
|
474
|
+
lastPongTime = Date.now();
|
475
|
+
loadThreads();
|
476
|
+
startConnectionCheck();
|
477
|
+
},
|
478
|
+
disconnected() {
|
479
|
+
console.log('Disconnected from chat channel');
|
480
|
+
updateStatusIcon('status-red');
|
481
|
+
},
|
482
|
+
received(data) {
|
483
|
+
const parsedData = JSON.parse(data).message;
|
484
|
+
switch(parsedData.type) {
|
485
|
+
case "ai":
|
486
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
487
|
+
break;
|
488
|
+
case "tool":
|
489
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
490
|
+
break;
|
491
|
+
case "error":
|
492
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
493
|
+
break;
|
494
|
+
case "pong":
|
495
|
+
lastPongTime = Date.now();
|
496
|
+
break;
|
497
|
+
}
|
498
|
+
}
|
499
|
+
});
|
500
|
+
|
501
|
+
function startConnectionCheck() {
|
502
|
+
if (connectionCheckInterval) {
|
503
|
+
clearInterval(connectionCheckInterval);
|
504
|
+
}
|
505
|
+
connectionCheckInterval = setInterval(updateConnectionStatus, 1000);
|
506
|
+
}
|
507
|
+
|
508
|
+
function updateConnectionStatus() {
|
509
|
+
const timeSinceLastPong = Date.now() - lastPongTime;
|
510
|
+
|
511
|
+
if (timeSinceLastPong < 30000) { // Less than 30 seconds
|
512
|
+
updateStatusIcon('status-green');
|
513
|
+
redStatusStartTime = null;
|
514
|
+
errorModalShown = false;
|
515
|
+
} else if (timeSinceLastPong < 50000) { // Between 30-50 seconds
|
516
|
+
updateStatusIcon('status-yellow');
|
517
|
+
redStatusStartTime = null;
|
518
|
+
errorModalShown = false;
|
519
|
+
} else { // More than 50 seconds
|
520
|
+
updateStatusIcon('status-red');
|
521
|
+
if (!redStatusStartTime) {
|
522
|
+
redStatusStartTime = Date.now();
|
523
|
+
} else if (Date.now() - redStatusStartTime > 5000 && !errorModalShown) { // 5 seconds in red status
|
524
|
+
showErrorModal();
|
525
|
+
}
|
526
|
+
}
|
527
|
+
}
|
528
|
+
|
529
|
+
function updateStatusIcon(statusClass) {
|
530
|
+
const statusIndicator = document.getElementById('connectionStatusIconForLlamaBot');
|
531
|
+
statusIndicator.classList.remove('status-green', 'status-yellow', 'status-red');
|
532
|
+
statusIndicator.classList.add(statusClass);
|
533
|
+
}
|
534
|
+
|
535
|
+
function showErrorModal() {
|
536
|
+
const modal = document.getElementById('errorModal');
|
537
|
+
const overlay = document.getElementById('modalOverlay');
|
538
|
+
modal.classList.add('visible');
|
539
|
+
overlay.classList.add('visible');
|
540
|
+
errorModalShown = true;
|
541
|
+
}
|
542
|
+
|
543
|
+
function closeErrorModal() {
|
544
|
+
const modal = document.getElementById('errorModal');
|
545
|
+
const overlay = document.getElementById('modalOverlay');
|
546
|
+
modal.classList.remove('visible');
|
547
|
+
overlay.classList.remove('visible');
|
548
|
+
}
|
549
|
+
|
550
|
+
// Toggle sidebar
|
551
|
+
document.getElementById('toggle-sidebar').addEventListener('click', function() {
|
552
|
+
const sidebar = document.getElementById('threads-sidebar');
|
553
|
+
const toggleButton = this;
|
554
|
+
isSidebarCollapsed = !isSidebarCollapsed;
|
555
|
+
|
556
|
+
sidebar.classList.toggle('collapsed');
|
557
|
+
toggleButton.classList.toggle('collapsed');
|
558
|
+
});
|
559
|
+
|
560
|
+
async function loadThreads() {
|
561
|
+
try {
|
562
|
+
const response = await fetch('/llama_bot/agent/threads');
|
563
|
+
const threads = await response.json();
|
564
|
+
console.log('Loaded threads:', threads); // Debug log
|
565
|
+
|
566
|
+
const threadsList = document.getElementById('threads-list');
|
567
|
+
threadsList.innerHTML = '';
|
568
|
+
|
569
|
+
if (!threads || threads.length === 0) {
|
570
|
+
console.log('No threads available');
|
571
|
+
// Start with a blank conversation
|
572
|
+
startNewConversation();
|
573
|
+
return;
|
574
|
+
}
|
575
|
+
|
576
|
+
|
577
|
+
//sort conversation threads by creation date.
|
578
|
+
threads.sort((a, b) => { // checkpoint_id in LangGraph checkpoints are monotonically increasing, so we know their order based on checkpoint_id
|
579
|
+
const checkpoint_id_a = a.state[2].configurable.checkpoint_id; //langgraph checkpoint object structure, derived from a breakpoint and inspecting object shape.
|
580
|
+
const checkpoint_id_b = b.state[2].configurable.checkpoint_id;
|
581
|
+
if (checkpoint_id_a === checkpoint_id_b) {
|
582
|
+
return a.thread_id.localeCompare(b.thread_id);
|
583
|
+
} else {
|
584
|
+
return checkpoint_id_b.localeCompare(checkpoint_id_a);
|
585
|
+
}
|
586
|
+
});
|
587
|
+
|
588
|
+
threads.forEach(thread => {
|
589
|
+
const threadElement = createThreadElement(thread);
|
590
|
+
threadsList.appendChild(threadElement);
|
591
|
+
});
|
592
|
+
|
593
|
+
// Start with a blank conversation instead of loading the first thread
|
594
|
+
startNewConversation();
|
595
|
+
} catch (error) {
|
596
|
+
console.error('Error loading threads:', error);
|
597
|
+
// Start with a blank conversation on error
|
598
|
+
startNewConversation();
|
599
|
+
}
|
600
|
+
}
|
601
|
+
|
602
|
+
function createThreadElement(thread) {
|
603
|
+
const threadElement = document.createElement('div');
|
604
|
+
threadElement.className = 'thread-item';
|
605
|
+
const threadId = thread.thread_id || thread.id;
|
606
|
+
|
607
|
+
// Parse timestamp from thread ID and format it nicely
|
608
|
+
let displayText;
|
609
|
+
if (threadId && threadId.match(/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/)) {
|
610
|
+
// Parse the timestamp format: YYYY-MM-DD_HH-MM-SS
|
611
|
+
const [datePart, timePart] = threadId.split('_');
|
612
|
+
const [year, month, day] = datePart.split('-');
|
613
|
+
const [hour, minute, second] = timePart.split('-');
|
614
|
+
|
615
|
+
const date = new Date(year, month - 1, day, hour, minute, second);
|
616
|
+
displayText = date.toLocaleString('en-US', {
|
617
|
+
month: 'short',
|
618
|
+
day: 'numeric',
|
619
|
+
hour: 'numeric',
|
620
|
+
minute: '2-digit',
|
621
|
+
hour12: true
|
622
|
+
});
|
623
|
+
} else {
|
624
|
+
displayText = threadId || 'New Chat';
|
625
|
+
}
|
626
|
+
|
627
|
+
threadElement.textContent = displayText;
|
628
|
+
threadElement.dataset.threadId = threadId;
|
629
|
+
threadElement.onclick = () => {
|
630
|
+
console.log('Clicked thread with ID:', threadId); // Debug log
|
631
|
+
loadThread(threadId);
|
632
|
+
};
|
633
|
+
return threadElement;
|
634
|
+
}
|
635
|
+
|
636
|
+
async function loadThread(threadId) {
|
637
|
+
console.log('Loading thread:', threadId); // Debug log
|
638
|
+
|
639
|
+
if (!threadId) {
|
640
|
+
console.error('No thread ID provided');
|
641
|
+
return;
|
642
|
+
}
|
643
|
+
|
644
|
+
currentThreadId = threadId;
|
645
|
+
const messagesDiv = document.getElementById('chat-messages');
|
646
|
+
messagesDiv.innerHTML = '';
|
647
|
+
|
648
|
+
try {
|
649
|
+
const response = await fetch(`/llama_bot/agent/chat-history/${threadId}`);
|
650
|
+
const threadState = await response.json();
|
651
|
+
console.log('Loaded thread state:', threadState); // Debug log
|
652
|
+
|
653
|
+
if (Array.isArray(threadState) && threadState.length > 0) {
|
654
|
+
// Get the messages array from the first state object
|
655
|
+
const messages = threadState[0].messages || [];
|
656
|
+
console.log('Processing messages:', messages); // Debug log
|
657
|
+
messages.forEach(message => { //NOTE: this is where you can access
|
658
|
+
if (message) {
|
659
|
+
addMessage(message.content, message.type, message);
|
660
|
+
}
|
661
|
+
});
|
662
|
+
}
|
663
|
+
|
664
|
+
// Update active thread in sidebar
|
665
|
+
document.querySelectorAll('.thread-item').forEach(item => {
|
666
|
+
item.classList.remove('active');
|
667
|
+
if (item.dataset.threadId === threadId) {
|
668
|
+
item.classList.add('active');
|
669
|
+
}
|
670
|
+
});
|
671
|
+
} catch (error) {
|
672
|
+
console.error('Error loading chat history:', error);
|
673
|
+
addMessage('Error loading chat history', 'error');
|
674
|
+
}
|
675
|
+
}
|
676
|
+
|
677
|
+
function startNewConversation() {
|
678
|
+
currentThreadId = null;
|
679
|
+
const messagesDiv = document.getElementById('chat-messages');
|
680
|
+
messagesDiv.innerHTML = '';
|
681
|
+
|
682
|
+
// Show welcome message
|
683
|
+
showWelcomeMessage();
|
684
|
+
|
685
|
+
// Clear active thread selection
|
686
|
+
document.querySelectorAll('.thread-item').forEach(item => {
|
687
|
+
item.classList.remove('active');
|
688
|
+
});
|
689
|
+
}
|
690
|
+
|
691
|
+
function showWelcomeMessage() {
|
692
|
+
const messagesDiv = document.getElementById('chat-messages');
|
693
|
+
const welcomeDiv = document.createElement('div');
|
694
|
+
welcomeDiv.className = 'welcome-message';
|
695
|
+
welcomeDiv.innerHTML = `
|
696
|
+
<h2>Welcome</h2>
|
697
|
+
<p>What's on the agenda?</p>
|
698
|
+
`;
|
699
|
+
messagesDiv.appendChild(welcomeDiv);
|
700
|
+
}
|
701
|
+
|
702
|
+
function sendMessage() {
|
703
|
+
const input = document.getElementById('message-input');
|
704
|
+
const message = input.value.trim();
|
705
|
+
|
706
|
+
if (message) {
|
707
|
+
// Clear welcome message if it exists
|
708
|
+
const welcomeMessage = document.querySelector('.welcome-message');
|
709
|
+
if (welcomeMessage) {
|
710
|
+
welcomeMessage.remove();
|
711
|
+
}
|
712
|
+
|
713
|
+
addMessage(message, 'human');
|
714
|
+
input.value = '';
|
715
|
+
|
716
|
+
// Generate timestamp-based thread ID if we don't have one
|
717
|
+
let threadId = currentThreadId;
|
718
|
+
if (!threadId || threadId === 'global_thread_id') {
|
719
|
+
// Create timestamp in format: YYYY-MM-DD_HH-MM-SS
|
720
|
+
const now = new Date();
|
721
|
+
threadId = now.getFullYear() + '-' +
|
722
|
+
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
723
|
+
String(now.getDate()).padStart(2, '0') + '_' +
|
724
|
+
String(now.getHours()).padStart(2, '0') + '-' +
|
725
|
+
String(now.getMinutes()).padStart(2, '0') + '-' +
|
726
|
+
String(now.getSeconds()).padStart(2, '0');
|
727
|
+
currentThreadId = threadId;
|
728
|
+
}
|
729
|
+
|
730
|
+
const messageData = {
|
731
|
+
message: message,
|
732
|
+
thread_id: threadId
|
733
|
+
};
|
734
|
+
|
735
|
+
console.log('Sending message with data:', messageData); // Debug log
|
736
|
+
subscription.send(messageData);
|
737
|
+
}
|
738
|
+
}
|
739
|
+
|
740
|
+
/**
|
741
|
+
* @param {string} text - The text content of the message
|
742
|
+
* @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.
|
743
|
+
* @param {object} base_message - The base message object. This is the object that is sent from LangGraph, and is used to parse the message.
|
744
|
+
* @returns {void}
|
745
|
+
*/
|
746
|
+
function addMessage(text, sender, base_message=null) {
|
747
|
+
console.log('🧠 Message from LlamaBot:', text, sender, base_message);
|
748
|
+
|
749
|
+
const messagesDiv = document.getElementById('chat-messages');
|
750
|
+
const messageDiv = document.createElement('div');
|
751
|
+
messageDiv.className = `message ${sender}-message`;
|
752
|
+
|
753
|
+
// Parse markdown for bot messages using Snarkdown, keep plain text for user messages
|
754
|
+
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.
|
755
|
+
if (text == ''){ //this is most likely a tool call.
|
756
|
+
let tool_call = base_message.additional_kwargs['tool_calls'][0];
|
757
|
+
|
758
|
+
// 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.
|
759
|
+
let function_name = tool_call.function.name;
|
760
|
+
let function_arguments = JSON.parse(tool_call.function.arguments);
|
761
|
+
|
762
|
+
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
|
763
|
+
let rails_console_command = function_arguments.rails_console_command;
|
764
|
+
let message_to_user = function_arguments.message_to_user;
|
765
|
+
let internal_thoughts = function_arguments.internal_thoughts;
|
766
|
+
|
767
|
+
messageDiv.innerHTML = `
|
768
|
+
<div class="tool-execution-block">
|
769
|
+
<!-- Main action message -->
|
770
|
+
<div class="tool-action-message">
|
771
|
+
<div class="tool-action-header">
|
772
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="tool-action-icon">
|
773
|
+
<path d="M9 12l2 2 4-4"/>
|
774
|
+
<circle cx="12" cy="12" r="10"/>
|
775
|
+
</svg>
|
776
|
+
<span class="tool-action-label">LlamaBot</span>
|
777
|
+
</div>
|
778
|
+
<div class="tool-action-content">${message_to_user}</div>
|
779
|
+
</div>
|
780
|
+
|
781
|
+
<!-- Internal reasoning -->
|
782
|
+
<div class="tool-reasoning">
|
783
|
+
<div class="tool-reasoning-header">
|
784
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="tool-reasoning-icon">
|
785
|
+
<circle cx="12" cy="12" r="5"/>
|
786
|
+
<path d="M12 1v6"/>
|
787
|
+
<path d="M12 17v6"/>
|
788
|
+
<path d="M4.22 4.22l4.24 4.24"/>
|
789
|
+
<path d="M15.54 15.54l4.24 4.24"/>
|
790
|
+
<path d="M1 12h6"/>
|
791
|
+
<path d="M17 12h6"/>
|
792
|
+
<path d="M4.22 19.78l4.24-4.24"/>
|
793
|
+
<path d="M15.54 8.46l4.24-4.24"/>
|
794
|
+
</svg>
|
795
|
+
<span class="tool-reasoning-label">Reasoning</span>
|
796
|
+
</div>
|
797
|
+
<div class="tool-reasoning-content">${internal_thoughts}</div>
|
798
|
+
</div>
|
799
|
+
|
800
|
+
<!-- Command execution -->
|
801
|
+
<div class="tool-command-block">
|
802
|
+
<div class="tool-command-content">
|
803
|
+
<span class="command-prompt">$</span> <code>${rails_console_command.replace(/;/g, ';<br>')}</code>
|
804
|
+
</div>
|
805
|
+
</div>
|
806
|
+
</div>
|
807
|
+
|
808
|
+
<style>
|
809
|
+
.tool-execution-block {
|
810
|
+
background: rgba(255, 255, 255, 0.02);
|
811
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
812
|
+
border-radius: 12px;
|
813
|
+
padding: 0;
|
814
|
+
overflow: hidden;
|
815
|
+
margin: 4px 0;
|
816
|
+
}
|
817
|
+
|
818
|
+
.tool-action-message {
|
819
|
+
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(33, 150, 243, 0.05) 100%);
|
820
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
821
|
+
padding: 16px 20px;
|
822
|
+
}
|
823
|
+
|
824
|
+
.tool-action-header {
|
825
|
+
display: flex;
|
826
|
+
align-items: center;
|
827
|
+
gap: 8px;
|
828
|
+
margin-bottom: 8px;
|
829
|
+
}
|
830
|
+
|
831
|
+
.tool-action-icon {
|
832
|
+
color: var(--accent-color);
|
833
|
+
flex-shrink: 0;
|
834
|
+
}
|
835
|
+
|
836
|
+
.tool-action-label {
|
837
|
+
font-size: 13px;
|
838
|
+
font-weight: 600;
|
839
|
+
color: var(--accent-color);
|
840
|
+
text-transform: uppercase;
|
841
|
+
letter-spacing: 0.5px;
|
842
|
+
}
|
843
|
+
|
844
|
+
.tool-action-content {
|
845
|
+
color: var(--text-primary);
|
846
|
+
font-size: 15px;
|
847
|
+
line-height: 1.5;
|
848
|
+
margin-left: 24px;
|
849
|
+
}
|
850
|
+
|
851
|
+
.tool-reasoning {
|
852
|
+
background: rgba(255, 255, 255, 0.02);
|
853
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
854
|
+
padding: 14px 20px;
|
855
|
+
}
|
856
|
+
|
857
|
+
.tool-reasoning-header {
|
858
|
+
display: flex;
|
859
|
+
align-items: center;
|
860
|
+
gap: 8px;
|
861
|
+
margin-bottom: 6px;
|
862
|
+
}
|
863
|
+
|
864
|
+
.tool-reasoning-icon {
|
865
|
+
color: var(--text-secondary);
|
866
|
+
flex-shrink: 0;
|
867
|
+
}
|
868
|
+
|
869
|
+
.tool-reasoning-label {
|
870
|
+
font-size: 12px;
|
871
|
+
font-weight: 500;
|
872
|
+
color: var(--text-secondary);
|
873
|
+
text-transform: uppercase;
|
874
|
+
letter-spacing: 0.5px;
|
875
|
+
}
|
876
|
+
|
877
|
+
.tool-reasoning-content {
|
878
|
+
color: var(--text-secondary);
|
879
|
+
font-size: 14px;
|
880
|
+
line-height: 1.4;
|
881
|
+
font-style: italic;
|
882
|
+
margin-left: 22px;
|
883
|
+
opacity: 0.8;
|
884
|
+
}
|
885
|
+
|
886
|
+
.tool-command-block {
|
887
|
+
background: rgba(255, 255, 255, 0.02);
|
888
|
+
padding: 16px 20px;
|
889
|
+
}
|
890
|
+
|
891
|
+
.tool-command-content {
|
892
|
+
background: rgba(0, 0, 0, 0.4);
|
893
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
894
|
+
border-radius: 8px;
|
895
|
+
padding: 14px 18px;
|
896
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Source Code Pro', monospace;
|
897
|
+
font-size: 13px;
|
898
|
+
line-height: 1.6;
|
899
|
+
color: #e5e7eb;
|
900
|
+
overflow-x: auto;
|
901
|
+
display: flex;
|
902
|
+
align-items: flex-start;
|
903
|
+
}
|
904
|
+
|
905
|
+
.command-prompt {
|
906
|
+
color: #10b981;
|
907
|
+
font-weight: 600;
|
908
|
+
margin-right: 8px;
|
909
|
+
flex-shrink: 0;
|
910
|
+
}
|
911
|
+
|
912
|
+
.tool-command-content code {
|
913
|
+
background: none;
|
914
|
+
padding: 0;
|
915
|
+
color: inherit;
|
916
|
+
font-size: inherit;
|
917
|
+
font-family: inherit;
|
918
|
+
flex: 1;
|
919
|
+
}
|
920
|
+
|
921
|
+
.tool-execution-block:hover {
|
922
|
+
border-color: rgba(255, 255, 255, 0.12);
|
923
|
+
}
|
924
|
+
|
925
|
+
.tool-execution-block:hover .tool-action-message {
|
926
|
+
background: linear-gradient(135deg, rgba(33, 150, 243, 0.12) 0%, rgba(33, 150, 243, 0.06) 100%);
|
927
|
+
}
|
928
|
+
</style>
|
929
|
+
`;
|
930
|
+
}
|
931
|
+
else {
|
932
|
+
messageDiv.innerHTML = `🔨 - ${function_name}`;
|
933
|
+
messageDiv.innerHTML += `<pre>${JSON.stringify(function_arguments, null, 2)}</pre>`;
|
934
|
+
}
|
935
|
+
|
936
|
+
}
|
937
|
+
else {
|
938
|
+
messageDiv.innerHTML = snarkdown(text);
|
939
|
+
}
|
940
|
+
} else if (sender === 'tool') { //tool messages are not parsed as markdown
|
941
|
+
if (base_message.name == 'run_rails_console_command') {
|
942
|
+
command_result = JSON.parse(base_message.content)['result'];
|
943
|
+
messageDiv.innerHTML = `🖥️ - ${command_result}`;
|
944
|
+
}
|
945
|
+
else {
|
946
|
+
messageDiv.textContent = `🔨 - ${text}`;
|
947
|
+
}
|
948
|
+
} else {
|
949
|
+
messageDiv.textContent = text;
|
950
|
+
}
|
951
|
+
messagesDiv.appendChild(messageDiv);
|
952
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
953
|
+
}
|
954
|
+
|
955
|
+
document.getElementById('message-input').addEventListener('keypress', function(e) {
|
956
|
+
if (e.key === 'Enter') {
|
957
|
+
sendMessage();
|
958
|
+
}
|
959
|
+
});
|
960
|
+
</script>
|
961
|
+
</body>
|
962
|
+
</html>
|