08-chat.js 10.1 KB
Newer Older
박민석's avatar
박민석 committed
1
/* eslint-disable no-undef */
minseok.park's avatar
minseok.park committed
2 3 4
;(function () {
  'use strict'

minseok.park's avatar
minseok.park committed
5 6 7
  const chatButton = document.querySelector('.chat-btn')
  const chat = document.querySelector('.chat')
  const chatIcon = document.querySelector('.chat-btn svg')
박민석's avatar
박민석 committed
8 9
  const chatTextArea = document.getElementById('chat-input')
  const sendButton = document.getElementById('send-btn')
박민석's avatar
박민석 committed
10
  const stopButton = document.getElementById('stop-btn')
minseok.park's avatar
minseok.park committed
11
  const chatBody = document.querySelector('.chat .chat-body')
12
  const expandButton = document.querySelector('#expand-btn svg')
minseok.park's avatar
minseok.park committed
13 14 15
  const productButton = document.getElementById('model-toggle')
  const productList = document.getElementById('model-popover')
  const backdrop = document.querySelector('.model .backdrop')
박민석's avatar
박민석 committed
16 17 18

  let INIT = false
  let isComposing = false
minseok.park's avatar
minseok.park committed
19
  let isLoading = false
박민석's avatar
박민석 committed
20 21
  let isWriting = false
  let isPause = false
박민석's avatar
박민석 committed
22
  let docType = 'all' // 선택된 제품 유형을 저장할 변수
minseok.park's avatar
minseok.park committed
23 24 25

  const openIconPath = `
    <path stroke-linecap="round" stroke-linejoin="round"
26 27 28 29 30 31 32 33
    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>
minseok.park's avatar
minseok.park committed
34 35
  `
  const closeIconPath = `
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
    <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>
minseok.park's avatar
minseok.park committed
60 61
  `

minseok.park's avatar
minseok.park committed
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
  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')
minseok.park's avatar
minseok.park committed
115
      backdrop.classList.add('is-active')
minseok.park's avatar
minseok.park committed
116 117 118 119 120 121 122 123 124 125 126 127 128
    }
  })

  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')
    }
  })

박민석's avatar
박민석 committed
129 130 131
  document.querySelectorAll('#model-popover li').forEach((item) => {
    item.addEventListener('click', function () {
      const selectedProduct = this.textContent
minseok.park's avatar
minseok.park committed
132 133 134
      docType = selectedProduct === '전체' ? 'all' : selectedProduct
      productList.classList.remove('show')
      backdrop.classList.remove('is-active')
박민석's avatar
박민석 committed
135

minseok.park's avatar
minseok.park committed
136
      productButton.textContent = selectedProduct
박민석's avatar
박민석 committed
137

minseok.park's avatar
minseok.park committed
138 139 140
      const message = docType === 'all'
        ? '전체 제품을 대상으로 매뉴얼 가이드를 제공해 드리겠습니다. 제품 선택 시 더 정확한 정보를 확인할 수 있습니다.'
        : `${docType.toUpperCase()} 매뉴얼 가이드에 대한 도움을 드리겠습니다.`
박민석's avatar
박민석 committed
141

minseok.park's avatar
minseok.park committed
142 143 144 145
      if (!isWriting && !isLoading) {
        isWriting = true
        createAiMessage(message)
      }
박민석's avatar
박민석 committed
146 147 148
    })
  })

minseok.park's avatar
minseok.park committed
149
  function initializeChat () {
박민석's avatar
박민석 committed
150 151 152
    if (!INIT) {
      const messageItem = document.createElement('pre')
      messageItem.className = 'message ai'
minseok.park's avatar
minseok.park committed
153
      messageItem.textContent = '제품 매뉴얼 가이드에 대한 도움을 드리겠습니다. 제품 선택 시 더 정확한 답변을 제공해 드립니다.'
박민석's avatar
박민석 committed
154
      chatBody.appendChild(messageItem)
박민석's avatar
박민석 committed
155

박민석's avatar
박민석 committed
156 157 158 159 160 161 162 163 164 165 166 167 168
      INIT = true

      // Composition events
      chatTextArea.addEventListener('compositionstart', () => {
        isComposing = true
      })

      chatTextArea.addEventListener('compositionend', () => {
        isComposing = false
      })

      // Keydown event
      chatTextArea.addEventListener('keydown', function (e) {
박민석's avatar
박민석 committed
169
        if (e.key === 'Enter' && !e.shiftKey && !isComposing && !isLoading && !isWriting) {
박민석's avatar
박민석 committed
170 171 172
          e.preventDefault() // Prevent the default action of Enter (which is to create a new line)
          const { value } = e.target
          if (value.trim() !== '') {
minseok.park's avatar
minseok.park committed
173
            handleUserMessage(value)
박민석's avatar
박민석 committed
174 175 176 177 178
          }
        }
      })

      sendButton.addEventListener('click', () => {
박민석's avatar
박민석 committed
179
        if (!isLoading && !isWriting) {
minseok.park's avatar
minseok.park committed
180 181 182
          const message = chatTextArea.value
          handleUserMessage(message)
        }
박민석's avatar
박민석 committed
183
      })
박민석's avatar
박민석 committed
184 185 186 187 188 189

      stopButton.addEventListener('click', () => {
        isPause = true
        stopButton.classList.remove('active')
        sendButton.classList.add('active')
      })
박민석's avatar
박민석 committed
190 191 192
    }
  }

