|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
const chatMessages = document.getElementById('chatMessages'); |
|
|
const userInput = document.getElementById('userInput'); |
|
|
const sendButton = document.getElementById('sendButton'); |
|
|
const micButton = document.getElementById('micButton'); |
|
|
const clearChat = document.getElementById('clearChat'); |
|
|
const recordingAlert = document.getElementById('recordingAlert'); |
|
|
const processingAlert = document.getElementById('processingAlert'); |
|
|
const typingAlert = document.getElementById('typingAlert'); |
|
|
const sourceList = document.getElementById('sourceList'); |
|
|
|
|
|
|
|
|
const retrieverType = document.getElementById('retrieverType'); |
|
|
const topK = document.getElementById('topK'); |
|
|
const temperatureSlider = document.getElementById('temperature'); |
|
|
const temperatureValue = document.getElementById('temperatureValue'); |
|
|
|
|
|
|
|
|
let mediaRecorder; |
|
|
let audioChunks = []; |
|
|
let isRecording = false; |
|
|
|
|
|
|
|
|
marked.setOptions({ |
|
|
renderer: new marked.Renderer(), |
|
|
highlight: function(code, language) { |
|
|
const validLang = hljs.getLanguage(language) ? language : 'plaintext'; |
|
|
return hljs.highlight(validLang, code).value; |
|
|
}, |
|
|
gfm: true, |
|
|
breaks: true |
|
|
}); |
|
|
|
|
|
|
|
|
loadChatHistory(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
userInput.addEventListener('keypress', function(e) { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
sendMessage(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
sendButton.addEventListener('click', sendMessage); |
|
|
|
|
|
|
|
|
micButton.addEventListener('click', toggleRecording); |
|
|
|
|
|
|
|
|
recordingAlert.addEventListener('click', stopRecording); |
|
|
|
|
|
|
|
|
clearChat.addEventListener('click', function() { |
|
|
if (confirm('์ ๋ง ๋ํ ๋ด์ฉ์ ๋ชจ๋ ์ง์ฐ์๊ฒ ์ต๋๊น?')) { |
|
|
chatMessages.innerHTML = ''; |
|
|
sourceList.innerHTML = '<div class="list-group-item text-center text-muted"><i>์ฐธ๊ณ ๋ฌธ์๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค</i></div>'; |
|
|
localStorage.removeItem('chatHistory'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
temperatureSlider.addEventListener('input', function() { |
|
|
temperatureValue.textContent = this.value; |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sendMessage() { |
|
|
const message = userInput.value.trim(); |
|
|
if (message === '') return; |
|
|
|
|
|
|
|
|
addUserMessage(message); |
|
|
userInput.value = ''; |
|
|
|
|
|
|
|
|
const retriever = retrieverType.value; |
|
|
const k = parseInt(topK.value); |
|
|
const temperature = parseFloat(temperatureSlider.value); |
|
|
|
|
|
|
|
|
getBotResponse(message, retriever, k, temperature); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleRecording() { |
|
|
if (!isRecording) { |
|
|
startRecording(); |
|
|
} else { |
|
|
stopRecording(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function startRecording() { |
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
|
mediaRecorder = new MediaRecorder(stream); |
|
|
audioChunks = []; |
|
|
|
|
|
mediaRecorder.addEventListener('dataavailable', event => { |
|
|
audioChunks.push(event.data); |
|
|
}); |
|
|
|
|
|
mediaRecorder.addEventListener('stop', async () => { |
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); |
|
|
await processAudio(audioBlob); |
|
|
}); |
|
|
|
|
|
mediaRecorder.start(); |
|
|
isRecording = true; |
|
|
micButton.classList.add('recording'); |
|
|
recordingAlert.classList.remove('d-none'); |
|
|
|
|
|
} catch (err) { |
|
|
console.error('๋ง์ดํฌ ์ ๊ทผ ์ค๋ฅ:', err); |
|
|
alert('๋ง์ดํฌ ์ ๊ทผ์ ์คํจํ์ต๋๋ค. ๋ง์ดํฌ ๊ถํ์ ํ์ธํด์ฃผ์ธ์.'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function stopRecording() { |
|
|
if (mediaRecorder && isRecording) { |
|
|
mediaRecorder.stop(); |
|
|
isRecording = false; |
|
|
micButton.classList.remove('recording'); |
|
|
recordingAlert.classList.add('d-none'); |
|
|
processingAlert.classList.remove('d-none'); |
|
|
|
|
|
|
|
|
mediaRecorder.stream.getTracks().forEach(track => track.stop()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function processAudio(audioBlob) { |
|
|
try { |
|
|
const formData = new FormData(); |
|
|
formData.append('audio', audioBlob, 'recording.wav'); |
|
|
|
|
|
const response = await fetch('/api/voice', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
processingAlert.classList.add('d-none'); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorData = await response.json(); |
|
|
throw new Error(errorData.error || '์์ฑ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
|
|
|
if (data.transcription) { |
|
|
addUserMessage(data.transcription); |
|
|
|
|
|
|
|
|
if (data.answer) { |
|
|
addBotMessage(data.answer, data.context_docs || []); |
|
|
} |
|
|
} else { |
|
|
throw new Error('์์ฑ ์ธ์์ ์คํจํ์ต๋๋ค.'); |
|
|
} |
|
|
|
|
|
} catch (err) { |
|
|
console.error('์์ฑ ์ฒ๋ฆฌ ์ค๋ฅ:', err); |
|
|
processingAlert.classList.add('d-none'); |
|
|
alert('์์ฑ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ' + err.message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function getBotResponse(message, retriever, k, temperature) { |
|
|
try { |
|
|
typingAlert.classList.remove('d-none'); |
|
|
sourceList.innerHTML = '<div class="list-group-item text-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div><span class="ms-2">์ฐธ๊ณ ๋ฌธ์ ๋ก๋ฉ์ค...</span></div>'; |
|
|
|
|
|
const response = await fetch('/api/chat', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
query: message, |
|
|
retriever_type: retriever, |
|
|
top_k: k, |
|
|
temperature: temperature |
|
|
}) |
|
|
}); |
|
|
|
|
|
typingAlert.classList.add('d-none'); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorData = await response.json(); |
|
|
throw new Error(errorData.error || '์๋ต์ ๊ฐ์ ธ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
addBotMessage(data.answer, data.context_docs || []); |
|
|
|
|
|
} catch (err) { |
|
|
console.error('API ์์ฒญ ์ค๋ฅ:', err); |
|
|
typingAlert.classList.add('d-none'); |
|
|
alert('๋ด ์๋ต์ ๊ฐ์ ธ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ' + err.message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addUserMessage(message) { |
|
|
const template = document.getElementById('userMessageTemplate'); |
|
|
const messageNode = template.content.cloneNode(true); |
|
|
|
|
|
messageNode.querySelector('.message-text').textContent = message; |
|
|
chatMessages.appendChild(messageNode); |
|
|
|
|
|
|
|
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
|
|
|
|
|
|
saveChatHistory(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addBotMessage(message, sources) { |
|
|
const template = document.getElementById('botMessageTemplate'); |
|
|
const messageNode = template.content.cloneNode(true); |
|
|
|
|
|
|
|
|
const sanitizedHTML = DOMPurify.sanitize(marked.parse(message)); |
|
|
messageNode.querySelector('.message-text').innerHTML = sanitizedHTML; |
|
|
|
|
|
|
|
|
messageNode.querySelectorAll('pre code').forEach((block) => { |
|
|
hljs.highlightBlock(block); |
|
|
}); |
|
|
|
|
|
chatMessages.appendChild(messageNode); |
|
|
|
|
|
|
|
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
|
|
|
|
|
|
updateSourceList(sources); |
|
|
|
|
|
|
|
|
saveChatHistory(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateSourceList(sources) { |
|
|
sourceList.innerHTML = ''; |
|
|
|
|
|
if (!sources || sources.length === 0) { |
|
|
sourceList.innerHTML = '<div class="list-group-item text-center text-muted"><i>์ฐธ๊ณ ๋ฌธ์๊ฐ ์์ต๋๋ค</i></div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
sources.forEach((source, index) => { |
|
|
const sourceItem = document.createElement('div'); |
|
|
sourceItem.className = 'list-group-item source-item'; |
|
|
|
|
|
const score = source.score || 0; |
|
|
const scoreValue = Math.round(score * 100) / 100; |
|
|
const scoreColor = getScoreColor(score); |
|
|
|
|
|
sourceItem.innerHTML = ` |
|
|
<div class="d-flex align-items-center"> |
|
|
<div class="source-score me-3" style="background-color: ${scoreColor}"> |
|
|
${scoreValue.toFixed(2)} |
|
|
</div> |
|
|
<div> |
|
|
<h6 class="mb-0">${source.source || '๋ฌธ์ #' + (index + 1)}</h6> |
|
|
<div class="small text-muted">๊ด๋ จ์ฑ ์ ์: ${scoreValue.toFixed(2)}</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
sourceList.appendChild(sourceItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getScoreColor(score) { |
|
|
if (score >= 0.8) return '#198754'; |
|
|
if (score >= 0.6) return '#0d6efd'; |
|
|
if (score >= 0.4) return '#fd7e14'; |
|
|
return '#6c757d'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function saveChatHistory() { |
|
|
const history = { |
|
|
messages: [], |
|
|
sources: [] |
|
|
}; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.chat-message').forEach(msg => { |
|
|
const isUser = msg.classList.contains('user-message'); |
|
|
const text = msg.querySelector('.message-text').textContent || msg.querySelector('.message-text').innerHTML; |
|
|
|
|
|
history.messages.push({ |
|
|
isUser: isUser, |
|
|
content: text |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const sourcesHtml = sourceList.innerHTML; |
|
|
history.sources = sourcesHtml; |
|
|
|
|
|
|
|
|
localStorage.setItem('chatHistory', JSON.stringify(history)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadChatHistory() { |
|
|
const history = localStorage.getItem('chatHistory'); |
|
|
if (!history) return; |
|
|
|
|
|
try { |
|
|
const historyData = JSON.parse(history); |
|
|
|
|
|
|
|
|
historyData.messages.forEach(msg => { |
|
|
if (msg.isUser) { |
|
|
const template = document.getElementById('userMessageTemplate'); |
|
|
const messageNode = template.content.cloneNode(true); |
|
|
messageNode.querySelector('.message-text').textContent = msg.content; |
|
|
chatMessages.appendChild(messageNode); |
|
|
} else { |
|
|
const template = document.getElementById('botMessageTemplate'); |
|
|
const messageNode = template.content.cloneNode(true); |
|
|
|
|
|
|
|
|
const sanitizedHTML = DOMPurify.sanitize(marked.parse(msg.content)); |
|
|
messageNode.querySelector('.message-text').innerHTML = sanitizedHTML; |
|
|
|
|
|
|
|
|
messageNode.querySelectorAll('pre code').forEach((block) => { |
|
|
hljs.highlightBlock(block); |
|
|
}); |
|
|
|
|
|
chatMessages.appendChild(messageNode); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (historyData.sources) { |
|
|
sourceList.innerHTML = historyData.sources; |
|
|
} |
|
|
|
|
|
|
|
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
|
|
|
} catch (err) { |
|
|
console.error('์ฑํ
๊ธฐ๋ก ๋ก๋ ์คํจ:', err); |
|
|
localStorage.removeItem('chatHistory'); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|