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.
@@ -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>