- Stack: GPT4All + Llama 3.2 3B, LangChain, Qdrant, FastAPI, Docker - 11 tarefas implementadas com 100% de conclusão - Funcionalidades: chat com IA, upload PDF, bot Telegram, agendamento - 6 livros PDF indexados (A Gênese, Livro dos Espíritos, etc.) - Documentação rica: README, BUILD, task reports, design specs - Deploy com Docker Compose - Production ready Tech stack: - AI: GPT4All 2.4.6, Llama 3.2 3B q4_0 - Backend: FastAPI 0.115.0, Celery 5.4.0, Redis 5.2.0 - RAG: LangChain 0.3.7, Qdrant 1.11.1 - Frontend: HTML5/CSS3/Vanilla JS - Deploy: Docker Compose Acceptance Criteria: ✅ CPU-only, <8GB RAM, ~5GB storage ✅ Suporta upload de PDFs ✅ Interface web rica e responsiva ✅ Bot Telegram funcional ✅ Agendamento de mensagens ✅ Código limpo e documentado ✅ Deploy via Docker Status: PRODUCTION READY Version: 1.0.0 Date: 2026-11-01
264 lines
7.6 KiB
JavaScript
264 lines
7.6 KiB
JavaScript
// API base URL
|
|
const API_BASE = '/api';
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadBooks();
|
|
loadSchedules();
|
|
|
|
// Event listeners
|
|
document.getElementById('send-btn').addEventListener('click', sendChatMessage);
|
|
document.getElementById('chat-input').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') sendChatMessage();
|
|
});
|
|
document.getElementById('upload-btn').addEventListener('click', uploadPDF);
|
|
document.getElementById('add-schedule-btn').addEventListener('click', addSchedule);
|
|
});
|
|
|
|
async function sendChatMessage() {
|
|
const input = document.getElementById('chat-input');
|
|
const message = input.value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
// Add user message to chat
|
|
addMessage(message, 'user');
|
|
input.value = '';
|
|
|
|
// Show loading
|
|
showLoading();
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message, top_k: 3 }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Erro ao enviar mensagem');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Remove loading
|
|
hideLoading();
|
|
|
|
// Add bot response
|
|
addMessage(data.response, 'bot');
|
|
|
|
// Add sources
|
|
if (data.sources && data.sources.length > 0) {
|
|
const sourceText = data.sources.map(s => `📖 ${s.metadata.title} (chunk ${s.metadata.chunk_id})`).join('\n');
|
|
addMessage(`Fontes: ${sourceText}`, 'bot', true);
|
|
}
|
|
} catch (error) {
|
|
hideLoading();
|
|
addMessage(`❌ Erro: ${error.message}`, 'bot');
|
|
}
|
|
}
|
|
|
|
function addMessage(content, type, isSource = false) {
|
|
const container = document.getElementById('chat-messages');
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${type}`;
|
|
|
|
if (isSource) {
|
|
messageDiv.style.fontSize = '0.85rem';
|
|
}
|
|
|
|
messageDiv.innerHTML = `<div class="message-content">${content}</div>`;
|
|
container.appendChild(messageDiv);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
function showLoading() {
|
|
const container = document.getElementById('chat-messages');
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.id = 'chat-loading';
|
|
loadingDiv.className = 'message bot';
|
|
loadingDiv.innerHTML = '<div class="message-content">Carregando...</div>';
|
|
container.appendChild(loadingDiv);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
function hideLoading() {
|
|
const loading = document.getElementById('chat-loading');
|
|
if (loading) loading.remove();
|
|
}
|
|
|
|
async function loadBooks() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/books`);
|
|
const books = await response.json();
|
|
|
|
const booksList = document.getElementById('books-list');
|
|
booksList.innerHTML = '';
|
|
|
|
if (books.length === 0) {
|
|
booksList.innerHTML = '<p style="color: #666;">Nenhum livro encontrado.</p>';
|
|
return;
|
|
}
|
|
|
|
books.forEach(book => {
|
|
const bookDiv = document.createElement('div');
|
|
bookDiv.className = 'book-item';
|
|
bookDiv.innerHTML = `
|
|
<div class="book-icon">📖</div>
|
|
<div class="book-name">${book.title}</div>
|
|
`;
|
|
booksList.appendChild(bookDiv);
|
|
});
|
|
|
|
// Update book dropdown
|
|
const select = document.getElementById('schedule-book');
|
|
select.innerHTML = '<option value="">Todos os livros</option>';
|
|
books.forEach(book => {
|
|
const option = document.createElement('option');
|
|
option.value = book.title;
|
|
option.textContent = book.title;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao carregar livros:', error);
|
|
}
|
|
}
|
|
|
|
async function uploadPDF() {
|
|
const fileInput = document.getElementById('file-input');
|
|
const file = fileInput.files[0];
|
|
|
|
if (!file) {
|
|
alert('Selecione um arquivo PDF primeiro');
|
|
return;
|
|
}
|
|
|
|
const statusDiv = document.getElementById('upload-status');
|
|
statusDiv.textContent = 'Enviando...';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const response = await fetch(`${API_BASE}/upload`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Erro ao fazer upload');
|
|
}
|
|
|
|
const data = await response.json();
|
|
statusDiv.textContent = `✅ ${data.filename} enviado com sucesso!`;
|
|
statusDiv.style.color = '#4caf50';
|
|
|
|
setTimeout(() => {
|
|
statusDiv.textContent = '';
|
|
fileInput.value = '';
|
|
}, 3000);
|
|
|
|
// Reload books
|
|
loadBooks();
|
|
} catch (error) {
|
|
statusDiv.textContent = `❌ Erro: ${error.message}`;
|
|
statusDiv.style.color = '#f44336';
|
|
}
|
|
}
|
|
|
|
async function loadSchedules() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/schedules`);
|
|
const schedules = await response.json();
|
|
|
|
renderSchedules(schedules);
|
|
} catch (error) {
|
|
console.error('Erro ao carregar agendamentos:', error);
|
|
}
|
|
}
|
|
|
|
function renderSchedules(schedules) {
|
|
const container = document.getElementById('schedules-list');
|
|
container.innerHTML = '';
|
|
|
|
if (schedules.length === 0) {
|
|
container.innerHTML = '<p style="color: #666;">Nenhum agendamento.</p>';
|
|
return;
|
|
}
|
|
|
|
schedules.forEach(schedule => {
|
|
const div = document.createElement('div');
|
|
div.className = 'schedule-item';
|
|
div.innerHTML = `
|
|
<div class="schedule-info">
|
|
<span class="schedule-time">🕐 ${formatDateTime(schedule.time)}</span>
|
|
<span class="schedule-book">${schedule.book || 'Todos os livros'}</span>
|
|
</div>
|
|
<button class="schedule-delete" data-id="${schedule.id}">🗑️</button>
|
|
`;
|
|
container.appendChild(div);
|
|
|
|
// Add delete handler
|
|
div.querySelector('.schedule-delete').addEventListener('click', () => deleteSchedule(schedule.id));
|
|
});
|
|
}
|
|
|
|
async function addSchedule() {
|
|
const timeInput = document.getElementById('schedule-time');
|
|
const bookSelect = document.getElementById('schedule-book');
|
|
|
|
if (!timeInput.value) {
|
|
alert('Selecione uma data e hora');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/schedules`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
time: timeInput.value,
|
|
book: bookSelect.value || null,
|
|
enabled: true,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Erro ao criar agendamento');
|
|
}
|
|
|
|
const schedule = await response.json();
|
|
loadSchedules();
|
|
|
|
timeInput.value = '';
|
|
bookSelect.value = '';
|
|
} catch (error) {
|
|
alert(`Erro: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async function deleteSchedule(scheduleId) {
|
|
if (!confirm('Tem certeza que deseja excluir este agendamento?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/schedules/${scheduleId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Erro ao excluir agendamento');
|
|
}
|
|
|
|
loadSchedules();
|
|
} catch (error) {
|
|
alert(`Erro: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function formatDateTime(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('pt-BR');
|
|
}
|