// ==================== 전역 변수 ====================
let socket;
let isStreaming = false;
let isRecording = false;
let currentScene = null;
let timeline = []; // 타임라인 데이터
let timelinePlaying = false; // 타임라인 재생 중 여부
let ragKnowledgeBase = []; // RAG 지식 베이스
let ragEnabled = false; // RAG 활성화 여부
let ragResponseCount = 0; // RAG 답변 횟수
// ==================== 초기화 ====================
document.addEventListener('DOMContentLoaded', function() {
initializeSocket();
loadInitialData();
setupEventListeners();
});
// ==================== SocketIO 초기화 ====================
function initializeSocket() {
socket = io();
socket.on('connect', function() {
console.log('서버에 연결됨');
updateConnectionStatus(true);
socket.emit('request_status');
});
socket.on('disconnect', function() {
console.log('서버 연결 해제됨');
updateConnectionStatus(false);
});
// 장면 변경 이벤트
socket.on('scene_changed', function(data) {
console.log('장면 변경:', data.scene_name);
currentScene = data.scene_name;
updateCurrentScene(data.scene_name);
updateSceneButtons(data.scene_name);
document.getElementById('currentSceneName').textContent = data.scene_name;
showToast(`장면 전환: ${data.scene_name}`);
});
// 타임라인 업데이트
socket.on('timeline_updated', function(data) {
console.log('타임라인 업데이트:', data);
timeline = data.timeline;
timelinePlaying = data.playing;
displayTimeline();
updateTimelineControls();
});
// 타임라인 재생 중
socket.on('timeline_playing', function(data) {
console.log('타임라인 재생:', data);
document.getElementById('timelineStatusText').textContent =
`재생 중: ${data.scene_name} (${data.index + 1}/${data.total})`;
document.getElementById('timelineProgress').textContent =
`${data.index + 1} / ${data.total}`;
});
// 타임라인 진행 상황
socket.on('timeline_progress', function(data) {
const percentage = (data.elapsed / data.duration) * 100;
document.getElementById('timelineProgressBar').style.width = percentage + '%';
});
// 타임라인 완료
socket.on('timeline_finished', function(data) {
console.log('타임라인 재생 완료');
timelinePlaying = false;
updateTimelineControls();
document.getElementById('timelineStatus').style.display = 'none';
document.getElementById('timelineProgressBar').style.width = '0%';
showToast('타임라인 재생 완료!', 'success');
});
// 타임라인 오류
socket.on('timeline_error', function(data) {
console.error('타임라인 오류:', data.error);
timelinePlaying = false;
updateTimelineControls();
showToast('타임라인 재생 오류: ' + data.error, 'error');
});
// RAG 업데이트
socket.on('rag_updated', function(data) {
console.log('RAG 업데이트:', data);
ragKnowledgeBase = data.knowledge_base;
ragEnabled = data.enabled;
displayRAGKnowledge();
updateRAGStatus();
});
// 스트리밍 상태 변경
socket.on('stream_state_changed', function(data) {
console.log('스트리밍 상태 변경:', data);
isStreaming = data.active;
updateStreamingStatus(data.active);
showToast(data.active ? '스트리밍 시작됨' : '스트리밍 정지됨');
});
// 녹화 상태 변경
socket.on('record_state_changed', function(data) {
console.log('녹화 상태 변경:', data);
isRecording = data.active;
updateRecordingStatus(data.active);
showToast(data.active ? '녹화 시작됨' : '녹화 정지됨');
});
// 음소거 상태 변경
socket.on('mute_state_changed', function(data) {
console.log('음소거 상태 변경:', data);
updateMuteButton(data.input_name, data.muted);
});
}
// ==================== 초기 데이터 로드 ====================
async function loadInitialData() {
try {
// 상태 로드
await loadStatus();
// 장면 로드
await loadScenes();
// 소스 로드
await loadSources();
// 타임라인 로드
await loadTimeline();
// RAG 지식 베이스 로드
await loadRAGKnowledge();
} catch (error) {
console.error('초기 데이터 로드 실패:', error);
showToast('데이터 로드 실패', 'error');
}
}
async function loadStatus() {
try {
const response = await fetch('/api/status');
if (!response.ok) throw new Error('상태 조회 실패');
const data = await response.json();
// 현재 상태 업데이트
currentScene = data.current_scene;
isStreaming = data.streaming;
isRecording = data.recording;
updateCurrentScene(data.current_scene);
updateStreamingStatus(data.streaming, data.stream_status);
updateRecordingStatus(data.recording, data.record_status);
updateVirtualCamStatus(data.virtual_cam);
} catch (error) {
console.error('상태 로드 실패:', error);
}
}
async function loadScenes() {
try {
const response = await fetch('/api/scenes');
if (!response.ok) throw new Error('장면 목록 조회 실패');
const data = await response.json();
displayScenes(data.scenes, data.current_scene);
} catch (error) {
console.error('장면 로드 실패:', error);
document.getElementById('sceneGrid').innerHTML =
'
장면 로드 실패
';
}
}
async function loadSources() {
try {
const response = await fetch('/api/sources');
if (!response.ok) throw new Error('소스 목록 조회 실패');
const data = await response.json();
displaySources(data.sources);
} catch (error) {
console.error('소스 로드 실패:', error);
document.getElementById('sourceList').innerHTML =
'소스 로드 실패
';
}
}
// ==================== UI 업데이트 ====================
function updateConnectionStatus(connected) {
const statusDot = document.getElementById('connectionStatus');
const statusText = document.getElementById('connectionText');
if (connected) {
statusDot.className = 'status-dot connected';
statusText.textContent = 'OBS 연결됨';
} else {
statusDot.className = 'status-dot disconnected';
statusText.textContent = 'OBS 연결 안됨';
}
}
function updateCurrentScene(sceneName) {
document.getElementById('currentScene').textContent = sceneName || '-';
}
function updateStreamingStatus(active, statusData) {
const statusEl = document.getElementById('streamStatus');
const infoEl = document.getElementById('streamInfo');
const startBtn = document.getElementById('startStreamBtn');
const stopBtn = document.getElementById('stopStreamBtn');
if (active) {
statusEl.textContent = '🔴 진행 중';
statusEl.style.color = 'var(--danger-color)';
startBtn.disabled = true;
stopBtn.disabled = false;
if (statusData && statusData.timecode) {
infoEl.textContent = `시간: ${statusData.timecode}`;
}
} else {
statusEl.textContent = '정지';
statusEl.style.color = 'var(--text-secondary)';
startBtn.disabled = false;
stopBtn.disabled = true;
infoEl.textContent = '';
}
}
function updateRecordingStatus(active, statusData) {
const statusEl = document.getElementById('recordStatus');
const infoEl = document.getElementById('recordInfo');
const startBtn = document.getElementById('startRecordBtn');
const pauseBtn = document.getElementById('pauseRecordBtn');
const stopBtn = document.getElementById('stopRecordBtn');
if (active) {
statusEl.textContent = '🔴 녹화 중';
statusEl.style.color = 'var(--danger-color)';
startBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
if (statusData && statusData.timecode) {
const pausedText = statusData.paused ? ' (일시정지)' : '';
infoEl.textContent = `시간: ${statusData.timecode}${pausedText}`;
}
} else {
statusEl.textContent = '정지';
statusEl.style.color = 'var(--text-secondary)';
startBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
infoEl.textContent = '';
}
}
function updateVirtualCamStatus(active) {
const statusEl = document.getElementById('vcamStatus');
if (active) {
statusEl.textContent = '🟢 활성';
statusEl.style.color = 'var(--success-color)';
} else {
statusEl.textContent = '비활성';
statusEl.style.color = 'var(--text-secondary)';
}
}
function displayScenes(scenes, currentScene) {
const sceneGrid = document.getElementById('sceneGrid');
if (!scenes || scenes.length === 0) {
sceneGrid.innerHTML = '장면이 없습니다
';
return;
}
sceneGrid.innerHTML = '';
scenes.forEach(sceneName => {
const btn = document.createElement('button');
btn.className = 'scene-btn';
if (sceneName === currentScene) {
btn.classList.add('active');
}
btn.textContent = sceneName;
// 클릭 시 타임라인에 추가 (더블클릭 시 즉시 전환)
btn.onclick = () => addToTimeline(sceneName);
btn.ondblclick = () => setScene(sceneName);
btn.title = '클릭: 타임라인 추가 | 더블클릭: 즉시 전환';
sceneGrid.appendChild(btn);
});
}
function updateSceneButtons(activeScene) {
const sceneBtns = document.querySelectorAll('.scene-btn');
sceneBtns.forEach(btn => {
if (btn.textContent === activeScene) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
function displaySources(sources) {
const sourceList = document.getElementById('sourceList');
if (!sources || sources.length === 0) {
sourceList.innerHTML = '오디오 소스가 없습니다
';
return;
}
// 오디오 관련 소스만 필터링
const audioSources = sources.filter(source =>
source.inputKind && (
source.inputKind.includes('audio') ||
source.inputKind.includes('wasapi') ||
source.inputKind.includes('coreaudio') ||
source.inputKind.includes('pulse')
)
);
if (audioSources.length === 0) {
sourceList.innerHTML = '오디오 소스가 없습니다
';
return;
}
sourceList.innerHTML = '';
audioSources.forEach(source => {
const item = document.createElement('div');
item.className = 'source-item';
item.innerHTML = `
${source.inputName}
${source.inputKind}
`;
sourceList.appendChild(item);
});
}
function updateMuteButton(sourceName, muted) {
const sourceBtns = document.querySelectorAll('.mute-btn');
sourceBtns.forEach(btn => {
if (btn.onclick.toString().includes(sourceName)) {
if (muted) {
btn.classList.add('muted');
btn.innerHTML = '🔇 음소거됨';
} else {
btn.classList.remove('muted');
btn.innerHTML = '🔊 활성';
}
}
});
}
// ==================== 이벤트 리스너 ====================
function setupEventListeners() {
// 스트리밍 제어
document.getElementById('startStreamBtn').addEventListener('click', startStreaming);
document.getElementById('stopStreamBtn').addEventListener('click', stopStreaming);
// 녹화 제어
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('pauseRecordBtn').addEventListener('click', pauseRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
// 가상 카메라
document.getElementById('toggleVcamBtn').addEventListener('click', toggleVirtualCam);
}
// ==================== API 호출 ====================
async function setScene(sceneName) {
try {
const response = await fetch('/api/scene/change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scene_name: sceneName })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '장면 전환 실패');
}
showToast(`장면 전환: ${sceneName}`, 'success');
} catch (error) {
console.error('장면 전환 실패:', error);
showToast(error.message || '장면 전환 실패', 'error');
}
}
async function startStreaming() {
try {
const response = await fetch('/api/stream/start', { method: 'POST' });
if (!response.ok) throw new Error('스트리밍 시작 실패');
} catch (error) {
console.error('스트리밍 시작 실패:', error);
showToast('스트리밍 시작 실패', 'error');
}
}
async function stopStreaming() {
try {
const response = await fetch('/api/stream/stop', { method: 'POST' });
if (!response.ok) throw new Error('스트리밍 정지 실패');
} catch (error) {
console.error('스트리밍 정지 실패:', error);
showToast('스트리밍 정지 실패', 'error');
}
}
async function startRecording() {
try {
const response = await fetch('/api/record/start', { method: 'POST' });
if (!response.ok) throw new Error('녹화 시작 실패');
} catch (error) {
console.error('녹화 시작 실패:', error);
showToast('녹화 시작 실패', 'error');
}
}
async function pauseRecording() {
try {
const response = await fetch('/api/record/pause', { method: 'POST' });
if (!response.ok) throw new Error('녹화 일시정지 실패');
const data = await response.json();
showToast(data.paused ? '녹화 일시정지' : '녹화 재개');
} catch (error) {
console.error('녹화 일시정지 실패:', error);
showToast('녹화 일시정지 실패', 'error');
}
}
async function stopRecording() {
try {
const response = await fetch('/api/record/stop', { method: 'POST' });
if (!response.ok) throw new Error('녹화 정지 실패');
const data = await response.json();
if (data.file) {
showToast(`녹화 완료: ${data.file}`);
}
} catch (error) {
console.error('녹화 정지 실패:', error);
showToast('녹화 정지 실패', 'error');
}
}
async function toggleMute(sourceName) {
try {
const response = await fetch(`/api/sources/${encodeURIComponent(sourceName)}/mute`, {
method: 'POST'
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '음소거 토글 실패');
}
const data = await response.json();
showToast(data.muted ? `${sourceName} 음소거` : `${sourceName} 음소거 해제`, 'success');
} catch (error) {
console.error('음소거 토글 실패:', error);
showToast(error.message || '음소거 토글 실패', 'error');
}
}
async function toggleVirtualCam() {
try {
const response = await fetch('/api/virtual-camera/toggle', { method: 'POST' });
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '가상 카메라 토글 실패');
}
const data = await response.json();
updateVirtualCamStatus(data.active);
showToast(data.active ? '가상 카메라 활성화' : '가상 카메라 비활성화');
} catch (error) {
console.error('가상 카메라 토글 실패:', error);
showToast('가상 카메라 토글 실패', 'error');
}
}
// ==================== 토스트 알림 ====================
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
// ==================== 타임라인 기능 ====================
async function loadTimeline() {
try {
const response = await fetch('/api/timeline');
if (!response.ok) throw new Error('타임라인 조회 실패');
const data = await response.json();
timeline = data.timeline;
timelinePlaying = data.playing;
if (data.current_scene) {
document.getElementById('currentSceneName').textContent = data.current_scene;
}
displayTimeline();
updateTimelineControls();
} catch (error) {
console.error('타임라인 로드 실패:', error);
}
}
async function addToTimeline(sceneName) {
try {
// 사용자에게 시간 입력 받기
const duration = prompt(`"${sceneName}" 장면 재생 시간을 입력하세요 (초):`, '30');
if (!duration || isNaN(duration)) {
showToast('유효한 시간을 입력해주세요', 'error');
return;
}
const response = await fetch('/api/timeline/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scene_name: sceneName,
duration: parseInt(duration)
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '타임라인 추가 실패');
}
const data = await response.json();
timeline = data.timeline;
displayTimeline();
showToast(`타임라인에 추가: ${sceneName} (${duration}초)`, 'success');
} catch (error) {
console.error('타임라인 추가 실패:', error);
showToast(error.message || '타임라인 추가 실패', 'error');
}
}
function displayTimeline() {
const timelineItems = document.getElementById('timelineItems');
const timelineEmpty = document.getElementById('timelineEmpty');
if (!timeline || timeline.length === 0) {
timelineEmpty.style.display = 'block';
timelineItems.innerHTML = '';
return;
}
timelineEmpty.style.display = 'none';
timelineItems.innerHTML = '';
timeline.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.className = 'timeline-item';
itemEl.style.cssText = `
background: var(--card-bg);
padding: 10px 15px;
border-radius: 8px;
border: 2px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 5px;
min-width: 150px;
position: relative;
`;
itemEl.innerHTML = `
#${index + 1}
🎭 ${item.scene_name}
⏱️ ${item.duration}초
`;
timelineItems.appendChild(itemEl);
});
}
async function removeFromTimeline(itemId) {
try {
const response = await fetch(`/api/timeline/remove/${itemId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('타임라인 제거 실패');
const data = await response.json();
timeline = data.timeline;
displayTimeline();
showToast('타임라인에서 제거됨', 'success');
} catch (error) {
console.error('타임라인 제거 실패:', error);
showToast('타임라인 제거 실패', 'error');
}
}
async function clearTimeline() {
if (!confirm('타임라인을 전체 삭제하시겠습니까?')) {
return;
}
try {
const response = await fetch('/api/timeline/clear', {
method: 'POST'
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '타임라인 삭제 실패');
}
timeline = [];
displayTimeline();
showToast('타임라인이 삭제되었습니다', 'success');
} catch (error) {
console.error('타임라인 삭제 실패:', error);
showToast(error.message || '타임라인 삭제 실패', 'error');
}
}
async function playTimeline() {
if (!timeline || timeline.length === 0) {
showToast('타임라인이 비어있습니다', 'error');
return;
}
try {
const response = await fetch('/api/timeline/play', {
method: 'POST'
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '재생 실패');
}
timelinePlaying = true;
updateTimelineControls();
document.getElementById('timelineStatus').style.display = 'block';
document.getElementById('timelineProgressBar').style.width = '0%';
showToast('타임라인 재생 시작', 'success');
} catch (error) {
console.error('타임라인 재생 실패:', error);
showToast(error.message || '타임라인 재생 실패', 'error');
}
}
async function stopTimeline() {
try {
const response = await fetch('/api/timeline/stop', {
method: 'POST'
});
if (!response.ok) throw new Error('재생 중지 실패');
timelinePlaying = false;
updateTimelineControls();
document.getElementById('timelineStatus').style.display = 'none';
showToast('타임라인 재생 중지', 'success');
} catch (error) {
console.error('타임라인 중지 실패:', error);
showToast('타임라인 중지 실패', 'error');
}
}
function updateTimelineControls() {
const playBtn = document.getElementById('playTimelineBtn');
const stopBtn = document.getElementById('stopTimelineBtn');
if (timelinePlaying) {
playBtn.style.display = 'none';
stopBtn.style.display = 'block';
} else {
playBtn.style.display = 'block';
stopBtn.style.display = 'none';
}
}
// ==================== RAG 시스템 기능 ====================
async function loadRAGKnowledge() {
try {
const response = await fetch('/api/rag/knowledge');
if (!response.ok) throw new Error('RAG 지식 조회 실패');
const data = await response.json();
ragKnowledgeBase = data.knowledge_base;
ragEnabled = data.enabled;
ragResponseCount = data.response_count;
displayRAGKnowledge();
updateRAGStatus();
} catch (error) {
console.error('RAG 지식 로드 실패:', error);
}
}
async function addRAGKnowledge() {
const question = document.getElementById('ragQuestion').value.trim();
const answer = document.getElementById('ragAnswer').value.trim();
const keywordsInput = document.getElementById('ragKeywords').value.trim();
if (!question || !answer) {
showToast('질문과 답변을 입력하세요', 'error');
return;
}
const keywords = keywordsInput ? keywordsInput.split(',').map(k => k.trim()) : [];
try {
const response = await fetch('/api/rag/knowledge/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, answer, keywords })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'RAG 지식 추가 실패');
}
const data = await response.json();
ragKnowledgeBase = data.knowledge_base;
displayRAGKnowledge();
clearRAGForm();
showToast('질의응답이 추가되었습니다', 'success');
} catch (error) {
console.error('RAG 지식 추가 실패:', error);
showToast(error.message || 'RAG 지식 추가 실패', 'error');
}
}
function clearRAGForm() {
document.getElementById('ragQuestion').value = '';
document.getElementById('ragAnswer').value = '';
document.getElementById('ragKeywords').value = '';
}
function displayRAGKnowledge() {
const list = document.getElementById('ragKnowledgeList');
const count = document.getElementById('ragKnowledgeCount');
if (!ragKnowledgeBase || ragKnowledgeBase.length === 0) {
list.innerHTML = '지식이 없습니다. 질의응답을 추가하세요.
';
count.textContent = '0';
return;
}
count.textContent = ragKnowledgeBase.length;
list.innerHTML = '';
ragKnowledgeBase.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.style.cssText = `
background: var(--card-bg);
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
`;
const keywords = item.keywords && item.keywords.length > 0
? item.keywords.slice(0, 5).join(', ')
: '없음';
const useCount = item.use_count || 0;
itemEl.innerHTML = `
#${index + 1} ${item.question}
${item.answer}
🏷️ ${keywords}
📊 사용: ${useCount}회
`;
list.appendChild(itemEl);
});
}
async function deleteRAGKnowledge(itemId) {
if (!confirm('이 질의응답을 삭제하시겠습니까?')) {
return;
}
try {
const response = await fetch(`/api/rag/knowledge/${itemId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('RAG 지식 삭제 실패');
const data = await response.json();
ragKnowledgeBase = data.knowledge_base;
displayRAGKnowledge();
showToast('질의응답이 삭제되었습니다', 'success');
} catch (error) {
console.error('RAG 지식 삭제 실패:', error);
showToast('RAG 지식 삭제 실패', 'error');
}
}
async function clearRAGKnowledge() {
if (!confirm('모든 질의응답을 삭제하시겠습니까?')) {
return;
}
try {
const response = await fetch('/api/rag/knowledge/clear', {
method: 'POST'
});
if (!response.ok) throw new Error('RAG 지식 삭제 실패');
ragKnowledgeBase = [];
ragResponseCount = 0;
displayRAGKnowledge();
updateRAGStatus();
showToast('모든 질의응답이 삭제되었습니다', 'success');
} catch (error) {
console.error('RAG 지식 삭제 실패:', error);
showToast('RAG 지식 삭제 실패', 'error');
}
}
async function toggleRAG(enabled) {
try {
const response = await fetch('/api/rag/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
if (!response.ok) throw new Error('RAG 토글 실패');
const data = await response.json();
ragEnabled = data.enabled;
updateRAGStatus();
showToast(ragEnabled ? 'RAG 자동 답변 활성화' : 'RAG 자동 답변 비활성화', 'success');
} catch (error) {
console.error('RAG 토글 실패:', error);
showToast('RAG 토글 실패', 'error');
}
}
function updateRAGStatus() {
const toggle = document.getElementById('ragToggle');
const statusText = document.getElementById('ragStatusText');
const responseCount = document.getElementById('ragResponseCount');
toggle.checked = ragEnabled;
statusText.textContent = ragEnabled ? '활성화' : '비활성화';
statusText.style.color = ragEnabled ? 'var(--primary-color)' : 'var(--text-secondary)';
responseCount.textContent = ragResponseCount;
}
async function testRAGAnswer() {
const question = document.getElementById('ragTestQuestion').value.trim();
if (!question) {
showToast('질문을 입력하세요', 'error');
return;
}
const resultDiv = document.getElementById('ragTestResult');
const answerText = document.getElementById('ragTestAnswerText');
const knowledgeUsed = document.getElementById('ragTestKnowledgeUsed');
resultDiv.style.display = 'block';
answerText.textContent = '답변 생성 중...';
knowledgeUsed.textContent = '';
try {
const response = await fetch('/api/rag/answer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '답변 생성 실패');
}
const data = await response.json();
answerText.textContent = data.answer;
if (data.relevant_knowledge && data.relevant_knowledge.length > 0) {
const questions = data.relevant_knowledge.map(k => k.question).join(', ');
knowledgeUsed.textContent = questions;
} else {
knowledgeUsed.textContent = '없음';
}
showToast('답변 생성 완료', 'success');
} catch (error) {
console.error('RAG 답변 테스트 실패:', error);
answerText.textContent = '오류: ' + error.message;
showToast(error.message || 'RAG 답변 테스트 실패', 'error');
}
}
async function exportRAGKnowledge() {
try {
const response = await fetch('/api/rag/export');
if (!response.ok) throw new Error('내보내기 실패');
const data = await response.json();
const jsonStr = JSON.stringify(data, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rag_knowledge_${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('지식 베이스를 내보냈습니다', 'success');
} catch (error) {
console.error('내보내기 실패:', error);
showToast('내보내기 실패', 'error');
}
}
function showImportRAGModal() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
const response = await fetch('/api/rag/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '가져오기 실패');
}
const result = await response.json();
ragKnowledgeBase = result.knowledge_base;
displayRAGKnowledge();
showToast('지식 베이스를 가져왔습니다', 'success');
} catch (error) {
console.error('가져오기 실패:', error);
showToast(error.message || '가져오기 실패', 'error');
}
};
input.click();
}
// ==================== RAG 채팅 모달 ====================
let ragChatHistory = []; // 채팅 히스토리
function openRAGChatModal() {
const modal = document.getElementById('ragChatModal');
modal.style.display = 'block';
document.getElementById('ragChatInput').focus();
}
function closeRAGChatModal() {
const modal = document.getElementById('ragChatModal');
modal.style.display = 'none';
}
function clearRAGChat() {
ragChatHistory = [];
const messagesDiv = document.getElementById('ragChatMessages');
messagesDiv.innerHTML = `
🤖
질문을 입력하면 RAG 시스템이 답변합니다
지식 베이스에 등록된 정보를 바탕으로 응답합니다
`;
}
async function sendRAGChatMessage() {
const input = document.getElementById('ragChatInput');
const question = input.value.trim();
if (!question) return;
// 입력 초기화
input.value = '';
// 사용자 메시지 표시
addRAGChatMessage('user', question);
// "답변 생성 중..." 표시
const botMsgId = addRAGChatMessage('bot', '답변 생성 중...', true);
try {
const response = await fetch('/api/rag/answer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '답변 생성 실패');
}
const data = await response.json();
// "답변 생성 중..." 메시지 업데이트
updateRAGChatMessage(botMsgId, data.answer, data.relevant_knowledge);
} catch (error) {
console.error('RAG 답변 실패:', error);
updateRAGChatMessage(botMsgId, `❌ 오류: ${error.message}`, []);
}
}
function addRAGChatMessage(sender, message, isLoading = false) {
const messagesDiv = document.getElementById('ragChatMessages');
// 첫 메시지인 경우 초기 안내 제거
if (ragChatHistory.length === 0) {
messagesDiv.innerHTML = '';
}
const msgId = `msg-${Date.now()}`;
const isUser = sender === 'user';
const messageEl = document.createElement('div');
messageEl.id = msgId;
messageEl.style.cssText = `
margin-bottom: 15px;
display: flex;
${isUser ? 'justify-content: flex-end;' : 'justify-content: flex-start;'}
`;
const bubbleStyle = isUser
? 'background: linear-gradient(135deg, var(--primary-color), #667eea); color: white;'
: 'background: var(--card-bg); color: var(--text-primary); border: 2px solid var(--border-color);';
messageEl.innerHTML = `
${isUser ? '👤 나' : '🤖 RAG Bot'}
${message}
${isLoading ? '
⏳
' : ''}
`;
messagesDiv.appendChild(messageEl);
// 스크롤을 맨 아래로
messagesDiv.scrollTop = messagesDiv.scrollHeight;
// 히스토리에 추가
ragChatHistory.push({ id: msgId, sender, message });
return msgId;
}
function updateRAGChatMessage(msgId, newMessage, relevantKnowledge = []) {
const messageEl = document.getElementById(msgId);
if (!messageEl) return;
// 봇 메시지는 항상 왼쪽 정렬
messageEl.style.cssText = `
margin-bottom: 15px;
display: flex;
justify-content: flex-start;
`;
// 말풍선 전체를 다시 렌더링 (로딩 아이콘 제거)
const bubbleStyle = 'background: var(--card-bg); color: var(--text-primary); border: 2px solid var(--border-color);';
let html = newMessage;
// 관련 지식 표시
if (relevantKnowledge && relevantKnowledge.length > 0) {
html += '';
html += '
📚 참고한 지식:
';
relevantKnowledge.forEach((item, index) => {
html += `
• ${item.question}
`;
});
html += '
';
}
// 전체 말풍선 HTML 재구성
messageEl.innerHTML = `
`;
// 스크롤을 맨 아래로
const messagesDiv = document.getElementById('ragChatMessages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
// 히스토리 업데이트
const historyItem = ragChatHistory.find(h => h.id === msgId);
if (historyItem) {
historyItem.message = newMessage;
}
}
// ==================== 주기적 상태 업데이트 ====================
setInterval(() => {
if (socket && socket.connected) {
socket.emit('request_status');
}
}, 2000); // 2초마다 상태 업데이트