jeongsoo's picture
fix
c9321b7
raw
history blame
21.8 kB
/**
* RAG 검색 챗봇 UI JavaScript
*/
// DOM μš”μ†Œ
const chatTab = document.getElementById('chatTab');
const docsTab = document.getElementById('docsTab');
const deviceTab = document.getElementById('deviceTab');
const chatSection = document.getElementById('chatSection');
const docsSection = document.getElementById('docsSection');
const deviceSection = document.getElementById('deviceSection');
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');
const llmSelect = document.getElementById('llmSelect');
const currentLLMInfo = document.getElementById('currentLLMInfo');
// LLM κ΄€λ ¨ λ³€μˆ˜
let currentLLM = 'openai';
let supportedLLMs = [];
// λ…ΉμŒ κ΄€λ ¨ λ³€μˆ˜
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;
}
}
/**
* LLM λͺ©λ‘ λ‘œλ“œ ν•¨μˆ˜
*/
async function loadLLMs() {
try {
// API μš”μ²­
const response = await fetch('/api/llm');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
supportedLLMs = data.supported_llms;
currentLLM = data.current_llm.id;
// LLM 선택 λ“œλ‘­λ‹€μš΄ μ—…λ°μ΄νŠΈ
llmSelect.innerHTML = '';
supportedLLMs.forEach(llm => {
const option = document.createElement('option');
option.value = llm.id;
option.textContent = llm.name;
option.selected = llm.current;
llmSelect.appendChild(option);
});
// ν˜„μž¬ LLM ν‘œμ‹œ
updateCurrentLLMInfo(data.current_llm);
} catch (error) {
console.error('LLM λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:', error);
}
}
/**
* LLM λ³€κ²½ ν•¨μˆ˜
* @param {string} llmId - λ³€κ²½ν•  LLM ID
*/
async function changeLLM(llmId) {
try {
// API μš”μ²­
const response = await fetch('/api/llm', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ llm_id: llmId })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
currentLLM = llmId;
updateCurrentLLMInfo(data.current_llm);
console.log(`LLM이 ${data.current_llm.name}(으)둜 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
// μ‹œμŠ€ν…œ λ©”μ‹œμ§€ μΆ”κ°€
const systemMessage = `LLM이 ${data.current_llm.name}(으)둜 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λͺ¨λΈ: ${data.current_llm.model}`;
addSystemNotification(systemMessage);
} else if (data.error) {
console.error('LLM λ³€κ²½ 였λ₯˜:', data.error);
alert(`LLM λ³€κ²½ 였λ₯˜: ${data.error}`);
}
} catch (error) {
console.error('LLM λ³€κ²½ μ‹€νŒ¨:', error);
alert('LLM λ³€κ²½ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.');
}
}
/**
* ν˜„μž¬ LLM 정보 ν‘œμ‹œ μ—…λ°μ΄νŠΈ
* @param {Object} llmInfo - LLM 정보 객체
*/
function updateCurrentLLMInfo(llmInfo) {
if (currentLLMInfo) {
currentLLMInfo.textContent = `${llmInfo.name} (${llmInfo.model})`;
}
}
/**
* μ‹œμŠ€ν…œ μ•Œλ¦Ό λ©”μ‹œμ§€ μΆ”κ°€
* @param {string} message - μ•Œλ¦Ό λ©”μ‹œμ§€
*/
function addSystemNotification(message) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', 'system');
const contentDiv = document.createElement('div');
contentDiv.classList.add('message-content');
const messageP = document.createElement('p');
messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
contentDiv.appendChild(messageP);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
// μŠ€ν¬λ‘€μ„ κ°€μž₯ μ•„λž˜λ‘œ 이동
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μ΄ˆκΈ°ν™”
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('앱이 μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
// 앱이 μ€€λΉ„λ˜λ©΄ LLM λͺ©λ‘ λ‘œλ“œ
loadLLMs();
}
}, 5000);
}
// νƒ­ μ „ν™˜ 이벀트 λ¦¬μŠ€λ„ˆ
chatTab.addEventListener('click', () => {
console.log("λŒ€ν™” νƒ­ 클릭");
switchTab('chat');
});
docsTab.addEventListener('click', () => {
console.log("λ¬Έμ„œκ΄€λ¦¬ νƒ­ 클릭");
switchTab('docs');
loadDocuments();
});
// μž₯μΉ˜κ΄€λ¦¬ νƒ­ 이벀트 λ¦¬μŠ€λ„ˆ μΆ”κ°€
deviceTab.addEventListener('click', () => {
console.log("μž₯μΉ˜κ΄€λ¦¬ νƒ­ 클릭");
switchTab('device');
// μž₯μΉ˜κ΄€λ¦¬ νƒ­μœΌλ‘œ μ „ν™˜ μ‹œ ν•„μš”ν•œ μ΄ˆκΈ°ν™” μž‘μ—…μ΄ μžˆλ‹€λ©΄ μ—¬κΈ°μ„œ 호좜
if (typeof checkDeviceStatus === 'function') {
console.log("μž₯치 μƒνƒœ 확인 ν•¨μˆ˜ 호좜");
checkDeviceStatus();
} else {
console.log("checkDeviceStatus ν•¨μˆ˜κ°€ μ •μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€");
}
});
// LLM 선택 이벀트 λ¦¬μŠ€λ„ˆ
llmSelect.addEventListener('change', (event) => {
changeLLM(event.target.value);
});
// λ©”μ‹œμ§€ 전솑 이벀트 λ¦¬μŠ€λ„ˆ
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();
}
});
/**
* νƒ­ μ „ν™˜ ν•¨μˆ˜
* @param {string} tabName - ν™œμ„±ν™”ν•  νƒ­ 이름 ('chat', 'docs', 'device')
*/
function switchTab(tabName) {
console.log(`switchTab ν•¨μˆ˜ 호좜: ${tabName}`);
if (tabName === 'chat') {
chatTab.classList.add('active');
docsTab.classList.remove('active');
deviceTab.classList.remove('active');
chatSection.classList.add('active');
docsSection.classList.remove('active');
deviceSection.classList.remove('active');
} else if (tabName === 'docs') {
chatTab.classList.remove('active');
docsTab.classList.add('active');
deviceTab.classList.remove('active');
chatSection.classList.remove('active');
docsSection.classList.add('active');
deviceSection.classList.remove('active');
} else if (tabName === 'device') {
chatTab.classList.remove('active');
docsTab.classList.remove('active');
deviceTab.classList.add('active');
chatSection.classList.remove('active');
docsSection.classList.remove('active');
deviceSection.classList.add('active');
console.log("μž₯μΉ˜κ΄€λ¦¬ νƒ­μœΌλ‘œ μ „ν™˜ μ™„λ£Œ");
}
}
/**
* μ±„νŒ… λ©”μ‹œμ§€ 전솑 ν•¨μˆ˜
*/
async function sendMessage() {
const message = userInput.value.trim();
if (!message) return;
// UI μ—…λ°μ΄νŠΈ
addMessage(message, 'user');
userInput.value = '';
adjustTextareaHeight();
// λ‘œλ”© λ©”μ‹œμ§€ μΆ”κ°€
const loadingMessageId = addLoadingMessage();
try {
// API μš”μ²­
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: message,
llm_id: currentLLM // ν˜„μž¬ μ„ νƒλœ LLM 전솑
})
});
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 {
// LLM 정보 μ—…λ°μ΄νŠΈ
if (data.llm) {
updateCurrentLLMInfo(data.llm);
}
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();
// UI μ—…λ°μ΄νŠΈ
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;
// UI μ—…λ°μ΄νŠΈ
micButton.style.display = 'flex';
recordingStatus.classList.add('hidden');
console.log('λ…ΉμŒ 쀑지됨');
}
/**
* λ…ΉμŒλœ μ˜€λ””μ˜€ λ©”μ‹œμ§€ 전솑 ν•¨μˆ˜
*/
async function sendAudioMessage() {
if (audioChunks.length === 0) return;
// μ˜€λ””μ˜€ Blob 생성
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
// λ‘œλ”© λ©”μ‹œμ§€ μΆ”κ°€
const loadingMessageId = addLoadingMessage();
try {
// FormData에 μ˜€λ””μ˜€ μΆ”κ°€
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav');
// ν˜„μž¬ μ„ νƒλœ LLM μΆ”κ°€
formData.append('llm_id', currentLLM);
// API μš”μ²­
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 {
// LLM 정보 μ—…λ°μ΄νŠΈ
if (data.llm) {
updateCurrentLLMInfo(data.llm);
}
// μ‚¬μš©μž λ©”μ‹œμ§€(μŒμ„± ν…μŠ€νŠΈ) μΆ”κ°€
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;
}
// UI μ—…λ°μ΄νŠΈ
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]);
// API μš”μ²­
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() {
// UI μ—…λ°μ΄νŠΈ
docsList.querySelector('tbody').innerHTML = '';
docsLoading.classList.remove('hidden');
noDocsMessage.classList.add('hidden');
try {
// API μš”μ²­
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 = 'λ¬Έμ„œ λͺ©λ‘μ„ λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.';
}
}
/**
* λ©”μ‹œμ§€ μΆ”κ°€ ν•¨μˆ˜
* @param {string} text - λ©”μ‹œμ§€ λ‚΄μš©
* @param {string} sender - λ©”μ‹œμ§€ λ°œμ‹ μž ('user' λ˜λŠ” 'bot' λ˜λŠ” 'system')
* @param {string|null} transcription - μŒμ„± 인식 ν…μŠ€νŠΈ (선택 사항)
* @param {Array|null} sources - μ†ŒμŠ€ 정보 λ°°μ—΄ (선택 사항)
*/
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) { // μ΅œλŒ€ 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;
}
/**
* λ‘œλ”© λ©”μ‹œμ§€ μΆ”κ°€ ν•¨μˆ˜
* @returns {string} λ‘œλ”© λ©”μ‹œμ§€ ID
*/
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;
}
/**
* λ‘œλ”© λ©”μ‹œμ§€ 제거 ν•¨μˆ˜
* @param {string} id - λ‘œλ”© λ©”μ‹œμ§€ ID
*/
function removeLoadingMessage(id) {
const loadingMessage = document.getElementById(id);
if (loadingMessage) {
loadingMessage.remove();
}
}
/**
* 였λ₯˜ λ©”μ‹œμ§€ μΆ”κ°€ ν•¨μˆ˜
* @param {string} errorText - 였λ₯˜ λ©”μ‹œμ§€ λ‚΄μš©
*/
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;
}
/**
* textarea 높이 μžλ™ μ‘°μ • ν•¨μˆ˜
*/
function adjustTextareaHeight() {
userInput.style.height = 'auto';
userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
}
// μ•± μ „μ—­μ—μ„œ switchTab ν•¨μˆ˜κ°€ μ‚¬μš© κ°€λŠ₯ν•˜λ„λ‘ export
window.switchTab = switchTab;