/** * RAG 검색 챗봇 UI 코어 JavaScript */ // 전역 변수 let currentLLM = 'openai'; let supportedLLMs = []; // DOM 변수 미리 선언 let chatTab, docsTab, deviceTab, chatSection, docsSection, deviceSection; let chatMessages, userInput, sendButton; let micButton, stopRecordingButton, recordingStatus; let llmSelect, currentLLMInfo; // 녹음 관련 변수 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('상태 확인 실패:', error); return false; } } /** * DOM 요소 초기화 함수 */ function initDomElements() { console.log('DOM 요소 초기화 중...'); // 탭 관련 요소 chatTab = document.getElementById('chatTab'); docsTab = document.getElementById('docsTab'); deviceTab = document.getElementById('deviceTab'); chatSection = document.getElementById('chatSection'); docsSection = document.getElementById('docsSection'); deviceSection = document.getElementById('deviceSection'); // 채팅 관련 요소 chatMessages = document.getElementById('chatMessages'); userInput = document.getElementById('userInput'); sendButton = document.getElementById('sendButton'); // 음성 녹음 관련 요소 micButton = document.getElementById('micButton'); stopRecordingButton = document.getElementById('stopRecordingButton'); recordingStatus = document.getElementById('recordingStatus'); // LLM 관련 요소 llmSelect = document.getElementById('llmSelect'); currentLLMInfo = document.getElementById('currentLLMInfo'); console.log('DOM 요소 초기화 완료'); } /** * 이벤트 리스너 초기화 함수 */ function initEventListeners() { console.log('이벤트 리스너 초기화 중...'); // 탭 전환 이벤트 리스너 chatTab.addEventListener('click', () => { switchTab('chat'); }); docsTab.addEventListener('click', () => { switchTab('docs'); loadDocuments(); }); deviceTab.addEventListener('click', () => { switchTab('device'); loadDeviceStatus(); }); // 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); // 자동 입력 필드 크기 조정 userInput.addEventListener('input', adjustTextareaHeight); console.log('이벤트 리스너 초기화 완료'); } /** * 탭 전환 함수 * @param {string} tabName - 활성화할 탭 이름 ('chat', 'docs', 또는 'device') */ function switchTab(tabName) { console.log(`탭 전환: ${tabName}`); // 모든 탭을 비활성화 [chatTab, docsTab, deviceTab].forEach(tab => tab.classList.remove('active')); [chatSection, docsSection, deviceSection].forEach(section => section.classList.remove('active')); // 선택한 탭 활성화 if (tabName === 'chat') { chatTab.classList.add('active'); chatSection.classList.add('active'); } else if (tabName === 'docs') { docsTab.classList.add('active'); docsSection.classList.add('active'); } else if (tabName === 'device') { deviceTab.classList.add('active'); deviceSection.classList.add('active'); } } /** * 시스템 알림 메시지 추가 * @param {string} message - 알림 메시지 */ function addSystemNotification(message) { console.log(`시스템 알림 추가: ${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 = ` ${message}`; contentDiv.appendChild(messageP); messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); // 스크롤을 가장 아래로 이동 chatMessages.scrollTop = chatMessages.scrollHeight; } /** * 메시지 추가 함수 * @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) { console.log(`메시지 추가: ${sender}`); 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() { console.log('로딩 메시지 추가'); 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 = '
생각 중...'; contentDiv.appendChild(loadingP); messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); // 스크롤을 가장 아래로 이동 chatMessages.scrollTop = chatMessages.scrollHeight; return id; } /** * 로딩 메시지 제거 함수 * @param {string} id - 로딩 메시지 ID */ function removeLoadingMessage(id) { console.log(`로딩 메시지 제거: ${id}`); const loadingMessage = document.getElementById(id); if (loadingMessage) { loadingMessage.remove(); } } /** * 오류 메시지 추가 함수 * @param {string} errorText - 오류 메시지 내용 */ function addErrorMessage(errorText) { console.log(`오류 메시지 추가: ${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 = ` ${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'; } // 앱 초기화 (페이지 로드 시) document.addEventListener('DOMContentLoaded', function() { console.log('페이지 로드 완료, 앱 초기화 시작'); // DOM 요소 초기화 initDomElements(); // 이벤트 리스너 초기화 initEventListeners(); // 앱 상태 확인 (로딩 페이지가 아닌 경우에만) 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(); // 활성 탭 확인 및 데이터 로드 if (docsSection.classList.contains('active')) { loadDocuments(); } else if (deviceSection.classList.contains('active')) { loadDeviceStatus(); } } }, 5000); } console.log('앱 초기화 완료'); });