✨ 새로운 기능: - FastAPI 기반 완전한 웹 UI 구현 - 반응형 디자인 (모바일/태블릿 지원) - 드래그앤드롭 파일 업로드 인터페이스 - 실시간 AI 챗봇 인터페이스 - 문서 관리 및 검색 시스템 - 진행률 표시 및 상태 알림 🎨 UI 구성: - 메인 대시보드: 서버 상태, 통계, 빠른 접근 - 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률 - 문서 관리: 검색, 정렬, 미리보기, 다운로드 - AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문 🔧 기술 스택: - FastAPI + Jinja2 템플릿 - 모던 CSS (그라디언트, 애니메이션) - Font Awesome 아이콘 - JavaScript (ES6+) 🚀 완성된 기능: - 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성 - 벡터 검색 및 RAG 기반 질의응답 - 다중 모델 지원 (기본/부스팅/영어 전용) - API 키 인증 및 CORS 설정 - NAS 연동 및 파일 내보내기
631 lines
17 KiB
HTML
631 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}AI 챗봇 - AI 문서 처리 서버{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="grid grid-2">
|
|
<!-- 채팅 영역 -->
|
|
<div class="card chat-card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">
|
|
<i class="fas fa-robot"></i> AI 문서 챗봇
|
|
</h2>
|
|
<div class="chat-status">
|
|
<span class="status status-success">
|
|
<i class="fas fa-circle"></i> 온라인
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-container">
|
|
<div class="chat-messages" id="chatMessages">
|
|
<div class="message assistant">
|
|
<div class="message-content">
|
|
<i class="fas fa-robot message-icon"></i>
|
|
<div class="message-text">
|
|
안녕하세요! 저는 문서 기반 AI 어시스턴트입니다.
|
|
업로드된 문서들에 대해 질문하시면 답변해드리겠습니다.
|
|
</div>
|
|
</div>
|
|
<div class="message-time">{{ current_time }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-input-area">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control chat-input" id="messageInput"
|
|
placeholder="메시지를 입력하세요..." maxlength="1000">
|
|
<button type="button" class="btn btn-primary" id="sendBtn" onclick="sendMessage()">
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
<div class="input-options">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="useRAG" checked>
|
|
<i class="fas fa-search"></i> 문서 검색 사용
|
|
</label>
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="forceBoost">
|
|
<i class="fas fa-rocket"></i> 고성능 모델 사용
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 설정 및 정보 패널 -->
|
|
<div class="settings-panel">
|
|
<!-- 빠른 질문 -->
|
|
<div class="card">
|
|
<h3><i class="fas fa-lightning-bolt"></i> 빠른 질문</h3>
|
|
<div class="quick-questions">
|
|
<button onclick="askQuickQuestion('전체 문서를 요약해주세요')" class="btn btn-sm btn-outline">
|
|
문서 요약
|
|
</button>
|
|
<button onclick="askQuickQuestion('주요 키워드를 추출해주세요')" class="btn btn-sm btn-outline">
|
|
키워드 추출
|
|
</button>
|
|
<button onclick="askQuickQuestion('이 문서의 핵심 내용은 무엇인가요?')" class="btn btn-sm btn-outline">
|
|
핵심 내용
|
|
</button>
|
|
<button onclick="askQuickQuestion('관련된 다른 문서가 있나요?')" class="btn btn-sm btn-outline">
|
|
관련 문서
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 결과 -->
|
|
<div class="card">
|
|
<h3><i class="fas fa-search"></i> 관련 문서 검색</h3>
|
|
<input type="text" class="form-control mb-2" id="searchInput"
|
|
placeholder="문서 내용 검색..." onkeyup="searchDocuments(event)">
|
|
<div class="search-results" id="searchResults">
|
|
<p class="text-muted">검색어를 입력하세요</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모델 정보 -->
|
|
<div class="card">
|
|
<h3><i class="fas fa-cog"></i> 모델 설정</h3>
|
|
<div class="model-info">
|
|
<div class="info-item">
|
|
<strong>기본 모델:</strong> {{ status.base_model }}
|
|
</div>
|
|
<div class="info-item">
|
|
<strong>부스트 모델:</strong> {{ status.boost_model }}
|
|
</div>
|
|
<div class="info-item">
|
|
<strong>임베딩 모델:</strong> {{ status.embedding_model }}
|
|
</div>
|
|
<div class="info-item">
|
|
<strong>인덱스 문서:</strong> {{ status.index_loaded }}개
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 채팅 기록 관리 -->
|
|
<div class="card">
|
|
<h3><i class="fas fa-history"></i> 대화 관리</h3>
|
|
<div class="chat-controls">
|
|
<button onclick="clearChat()" class="btn btn-sm btn-secondary w-100 mb-2">
|
|
<i class="fas fa-trash"></i> 대화 기록 삭제
|
|
</button>
|
|
<button onclick="exportChat()" class="btn btn-sm btn-outline w-100">
|
|
<i class="fas fa-download"></i> 대화 내용 내보내기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.chat-card {
|
|
height: 600px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chat-card .card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.chat-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.chat-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1rem;
|
|
background-color: #f8f9fa;
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.message {
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.message.user {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.message.assistant {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.message-content {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
max-width: 80%;
|
|
}
|
|
|
|
.message.user .message-content {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.message-icon {
|
|
font-size: 1.2rem;
|
|
padding: 0.5rem;
|
|
border-radius: 50%;
|
|
min-width: 40px;
|
|
min-height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.message.user .message-icon {
|
|
background-color: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.message.assistant .message-icon {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.message-text {
|
|
background: white;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 10px;
|
|
border: 1px solid #e9ecef;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.message.user .message-text {
|
|
background: #667eea;
|
|
color: white;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.message-time {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
margin-top: 0.25rem;
|
|
margin-left: 3rem;
|
|
}
|
|
|
|
.message.user .message-time {
|
|
margin-left: 0;
|
|
margin-right: 3rem;
|
|
text-align: right;
|
|
}
|
|
|
|
.chat-input-area {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.input-group .form-control {
|
|
flex: 1;
|
|
}
|
|
|
|
.input-options {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
cursor: pointer;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.settings-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.quick-questions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-outline {
|
|
background: transparent;
|
|
border: 1px solid #667eea;
|
|
color: #667eea;
|
|
text-align: left;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.btn-outline:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.search-results {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.search-result-item {
|
|
padding: 0.5rem;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 5px;
|
|
margin-bottom: 0.5rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.search-result-title {
|
|
font-weight: 500;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.search-result-snippet {
|
|
font-size: 0.85rem;
|
|
color: #6c757d;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.info-item {
|
|
margin-bottom: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #f8f9fa;
|
|
}
|
|
|
|
.info-item:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.chat-controls .btn {
|
|
justify-content: center;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.w-100 { width: 100%; }
|
|
.mb-2 { margin-bottom: 0.5rem; }
|
|
.text-muted { color: #6c757d; }
|
|
|
|
/* 로딩 애니메이션 */
|
|
.typing-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.75rem 1rem;
|
|
background: white;
|
|
border-radius: 10px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background-color: #6c757d;
|
|
border-radius: 50%;
|
|
animation: typing 1.5s infinite;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
|
|
@keyframes typing {
|
|
0%, 60%, 100% { opacity: 0.3; }
|
|
30% { opacity: 1; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let chatHistory = [];
|
|
let isWaitingForResponse = false;
|
|
|
|
// DOM 요소들
|
|
const chatMessages = document.getElementById('chatMessages');
|
|
const messageInput = document.getElementById('messageInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const searchResults = document.getElementById('searchResults');
|
|
const useRAGCheckbox = document.getElementById('useRAG');
|
|
const forceBoostCheckbox = document.getElementById('forceBoost');
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
messageInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
// URL 파라미터 확인 (특정 문서로 대화하기)
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const doc = urlParams.get('doc');
|
|
if (doc) {
|
|
addMessage('assistant', `"${doc}" 문서에 대해 질문해주세요. 어떤 내용이 궁금하신가요?`);
|
|
}
|
|
});
|
|
|
|
// 메시지 전송
|
|
async function sendMessage() {
|
|
const message = messageInput.value.trim();
|
|
if (!message || isWaitingForResponse) return;
|
|
|
|
// 사용자 메시지 추가
|
|
addMessage('user', message);
|
|
messageInput.value = '';
|
|
isWaitingForResponse = true;
|
|
sendBtn.disabled = true;
|
|
|
|
// 타이핑 인디케이터 표시
|
|
const typingId = addTypingIndicator();
|
|
|
|
try {
|
|
const response = await fetch('/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': '{{ api_key }}'
|
|
},
|
|
body: JSON.stringify({
|
|
messages: chatHistory,
|
|
use_rag: useRAGCheckbox.checked,
|
|
force_boost: forceBoostCheckbox.checked,
|
|
top_k: 5
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// 타이핑 인디케이터 제거
|
|
removeTypingIndicator(typingId);
|
|
|
|
// AI 응답 추가
|
|
addMessage('assistant', data.response.message?.content || data.response.response || '응답을 받지 못했습니다.');
|
|
|
|
} catch (error) {
|
|
console.error('Chat error:', error);
|
|
removeTypingIndicator(typingId);
|
|
addMessage('assistant', '죄송합니다. 응답 중 오류가 발생했습니다. 다시 시도해주세요.');
|
|
} finally {
|
|
isWaitingForResponse = false;
|
|
sendBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// 메시지 추가
|
|
function addMessage(sender, content) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${sender}`;
|
|
|
|
const now = new Date().toLocaleTimeString('ko-KR', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
|
|
const icon = sender === 'user' ? 'fas fa-user' : 'fas fa-robot';
|
|
|
|
messageDiv.innerHTML = `
|
|
<div class="message-content">
|
|
<i class="${icon} message-icon"></i>
|
|
<div class="message-text">${content}</div>
|
|
</div>
|
|
<div class="message-time">${now}</div>
|
|
`;
|
|
|
|
chatMessages.appendChild(messageDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
|
// 채팅 히스토리에 추가
|
|
chatHistory.push({
|
|
role: sender === 'user' ? 'user' : 'assistant',
|
|
content: content
|
|
});
|
|
}
|
|
|
|
// 타이핑 인디케이터 추가
|
|
function addTypingIndicator() {
|
|
const typingDiv = document.createElement('div');
|
|
typingDiv.className = 'message assistant';
|
|
typingDiv.id = 'typing-' + Date.now();
|
|
|
|
typingDiv.innerHTML = `
|
|
<div class="message-content">
|
|
<i class="fas fa-robot message-icon"></i>
|
|
<div class="typing-indicator">
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
chatMessages.appendChild(typingDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
|
return typingDiv.id;
|
|
}
|
|
|
|
// 타이핑 인디케이터 제거
|
|
function removeTypingIndicator(typingId) {
|
|
const typingDiv = document.getElementById(typingId);
|
|
if (typingDiv) {
|
|
typingDiv.remove();
|
|
}
|
|
}
|
|
|
|
// 빠른 질문
|
|
function askQuickQuestion(question) {
|
|
messageInput.value = question;
|
|
sendMessage();
|
|
}
|
|
|
|
// 문서 검색
|
|
async function searchDocuments(event) {
|
|
if (event.key !== 'Enter') return;
|
|
|
|
const query = searchInput.value.trim();
|
|
if (!query) {
|
|
searchResults.innerHTML = '<p class="text-muted">검색어를 입력하세요</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/search', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
query: query,
|
|
top_k: 5
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
displaySearchResults(data.results);
|
|
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
searchResults.innerHTML = '<p class="text-muted">검색 중 오류가 발생했습니다</p>';
|
|
}
|
|
}
|
|
|
|
// 검색 결과 표시
|
|
function displaySearchResults(results) {
|
|
if (!results || results.length === 0) {
|
|
searchResults.innerHTML = '<p class="text-muted">검색 결과가 없습니다</p>';
|
|
return;
|
|
}
|
|
|
|
const resultsHtml = results.map(result => `
|
|
<div class="search-result-item" onclick="useSearchResult('${result.text.replace(/'/g, "\\'")}')">
|
|
<div class="search-result-title">문서 ID: ${result.id}</div>
|
|
<div class="search-result-snippet">${result.text.substring(0, 100)}...</div>
|
|
<small class="text-muted">유사도: ${(result.score * 100).toFixed(1)}%</small>
|
|
</div>
|
|
`).join('');
|
|
|
|
searchResults.innerHTML = resultsHtml;
|
|
}
|
|
|
|
// 검색 결과 활용
|
|
function useSearchResult(text) {
|
|
messageInput.value = `다음 내용에 대해 설명해주세요: "${text.substring(0, 50)}..."`;
|
|
}
|
|
|
|
// 대화 기록 삭제
|
|
function clearChat() {
|
|
if (!confirm('정말로 대화 기록을 모두 삭제하시겠습니까?')) return;
|
|
|
|
chatHistory = [];
|
|
chatMessages.innerHTML = `
|
|
<div class="message assistant">
|
|
<div class="message-content">
|
|
<i class="fas fa-robot message-icon"></i>
|
|
<div class="message-text">
|
|
안녕하세요! 저는 문서 기반 AI 어시스턴트입니다.
|
|
업로드된 문서들에 대해 질문하시면 답변해드리겠습니다.
|
|
</div>
|
|
</div>
|
|
<div class="message-time">${new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 대화 내용 내보내기
|
|
function exportChat() {
|
|
if (chatHistory.length === 0) {
|
|
showToast('내보낼 대화 내용이 없습니다.', 'info');
|
|
return;
|
|
}
|
|
|
|
const chatText = chatHistory.map(msg =>
|
|
`${msg.role === 'user' ? '사용자' : 'AI'}: ${msg.content}`
|
|
).join('\n\n');
|
|
|
|
const blob = new Blob([chatText], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `chat-export-${new Date().toISOString().slice(0, 10)}.txt`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// 토스트 알림
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `alert alert-${type}`;
|
|
toast.textContent = message;
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 1001;
|
|
min-width: 300px;
|
|
animation: slideIn 0.3s ease;
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => {
|
|
if (document.body.contains(toast)) {
|
|
document.body.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
{% endblock %} |