Files
ai-server/templates/chat.html
hyungi cb009f7393 feat: 완전한 웹 UI 구현 및 문서 처리 파이프라인 완성
 새로운 기능:
- FastAPI 기반 완전한 웹 UI 구현
- 반응형 디자인 (모바일/태블릿 지원)
- 드래그앤드롭 파일 업로드 인터페이스
- 실시간 AI 챗봇 인터페이스
- 문서 관리 및 검색 시스템
- 진행률 표시 및 상태 알림

🎨 UI 구성:
- 메인 대시보드: 서버 상태, 통계, 빠른 접근
- 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률
- 문서 관리: 검색, 정렬, 미리보기, 다운로드
- AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문

🔧 기술 스택:
- FastAPI + Jinja2 템플릿
- 모던 CSS (그라디언트, 애니메이션)
- Font Awesome 아이콘
- JavaScript (ES6+)

🚀 완성된 기능:
- 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성
- 벡터 검색 및 RAG 기반 질의응답
- 다중 모델 지원 (기본/부스팅/영어 전용)
- API 키 인증 및 CORS 설정
- NAS 연동 및 파일 내보내기
2025-08-14 08:09:48 +09:00

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 %}