PageSourceSearch

https://fluencypay.ai/assets/campaigns/campaign_web_chat_controlle…b33edd1618d0b8ae160514e7f02cfac17e21a751.js

js fluencypay.ai collected 2026-10-05 15:34:45 UTC 4,049 bytes, 147 lines download raw bytes

1import { Controller } from '@hotwired/stimulus'
2import { createConsumer } from '@rails/actioncable'
3
4export default class extends Controller {
5  static targets = ['input', 'messages', 'typingIndicator', 'loader', 'formContainer']
6  static values = {
7    campaignId: Number,
8    sessionId: String
9  }
10
11  connect () {
12    this.lastSentAt = 0
13    this.minDelay = 1000 // 1 second between messages
14
15    const consumer = createConsumer()
16    this.channel = consumer.subscriptions.create(
17      {
18        channel: 'CampaignWebChatChannel',
19        campaign_id: this.campaignIdValue
20      },
21      {
22        received: (data) => {
23          if (data.type === 'typing') {
24            this.showTypingIndicator()
25          } else if (data.type === 'stopped_typing') {
26            this.hideTypingIndicator()
27          } else if (data.type === 'message_received') {
28            this.hideTypingIndicator()
29            this.appendMessage(data)
30          } else if (data.type === 'onboarding_complete') {
31            this.hideTypingIndicator()
32            this.showGeneratingLoader()
33          } else if (data.type === 'page_updated') {
34            window.location.reload()
35          }
36        }
37      }
38    )
39  }
40
41  disconnect () {
42    if (this.channel) {
43      this.channel.unsubscribe()
44    }
45  }
46
47  send (event) {
48    event.preventDefault()
49
50    const text = this.inputTarget.value.trim()
51
52    if (!text) return
53
54    // Rate limiting - prevent spam
55    const now = Date.now()
56    const timeSinceLastSend = now - this.lastSentAt
57    if (timeSinceLastSend < this.minDelay) {
58      return
59    }
60
61    // Disable submit button temporarily
62    const submitButton = this.element.querySelector('[data-campaign-web-chat-target="submit"]')
63    if (submitButton) {
64      submitButton.disabled = true
65    }
66
67    this.lastSentAt = now
68
69    // Send message through channel
70    this.channel.send({ text })
71
72    // Add message to UI immediately
73    this.appendMessage({
74      text,
75      outgoing: false,
76      created_at_ago: 'less than a minute'
77    })
78
79    // Clear input
80    this.inputTarget.value = ''
81
82    // Re-enable submit button after delay
83    setTimeout(() => {
84      if (submitButton) {
85        submitButton.disabled = false
86      }
87    }, this.minDelay)
88  }
89
90  appendMessage (data) {
91    const messageDiv = document.createElement('div')
92    messageDiv.className = `flex ${data.outgoing ? 'justify-start' : 'justify-end'} animate-fade-in`
93
94    messageDiv.innerHTML = `
95      <div class="max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${data.outgoing ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-100'}">
96        <p>${data.text}</p>
97        <div class="text-xs mt-1 opacity-75">${data.created_at_ago} ago</div>
98      </div>
99    `
100
101    this.messagesTarget.prepend(messageDiv)
102
103    // Move typing indicator to the top (bottom visually) if it exists
104    if (this.hasTypingIndicatorTarget) {
105      this.messagesTarget.prepend(this.typingIndicatorTarget)
106    }
107
108    // Smooth scroll to bottom
109    this.smoothScrollToBottom()
110  }
111
112  smoothScrollToBottom () {
113    // For flex-col-reverse, scrollTop = 0 is the bottom
114    this.messagesTarget.scrollTo({
115      top: 0,
116      behavior: 'smooth'
117    })
118  }
119
120  showTypingIndicator () {
121    if (this.hasTypingIndicatorTarget) {
122      this.typingIndicatorTarget.classList.remove('hidden', 'opacity-0')
123      this.typingIndicatorTarget.classList.add('opacity-100')
124
125      // Move to top of messages container (bottom visually with flex-col-reverse)
126      this.messagesTarget.prepend(this.typingIndicatorTarget)
127
128      this.smoothScrollToBottom()
129    }
130  }
131
132  hideTypingIndicator () {
133    if (this.hasTypingIndicatorTarget) {
134      this.typingIndicatorTarget.classList.remove('opacity-100')
135      this.typingIndicatorTarget.classList.add('hidden', 'opacity-0')
136    }
137  }
138
139  showGeneratingLoader () {
140    // Hide the messages container and input form
141    this.messagesTarget.classList.add('hidden')
142    this.formContainerTarget.classList.add('hidden')
143
144    // Show the loader
145    this.loaderTarget.classList.remove('hidden')
146  }
147};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.