Usa o modelo multilingual, ignora trechos curtos e o template do Llama 3.2 para a resposta sair em uma frase completa. O chat deixa a API livre durante a geração, o upload indexa o PDF e os agendamentos não são gravados duas vezes.
274 lines
8.3 KiB
JavaScript
274 lines
8.3 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 button = document.getElementById('send-btn');
|
|
const message = input.value.trim();
|
|
|
|
if (!message || button.disabled) return;
|
|
button.disabled = true;
|
|
|
|
// 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 }),
|
|
});
|
|
|
|
const data = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const detail = typeof data.detail === 'string' ? data.detail : 'Erro ao enviar mensagem';
|
|
throw new Error(detail);
|
|
}
|
|
|
|
hideLoading();
|
|
addMessage(data.response, 'bot');
|
|
|
|
if (data.sources && data.sources.length > 0) {
|
|
const sourceText = data.sources.map((source) => {
|
|
const meta = source.metadata || source;
|
|
return `📖 ${meta.title || 'Desconhecido'} (trecho ${meta.chunk_id ?? '?'})`;
|
|
}).join('\n');
|
|
addMessage(`Fontes:\n${sourceText}`, 'bot');
|
|
}
|
|
} catch (error) {
|
|
hideLoading();
|
|
addMessage(`Erro: ${error.message}`, 'bot');
|
|
} finally {
|
|
button.disabled = false;
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
function addMessage(content, type) {
|
|
const container = document.getElementById('chat-messages');
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${type}`;
|
|
const body = document.createElement('div');
|
|
body.className = 'message-content';
|
|
body.textContent = content;
|
|
messageDiv.appendChild(body);
|
|
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';
|
|
const icon = document.createElement('div');
|
|
icon.className = 'book-icon';
|
|
icon.textContent = '📖';
|
|
const name = document.createElement('div');
|
|
name.className = 'book-name';
|
|
name.textContent = book.title;
|
|
bookDiv.append(icon, name);
|
|
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';
|
|
const info = document.createElement('div');
|
|
info.className = 'schedule-info';
|
|
const time = document.createElement('span');
|
|
time.className = 'schedule-time';
|
|
time.textContent = formatDateTime(schedule.time);
|
|
const book = document.createElement('span');
|
|
book.className = 'schedule-book';
|
|
book.textContent = schedule.book || 'Todos os livros';
|
|
info.append(time, book);
|
|
const remove = document.createElement('button');
|
|
remove.className = 'schedule-delete';
|
|
remove.type = 'button';
|
|
remove.textContent = 'Excluir';
|
|
remove.addEventListener('click', () => deleteSchedule(schedule.id));
|
|
div.append(info, remove);
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
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');
|
|
}
|