llama_bot_rails 0.1.7 → 0.1.9

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.
@@ -145,30 +145,7 @@
145
145
  display: block;
146
146
  }
147
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
148
 
165
- .status-yellow {
166
- background-color: #eab308 !important;
167
- }
168
-
169
- .status-red {
170
- background-color: #ef4444 !important;
171
- }
172
149
 
173
150
  .error-modal {
174
151
  display: none;
@@ -406,7 +383,7 @@
406
383
  color: var(--text-secondary);
407
384
  }
408
385
 
409
- /* Clean loading indicator - just animated text */
386
+ /* Enhanced loading indicator with status updates */
410
387
  .loading-indicator {
411
388
  display: none;
412
389
  align-items: center;
@@ -417,6 +394,8 @@
417
394
  background: rgba(255, 255, 255, 0.02);
418
395
  border-radius: 8px;
419
396
  border: 1px solid rgba(255, 255, 255, 0.08);
397
+ transition: all 0.3s ease;
398
+ min-height: 50px; /* Prevent layout shift during status updates */
420
399
  }
421
400
 
422
401
  .loading-indicator.visible {
@@ -425,11 +404,15 @@
425
404
 
426
405
  .loading-text {
427
406
  font-style: italic;
407
+ flex: 1;
408
+ transition: color 0.3s ease;
409
+ line-height: 1.4;
428
410
  }
429
411
 
430
412
  .loading-dots::after {
431
413
  content: '';
432
414
  animation: dots 1.5s steps(4, end) infinite;
415
+ opacity: 0.7;
433
416
  }
434
417
 
435
418
  @keyframes dots {
@@ -439,6 +422,22 @@
439
422
  80%, 100% { content: '...'; }
440
423
  }
441
424
 
425
+ /* Status-specific styling */
426
+ .loading-indicator:has(.loading-text:contains("Error")) {
427
+ border-color: rgba(244, 67, 54, 0.3);
428
+ background: rgba(244, 67, 54, 0.05);
429
+ }
430
+
431
+ .loading-indicator:has(.loading-text:contains("Complete")) {
432
+ border-color: rgba(76, 175, 80, 0.3);
433
+ background: rgba(76, 175, 80, 0.05);
434
+ }
435
+
436
+ .loading-indicator:has(.loading-text:contains("Connected")) {
437
+ border-color: rgba(33, 150, 243, 0.3);
438
+ background: rgba(33, 150, 243, 0.05);
439
+ }
440
+
442
441
  /* Suggested Prompts Styling - Always visible above input */
443
442
  .suggested-prompts {
444
443
  margin-bottom: 16px;
@@ -510,16 +509,6 @@
510
509
  }
511
510
  </style>
512
511
 
513
- <% if defined?(javascript_importmap_tags) %> <!-- Rails 7+ -->
514
- <%= javascript_importmap_tags %>
515
- <% else %> <!-- Rails 6 -->
516
- <%= javascript_include_tag "application" %>
517
- <% end %>
518
-
519
- <%= javascript_include_tag "llama_bot_rails/application" %>
520
- <% if defined?(action_cable_meta_tag) %>
521
- <%= action_cable_meta_tag %>
522
- <% end %>
523
512
  <!-- Add Snarkdown CDN -->
524
513
  <script src="https://unpkg.com/snarkdown/dist/snarkdown.umd.js"></script>
525
514
  </head>
@@ -542,7 +531,6 @@
542
531
  </button>
543
532
  <div class="logo-container">
544
533
  <img src="https://service-jobs-images.s3.us-east-2.amazonaws.com/7rl98t1weu387r43il97h6ipk1l7" alt="LlamaBot Logo" class="logo">
545
- <div id="connectionStatusIconForLlamaBot" class="connection-status status-yellow"></div>
546
534
  </div>
547
535
  <h1>LlamaBot Chat</h1>
548
536
  </div>
@@ -597,95 +585,19 @@
597
585
  <script>
598
586
  let currentThreadId = null;
599
587
  let isSidebarCollapsed = false;
600
- let lastPongTime = Date.now();
601
- let redStatusStartTime = null;
602
- let errorModalShown = false;
603
- let connectionCheckInterval;
604
- let subscription = null;
605
-
606
- function waitForCableConnection(callback) {
607
- const interval = setInterval(() => {
608
- if (window.LlamaBotRails && LlamaBotRails.cable) {
609
- clearInterval(interval);
610
- callback(LlamaBotRails.cable);
611
- }
612
- }, 50);
613
- }
614
-
615
- waitForCableConnection((consumer) => {
616
- const sessionId = crypto.randomUUID();
617
-
618
- subscription = consumer.subscriptions.create({channel: 'LlamaBotRails::ChatChannel', session_id: sessionId}, {
619
- connected() {
620
- console.log('Connected to chat channel');
621
- lastPongTime = Date.now();
622
- loadThreads();
623
- startConnectionCheck();
624
- },
625
- disconnected() {
626
- console.log('Disconnected from chat channel');
627
- updateStatusIcon('status-red');
628
- },
629
- received(data) {
630
- const parsedData = JSON.parse(data).message;
631
- switch (parsedData.type) {
632
- case "ai":
633
- addMessage(parsedData.content, parsedData.type, parsedData.base_message);
634
- break;
635
- case "tool":
636
- addMessage(parsedData.content, parsedData.type, parsedData.base_message);
637
- break;
638
- case "error":
639
- addMessage(parsedData.content, parsedData.type, parsedData.base_message);
640
- break;
641
- case "pong":
642
- lastPongTime = Date.now();
643
- break;
644
- }
645
- }
646
- });
647
- });
588
+ let streamingTimeout = null;
589
+ const STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout
648
590
 
649
- function startConnectionCheck() {
650
- if (connectionCheckInterval) {
651
- clearInterval(connectionCheckInterval);
652
- }
653
- connectionCheckInterval = setInterval(updateConnectionStatus, 1000);
654
- }
655
-
656
- function updateConnectionStatus() {
657
- const timeSinceLastPong = Date.now() - lastPongTime;
658
-
659
- if (timeSinceLastPong < 30000) { // Less than 30 seconds
660
- updateStatusIcon('status-green');
661
- redStatusStartTime = null;
662
- errorModalShown = false;
663
- } else if (timeSinceLastPong < 50000) { // Between 30-50 seconds
664
- updateStatusIcon('status-yellow');
665
- redStatusStartTime = null;
666
- errorModalShown = false;
667
- } else { // More than 50 seconds
668
- updateStatusIcon('status-red');
669
- if (!redStatusStartTime) {
670
- redStatusStartTime = Date.now();
671
- } else if (Date.now() - redStatusStartTime > 5000 && !errorModalShown) { // 5 seconds in red status
672
- showErrorModal();
673
- }
674
- }
675
- }
676
-
677
- function updateStatusIcon(statusClass) {
678
- const statusIndicator = document.getElementById('connectionStatusIconForLlamaBot');
679
- statusIndicator.classList.remove('status-green', 'status-yellow', 'status-red');
680
- statusIndicator.classList.add(statusClass);
681
- }
591
+ // Initialize the app
592
+ document.addEventListener('DOMContentLoaded', function() {
593
+ loadThreads();
594
+ });
682
595
 
683
596
  function showErrorModal() {
684
597
  const modal = document.getElementById('errorModal');
685
598
  const overlay = document.getElementById('modalOverlay');
686
599
  modal.classList.add('visible');
687
600
  overlay.classList.add('visible');
688
- errorModalShown = true;
689
601
  }
690
602
 
691
603
  function closeErrorModal() {
@@ -852,6 +764,54 @@
852
764
  loadingIndicator.classList.remove('visible');
853
765
  }
854
766
 
767
+
768
+
769
+ function setupStreamingTimeout() {
770
+ // Clear any existing timeout
771
+ if (streamingTimeout) {
772
+ clearTimeout(streamingTimeout);
773
+ }
774
+
775
+ // Set up new timeout
776
+ streamingTimeout = setTimeout(() => {
777
+ console.warn('Streaming timeout reached');
778
+ hideLoadingIndicator();
779
+ addMessage('Request timed out. LlamaBot may be processing a complex request. Please try again.', 'error');
780
+ }, STREAMING_TIMEOUT_MS);
781
+ }
782
+
783
+ function clearStreamingTimeout() {
784
+ if (streamingTimeout) {
785
+ clearTimeout(streamingTimeout);
786
+ streamingTimeout = null;
787
+ }
788
+ }
789
+
790
+ // console.log('🤖 Testing streaming');
791
+ // testStreaming();
792
+
793
+ async function testStreaming() {
794
+ const response = await fetch('/llama_bot/agent/test_streaming_2');
795
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
796
+ const decoder = new TextDecoder();
797
+ let buffer = '';
798
+ debugger;
799
+ let infinite_loop_protector = 0;
800
+ let in_infinite_loop = false;
801
+ while(!in_infinite_loop){ //infinite_loop_protector to protect against infinite loop
802
+ infinite_loop_protector++;
803
+ if (infinite_loop_protector > 10000) { //we shouldn't have 10000 loops, but just in case.
804
+ in_infinite_loop = true;
805
+ console.error('∞ ∞ ∞ Infinite loop prevented! ∞ ∞ ∞');
806
+ }
807
+ const { done, value } = reader.read();
808
+ console.log('🤖 SSE message:', value);
809
+ if (done) {
810
+ break;
811
+ }
812
+ }
813
+ }
814
+
855
815
  function selectPrompt(buttonElement) {
856
816
  const promptText = buttonElement.textContent;
857
817
  const messageInput = document.getElementById('message-input');
@@ -869,18 +829,11 @@
869
829
  }, 150);
