1import {Controller} from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static entityMap = { 5 '&': '&', 6 '<': '<', 7 '>': '>', 8 '"': '"', 9 "'": ''', 10 '/': '/', 11 '`': '`', 12 '=': '=' 13 }; 14 15 static targets = ['form', 'message', 'hiddenJobIdInput', 'hiddenMessageContentKindInput', 'sendButton', 'candidateMessageTemplate', 'overlay', 'overlayContentContainer'] 16 17 static values = {conversationState: String, giveUpTimeout: Number} 18 19 connect() { 20 this.conversationStateWriteObserver = new MutationObserver(() => this.conversationStateValueWritten()) 21 this.conversationStateWriteObserver.observe(this.element, {attributeFilter: ['data-chatbot--window-conversation-state-value']}) 22 } 23 24 disconnect() { 25 this.conversationStateWriteObserver.disconnect() 26 this.clearGiveUpTimer() 27 } 28 29 conversationStateValueChanged() { 30 this.syncSendButton() 31 this.clearGiveUpTimer() 32 if (this.replyInProgress()) { 33 this.startGiveUpTimer() 34 } 35 } 36 37 // Unlike Stimulus's conversationStateValueChanged, this also fires when a streamed chunk rewrites the same value 38 conversationStateValueWritten() { 39 if (this.replyInProgress()) { 40 this.restartGiveUpTimer() 41 } 42 } 43 44 restartGiveUpTimer() { 45 this.clearGiveUpTimer() 46 this.startGiveUpTimer() 47 } 48 49 startGiveUpTimer() { 50 if (this.hasGiveUpTimeoutValue) { 51 this.giveUpTimer = setTimeout(() => this.abandonReply(), this.giveUpTimeoutValue) 52 } 53 } 54 55 abandonReply() { 56 this.conversationStateValue = 'idle' 57 } 58 59 clearGiveUpTimer() { 60 clearTimeout(this.giveUpTimer) 61 } 62 63 formSubmit(e) { 64 if (this.emptyMessage() || this.sendButtonTarget.disabled) { 65 e.preventDefault() 66 } 67 } 68 69 turboSubmit(e) { 70 this.conversationStateValue = 'awaiting' 71 this.renderTurboStreamWithCandidateMessage() 72 this.resetForm() 73 } 74 75 endSubmit(e) { 76 if (!e.detail.success) { 77 this.conversationStateValue = 'idle' 78 } 79 } 80 81 syncSendButton() { 82 if (this.hasSendButtonTarget) { 83 this.sendButtonTarget.disabled = this.replyInProgress() 84 } 85 } 86 87 replyInProgress() { 88 return this.conversationStateValue !== 'idle' 89 } 90 91 renderTurboStreamWithCandidateMessage() { 92 const turboStreamsWithPlaceholders = this.candidateMessageTemplateTarget.content.querySelectorAll('turbo-stream:not([action="playNotification"])') 93 for (const turboStreamWithPlaceholder of turboStreamsWithPlaceholders) { 94 const tempTurboStreamContainer = document.createElement('div') 95 tempTurboStreamContainer.innerHTML = turboStreamWithPlaceholder.outerHTML 96 .replace('{{message_text}}', this.escapeHtml(this.messageTarget.value)) 97 .replace('{{message_label}}', this.escapeHtml(this.messageTarget.dataset.messageLabel)) 98 .replace('{{messageContentKind}}', this.escapeHtml(this.hiddenMessageContentKindInputTarget.value.replaceAll('_', '-'))) 99 .replace('{{messageIconClass}}', this.escapeHtml(this.messageIconClass || '')) 100 Turbo.renderStreamMessage(tempTurboStreamContainer.innerHTML) 101 } 102 } 103 104 resetForm() { 105 this.messageTarget.value = '' 106 this.hiddenMessageContentKindInputTarget.value = this.hiddenMessageContentKindInputTarget.dataset.defaultValue 107 this.messageIconClass = '' 108 this.toggleSubmitButtonVisibility() 109 } 110 111 toggleSubmitButtonVisibility() { 112 this.sendButtonTarget.classList.toggle('d-none', this.emptyMessage()) 113 } 114 115 emptyMessage() { 116 return this.messageTarget.value.trim().length === 0 117 } 118 119 keyEnterEvent(e) { 120 e.preventDefault() 121 this.formTarget.requestSubmit() 122 } 123 124 focusInputText() { 125 this.formTarget.classList.add('active') 126 } 127 128 blurInputText() { 129 this.formTarget.classList.remove('active') 130 } 131 132 closeOverlay() { 133 this.overlayTarget.classList.add('d-none') 134 this.overlayContentContainerTarget.innerHTML = "" 135 } 136 137 sendButtonDataAsMessage({currentTarget: {dataset: {message, jobId, messageContentKind, messageIconClass}}}) { 138 this.messageTarget.value = message 139 this.hiddenJobIdInputTarget.value = jobId 140 if (messageContentKind) { 141 this.hiddenMessageContentKindInputTarget.value = messageContentKind 142 } 143 this.messageIconClass = messageIconClass 144 this.formTarget.requestSubmit() 145 146 if (this.hasOverlayTarget && !this.overlayTarget.classList.contains('d-none')) { 147 this.closeOverlay() 148 } 149 } 150 151 escapeHtml(string) { 152 return String(string).replace(/[&<>"'`=\/]/g, (s) => this.constructor.entityMap[s]) 153 } 154}
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.