| | |
| | |
| | |
| |
|
| | |
| | const chatTab = document.getElementById('chatTab'); |
| | const docsTab = document.getElementById('docsTab'); |
| | const chatSection = document.getElementById('chatSection'); |
| | const docsSection = document.getElementById('docsSection'); |
| | const chatMessages = document.getElementById('chatMessages'); |
| | const userInput = document.getElementById('userInput'); |
| | const sendButton = document.getElementById('sendButton'); |
| | const micButton = document.getElementById('micButton'); |
| | const stopRecordingButton = document.getElementById('stopRecordingButton'); |
| | const recordingStatus = document.getElementById('recordingStatus'); |
| | const uploadForm = document.getElementById('uploadForm'); |
| | const documentFile = document.getElementById('documentFile'); |
| | const fileName = document.getElementById('fileName'); |
| | const uploadButton = document.getElementById('uploadButton'); |
| | const uploadStatus = document.getElementById('uploadStatus'); |
| | const refreshDocsButton = document.getElementById('refreshDocsButton'); |
| | const docsList = document.getElementById('docsList'); |
| | const docsLoading = document.getElementById('docsLoading'); |
| | const noDocsMessage = document.getElementById('noDocsMessage'); |
| |
|
| | |
| | let mediaRecorder = null; |
| | let audioChunks = []; |
| | let isRecording = false; |
| |
|
| | |
| | async function checkAppStatus() { |
| | try { |
| | const response = await fetch('/api/status'); |
| | if (!response.ok) { |
| | return false; |
| | } |
| | const data = await response.json(); |
| | return data.ready; |
| | } catch (error) { |
| | console.error('Status check failed:', error); |
| | return false; |
| | } |
| | } |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) { |
| | |
| | const statusInterval = setInterval(async () => { |
| | const isReady = await checkAppStatus(); |
| | if (isReady) { |
| | clearInterval(statusInterval); |
| | console.log('μ±μ΄ μ€λΉλμμ΅λλ€.'); |
| | } |
| | }, 5000); |
| | } |
| |
|
| | |
| | chatTab.addEventListener('click', () => { |
| | switchTab('chat'); |
| | }); |
| | |
| | docsTab.addEventListener('click', () => { |
| | switchTab('docs'); |
| | loadDocuments(); |
| | }); |
| | |
| | |
| | sendButton.addEventListener('click', sendMessage); |
| | userInput.addEventListener('keydown', (event) => { |
| | if (event.key === 'Enter' && !event.shiftKey) { |
| | event.preventDefault(); |
| | sendMessage(); |
| | } |
| | }); |
| | |
| | |
| | micButton.addEventListener('click', startRecording); |
| | stopRecordingButton.addEventListener('click', stopRecording); |
| | |
| | |
| | documentFile.addEventListener('change', (event) => { |
| | if (event.target.files.length > 0) { |
| | fileName.textContent = event.target.files[0].name; |
| | } else { |
| | fileName.textContent = 'μ νλ νμΌ μμ'; |
| | } |
| | }); |
| | |
| | uploadForm.addEventListener('submit', (event) => { |
| | event.preventDefault(); |
| | uploadDocument(); |
| | }); |
| | |
| | |
| | refreshDocsButton.addEventListener('click', loadDocuments); |
| | |
| | |
| | userInput.addEventListener('input', adjustTextareaHeight); |
| | |
| | |
| | if (docsSection.classList.contains('active')) { |
| | loadDocuments(); |
| | } |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | function switchTab(tabName) { |
| | if (tabName === 'chat') { |
| | chatTab.classList.add('active'); |
| | docsTab.classList.remove('active'); |
| | chatSection.classList.add('active'); |
| | docsSection.classList.remove('active'); |
| | } else if (tabName === 'docs') { |
| | chatTab.classList.remove('active'); |
| | docsTab.classList.add('active'); |
| | chatSection.classList.remove('active'); |
| | docsSection.classList.add('active'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async function sendMessage() { |
| | const message = userInput.value.trim(); |
| | if (!message) return; |
| | |
| | |
| | addMessage(message, 'user'); |
| | userInput.value = ''; |
| | adjustTextareaHeight(); |
| | |
| | |
| | const loadingMessageId = addLoadingMessage(); |
| | |
| | try { |
| | |
| | const response = await fetch('/api/chat', { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json' |
| | }, |
| | body: JSON.stringify({ query: message }) |
| | }); |
| | |
| | if (!response.ok) { |
| | throw new Error(`HTTP error! status: ${response.status}`); |
| | } |
| | |
| | const data = await response.json(); |
| | |
| | |
| | removeLoadingMessage(loadingMessageId); |
| | |
| | |
| | if (data.error) { |
| | addErrorMessage(data.error); |
| | } else { |
| | addMessage(data.answer, 'bot', null, data.sources); |
| | } |
| | } catch (error) { |
| | console.error('Error:', error); |
| | removeLoadingMessage(loadingMessageId); |
| | addErrorMessage('μ€λ₯κ° λ°μνμ΅λλ€. λ€μ μλν΄ μ£ΌμΈμ.'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async function startRecording() { |
| | if (isRecording) return; |
| | |
| | try { |
| | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| | isRecording = true; |
| | audioChunks = []; |
| | |
| | mediaRecorder = new MediaRecorder(stream); |
| | |
| | mediaRecorder.addEventListener('dataavailable', (event) => { |
| | if (event.data.size > 0) audioChunks.push(event.data); |
| | }); |
| | |
| | mediaRecorder.addEventListener('stop', sendAudioMessage); |
| | |
| | |
| | mediaRecorder.start(); |
| | |
| | |
| | micButton.style.display = 'none'; |
| | recordingStatus.classList.remove('hidden'); |
| | |
| | console.log('λ
Ήμ μμλ¨'); |
| | } catch (error) { |
| | console.error('μμ± λ
Ήμ κΆνμ μ»μ μ μμ΅λλ€:', error); |
| | alert('λ§μ΄ν¬ μ κ·Ό κΆνμ΄ νμν©λλ€.'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function stopRecording() { |
| | if (!isRecording || !mediaRecorder) return; |
| | |
| | mediaRecorder.stop(); |
| | isRecording = false; |
| | |
| | |
| | micButton.style.display = 'flex'; |
| | recordingStatus.classList.add('hidden'); |
| | |
| | console.log('λ
Ήμ μ€μ§λ¨'); |
| | } |
| |
|
| | |
| | |
| | |
| | async function sendAudioMessage() { |
| | if (audioChunks.length === 0) return; |
| | |
| | |
| | const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); |
| | |
| | |
| | const loadingMessageId = addLoadingMessage(); |
| | |
| | try { |
| | |
| | const formData = new FormData(); |
| | formData.append('audio', audioBlob, 'recording.wav'); |
| | |
| | |
| | const response = await fetch('/api/voice', { |
| | method: 'POST', |
| | body: formData |
| | }); |
| | |
| | if (!response.ok) { |
| | throw new Error(`HTTP error! status: ${response.status}`); |
| | } |
| | |
| | const data = await response.json(); |
| | |
| | |
| | removeLoadingMessage(loadingMessageId); |
| | |
| | |
| | if (data.error) { |
| | addErrorMessage(data.error); |
| | } else { |
| | |
| | if (data.transcription) { |
| | addMessage(data.transcription, 'user'); |
| | } |
| | |
| | |
| | addMessage(data.answer, 'bot', data.transcription, data.sources); |
| | } |
| | } catch (error) { |
| | console.error('Error:', error); |
| | removeLoadingMessage(loadingMessageId); |
| | addErrorMessage('μ€λμ€ μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€. λ€μ μλν΄ μ£ΌμΈμ.'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async function uploadDocument() { |
| | if (documentFile.files.length === 0) { |
| | alert('νμΌμ μ νν΄ μ£ΌμΈμ.'); |
| | return; |
| | } |
| | |
| | |
| | uploadStatus.classList.remove('hidden'); |
| | uploadStatus.className = 'upload-status'; |
| | uploadStatus.innerHTML = '<div class="spinner"></div><p>μ
λ‘λ μ€...</p>'; |
| | uploadButton.disabled = true; |
| | |
| | try { |
| | const formData = new FormData(); |
| | formData.append('document', documentFile.files[0]); |
| | |
| | |
| | const response = await fetch('/api/upload', { |
| | method: 'POST', |
| | body: formData |
| | }); |
| | |
| | const data = await response.json(); |
| | |
| | |
| | if (data.error) { |
| | uploadStatus.className = 'upload-status error'; |
| | uploadStatus.textContent = `μ€λ₯: ${data.error}`; |
| | } else if (data.warning) { |
| | uploadStatus.className = 'upload-status warning'; |
| | uploadStatus.textContent = data.message; |
| | } else { |
| | uploadStatus.className = 'upload-status success'; |
| | uploadStatus.textContent = data.message; |
| | |
| | |
| | loadDocuments(); |
| | |
| | |
| | documentFile.value = ''; |
| | fileName.textContent = 'μ νλ νμΌ μμ'; |
| | } |
| | } catch (error) { |
| | console.error('Error:', error); |
| | uploadStatus.className = 'upload-status error'; |
| | uploadStatus.textContent = 'μ
λ‘λ μ€ μ€λ₯κ° λ°μνμ΅λλ€. λ€μ μλν΄ μ£ΌμΈμ.'; |
| | } finally { |
| | uploadButton.disabled = false; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async function loadDocuments() { |
| | |
| | docsList.querySelector('tbody').innerHTML = ''; |
| | docsLoading.classList.remove('hidden'); |
| | noDocsMessage.classList.add('hidden'); |
| | |
| | try { |
| | |
| | const response = await fetch('/api/documents'); |
| | |
| | if (!response.ok) { |
| | throw new Error(`HTTP error! status: ${response.status}`); |
| | } |
| | |
| | const data = await response.json(); |
| | |
| | |
| | docsLoading.classList.add('hidden'); |
| | |
| | if (!data.documents || data.documents.length === 0) { |
| | noDocsMessage.classList.remove('hidden'); |
| | return; |
| | } |
| | |
| | |
| | const tbody = docsList.querySelector('tbody'); |
| | data.documents.forEach(doc => { |
| | const row = document.createElement('tr'); |
| | |
| | const fileNameCell = document.createElement('td'); |
| | fileNameCell.textContent = doc.filename || doc.source; |
| | row.appendChild(fileNameCell); |
| | |
| | const chunksCell = document.createElement('td'); |
| | chunksCell.textContent = doc.chunks; |
| | row.appendChild(chunksCell); |
| | |
| | const typeCell = document.createElement('td'); |
| | typeCell.textContent = doc.filetype || '-'; |
| | row.appendChild(typeCell); |
| | |
| | tbody.appendChild(row); |
| | }); |
| | } catch (error) { |
| | console.error('Error:', error); |
| | docsLoading.classList.add('hidden'); |
| | noDocsMessage.classList.remove('hidden'); |
| | noDocsMessage.querySelector('p').textContent = 'λ¬Έμ λͺ©λ‘μ λΆλ¬μ€λ μ€ μ€λ₯κ° λ°μνμ΅λλ€.'; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function addMessage(text, sender, transcription = null, sources = null) { |
| | const messageDiv = document.createElement('div'); |
| | messageDiv.classList.add('message', sender); |
| | |
| | const contentDiv = document.createElement('div'); |
| | contentDiv.classList.add('message-content'); |
| | |
| | |
| | if (transcription && sender === 'bot') { |
| | const transcriptionP = document.createElement('p'); |
| | transcriptionP.classList.add('transcription'); |
| | transcriptionP.textContent = `"${transcription}"`; |
| | contentDiv.appendChild(transcriptionP); |
| | } |
| | |
| | |
| | const textP = document.createElement('p'); |
| | textP.textContent = text; |
| | contentDiv.appendChild(textP); |
| | |
| | |
| | if (sources && sources.length > 0 && sender === 'bot') { |
| | const sourcesDiv = document.createElement('div'); |
| | sourcesDiv.classList.add('sources'); |
| | |
| | const sourcesTitle = document.createElement('strong'); |
| | sourcesTitle.textContent = 'μΆμ²: '; |
| | sourcesDiv.appendChild(sourcesTitle); |
| | |
| | sources.forEach((source, index) => { |
| | if (index < 3) { |
| | const sourceSpan = document.createElement('span'); |
| | sourceSpan.classList.add('source-item'); |
| | sourceSpan.textContent = source.source; |
| | sourcesDiv.appendChild(sourceSpan); |
| | } |
| | }); |
| | |
| | contentDiv.appendChild(sourcesDiv); |
| | } |
| | |
| | messageDiv.appendChild(contentDiv); |
| | chatMessages.appendChild(messageDiv); |
| | |
| | |
| | chatMessages.scrollTop = chatMessages.scrollHeight; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function addLoadingMessage() { |
| | const id = 'loading-' + Date.now(); |
| | const messageDiv = document.createElement('div'); |
| | messageDiv.classList.add('message', 'bot'); |
| | messageDiv.id = id; |
| | |
| | const contentDiv = document.createElement('div'); |
| | contentDiv.classList.add('message-content'); |
| | |
| | const loadingP = document.createElement('p'); |
| | loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> μκ° μ€...'; |
| | contentDiv.appendChild(loadingP); |
| | |
| | messageDiv.appendChild(contentDiv); |
| | chatMessages.appendChild(messageDiv); |
| | |
| | |
| | chatMessages.scrollTop = chatMessages.scrollHeight; |
| | |
| | return id; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function removeLoadingMessage(id) { |
| | const loadingMessage = document.getElementById(id); |
| | if (loadingMessage) { |
| | loadingMessage.remove(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function addErrorMessage(errorText) { |
| | const messageDiv = document.createElement('div'); |
| | messageDiv.classList.add('message', 'system'); |
| | |
| | const contentDiv = document.createElement('div'); |
| | contentDiv.classList.add('message-content'); |
| | contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; |
| | contentDiv.style.color = 'var(--error-color)'; |
| | |
| | const errorP = document.createElement('p'); |
| | errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`; |
| | contentDiv.appendChild(errorP); |
| | |
| | messageDiv.appendChild(contentDiv); |
| | chatMessages.appendChild(messageDiv); |
| | |
| | |
| | chatMessages.scrollTop = chatMessages.scrollHeight; |
| | } |
| |
|
| | |
| | |
| | |
| | function adjustTextareaHeight() { |
| | userInput.style.height = 'auto'; |
| | userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px'; |
| | } |
| |
|