minseok.park's avatar
minseok.park committed
193 194 195 196 197 198 199
  function handleUserMessage (message) {
    if (message.trim() !== '') {
      addMessageToChatBody(message)
      chatTextArea.value = ''
    }
  }

minseok.park's avatar
minseok.park committed
200
  function addMessageToChatBody (message) {
박민석's avatar
박민석 committed
201 202 203 204 205 206 207
    if (message.trim() !== '') {
      const user = document.createElement('pre')
      user.className = 'message user'
      user.textContent = message
      chatBody.appendChild(user)
      chatBody.scrollTop = chatBody.scrollHeight

박민석's avatar
박민석 committed
208 209 210 211 212 213 214
      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
minseok.park's avatar
minseok.park committed
215

박민석's avatar
박민석 committed
216
      isLoading = true
박민석's avatar
박민석 committed
217

박민석's avatar
박민석 committed
218 219
      const payload = {
        question: message,
박민석's avatar
박민석 committed
220
        docsType: docType.toLowerCase(),
박민석's avatar
박민석 committed
221 222
      }

박민석's avatar
박민석 committed
223
      fetch('https://docs.bxi.link/ask', {
박민석's avatar
박민석 committed
224 225 226 227 228
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
박민석's avatar
박민석 committed
229 230
      })
        .then((response) => {
박민석's avatar
박민석 committed
231
          const reader = response.body.getReader()
박민석's avatar
박민석 committed
232 233
          const decoder = new TextDecoder('utf-8')

박민석's avatar
박민석 committed
234
          sendButton.classList.remove('active')
박민석's avatar
박민석 committed
235
          stopButton.classList.add('active')
박민석's avatar
박민석 committed
236

박민석's avatar
박민석 committed
237 238
          const messageItem = document.createElement('pre')
          messageItem.className = 'message ai'
박민석's avatar
박민석 committed
239
          chatBody.appendChild(messageItem) // 실제 메시지가 들어갈 자리
박민석's avatar
박민석 committed
240

박민석's avatar
박민석 committed
241 242
          isWriting = true

박민석's avatar
박민석 committed
243
          function readStream () {
박민석's avatar
박민석 committed
244
            return reader.read().then(({ done, value }) => {
박민석's avatar
박민석 committed
245 246 247 248
              if (done) {
                isLoading = false
                return
              }
박민석's avatar
박민석 committed
249

박민석's avatar
박민석 committed
250
              const chunk = decoder.decode(value, { stream: true })
박민석's avatar
박민석 committed
251 252

              // 로더 제거 후 메시지 표시
박민석's avatar
박민석 committed
253
              if (chatLoader) chatBody.removeChild(chatLoader)
박민석's avatar
박민석 committed
254 255 256 257

              typeEffect(messageItem, chunk, 30)
              chatBody.scrollTop = chatBody.scrollHeight

박민석's avatar
박민석 committed
258 259 260 261 262
              return readStream()
            })
          }

          return readStream()
박민석's avatar
박민석 committed
263 264 265
        })
        .catch((error) => {
          console.error('Chatbot Error', error)
박민석's avatar
박민석 committed
266
          isLoading = false
박민석's avatar
박민석 committed
267
        })
박민석's avatar
박민석 committed
268 269
    }
  }
박민석's avatar
박민석 committed
270

박민석's avatar
박민석 committed
271 272 273 274 275
  // 타이핑 효과 함수
  function typeEffect (element, text, speed = 50) {
    let index = 0

    function type () {
박민석's avatar
박민석 committed
276 277 278 279 280 281
      if (isPause) {
        isPause = false
        isWriting = false
        return
      }

박민석's avatar
박민석 committed
282 283 284 285 286
      if (index < text.length) {
        element.textContent += text.charAt(index)
        chatBody.scrollTop = chatBody.scrollHeight
        index++
        setTimeout(type, speed) // 한글자씩 추가
박민석's avatar
박민석 committed
287 288 289 290
      } else {
        sendButton.classList.add('active')
        stopButton.classList.remove('active')
        isWriting = false
박민석's avatar
박민석 committed
291 292 293 294 295 296
      }
    }

    type()
  }

박민석's avatar
박민석 committed
297
  function createAiMessage (message) {
박민석's avatar
박민석 committed
298 299 300
    const messageItem = document.createElement('pre')
    messageItem.className = 'message ai'
    chatBody.appendChild(messageItem)
박민석's avatar
박민석 committed
301 302

    typeEffect(messageItem, message, 30)
박민석's avatar
박민석 committed
303 304
    chatBody.scrollTop = chatBody.scrollHeight
  }
minseok.park's avatar
minseok.park committed
305
})()