jeongsoo's picture
Add application file
babf3f3
/**
* RAG ์ฑ—๋ด‡ ํด๋ผ์ด์–ธํŠธ - ์ฑ„ํŒ… ๊ธฐ๋Šฅ
* ํ…์ŠคํŠธ ๋ฐ ์Œ์„ฑ ๋Œ€ํ™” ๊ธฐ๋Šฅ ๊ตฌํ˜„
*/
document.addEventListener('DOMContentLoaded', function() {
// DOM ์š”์†Œ
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.js ์„ค์ • (๋งˆํฌ๋‹ค์šด ํŒŒ์„œ)
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());
}
}
/**
* ๋…น์Œ๋œ ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
* @param {Blob} audioBlob - ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ
*/
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);
}
}
/**
* API๋กœ๋ถ€ํ„ฐ ๋ด‡ ์‘๋‹ต ๊ฐ€์ ธ์˜ค๊ธฐ
* @param {string} message - ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€
* @param {string} retriever - ๊ฒ€์ƒ‰๊ธฐ ์œ ํ˜•
* @param {number} k - Top-K ๋ฌธ์„œ ์ˆ˜
* @param {number} temperature - ์ƒ์„ฑ ๋‹ค์–‘์„ฑ
*/
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);
}
}
/**
* ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
* @param {string} 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();
}
/**
* ๋ด‡ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
* @param {string} message - ๋ด‡ ๋ฉ”์‹œ์ง€
* @param {Array} sources - ์ฐธ๊ณ  ๋ฌธ์„œ ๋ชฉ๋ก
*/
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();
}
/**
* ์ฐธ๊ณ  ๋ฌธ์„œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜
* @param {Array} sources - ์ฐธ๊ณ  ๋ฌธ์„œ ๋ชฉ๋ก
*/
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);
});
}
/**
* ์ ์ˆ˜ ์ƒ‰์ƒ ๊ณ„์‚ฐ ํ•จ์ˆ˜
* @param {number} score - ๊ด€๋ จ์„ฑ ์ ์ˆ˜ (0-1)
* @returns {string} - ์ƒ‰์ƒ ์ฝ”๋“œ
*/
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');
}
}
});