/* eslint-disable no-undef */ ;(function () { 'use strict' const chatButton = document.querySelector('.sidebar .chat-btn') const chat = document.querySelector('.sidebar .chat') const chatIcon = document.querySelector('.sidebar .chat-btn svg') const chatTextArea = document.getElementById('chat-input') const sendButton = document.getElementById('send-btn') const stopButton = document.getElementById('stop-btn') const chatBody = document.querySelector('.sidebar .chat .chat-body') const expandButton = document.querySelector('#expand-btn svg') const productButton = document.getElementById('model-toggle') const productList = document.getElementById('model-popover') const backdrop = document.querySelector('.model .backdrop') let INIT = false let isComposing = false let isLoading = false let isWriting = false let isPause = false let docType = '' // 선택된 제품 유형을 저장할 변수 const openIconPath = ` <path stroke-linecap="round" stroke-linejoin="round" d="M7.188 10a.312.312 0 1 1-.625 0 .312.312 0 0 1 .625 0Zm0 0h-.313m3.438 0a.312.312 0 1 1-.625 0 .312.312 0 0 1 .625 0Zm0 0h-.313m3.437 0a.312.312 0 1 1-.625 0 .312.312 0 0 1 .625 0Zm0 0h-.312M17.5 10c0 3.797-3.353 6.875-7.5 6.875a8.735 8.735 0 0 1-2.292-.303 5.368 5.368 0 0 1-3.639 1.576 5.364 5.364 0 0 1-.428-.059 4.025 4.025 0 0 0 .88-1.818c.081-.409 -.12-.806-.422-1.098C3.273 13.484 2.5 11.825 2.5 10c0-3.797 3.353-6.875 7.5-6.875s7.5 3.078 7.5 6.875Z"></path> ` const closeIconPath = ` <path clip-rule="evenodd" fill-rule="evenodd" d="M12.53 16.28a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 0 1 1.06-1.06L12 14.69l6.97-6.97a.75.75 0 1 1 1.06 1.06l-7.5 7.5Z"></path> ` const collapseIconPath = ` <path d="M3.28 2.22a.75.75 0 0 0-1.06 1.06L5.44 6.5H2.75a.75.75 0 0 0 0 1.5h4.5 A.75.75 0 0 0 8 7.25v-4.5a.75.75 0 0 0-1.5 0v2.69L3.28 2.22ZM13.5 2.75 a.75.75 0 0 0-1.5 0v4.5c0 .414.336.75.75.75h4.5a.75.75 0 0 0 0-1.5h-2.69l3.22-3.22 a.75.75 0 0 0-1.06-1.06L13.5 5.44V2.75ZM3.28 17.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 1 0 1.06 1.06ZM13.5 14.56l3.22 3.22a.75.75 0 1 0 1.06-1.06l-3.22-3.22h2.69 a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0v-2.69Z"> </path> ` const expandIconPath = ` <path d=" m13.28 7.78 3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 0 0 1.06 1.06ZM2 17.25v-4.5a.75.75 0 0 1 1.5 0v2.69l3.22-3.22a.75.75 0 0 1 1.06 1.06L4.56 16.5h2.69a.75.75 0 0 1 0 1.5h-4.5a.747.747 0 0 1-.75-.75ZM12.22 13.28l3.22 3.22h-2.69a.75.75 0 0 0 0 1.5h4.5a.747.747 0 0 0 .75-.75v-4.5a.75.75 0 0 0-1.5 0v2.69l-3.22-3.22a.75.75 0 1 0-1.06 1.06ZM3.5 4.56l3.22 3.22a.75.75 0 0 0 1.06-1.06L4.56 3.5h2.69a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0V4.56Z"> </path> ` chatButton.addEventListener('click', () => { chat.classList.toggle('show') if (chat.classList.contains('show')) { chat.classList.add('expand') chatIcon.innerHTML = closeIconPath chatIcon.setAttribute('fill', 'none') chatIcon.setAttribute('stroke', '#fff') chatIcon.setAttribute('viewBox', '0 -1 24 24') expandButton.classList.add('expand') expandButton.innerHTML = collapseIconPath expandButton.setAttribute('fill', '#fff') expandButton.setAttribute('viewBox', '0 0 20 20') } else { chatIcon.innerHTML = openIconPath chatIcon.setAttribute('fill', '#fff') chatIcon.setAttribute('stroke', '#0072ce') chatIcon.setAttribute('viewBox', '0 0 20 20') expandButton.classList.remove('expand') expandButton.innerHTML = expandIconPath expandButton.setAttribute('fill', '#fff') expandButton.setAttribute('viewBox', '0 0 20 20') } if (!INIT) { initializeChat() } }) expandButton.addEventListener('click', () => { expandButton.classList.toggle('expand') if (expandButton.classList.contains('expand')) { expandButton.innerHTML = collapseIconPath expandButton.setAttribute('fill', '#fff') expandButton.setAttribute('viewBox', '0 0 20 20') chat.classList.add('expand') } else { expandButton.innerHTML = expandIconPath expandButton.setAttribute('fill', '#fff') expandButton.setAttribute('viewBox', '0 0 20 20') chat.classList.remove('expand') } }) productButton.addEventListener('click', () => { if (productList.classList.contains('show')) { productList.classList.remove('show') backdrop.classList.remove('is-active') } else { productList.classList.add('show') backdrop.classList.add('is-active') } }) backdrop.addEventListener('click', () => { if (backdrop.classList.contains('is-active')) { productList.classList.remove('show') backdrop.classList.remove('is-active') } else { productList.classList.add('show') backdrop.classList.add('is-active') } }) document.querySelectorAll('#model-popover li').forEach((item) => { item.addEventListener('click', function () { const selectedProduct = this.textContent docType = selectedProduct === '전체' ? 'all' : selectedProduct productList.classList.remove('show') backdrop.classList.remove('is-active') productButton.textContent = selectedProduct const message = docType === 'all' ? '전체 제품을 대상으로 매뉴얼 가이드를 제공해 드리겠습니다. 제품 선택 시 더 정확한 정보를 확인할 수 있습니다.' : `${docType.toUpperCase()} 매뉴얼 가이드에 대한 도움을 드리겠습니다.` if (!isWriting && !isLoading) { isWriting = true createAiMessage(message) } }) }) function initializeChat () { if (!INIT) { const messageItem = document.createElement('pre') messageItem.className = 'message ai' messageItem.textContent = '제품 매뉴얼 가이드에 대한 도움을 드리겠습니다. 제품 선택 시 더 정확한 답변을 제공해 드립니다.' chatBody.appendChild(messageItem) INIT = true // Composition events chatTextArea.addEventListener('compositionstart', () => { isComposing = true }) chatTextArea.addEventListener('compositionend', () => { isComposing = false }) // Keydown event chatTextArea.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey && !isComposing && !isLoading && !isWriting) { e.preventDefault() // Prevent the default action of Enter (which is to create a new line) const { value } = e.target if (value.trim() !== '') { handleUserMessage(value) } } }) sendButton.addEventListener('click', () => { if (!isLoading && !isWriting) { const message = chatTextArea.value handleUserMessage(message) } }) stopButton.addEventListener('click', () => { isPause = true stopButton.classList.remove('active') sendButton.classList.add('active') }) } } function handleUserMessage (message) { if (message.trim() !== '') { addMessageToChatBody(message) chatTextArea.value = '' } } function addMessageToChatBody (message) { if (message.trim() !== '') { const user = document.createElement('pre') user.className = 'message user' user.textContent = message chatBody.appendChild(user) chatBody.scrollTop = chatBody.scrollHeight const chatLoader = document.createElement('div') chatLoader.className = 'chat-loader' const loader = document.createElement('div') loader.className = 'loader' chatLoader.appendChild(loader) chatBody.appendChild(chatLoader) chatBody.scrollTop = chatBody.scrollHeight isLoading = true const payload = { question: message, docsType: docType.toLowerCase(), } fetch('https://docs.bxi.link/ask', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }) .then((response) => { const reader = response.body.getReader() const decoder = new TextDecoder('utf-8') sendButton.classList.remove('active') stopButton.classList.add('active') const messageItem = document.createElement('pre') messageItem.className = 'message ai' chatBody.appendChild(messageItem) // 실제 메시지가 들어갈 자리 isWriting = true function readStream () { return reader.read().then(({ done, value }) => { if (done) { isLoading = false return } const chunk = decoder.decode(value, { stream: true }) // 로더 제거 후 메시지 표시 if (chatLoader) chatBody.removeChild(chatLoader) typeEffect(messageItem, chunk, 30) chatBody.scrollTop = chatBody.scrollHeight return readStream() }) } return readStream() }) .catch((error) => { console.error('Chatbot Error', error) isLoading = false }) } } // 타이핑 효과 함수 function typeEffect (element, text, speed = 50) { let index = 0 function type () { if (isPause) { isPause = false isWriting = false return } if (index < text.length) { element.textContent += text.charAt(index) chatBody.scrollTop = chatBody.scrollHeight index++ setTimeout(type, speed) // 한글자씩 추가 } else { sendButton.classList.add('active') stopButton.classList.remove('active') isWriting = false } } type() } function createAiMessage (message) { const messageItem = document.createElement('pre') messageItem.className = 'message ai' chatBody.appendChild(messageItem) typeEffect(messageItem, message, 30) chatBody.scrollTop = chatBody.scrollHeight } })()