870
830
  }
871
831
 
872
- function sendMessage() {
832
+ async function sendMessage() {
873
833
  const input = document.getElementById('message-input');
874
834
  const message = input.value.trim();
875
835
 
876
836
  if (message) {
877
- // Check if subscription is available
878
- if (!subscription) {
879
- console.error('WebSocket connection not established yet');
880
- addMessage('Connection not ready. Please wait...', 'error');
881
- return;
882
- }
883
-
884
837
  // Clear welcome message if it exists
885
838
  const welcomeMessage = document.querySelector('.welcome-message');
886
839
  if (welcomeMessage) {
@@ -912,8 +865,178 @@
912
865
  thread_id: threadId
913
866
  };
914
867
 
915
- console.log('Sending message with data:', messageData); // Debug log
916
- subscription.send(messageData);
868
+ console.log('Sending message with data:', messageData);
869
+
870
+ try {
871
+ // Set up fetch for streaming
872
+ const response = await fetch('/llama_bot/agent/send_message', {
873
+ method: 'POST',
874
+ headers: {
875
+ 'Content-Type': 'application/json',
876
+ },
877
+ body: JSON.stringify(messageData)
878
+ });
879
+
880
+ if (!response.ok) {
881
+ throw new Error(`HTTP error! status: ${response.status}`);
882
+ }
883
+
884
+ // Set up streaming timeout
885
+ setupStreamingTimeout();
886
+
887
+ // Set up the reader for the stream
888
+ const reader = response.body.getReader();
889
+ const decoder = new TextDecoder();
890
+ let buffer = '';
891
+
892
+ // const response_2 = await fetch('/llama_bot/agent/test_streaming', {
893
+ // method: 'GET',
894
+ // headers: {
895
+ // 'Content-Type': 'text/event-stream'
896
+ // },
897
+ // // body: JSON.stringify({
898
+ // // "message": message,
899
+ // // "thread_id": threadId
900
+ // // })
901
+ // });
902
+
903
+ // const reader_2 = response_2.body.pipeThrough(new TextDecoderStream()).getReader();
904
+
905
+ // while (true) {
906
+ // const { done, value } = await reader_2.read();
907
+ // if (done) {
908
+ // break;
909
+ // }
910
+ // // Process each SSE message (value)
911
+ // console.log('🤖 SSE message:', value);
912
+ // }
913
+
914
+ try {
915
+ while (true) {
916
+ const { done, value } = await reader.read();
917
+ console.log('Got a value from the stream 🧠 Stream value:', value);
918
+
919
+ if (done) {
920
+ console.log('Stream completed');
921
+ clearStreamingTimeout();
922
+ break;
923
+ }
924
+
925
+ // Decode the chunk and add to buffer
926
+ buffer += decoder.decode(value, { stream: true });
927
+
928
+ // Process complete SSE events (separated by \n\n). These \n\n are added by the Rails middleware to make sure it exists.
929
+ while (buffer.includes('\n\n')) {
930
+ const eventEnd = buffer.indexOf('\n\n');
931
+ const eventBlock = buffer.slice(0, eventEnd);
932
+ buffer = buffer.slice(eventEnd + 2);
933
+
934
+ // Parse SSE event
935
+ const dataLines = eventBlock.split('\n').filter(line => line.startsWith('data:'));
936
+ if (dataLines.length > 0) {
937
+
938
+ //remove the 'data:' prefix from the line
939
+ const jsonData = dataLines.map(line => line.substring(5).trim()).join('');
940
+
941
+ try {
942
+ const chunk = JSON.parse(jsonData);
943
+ console.log('Processing chunk:', chunk);
944
+
945
+ // Your existing chunk processing logic here...
946
+ if (chunk.type === 'start') {
947
+ console.log('Stream started:', chunk.request_id || 'unknown');
948
+ } else if (chunk.type === 'final') {
949
+ console.log('Stream completed');
950
+ hideLoadingIndicator();
951
+ clearStreamingTimeout();
952
+ } else if (chunk.type === 'error') {
953
+ console.error('Server error:', chunk);
954
+ hideLoadingIndicator();
955
+ clearStreamingTimeout();
956
+ addMessage(`Error: ${chunk.content || 'Unknown error occurred'}`, 'error');
957
+ } else if (chunk.type === 'ai') {
958
+ addMessage(chunk.content, 'ai', chunk);
959
+ hideLoadingIndicator();
960
+ clearStreamingTimeout();
961
+ } else if (chunk.type === 'tool') {
962
+ addMessage(chunk.content, 'tool', chunk);
963
+ } else {
964
+ if (chunk.content) {
965
+ addMessage(chunk.content, chunk.type || 'unknown', chunk);
966
+ }
967
+ console.log('Other chunk type:', chunk.type, chunk);
968
+ }
969
+
970
+ } catch (parseError) {
971
+ console.error('Error parsing SSE data:', parseError, 'Data:', jsonData);
972
+ }
973
+ }
974
+ }
975
+ }
976
+ } finally {
977
+ reader.releaseLock();
978
+ }
979
+
980
+ } catch (error) {
981
+ console.error('Error in sendMessage:', error);
982
+ hideLoadingIndicator();
983
+ clearStreamingTimeout();
984
+
985
+ // Show specific error message based on error type
986
+ let errorMessage = 'Error sending message. Please try again.';
987
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
988
+ errorMessage = 'Connection error. Please check if LlamaBot is running.';
989
+ } else if (error.message.includes('HTTP error')) {
990
+ errorMessage = `Server error: ${error.message}`;
991
+ }
992
+
993
+ addMessage(errorMessage, 'error');
994
+ }
995
+ }
996
+ }
997
+
998
+ function processChunk(chunk) {
999
+ console.log('Processing chunk in fallback handler:', chunk);
1000
+
1001
+ // Handle specific chunk types from Python backend
1002
+ switch (chunk.type) {
1003
+ case 'start':
1004
+ console.log('Stream started:', chunk.request_id || 'unknown');
1005
+ // Loading indicator already showing
1006
+ break;
1007
+
1008
+ case 'final':
1009
+ console.log('Stream completed');
1010
+ hideLoadingIndicator();
1011
+ clearStreamingTimeout();
1012
+ break;
1013
+
1014
+ case 'error':
1015
+ console.error('Server error:', chunk);
1016
+ hideLoadingIndicator();
1017
+ clearStreamingTimeout();
1018
+ addMessage(`Error: ${chunk.content || 'Unknown error occurred'}`, 'error');
1019
+ break;
1020
+
1021
+ case 'ai':
1022
+ // AI message from LangGraph
1023
+ addMessage(chunk.content, 'ai', chunk);
1024
+ hideLoadingIndicator();
1025
+ clearStreamingTimeout();
1026
+ break;
1027
+
1028
+ case 'tool':
1029
+ // Tool message from LangGraph
1030
+ addMessage(chunk.content, 'tool', chunk);
1031
+ break;
1032
+
1033
+ default:
1034
+ // Handle any other message types
1035
+ if (chunk.content) {
1036
+ addMessage(chunk.content, chunk.type || 'unknown', chunk);
1037
+ }
1038
+ console.log('Other chunk type:', chunk.type, chunk);
1039
+ break;
917
1040
  }
918
1041
  }
919
1042