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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +249 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/llama_bot_rails_manifest.js +1 -0
  6. data/app/assets/javascripts/llama_bot_rails/application.js +7 -0
  7. data/app/assets/javascripts/llama_bot_rails/chat.js +13 -0
  8. data/app/assets/stylesheets/llama_bot_rails/application.css +15 -0
  9. data/app/channels/llama_bot_rails/application_cable/channel.rb +8 -0
  10. data/app/channels/llama_bot_rails/application_cable/connection.rb +13 -0
  11. data/app/channels/llama_bot_rails/chat_channel.rb +306 -0
  12. data/app/controllers/llama_bot_rails/agent_controller.rb +72 -0
  13. data/app/controllers/llama_bot_rails/application_controller.rb +4 -0
  14. data/app/helpers/llama_bot_rails/application_helper.rb +4 -0
  15. data/app/javascript/channels/consumer.js +4 -0
  16. data/app/jobs/llama_bot_rails/application_job.rb +4 -0
  17. data/app/models/llama_bot_rails/application_record.rb +5 -0
  18. data/app/views/layouts/llama_bot_rails/application.html.erb +17 -0
  19. data/app/views/llama_bot_rails/agent/chat.html.erb +962 -0
  20. data/bin/rails +26 -0
  21. data/bin/rubocop +8 -0
  22. data/config/initializers/llama_bot_rails.rb +2 -0
  23. data/config/routes.rb +6 -0
  24. data/lib/llama_bot_rails/agent_state_builder.rb +17 -0
  25. data/lib/llama_bot_rails/engine.rb +23 -0
  26. data/lib/llama_bot_rails/llama_bot.rb +25 -0
  27. data/lib/llama_bot_rails/tools/rails_console_tool.rb +20 -0
  28. data/lib/llama_bot_rails/version.rb +3 -0
  29. data/lib/llama_bot_rails.rb +10 -0
  30. data/lib/tasks/llama_bot_rails_tasks.rake +4 -0
  31. 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>