Nenhum conhecimento aprendido ainda. A assistente aprende automaticamente quando você ensinar algo novo!
';
return;
}
knowledgeList.innerHTML = window.conhecimento.facts.map(f => `
${pinIcon}
${escapeHtml(chat.title)}
${tagBadges ? `
${tagBadges}
` : ''}
')) {
contentHtml = content;
} else {
contentHtml = marked.parse(content);
}
}
}
const actionsHtml = !isUser ? `
` : `
`;
row.innerHTML = `
${avatarHtml}
${contentHtml}
${actionsHtml}
`;
conversationList.appendChild(row);
// Colorir blocos de código com Highlight.js
row.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// Event listener para copiar mensagem
const copyBtn = row.querySelector('.btn-copy-msg');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const text = copyBtn.dataset.content;
navigator.clipboard.writeText(text).then(() => {
const span = copyBtn.querySelector('span');
const original = span.textContent;
span.textContent = 'Copiado!';
setTimeout(() => span.textContent = original, 2000);
});
});
}
// Event listener para editar/reenviar mensagem
const editBtn = row.querySelector('.btn-edit');
if (editBtn) {
editBtn.addEventListener('click', () => {
const text = editBtn.dataset.content;
const chat = chats.find(c => c.id === currentChatId);
if (!chat) return;
// Encontrar índice desta mensagem no chat
const rows = conversationList.querySelectorAll('.message-row.user');
let msgRow = null;
for (const r of rows) {
const btn = r.querySelector('.btn-edit');
if (btn && btn.dataset.content === text) {
msgRow = r;
break;
}
}
// Remover esta mensagem e todas as subsequentes (do user e do bot)
if (msgRow) {
const allRows = Array.from(conversationList.querySelectorAll('.message-row'));
const idx = allRows.indexOf(msgRow);
for (let i = allRows.length - 1; i >= idx; i--) {
allRows[i].remove();
}
// Remover do estado também
const msgIndex = chat.messages.findIndex(m => m.role === 'user' && m.content === text);
if (msgIndex >= 0) {
chat.messages = chat.messages.slice(0, msgIndex);
}
saveChats();
}
// Colocar texto no input e focar
userInput.value = text;
userInput.focus();
userInput.dispatchEvent(new Event('input'));
btnSend.disabled = false;
});
}
// Event listener para ouvir resposta (TTS nativo)
const speakBtn = row.querySelector('.btn-speak');
if (speakBtn) {
speakBtn.addEventListener('click', () => {
const text = speakBtn.dataset.content;
speakText(text, speakBtn);
});
}
// Event listener para regenerar resposta
const regenBtn = row.querySelector('.btn-regenerate');
if (regenBtn) {
regenBtn.addEventListener('click', () => {
if (isGenerating) return;
regenerateLastResponse();
});
}
return row;
};
// Regenerar última resposta do bot
const regenerateLastResponse = () => {
const chat = chats.find(c => c.id === currentChatId);
if (!chat || chat.messages.length === 0) return;
// Encontrar última mensagem do bot
let lastBotIdx = -1;
for (let i = chat.messages.length - 1; i >= 0; i--) {
if (chat.messages[i].role === 'assistant') {
lastBotIdx = i;
break;
}
}
if (lastBotIdx === -1) return;
// Remover última mensagem do bot e todas as mensagens depois da última mensagem do usuário
let lastUserIdx = -1;
for (let i = lastBotIdx - 1; i >= 0; i--) {
if (chat.messages[i].role === 'user') {
lastUserIdx = i;
break;
}
}
// Cortar mensagens até a última do usuário
if (lastUserIdx >= 0) {
chat.messages = chat.messages.slice(0, lastUserIdx + 1);
} else {
chat.messages = [];
}
// Remover da UI as mensagens depois do último user
const rows = conversationList.querySelectorAll('.message-row');
for (let i = rows.length - 1; i >= 0; i--) {
const row = rows[i];
const isBot = row.classList.contains('assistant');
const isUser = row.classList.contains('user');
if (isBot) {
row.remove();
} else if (isUser) {
break;
}
}
saveChats();
scrollToBottom();
// Reenviar última mensagem do usuário para gerar nova resposta
const lastUserMsg = chat.messages[chat.messages.length - 1];
if (lastUserMsg && lastUserMsg.role === 'user') {
handleSendMessage(lastUserMsg.content, true);
}
};
const scrollToBottom = () => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
const getProgressSteps = (promptText) => {
const promptLower = (promptText || '').toLowerCase();
if (promptLower.includes('historia') || promptLower.includes('conto') || promptLower.includes('história') || promptLower.includes('escrever')) {
return [
{ text: "Analisando a solicitação da professora...", progress: 15 },
{ text: "Esboçando o enredo e personagens infantis...", progress: 40 },
{ text: "Elaborando a história pedagógica...", progress: 70 },
{ text: "Fazendo sugestões para uso pedagógico...", progress: 90 }
];
}
if (promptLower.includes('relatorio') || promptLower.includes('relatório') || promptLower.includes('observa') || promptLower.includes('htpc')) {
return [
{ text: "Analisando as observações pedagógicas...", progress: 15 },
{ text: "Sintetizando os avanços e interações...", progress: 40 },
{ text: "Estruturando os blocos do relatório individual...", progress: 65 },
{ text: "Refinando o texto e alinhando com a BNCC...", progress: 85 },
{ text: "Ajustando o tom formal-pedagógico...", progress: 95 }
];
}
if (promptLower.includes('planejamento') || promptLower.includes('aula') || promptLower.includes('atividade') || promptLower.includes('projeto') || promptLower.includes('brincar')) {
return [
{ text: "Revisando os campos de experiência da BNCC...", progress: 20 },
{ text: "Estruturando os momentos da vivência lúdica...", progress: 45 },
{ text: "Definindo a intencionalidade educativa e mediação...", progress: 70 },
{ text: "Finalizando as comandas e recursos pedagógicos...", progress: 90 }
];
}
// Caso geral
return [
{ text: "Analisando a solicitação...", progress: 20 },
{ text: "Processando as diretrizes pedagógicas...", progress: 45 },
{ text: "Elaborando a resposta...", progress: 75 },
{ text: "Refinando a formatação e estilo...", progress: 90 }
];
};
// Função principal de envio de mensagens
const handleSendMessage = async (text, skipUserAppend = false, files = null, forcedIntent = null) => {
if (!text && (!files || files.length === 0)) return;
// Se não recebe files por parâmetro, usa os anexados globalmente
const filesToSend = files || attachedFiles;
let fullText = text || '';
// Processar anexos: evitar base64 gigantesco em imagens e erro de leitura binária em PDF/DOCX
if (filesToSend.length > 0) {
for (const file of filesToSend) {
if (file.type.startsWith('image/')) {
// Apenas anexa uma referência textual limpa da imagem para a IA saber que ela existe
fullText += `\n\n*[Imagem anexada: ${file.name} (${Math.round(file.size/1024)} KB)]*`;
} else if (file.name.endsWith('.txt') || file.name.endsWith('.md') || file.name.endsWith('.json') || file.name.endsWith('.csv') || file.name.endsWith('.js') || file.name.endsWith('.css') || file.name.endsWith('.html')) {
const reader = new FileReader();
const content = await new Promise((resolve) => {
reader.onload = (e) => resolve(e.target.result);
reader.readAsText(file);
});
const truncated = content.length > 3000 ? content.substring(0, 3000) + '... [conteúdo truncado]' : content;
fullText += `\n\n*[Conteúdo do arquivo "${file.name}":]*\n\`\`\`\n${truncated}\n\`\`\``;
} else {
// Para arquivos binários como PDF/DOCX que não podem ser lidos diretamente como texto cru no frontend
fullText += `\n\n*[Arquivo anexado: ${file.name} (${Math.round(file.size/1024)} KB) - formato não textual]*`;
}
}
// Limpar anexos após envio
attachedFiles = [];
if (attachPreview) {
attachPreview.innerHTML = '';
attachPreview.style.display = 'none';
btnAttach.classList.remove('has-files');
}
}
isGenerating = true;
btnSend.disabled = true;
userInput.disabled = true;
userInput.value = '';
userInput.style.height = 'auto';
// Ocultar tela de boas-vindas se for a primeira mensagem
if (welcomeContainer.style.display !== 'none') {
welcomeContainer.style.display = 'none';
}
// Criar o chat se for novo
if (!currentChatId) {
const newId = 'chat_' + Date.now();
const firstLine = fullText.split('\n')[0];
const title = firstLine.length > 26 ? firstLine.substring(0, 26) + '...' : firstLine;
chats.push({
id: newId,
title: title,
tags: [],
pinned: false,
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
});
currentChatId = newId;
}
// Achar o chat atual no estado
const chat = chats.find(c => c.id === currentChatId);
if (!chat) return;
// Adicionar mensagem do usuário no estado e na tela (pula se for regeneração)
if (!skipUserAppend) {
const userMsg = { role: 'user', content: fullText };
chat.messages.push(userMsg);
chat.updatedAt = Date.now();
appendMessageUI('user', fullText);
scrollToBottom();
saveChats();
renderHistory();
}
// Criar container para a resposta do bot (PedaGog) na tela
const botRow = document.createElement('div');
botRow.className = 'message-row assistant';
botRow.innerHTML = `
`;
conversationList.appendChild(botRow);
scrollToBottom();
const botContentDiv = botRow.querySelector('.message-content');
let assistantContent = '';
// Obter etapas de progresso personalizadas
const progressSteps = getProgressSteps(fullText);
let currentStepIdx = 0;
// Renderizar barra de progresso inicial
botContentDiv.innerHTML = `
`;
scrollToBottom();
// Fechar barra de prompt no mobile se estiver aberta para liberar espaço
const mobilePromptBar = document.querySelector('.prompt-modes-bar');
const mobileToggleBtn = document.getElementById('fabTools');
if (mobilePromptBar && window.innerWidth <= 768) {
mobilePromptBar.classList.remove('show');
if (mobileToggleBtn) mobileToggleBtn.classList.remove('active');
}
// Iniciar timer para avançar as etapas
let progressTimer = setInterval(() => {
if (currentStepIdx < progressSteps.length - 1) {
currentStepIdx++;
const step = progressSteps[currentStepIdx];
const stepTextEl = botContentDiv.querySelector('#iaProgressStepText');
const barFillEl = botContentDiv.querySelector('#iaProgressBarFill');
if (stepTextEl && barFillEl) {
stepTextEl.textContent = step.text;
barFillEl.style.width = `${step.progress}%`;
}
}
}, 1500);
try {
// Filtrar mensagens para enviar apenas o histórico necessário ao OpenRouter, removendo dados base64 pesados de mídias
const messagesPayload = chat.messages.map(m => {
let cleanContent = m.content || '';
if (cleanContent.includes('data:image/') || cleanContent.includes('data:audio/') || cleanContent.includes('data:video/')) {
const typeLabel = cleanContent.includes('custom-audio-player') ? 'Áudio/Música' : (cleanContent.includes('media-video-container') ? 'Vídeo' : 'Imagem');
cleanContent = `[Mídia gerada anteriormente: ${typeLabel}]`;
}
return { role: m.role, content: cleanContent };
});
// Fazer a chamada HTTP usando stream
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messages: messagesPayload, forcedIntent: forcedIntent })
});
if (!response.ok) {
throw new Error('Falha na resposta do servidor');
}
// Processar stream de dados (SSE)
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let isMediaResponse = false;
let mediaMsgData = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// Deixar a última parte inacabada no buffer
buffer = lines.pop();
for (const line of lines) {
const cleanLine = line.trim();
if (cleanLine.startsWith('data: ')) {
const dataContent = cleanLine.slice(6);
if (dataContent === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(dataContent);
if (parsed.content) {
assistantContent += parsed.content;
// Mantemos o progresso visível sem renderizar o texto parcial
} else if (parsed.type === 'media_status') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
botContentDiv.innerHTML = `
`;
scrollToBottom();
} else if (parsed.type === 'image') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
mediaMsgData = { type: 'image', url: parsed.url, originalPrompt: parsed.originalPrompt };
assistantContent = `
`;
botContentDiv.innerHTML = assistantContent;
scrollToBottom();
} else if (parsed.type === 'error') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
botContentDiv.innerHTML = `
⚠️ Erro na geração: ${escapeHtml(parsed.message)}
`;
scrollToBottom();
} else if (parsed.type === 'audio') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
mediaMsgData = { type: 'audio', url: parsed.url, originalPrompt: parsed.originalPrompt };
const audioId = 'audio_' + Date.now();
assistantContent = `
`;
botContentDiv.innerHTML = assistantContent;
scrollToBottom();
} else if (parsed.type === 'video') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
mediaMsgData = { type: 'video', url: parsed.url, originalPrompt: parsed.originalPrompt };
assistantContent = `
`;
botContentDiv.innerHTML = assistantContent;
scrollToBottom();
}
} catch (err) {
// Ignorar erros de JSON incompletos do buffer
}
}
}
}
// Remover o cursor de digitação no final se não for mensagem de mídia
const cursor = botContentDiv.querySelector('.typing-cursor');
if (cursor) cursor.remove();
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
// Remover blocos
nativos de modelos de reasoning antes de renderizar e extrair conhecimento
assistantContent = assistantContent.replace(/[\s\S]*?(?:<\/think>|$)/g, '').trim();
// Parse final limpo do markdown (se for texto normal)
if (!isMediaResponse) {
const stepTextEl = botContentDiv.querySelector('#iaProgressStepText');
const barFillEl = botContentDiv.querySelector('#iaProgressBarFill');
if (stepTextEl && barFillEl) {
stepTextEl.textContent = "Concluído com sucesso!";
barFillEl.style.width = "100%";
}
// Aguardar um instante para o feedback visual de conclusão ser percebido
await new Promise(resolve => setTimeout(resolve, 400));
botContentDiv.innerHTML = marked.parse(assistantContent);
botContentDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
// Extrair e salvar conhecimento aprendido (tags [KNOWLEDGE: ...])
window.extractAndSaveKnowledge(assistantContent);
// Adicionar botões de ação (Copiar, Regenerar, Ouvir) após o streaming
const escapedContent = escapeHtml(assistantContent).replace(/"/g, '"');
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
actionsDiv.innerHTML = `
`;
const wrapper = botRow.querySelector('.message-content-wrapper');
wrapper.appendChild(actionsDiv);
// Vincular listeners dos botões
const copyBtn = actionsDiv.querySelector('.btn-copy-msg');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(assistantContent).then(() => {
const span = copyBtn.querySelector('span');
const original = span.textContent;
span.textContent = 'Copiado!';
setTimeout(() => span.textContent = original, 2000);
});
});
const regenBtn = actionsDiv.querySelector('.btn-regenerate');
regenBtn.addEventListener('click', () => {
if (!isGenerating) regenerateLastResponse();
});
const speakBtn = actionsDiv.querySelector('.btn-speak');
speakBtn.addEventListener('click', () => {
speakText(assistantContent, speakBtn);
});
scrollToBottom();
// AUTO-TTS: Se a mensagem veio do microfone, ler a resposta em voz alta automaticamente
if (lastInputWasVoice && assistantContent && !isMediaResponse) {
console.log('[VOZ] Mensagem veio do microfone — respondendo por voz automaticamente.');
setTimeout(() => {
speakText(assistantContent, speakBtn);
}, 400);
}
lastInputWasVoice = false;
// Salvar resposta no estado e localStorage
const assistantMsg = { role: 'assistant', content: assistantContent };
chat.messages.push(assistantMsg);
chat.updatedAt = Date.now();
saveChats();
renderHistory();
} catch (error) {
console.error('Erro na resposta do chat:', error);
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
const cursor = botContentDiv.querySelector('.typing-cursor');
if (cursor) cursor.remove();
botContentDiv.innerHTML = `
Desculpe, ocorreu um erro ao obter resposta da PedaGog. Por favor, tente novamente.
`;
scrollToBottom();
} finally {
isGenerating = false;
btnSend.disabled = userInput.value.trim() === '';
userInput.disabled = false;
userInput.focus();
}
};
// Envio pelo Formulário
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = userInput.value.trim();
handleSendMessage(text);
});
// Botões de Modos Rápidos de Prompt (Rodapé)
document.querySelectorAll('.btn-prompt-mode').forEach(btn => {
btn.addEventListener('click', () => {
if (window.innerWidth <= 768) {
if (promptModesBar) promptModesBar.classList.remove('show');
if (fabTools) fabTools.classList.remove('active');
}
if (btn.dataset.prompt) {
handleSendMessage(btn.dataset.prompt);
return;
}
if (btn.dataset.fill) {
userInput.value = btn.dataset.fill;
userInput.focus();
btnSend.disabled = false;
return;
}
if (btn.id === 'barBtnEstudio') return; // Manipulado separadamente
if (btn.id === 'barBtnComics') return; // Manipulado separadamente
if (btn.id === 'barBtnMusica') return;
if (btn.id === 'barBtnHistoria') return;
if (btn.id === 'barBtnMindLab') return;
const mode = btn.dataset.mode;
const text = userInput.value.trim();
let suggestion = "";
let baseText = text;
// Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar
const suggestionsList = [
"Crie uma música pedagógica alegre e animada sobre ",
"Crie um roteiro detalhado para um vídeo educativo sobre ",
"Crie uma ilustração pedagógica com cores vibrantes mostrando ",
"Escreva uma poesia infantil rimada e educativa sobre ",
"Escreva uma história educativa infantil curta sobre "
];
for (const sugg of suggestionsList) {
if (baseText.startsWith(sugg)) {
baseText = baseText.replace(sugg, "").trim();
break;
}
}
if (mode === 'AUDIO') {
suggestion = "Crie uma música pedagógica alegre e animada sobre ";
} else if (mode === 'VIDEO') {
suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre ";
} else if (mode === 'IMAGE') {
suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando ";
} else if (mode === 'POETRY') {
suggestion = "Escreva uma poesia infantil rimada e educativa sobre ";
} else if (mode === 'TEXT') {
suggestion = "Escreva uma história educativa infantil curta sobre ";
}
userInput.value = suggestion + baseText;
userInput.focus();
// Mover cursor para o final do texto
userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
// Ajustar altura do textarea para acomodar a sugestão
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
if (btnSend) btnSend.disabled = false;
});
});
// Sugestões de Prompt Rápidas
suggestionCards.forEach(card => {
card.addEventListener('click', () => {
const prompt = card.dataset.prompt;
if (prompt) {
handleSendMessage(prompt);
}
});
});
// ==========================================================================
// LOGOUT & INICIALIZAÇÃO
// ==========================================================================
btnLogout.addEventListener('click', async () => {
try {
const response = await fetch('/api/logout', {
method: 'POST'
});
if (response.ok) {
window.location.href = '/login';
}
} catch (error) {
console.error('Erro ao fazer logout:', error);
}
});
// Inicializar interface
if (currentChatId) {
// Tenta carregar o chat ativo
const activeChat = chats.find(c => c.id === currentChatId);
if (activeChat) {
selectChat(currentChatId);
} else {
createNewChat();
}
} else {
createNewChat();
}
// Inicializar reconhecimento de voz
initVoiceRecognition();
// ============================================================
// SISTEMA DE OBSERVAÇÃO POR VOZ
// ============================================================
const recordModal = document.getElementById('recordModal');
const fabRecord = document.getElementById('fabRecord');
const btnExitRecord = document.getElementById('btnExitRecord');
const btnStopRecord = document.getElementById('btnStopRecord');
const recordStateRecording = document.getElementById('recordStateRecording');
const recordStateProcessing = document.getElementById('recordStateProcessing');
const recordStatePreview = document.getElementById('recordStatePreview');
const recordTimer = document.getElementById('recordTimer');
const reportContent = document.getElementById('reportContent');
const previewDateTime = document.getElementById('previewDateTime');
const btnDiscardReport = document.getElementById('btnDiscardReport');
const btnSaveReport = document.getElementById('btnSaveReport');
// Novos elementos de transcrição
const recordStateTranscribed = document.getElementById('recordStateTranscribed');
const transcriptionTextarea = document.getElementById('transcriptionTextarea');
const btnDiscardTranscription = document.getElementById('btnDiscardTranscription');
const btnAnalyzeTranscription = document.getElementById('btnAnalyzeTranscription');
const recordProcessingText = document.getElementById('recordProcessingText');
const recordProcessingSubtext = document.getElementById('recordProcessingSubtext');
// Variáveis de gravação
let mediaRecorder = null;
let audioChunks = [];
let recordingStartTime = null;
let timerInterval = null;
let audioContext = null;
let analyser = null;
let frequencyData = null;
let silenceFrames = 0;
let totalFrames = 0;
let vadCheckInterval = null;
let generatedReport = null;
let generatedCriancas = null;
let generatedTurma = 'não informada';
let generatedTags = [];
// Fechar modal - colocada no topo para evitar problemas de inicialização
function closeRecordModal() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
try {
mediaRecorder.stop();
} catch (e) {}
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
}
clearInterval(timerInterval);
clearInterval(vadCheckInterval);
if (audioContext) { audioContext.close(); audioContext = null; }
if (analyser) { analyser = null; frequencyData = null; }
audioChunks = [];
generatedReport = null;
recordModal.style.display = 'none';
}
// Função para controlar a exibição dos estados
function showRecordState(state) {
recordStateRecording.style.display = 'none';
recordStateProcessing.style.display = 'none';
if (recordStateTranscribed) recordStateTranscribed.style.display = 'none';
recordStatePreview.style.display = 'none';
if (state === 'recording') {
recordStateRecording.style.display = 'flex';
} else if (state === 'processing') {
recordStateProcessing.style.display = 'flex';
} else if (state === 'transcribed') {
if (recordStateTranscribed) recordStateTranscribed.style.display = 'flex';
} else if (state === 'preview') {
recordStatePreview.style.display = 'flex';
}
}
// Abrir modal e iniciar gravação
if (fabRecord) {
fabRecord.addEventListener('click', async () => {
audioChunks = [];
generatedReport = null;
// Mostra estado inicial (gravando)
showRecordState('recording');
recordModal.style.display = 'flex';
// Inicia timer
recordingStartTime = Date.now();
timerInterval = setInterval(updateTimer, 1000);
updateTimer();
// Solicita permissão e inicia gravação
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
let options = {};
if (typeof MediaRecorder.isTypeSupported === 'function') {
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
options = { mimeType: 'audio/webm;codecs=opus' };
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
options = { mimeType: 'audio/webm' };
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
options = { mimeType: 'audio/ogg' };
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
options = { mimeType: 'audio/mp4' };
}
}
// Configurar AudioContext para VAD
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
frequencyData = new Uint8Array(analyser.frequencyBinCount);
// Iniciar verificação de silêncio (VAD)
silenceFrames = 0;
totalFrames = 0;
vadCheckInterval = setInterval(() => {
if (!analyser) return;
analyser.getByteFrequencyData(frequencyData);
const avg = frequencyData.reduce((a, b) => a + b, 0) / frequencyData.length;
totalFrames++;
if (avg < 10) {
silenceFrames++;
} else {
silenceFrames = Math.max(0, silenceFrames - 2); // recupera mais rápido
}
// Atualizar indicator visual
const recordWaveform = document.getElementById('recordWaveform');
if (recordWaveform && recordWaveform.children[0]) {
const bars = recordWaveform.children;
bars.forEach((bar) => {
const h = Math.max(4, (avg / 255) * 50 + Math.random() * 10);
bar.style.height = `${h}px`;
});
}
// Se mais de 60% dos últimos 30 frames for silêncio, avisar
if (totalFrames > 30 && (silenceFrames / 30) > 0.6) {
const btnStopRecord = document.getElementById('btnStopRecord');
if (btnStopRecord && !btnStopRecord.disabled) {
btnStopRecord.style.borderColor = '#ff6b6b';
btnStopRecord.title = 'Silêncio detectado - Clique se quiser parar';
}
} else {
const btnStopRecord = document.getElementById('btnStopRecord');
if (btnStopRecord) {
btnStopRecord.style.borderColor = '';
btnStopRecord.title = '';
}
}
}, 100);
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.start(100); // chunk a cada 100ms
} catch (err) {
console.error('Erro ao acessar microfone:', err);
alert('Não foi possível acessar o microfone. Verifique as permissões.');
closeRecordModal();
}
});
}
// Atualizar timer
function updateTimer() {
if (!recordingStartTime) return;
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
const secs = (elapsed % 60).toString().padStart(2, '0');
recordTimer.textContent = `${mins}:${secs}`;
}
// Parar gravação e transcrever
if (btnStopRecord) {
btnStopRecord.addEventListener('click', async () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
try {
mediaRecorder.stop();
} catch (e) {}
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
}
clearInterval(timerInterval);
clearInterval(vadCheckInterval);
if (audioContext) { audioContext.close(); audioContext = null; }
if (analyser) { analyser = null; frequencyData = null; }
// Mostra processando com texto de transcrição
if (recordProcessingText) recordProcessingText.textContent = 'Transcrevendo áudio...';
if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Aguarde a transcrição por Whisper';
showRecordState('processing');
// Envia áudio para servidor
const blob = new Blob(audioChunks, { type: 'audio/webm' });
const formData = new FormData();
formData.append('audio', blob, 'recording.webm');
try {
const res = await fetch('/api/observacao/transcrever', {
method: 'POST',
body: formData
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro na transcrição');
}
const data = await res.json();
// Exibe o texto transcrito na área de texto para edição
if (transcriptionTextarea) {
transcriptionTextarea.value = data.transcribedText || '';
}
showRecordState('transcribed');
} catch (err) {
console.error('Erro ao processar observação:', err);
alert('Erro ao transcrever áudio: ' + err.message);
closeRecordModal();
}
});
}
// Ação de analisar transcrição
if (btnAnalyzeTranscription) {
btnAnalyzeTranscription.addEventListener('click', async () => {
const text = transcriptionTextarea ? transcriptionTextarea.value.trim() : '';
if (!text || text.length < 5) {
alert('Por favor, digite ou revise a transcrição (mínimo de 5 caracteres).');
return;
}
// Mostra processando com texto de análise
if (recordProcessingText) recordProcessingText.textContent = 'Analisando observação...';
if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Gerando relatório pedagógico estruturado';
showRecordState('processing');
try {
const res = await fetch('/api/observacao/analisar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transcribedText: text })
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro na análise');
}
const data = await res.json();
generatedReport = data.relatorio || data.report;
generatedCriancas = data.criancas;
generatedTurma = data.turma || 'não informada';
generatedTags = data.tags || [];
showReportPreview(data);
} catch (err) {
console.error('Erro ao analisar observação:', err);
alert('Erro ao analisar observação: ' + err.message);
showRecordState('transcribed');
}
});
}
// Ação de descartar transcrição
if (btnDiscardTranscription) {
btnDiscardTranscription.addEventListener('click', () => {
closeRecordModal();
});
}
// Mostrar prévia do relatório
function showReportPreview(data) {
showRecordState('preview');
const now = new Date();
previewDateTime.textContent = now.toLocaleString('pt-BR', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
// Monta HTML com metadados
const criancasHtml = data.criancas && data.criancas.length
? `👧 Crianças: ${data.criancas.join(', ')}
`
: `👧 Crianças: não identificadas
`;
const tagsHtml = data.tags && data.tags.length
? `🏷️ Tags: ${data.tags.map(t => `${t}`).join(' ')}
`
: '';
const turmaHtml = data.turma && data.turma !== 'não informada'
? `🏫 Turma: ${data.turma}
`
: '';
reportContent.innerHTML = `
${criancasHtml}${turmaHtml}${tagsHtml}
${marked.parse(generatedReport || 'Relatório não disponível.')}
`;
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
}
// Descartar relatório
if (btnDiscardReport) {
btnDiscardReport.addEventListener('click', () => {
generatedReport = null;
closeRecordModal();
});
}
// Salvar relatório
if (btnSaveReport) {
btnSaveReport.addEventListener('click', async () => {
if (!generatedReport) return;
btnSaveReport.disabled = true;
btnSaveReport.textContent = 'Salvando...';
try {
const res = await fetch('/api/observacao/salvar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
report: generatedReport,
criancas: generatedCriancas,
turma: generatedTurma,
tags: generatedTags
})
});
const data = await res.json();
if (data.success) {
alert('Relatório salvo com sucesso!');
} else {
alert('Erro ao salvar: ' + (data.error || 'desconhecido'));
}
} catch (err) {
console.error('Erro ao salvar:', err);
alert('Erro ao salvar o relatório.');
} finally {
btnSaveReport.disabled = false;
btnSaveReport.textContent = 'Salvar';
closeRecordModal();
}
});
}
// Sair / descartar
if (btnExitRecord) {
btnExitRecord.addEventListener('click', closeRecordModal);
}
// Fechar clicando fora
if (recordModal) {
recordModal.addEventListener('click', (e) => {
if (e.target === recordModal) closeRecordModal();
});
}
// ============================================================
// MODAL: MINHAS OBSERVAÇÕES
// ============================================================
const obsModal = document.getElementById('obsModal');
const btnObservacoes = document.getElementById('btnObservacoes');
const btnObsEspeciais = document.getElementById('btnObsEspeciais');
const btnCloseObsModal = document.getElementById('btnCloseObsModal');
const obsModalTitle = document.getElementById('obsModalTitle');
const obsList = document.getElementById('obsList');
const obsFilterMes = document.getElementById('obsFilterMes');
const obsFilterTag = document.getElementById('obsFilterTag');
const obsSearchCrianca = document.getElementById('obsSearchCrianca');
let allObservations = [];
let isSpecialObsMode = false;
// Abrir modal de observações
if (btnObservacoes) {
btnObservacoes.addEventListener('click', async () => {
isSpecialObsMode = false;
if (obsModalTitle) obsModalTitle.textContent = '📒 Minhas Observações';
obsModal.style.display = 'flex';
obsList.innerHTML = 'Carregando...
';
try {
const res = await fetch('/api/observacoes');
const data = await res.json();
allObservations = data.observations || [];
populateMonthFilter();
renderObsList();
} catch (err) {
console.error('Erro ao carregar observações:', err);
obsList.innerHTML = 'Erro ao carregar observações.
';
}
});
}
// Abrir modal de observações Especiais
if (btnObsEspeciais) {
btnObsEspeciais.addEventListener('click', async () => {
isSpecialObsMode = true;
if (obsModalTitle) obsModalTitle.textContent = '📒 Observações Educação Especial';
obsModal.style.display = 'flex';
obsList.innerHTML = 'Carregando...
';
try {
const res = await fetch('/api/observacoes');
const data = await res.json();
allObservations = data.observations || [];
populateMonthFilter();
renderObsList();
} catch (err) {
console.error('Erro ao carregar observações:', err);
obsList.innerHTML = 'Erro ao carregar observações.
';
}
});
}
// Fechar modal
if (btnCloseObsModal) {
btnCloseObsModal.addEventListener('click', () => {
obsModal.style.display = 'none';
});
}
if (obsModal) {
obsModal.addEventListener('click', (e) => {
if (e.target === obsModal) obsModal.style.display = 'none';
});
}
// Filtros
if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList);
if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList);
if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList);
// Preenche select de meses
function populateMonthFilter() {
if (!obsFilterMes) return;
const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))]
.filter(Boolean).sort().reverse();
obsFilterMes.innerHTML = '' +
months.map(m => ``).join('');
}
// Renderiza lista filtrada
function renderObsList() {
if (!obsList) return;
const mes = obsFilterMes?.value || '';
const tag = obsFilterTag?.value || '';
const search = obsSearchCrianca?.value.toLowerCase() || '';
let especialKidsNames = [];
if (isSpecialObsMode) {
const turma = JSON.parse(localStorage.getItem('camila_turma') || '[]');
especialKidsNames = turma.filter(c => c.especial).map(c => (c.nome || '').toLowerCase().trim()).filter(Boolean);
if (especialKidsNames.length === 0) {
obsList.innerHTML = 'Nenhuma criança com "Educação Especial" cadastrada na Minha Turma.
';
return;
}
}
const filtered = allObservations.filter(o => {
if (mes && !o.date?.startsWith(mes)) return false;
if (tag && !o.tags?.includes(tag)) return false;
const nomes = (o.criancas || []).join(' ').toLowerCase();
if (isSpecialObsMode) {
// Must contain at least one special kid's name
const preview = (o.preview || '').toLowerCase();
const isEspecial = especialKidsNames.some(ekn => nomes.includes(ekn) || preview.includes(ekn));
if (!isEspecial) return false;
}
if (search) {
if (!nomes.includes(search) && !(o.preview || '').toLowerCase().includes(search)) return false;
}
return true;
});
if (filtered.length === 0) {
obsList.innerHTML = 'Nenhuma observação encontrada.
';
return;
}
obsList.innerHTML = filtered.map(o => {
const tagsHtml = (o.tags || []).map(t => `${t}`).join('');
const criancasHtml = (o.criancas || []).map(c => `${c}`).join('');
const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : '';
return `
${o.preview || 'Sem prévia disponível.'}
`;
}).join('');
// Click para abrir observação
obsList.querySelectorAll('.obs-item').forEach(item => {
item.addEventListener('click', async () => {
const ym = item.dataset.yearmonth;
const fn = item.dataset.filename;
if (!ym || !fn) return;
try {
const res = await fetch(`/api/observacoes/${ym}/${fn}`);
const text = await res.text();
showObsDetail(text);
} catch (err) {
console.error('Erro ao carregar observação:', err);
}
});
});
};
// ============================================================
// MODAL: MINHA TURMA
// ============================================================
const minhaTurmaModal = document.getElementById('minhaTurmaModal');
const btnMinhaTurma = document.getElementById('btnMinhaTurma');
const btnCloseMinhaTurmaModal = document.getElementById('btnCloseMinhaTurmaModal');
const childrenList = document.getElementById('childrenList');
const btnCreateChild = document.getElementById('btnCreateChild');
const btnCancelChild = document.getElementById('btnCancelChild');
const btnSaveChild = document.getElementById('btnSaveChild');
const childFormTitle = document.getElementById('childFormTitle');
// Fields
const cFormId = document.getElementById('childFormId');
const cFormTurma = document.getElementById('childFormTurma');
const cFormSala = document.getElementById('childFormSala');
const cFormPeriodo = document.getElementById('childFormPeriodo');
const cFormAuxiliares = document.getElementById('childFormAuxiliares');
const cFormNome = document.getElementById('childFormNome');
const cFormApelido = document.getElementById('childFormApelido');
const cFormDataNasc = document.getElementById('childFormDataNasc');
const cFormEspecial = document.getElementById('childFormEspecial');
const divEspecialDetalhes = document.getElementById('divEspecialDetalhes');
const cFormEspecialDetalhes = document.getElementById('childFormEspecialDetalhes');
const cFormPais = document.getElementById('childFormPais');
const cFormAutorizados = document.getElementById('childFormAutorizados');
// Toggle Especial Fields
if (cFormEspecial) {
cFormEspecial.addEventListener('change', () => {
if (cFormEspecial.checked) {
divEspecialDetalhes.style.display = 'flex';
} else {
divEspecialDetalhes.style.display = 'none';
}
});
}
const loadTurma = () => {
return JSON.parse(localStorage.getItem('camila_turma') || '[]');
};
const saveTurma = (turma) => {
localStorage.setItem('camila_turma', JSON.stringify(turma));
};
const renderTurmaList = () => {
if (!childrenList) return;
const turma = loadTurma();
if (turma.length === 0) {
childrenList.innerHTML = 'Nenhuma criança cadastrada.
';
return;
}
// Sort by name
turma.sort((a, b) => (a.nome || '').localeCompare(b.nome || ''));
childrenList.innerHTML = turma.map(c => `
${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}
${escapeHtml(c.turma || '')} - ${escapeHtml(c.periodo || '')}
`).join('');
};
const clearChildForm = () => {
cFormId.value = '';
cFormTurma.value = '';
cFormSala.value = '';
cFormPeriodo.value = 'Integral';
cFormAuxiliares.value = '';
cFormNome.value = '';
cFormApelido.value = '';
cFormDataNasc.value = '';
cFormEspecial.checked = false;
cFormEspecialDetalhes.value = '';
divEspecialDetalhes.style.display = 'none';
cFormPais.value = '';
cFormAutorizados.value = '';
childFormTitle.textContent = 'Cadastrar Criança';
};
window.editChild = (id) => {
const turma = loadTurma();
const c = turma.find(x => x.id === id);
if (!c) return;
cFormId.value = c.id;
cFormTurma.value = c.turma || '';
cFormSala.value = c.sala || '';
cFormPeriodo.value = c.periodo || 'Integral';
cFormAuxiliares.value = c.auxiliares || '';
cFormNome.value = c.nome || '';
cFormApelido.value = c.apelido || '';
cFormDataNasc.value = c.dataNasc || '';
cFormEspecial.checked = !!c.especial;
cFormEspecialDetalhes.value = c.especialDetalhes || '';
divEspecialDetalhes.style.display = c.especial ? 'flex' : 'none';
cFormPais.value = c.pais || '';
cFormAutorizados.value = c.autorizados || '';
childFormTitle.textContent = 'Editar Criança';
};
window.deleteChild = async (id) => {
const isOk = await showCustomConfirm('Excluir', 'Deseja excluir esta criança? Isso não removerá as observações já feitas, apenas removerá da lista da turma.', true);
if (isOk) {
let turma = loadTurma();
turma = turma.filter(x => x.id !== id);
saveTurma(turma);
renderTurmaList();
clearChildForm();
}
};
if (btnMinhaTurma) {
btnMinhaTurma.addEventListener('click', () => {
minhaTurmaModal.style.display = 'flex';
renderTurmaList();
clearChildForm();
});
}
if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none');
if (minhaTurmaModal) {
minhaTurmaModal.addEventListener('click', (e) => {
if (e.target === minhaTurmaModal) minhaTurmaModal.style.display = 'none';
});
}
if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm);
if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm);
if (btnSaveChild) {
btnSaveChild.addEventListener('click', () => {
if (!cFormNome.value.trim()) {
showCustomAlert('Aviso', 'O Nome Completo é obrigatório.');
return;
}
let turma = loadTurma();
const id = cFormId.value || 'child_' + Date.now();
const newChild = {
id,
turma: cFormTurma.value.trim(),
sala: cFormSala.value.trim(),
periodo: cFormPeriodo.value,
auxiliares: cFormAuxiliares.value.trim(),
nome: cFormNome.value.trim(),
apelido: cFormApelido.value.trim(),
dataNasc: cFormDataNasc.value,
especial: cFormEspecial.checked,
especialDetalhes: cFormEspecialDetalhes.value.trim(),
pais: cFormPais.value.trim(),
autorizados: cFormAutorizados.value.trim()
};
const idx = turma.findIndex(x => x.id === id);
if (idx > -1) {
turma[idx] = newChild;
} else {
turma.push(newChild);
}
saveTurma(turma);
renderTurmaList();
clearChildForm();
});
}
// Preencher com Voz
const btnVoiceRecordChild = document.getElementById('btnVoiceRecordChild');
if (btnVoiceRecordChild && window.SpeechRecognition || window.webkitSpeechRecognition) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const formRecognition = new SpeechRecognition();
formRecognition.lang = 'pt-BR';
formRecognition.continuous = false;
formRecognition.interimResults = false;
formRecognition.onstart = () => {
btnVoiceRecordChild.innerHTML = '🛑 Gravando... (Clique para parar)';
btnVoiceRecordChild.style.background = 'rgba(239, 68, 68, 0.1)';
btnVoiceRecordChild.style.color = '#ef4444';
btnVoiceRecordChild.style.borderColor = '#ef4444';
};
formRecognition.onresult = async (event) => {
const transcript = event.results[0][0].transcript;
btnVoiceRecordChild.innerHTML = '⏳ Processando com IA...';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{
role: 'user',
content: `A professora ditou as seguintes informações para cadastrar uma criança na turma: "${transcript}".
Por favor, extraia os dados e retorne APENAS um JSON válido com os seguintes campos:
"turma", "sala", "periodo", "auxiliares", "nome", "apelido", "dataNasc" (formato YYYY-MM-DD), "especial" (boolean), "especialDetalhes", "pais", "autorizados".
Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
}],
temperature: 0.1
})
});
if (response.ok) {
const data = await response.json();
const assistantReply = data.message;
// Extract JSON block
const match = assistantReply.match(/```json\n([\s\S]*?)\n```/) || assistantReply.match(/\{[\s\S]*\}/);
if (match) {
const parsed = JSON.parse(match[1] || match[0]);
if (parsed.turma) cFormTurma.value = parsed.turma;
if (parsed.sala) cFormSala.value = parsed.sala;
if (parsed.periodo) cFormPeriodo.value = parsed.periodo;
if (parsed.auxiliares) cFormAuxiliares.value = parsed.auxiliares;
if (parsed.nome) cFormNome.value = parsed.nome;
if (parsed.apelido) cFormApelido.value = parsed.apelido;
if (parsed.dataNasc) cFormDataNasc.value = parsed.dataNasc;
if (parsed.especial) cFormEspecial.checked = parsed.especial;
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
if (parsed.especialDetalhes) cFormEspecialDetalhes.value = parsed.especialDetalhes;
if (parsed.pais) cFormPais.value = parsed.pais;
if (parsed.autorizados) cFormAutorizados.value = parsed.autorizados;
}
}
} catch (err) {
console.error('Erro ao processar voz:', err);
showCustomAlert('Erro', 'Não foi possível extrair os dados. Tente novamente ou preencha manualmente.');
}
};
formRecognition.onend = () => {
btnVoiceRecordChild.innerHTML = '🎤 Preencher com Voz';
btnVoiceRecordChild.style.background = 'rgba(16, 163, 127, 0.1)';
btnVoiceRecordChild.style.color = 'var(--brand-green)';
btnVoiceRecordChild.style.borderColor = 'var(--brand-green)';
};
btnVoiceRecordChild.addEventListener('click', () => {
if (btnVoiceRecordChild.innerHTML.includes('Gravando')) {
formRecognition.stop();
} else {
formRecognition.start();
}
});
}
// Mostra detalhe da observação em modal
const showObsDetail = (mdText) => {
const modal = document.createElement('div');
modal.className = 'settings-modal';
modal.style.display = 'flex';
modal.innerHTML = `
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
};
// ============================================================
// CRUD DE MODELOS DE RELATÓRIO
// ============================================================
const modelosRelatorioModal = document.getElementById('modelosRelatorioModal');
const btnModelosRelatorio = document.getElementById('btnModelosRelatorio');
const btnCloseModelosModal = document.getElementById('btnCloseModelosModal');
const templatesList = document.getElementById('templatesList');
const btnCreateTemplate = document.getElementById('btnCreateTemplate');
const btnCancelTemplate = document.getElementById('btnCancelTemplate');
const btnSaveTemplate = document.getElementById('btnSaveTemplate');
const templateFormId = document.getElementById('templateFormId');
const templateFormNome = document.getElementById('templateFormNome');
const templateFormFinalidade = document.getElementById('templateFormFinalidade');
const templateFormPeriodicidade = document.getElementById('templateFormPeriodicidade');
const templateFormEstrutura = document.getElementById('templateFormEstrutura');
const templateFormTitle = document.getElementById('templateFormTitle');
let allTemplates = [];
// Abrir modal de modelos
if (btnModelosRelatorio) {
btnModelosRelatorio.addEventListener('click', async () => {
modelosRelatorioModal.style.display = 'flex';
clearTemplateForm();
await fetchTemplates();
});
}
// Fechar modal de modelos
if (btnCloseModelosModal) {
btnCloseModelosModal.addEventListener('click', () => {
modelosRelatorioModal.style.display = 'none';
});
}
if (modelosRelatorioModal) {
modelosRelatorioModal.addEventListener('click', (e) => {
if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
});
}
// Buscar modelos do backend
async function fetchTemplates() {
templatesList.innerHTML = 'Carregando...
';
try {
const res = await fetch('/api/modelos');
allTemplates = await res.json();
renderTemplatesList();
} catch (err) {
console.error('Erro ao buscar modelos:', err);
templatesList.innerHTML = 'Erro ao carregar modelos.
';
}
}
// Renderizar a lista de modelos na esquerda
function renderTemplatesList() {
if (allTemplates.length === 0) {
templatesList.innerHTML = 'Nenhum modelo cadastrado.
';
return;
}
templatesList.innerHTML = allTemplates.map(t => `
${t.nome}
${t.periodicidade} • ${t.finalidade || 'Sem finalidade'}
`).join('');
// Eventos de clique na lista para editar/deletar/duplicar
templatesList.querySelectorAll('.obs-item').forEach(item => {
item.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') return;
const id = item.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) fillTemplateForm(template);
});
});
templatesList.querySelectorAll('.template-duplicate-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) {
fillTemplateForm(template);
templateFormId.value = '';
templateFormNome.value = template.nome + ' (Cópia)';
templateFormTitle.textContent = 'Duplicar Modelo';
if (templateFormNome) templateFormNome.focus();
}
});
});
templatesList.querySelectorAll('.template-edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) fillTemplateForm(template);
});
});
templatesList.querySelectorAll('.template-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (!template) return;
if (confirm(`Tem certeza que deseja excluir o modelo "${template.nome}"?`)) {
try {
const res = await fetch(`/api/modelos/${id}`, { method: 'DELETE' });
if (res.ok) {
alert('Modelo excluído com sucesso!');
clearTemplateForm();
await fetchTemplates();
} else {
const err = await res.json();
alert('Erro ao excluir: ' + err.error);
}
} catch (err) {
console.error('Erro ao excluir modelo:', err);
}
}
});
});
}
// Preencher formulário de edição
function fillTemplateForm(template) {
templateFormId.value = template.id;
templateFormNome.value = template.nome;
templateFormFinalidade.value = template.finalidade || '';
templateFormPeriodicidade.value = template.periodicidade || 'Semanal';
templateFormEstrutura.value = template.estrutura || '';
templateFormTitle.textContent = 'Editar Modelo';
}
// Limpar formulário
function clearTemplateForm() {
templateFormId.value = '';
templateFormNome.value = '';
templateFormFinalidade.value = '';
templateFormPeriodicidade.value = 'Semanal';
templateFormEstrutura.value = '';
templateFormTitle.textContent = 'Novo Modelo';
}
// Botão "Novo Modelo"
if (btnCreateTemplate) {
btnCreateTemplate.addEventListener('click', clearTemplateForm);
}
// Botão "Cancelar"
if (btnCancelTemplate) {
btnCancelTemplate.addEventListener('click', () => {
clearTemplateForm();
if (modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
});
}
// Botão "Salvar Modelo"
if (btnSaveTemplate) {
btnSaveTemplate.addEventListener('click', async () => {
const id = templateFormId.value;
const nome = templateFormNome.value.trim();
const finalidade = templateFormFinalidade.value.trim();
const periodicidade = templateFormPeriodicidade.value;
const estrutura = templateFormEstrutura.value.trim();
if (!nome || !estrutura) {
alert('Nome e estrutura do modelo são obrigatórios.');
return;
}
const body = { nome, finalidade, periodicidade, estrutura };
const url = id ? `/api/modelos/${id}` : '/api/modelos';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
alert('Modelo salvo com sucesso!');
clearTemplateForm();
await fetchTemplates();
} else {
const err = await res.json();
alert('Erro ao salvar modelo: ' + err.error);
}
} catch (err) {
console.error('Erro ao salvar modelo:', err);
}
});
}
// ============================================================
// GERAÇÃO / EMISSÃO DE RELATÓRIO PEDAGÓGICO
// ============================================================
const emitirRelatorioModal = document.getElementById('emitirRelatorioModal');
const btnEmitirRelatorio = document.getElementById('btnEmitirRelatorio');
const btnCloseEmitirModal = document.getElementById('btnCloseEmitirModal');
const emitirFilterAno = document.getElementById('emitirFilterAno');
const emitirFilterTurma = document.getElementById('emitirFilterTurma');
const emitirFilterCrianca = document.getElementById('emitirFilterCrianca');
const emitirFilterDataInicio = document.getElementById('emitirFilterDataInicio');
const emitirFilterDataFim = document.getElementById('emitirFilterDataFim');
const emitirTagsContainer = document.getElementById('emitirTagsContainer');
const emitirFilterTemplate = document.getElementById('emitirFilterTemplate');
let emitirSelectedTags = [];
const emitirObsCountInfo = document.getElementById('emitirObsCountInfo');
const btnGenerateCompiledReport = document.getElementById('btnGenerateCompiledReport');
const emitirReportPreviewArea = document.getElementById('emitirReportPreviewArea');
const btnExportDocx = document.getElementById('btnExportDocx');
const btnExportPdf = document.getElementById('btnExportPdf');
let activeCompiledReport = null; // Guarda o texto do relatório compilado ativo
// Abrir modal de emissão
if (btnEmitirRelatorio) {
btnEmitirRelatorio.addEventListener('click', async () => {
emitirRelatorioModal.style.display = 'flex';
activeCompiledReport = null;
emitirReportPreviewArea.innerHTML = `
Escolha uma criança e um modelo de relatório para começar.
`;
btnExportDocx.disabled = true;
btnExportDocx.style.cursor = 'not-allowed';
btnExportDocx.style.borderColor = 'var(--border-light)';
btnExportDocx.style.color = 'var(--text-secondary)';
btnExportPdf.disabled = true;
btnExportPdf.style.cursor = 'not-allowed';
btnExportPdf.style.borderColor = 'var(--border-light)';
btnExportPdf.style.color = 'var(--text-secondary)';
emitirFilterDataInicio.value = '';
emitirFilterDataFim.value = '';
emitirSelectedTags = [];
if (emitirTagsContainer) {
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(p => p.classList.remove('active'));
}
await loadFiltersData();
});
}
// Fechar modal de emissão
if (btnCloseEmitirModal) {
btnCloseEmitirModal.addEventListener('click', () => {
emitirRelatorioModal.style.display = 'none';
});
}
if (emitirRelatorioModal) {
emitirRelatorioModal.addEventListener('click', (e) => {
if (e.target === emitirRelatorioModal) emitirRelatorioModal.style.display = 'none';
});
}
// Buscar crianças com base nos filtros de Ano e Turma selecionados
async function loadChildrenList() {
try {
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
const activeCrianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
let url = '/api/observacoes/criancas';
const params = [];
if (activeAno) params.push(`ano=${activeAno}`);
if (activeTurma) params.push(`turma=${encodeURIComponent(activeTurma)}`);
if (params.length > 0) url += '?' + params.join('&');
const res = await fetch(url);
const criancas = await res.json();
emitirFilterCrianca.innerHTML = '' +
`` +
(Array.isArray(criancas) ? criancas : []).map(c => ``).join('');
} catch (err) {
console.error('Erro ao carregar lista de crianças:', err);
}
}
// Carregar dados de filtros (anos, turmas, crianças e templates)
async function loadFiltersData() {
try {
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
const res = await fetch('/api/observacoes/metadata');
const meta = await res.json();
if (emitirFilterAno) {
emitirFilterAno.innerHTML = '' +
(meta.anos || []).map(y => ``).join('');
}
if (emitirFilterTurma) {
emitirFilterTurma.innerHTML = '' +
(meta.turmas || []).map(t => ``).join('');
}
} catch (err) {
console.error('Erro ao buscar metadados de filtros:', err);
}
await loadChildrenList();
try {
const activeTemplate = emitirFilterTemplate ? emitirFilterTemplate.value : '';
const res = await fetch('/api/modelos');
const modelos = await res.json();
emitirFilterTemplate.innerHTML = '' +
(Array.isArray(modelos) ? modelos : []).map(m => ``).join('');
} catch (err) {
console.error('Erro ao buscar modelos para filtros:', err);
}
updateObservationsCount();
}
// Ouvintes para atualizar contagem e filtros de criança
if (emitirFilterAno) {
emitirFilterAno.addEventListener('change', async () => {
await loadChildrenList();
updateObservationsCount();
});
}
if (emitirFilterTurma) {
emitirFilterTurma.addEventListener('change', async () => {
await loadChildrenList();
updateObservationsCount();
});
}
[emitirFilterCrianca, emitirFilterDataInicio, emitirFilterDataFim, emitirFilterTemplate].forEach(el => {
if (el) el.addEventListener('change', updateObservationsCount);
});
if (emitirFilterCrianca) {
emitirFilterCrianca.addEventListener('input', updateObservationsCount);
}
// Adicionar ouvintes para as pills de tags
if (emitirTagsContainer) {
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(pill => {
pill.addEventListener('click', () => {
const tag = pill.getAttribute('data-tag');
if (pill.classList.contains('active')) {
pill.classList.remove('active');
emitirSelectedTags = emitirSelectedTags.filter(t => t !== tag);
} else {
pill.classList.add('active');
emitirSelectedTags.push(tag);
}
updateObservationsCount();
});
});
}
// Recalcular quantidade de observações elegíveis
async function updateObservationsCount() {
const crianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
const dataInicio = emitirFilterDataInicio ? emitirFilterDataInicio.value : '';
const dataFim = emitirFilterDataFim ? emitirFilterDataFim.value : '';
const tag = emitirSelectedTags.join(',');
const modeloId = emitirFilterTemplate ? emitirFilterTemplate.value : '';
const ano = emitirFilterAno ? emitirFilterAno.value : '';
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
if (!crianca || !modeloId) {
emitirObsCountInfo.textContent = 'Escolha uma criança (ou "Todas") e um modelo para prosseguir.';
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
btnGenerateCompiledReport.style.cursor = 'not-allowed';
return;
}
try {
let url = `/api/observacoes/contar?crianca=${encodeURIComponent(crianca)}`;
if (dataInicio) url += `&dataInicio=${dataInicio}`;
if (dataFim) url += `&dataFim=${dataFim}`;
if (tag) url += `&tags=${encodeURIComponent(tag)}`;
if (ano) url += `&ano=${ano}`;
if (turma) url += `&turma=${encodeURIComponent(turma)}`;
const res = await fetch(url);
const data = await res.json();
const count = data.count || 0;
if (count === 0) {
emitirObsCountInfo.innerHTML = `⚠️ Nenhuma observação encontrada para esses filtros.`;
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
btnGenerateCompiledReport.style.cursor = 'not-allowed';
} else {
emitirObsCountInfo.innerHTML = `✨ ${count} observações selecionadas para compilação.`;
btnGenerateCompiledReport.disabled = false;
btnGenerateCompiledReport.style.opacity = '1';
btnGenerateCompiledReport.style.cursor = 'pointer';
}
} catch (err) {
console.error('Erro ao contar observações:', err);
}
}
// Gerar relatório consolidado com IA
if (btnGenerateCompiledReport) {
btnGenerateCompiledReport.addEventListener('click', async () => {
const crianca = emitirFilterCrianca.value;
const dataInicio = emitirFilterDataInicio.value;
const dataFim = emitirFilterDataFim.value;
const tags = emitirSelectedTags;
const modeloId = emitirFilterTemplate.value;
const ano = emitirFilterAno ? emitirFilterAno.value : '';
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
if (!crianca || !modeloId) return;
emitirReportPreviewArea.innerHTML = `
Analisando observações e gerando relatório pedagógico...
`;
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
try {
const res = await fetch('/api/modelos/gerar-relatorio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
crianca,
dataInicio,
dataFim,
tags: tags.length > 0 ? tags : undefined,
modeloId,
ano,
turma
})
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro desconhecido');
}
const data = await res.json();
activeCompiledReport = data;
// Renderizar prévia do relatório em markdown
emitirReportPreviewArea.innerHTML = `
Estudante: ${data.crianca}
Período: ${data.periodo}
Modelo Aplicado: ${data.templateNome}
Quantidade de Observações Consolidadas: ${data.totalObservacoes}
${marked.parse(data.report)}
`;
// Ativar botões de exportação
btnExportDocx.disabled = false;
btnExportDocx.style.cursor = 'pointer';
btnExportDocx.style.borderColor = 'var(--brand-green)';
btnExportDocx.style.color = 'var(--brand-green)';
btnExportPdf.disabled = false;
btnExportPdf.style.cursor = 'pointer';
btnExportPdf.style.borderColor = 'var(--brand-green)';
btnExportPdf.style.color = 'var(--brand-green)';
// Rolar suavemente até o relatório no mobile
if (window.innerWidth <= 768) {
const modalBody = emitirRelatorioModal.querySelector('.settings-modal-body');
if (modalBody) {
setTimeout(() => {
modalBody.scrollTo({
top: modalBody.scrollHeight,
behavior: 'smooth'
});
}, 150);
}
}
} catch (err) {
console.error('Erro ao gerar relatório compilado:', err);
emitirReportPreviewArea.innerHTML = `
Erro ao gerar relatório: ${err.message}
`;
} finally {
btnGenerateCompiledReport.disabled = false;
btnGenerateCompiledReport.style.opacity = '1';
}
});
}
// Exportar para DOCX (Formato HTML-DOCX)
if (btnExportDocx) {
btnExportDocx.addEventListener('click', () => {
if (!activeCompiledReport) return;
const htmlContent = `
Relatório Pedagógico - ${activeCompiledReport.crianca}
${marked.parse(activeCompiledReport.report)}
`;
const blob = new Blob(['\ufeff' + htmlContent], { type: 'application/msword' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `relatorio_${activeCompiledReport.crianca.replace(/\s+/g, '_')}_${Date.now()}.doc`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
// Exportar para PDF (Abre janela de impressão do navegador)
if (btnExportPdf) {
btnExportPdf.addEventListener('click', () => {
if (!activeCompiledReport) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
Relatório Pedagógico - ${activeCompiledReport.crianca}
Relatório Pedagógico de Desenvolvimento
${marked.parse(activeCompiledReport.report)}
`);
printWindow.document.close();
});
}
// ============================================================
// ESTÚDIO MUSICAL (Criação de música cantada com MiniMax)
// ============================================================
const estudioMusicalModal = document.getElementById('estudioMusicalModal');
const btnCloseEstudioModal = document.getElementById('btnCloseEstudioModal');
const sidebarBtnEstudio = document.getElementById('sidebarBtnEstudio');
const barBtnEstudio = document.getElementById('barBtnEstudio');
const welcomeCardEstudio = document.getElementById('welcomeCardEstudio');
const btnGenerateStudioMusic = document.getElementById('btnGenerateStudioMusic');
const musicTemaInput = document.getElementById('musicTemaInput');
const musicGenerateLoader = document.getElementById('musicGenerateLoader');
const musicLoaderText = document.getElementById('musicLoaderText');
const musicResultArea = document.getElementById('musicResultArea');
const musicStudioAudioPlayer = document.getElementById('musicStudioAudioPlayer');
const musicStudioDownloadBtn = document.getElementById('musicStudioDownloadBtn');
const musicStudioLyricsArea = document.getElementById('musicStudioLyricsArea');
const btnCopyMusicLyrics = document.getElementById('btnCopyMusicLyrics');
const btnDownloadMusicLyrics = document.getElementById('btnDownloadMusicLyrics');
let selectedVoice = 'mulher';
let selectedRhythm = 'roda';
let selectedDuration = '30s';
// Toggle classes active para botões de opções de voz do Estúdio Musical
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedVoice = btn.dataset.voice;
});
});
document.querySelectorAll('.btn-music-rhythm').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-music-rhythm').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedRhythm = btn.dataset.rhythm;
});
});
document.querySelectorAll('.btn-music-duration').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-music-duration').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedDuration = btn.dataset.duration;
});
});
// Abrir e fechar modal
const openEstudioModal = () => {
estudioMusicalModal.style.display = 'flex';
musicTemaInput.value = '';
musicResultArea.style.display = 'none';
musicStudioAudioPlayer.src = '';
musicStudioLyricsArea.textContent = '';
if (musicGenerateLoader) musicGenerateLoader.style.display = 'none';
btnGenerateStudioMusic.disabled = false;
};
if (sidebarBtnEstudio) {
sidebarBtnEstudio.addEventListener('click', openEstudioModal);
}
if (barBtnEstudio) {
barBtnEstudio.addEventListener('click', openEstudioModal);
}
if (welcomeCardEstudio) {
welcomeCardEstudio.addEventListener('click', openEstudioModal);
}
if (btnCloseEstudioModal) {
btnCloseEstudioModal.addEventListener('click', () => {
estudioMusicalModal.style.display = 'none';
musicStudioAudioPlayer.pause();
});
}
// Fechar modal clicando fora
window.addEventListener('click', (e) => {
if (e.target === estudioMusicalModal) {
estudioMusicalModal.style.display = 'none';
musicStudioAudioPlayer.pause();
}
});
// Ação de gerar música no estúdio
if (btnGenerateStudioMusic) {
btnGenerateStudioMusic.addEventListener('click', async () => {
const tema = musicTemaInput.value.trim();
if (!tema) {
alert('Por favor, digite um tema para a música.');
return;
}
btnGenerateStudioMusic.disabled = true;
musicGenerateLoader.style.display = 'flex';
musicResultArea.style.display = 'none';
const statusInterval = setInterval(() => {
const statusMessages = [
'Compondo a letra pedagógica...',
'Ajustando a métrica e as rimas...',
'Gerando o arranjo musical...',
'Adicionando os vocais cantados...',
'Quase pronto, finalizando o áudio...'
];
const randomMsg = statusMessages[Math.floor(Math.random() * statusMessages.length)];
musicLoaderText.textContent = randomMsg;
}, 5000);
try {
const response = await fetch('/api/music/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tema,
voz: selectedVoice,
ritmo: selectedRhythm,
duracao: selectedDuration
})
});
clearInterval(statusInterval);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error || 'Erro na geração de áudio');
}
const data = await response.json();
// Exibir resultados
musicStudioLyricsArea.textContent = data.lyrics;
musicStudioAudioPlayer.src = data.audioUrl;
musicStudioDownloadBtn.href = data.audioUrl;
musicGenerateLoader.style.display = 'none';
musicResultArea.style.display = 'flex';
} catch (err) {
clearInterval(statusInterval);
console.error('Erro ao gerar música no estúdio:', err);
alert('Erro ao gerar música: ' + err.message);
musicGenerateLoader.style.display = 'none';
btnGenerateStudioMusic.disabled = false;
}
});
}
// Copiar letra
if (btnCopyMusicLyrics) {
btnCopyMusicLyrics.addEventListener('click', () => {
const lyrics = musicStudioLyricsArea.textContent;
if (lyrics) {
navigator.clipboard.writeText(lyrics)
.then(() => {
btnCopyMusicLyrics.textContent = '✅ Copiado!';
setTimeout(() => {
btnCopyMusicLyrics.textContent = '📋 Copiar Letra';
}, 2000);
})
.catch(err => {
console.error('Falha ao copiar:', err);
});
}
});
}
// Baixar letra em arquivo de texto (TXT)
if (btnDownloadMusicLyrics) {
btnDownloadMusicLyrics.addEventListener('click', () => {
const lyrics = musicStudioLyricsArea.textContent;
const tema = musicTemaInput.value.trim() || 'cantiga';
if (lyrics) {
const blob = new Blob([lyrics], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `letra_${tema.replace(/\s+/g, '_').toLowerCase()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
}
// =====================================
// HISTÓRICO DO ESTÚDIO MUSICAL
// =====================================
const btnOpenEstudioHistory = document.getElementById('btnOpenEstudioHistory');
const estudioHistoryModal = document.getElementById('estudioHistoryModal');
const btnCloseEstudioHistory = document.getElementById('btnCloseEstudioHistory');
const estudioHistoryList = document.getElementById('estudioHistoryList');
if (btnOpenEstudioHistory) {
btnOpenEstudioHistory.addEventListener('click', async () => {
estudioHistoryModal.style.display = 'flex';
estudioHistoryList.innerHTML = 'Carregando histórico...
';
try {
const res = await fetch('/api/music/list');
const data = await res.json();
estudioHistoryList.innerHTML = '';
if (data.length === 0) {
estudioHistoryList.innerHTML = 'Nenhuma canção salva ainda no estúdio.
';
return;
}
data.forEach(item => {
const div = document.createElement('div');
div.style.background = 'var(--bg-tertiary)';
div.style.padding = '12px 15px';
div.style.borderRadius = '8px';
div.style.display = 'flex';
div.style.justifyContent = 'space-between';
div.style.alignItems = 'center';
div.innerHTML = `
🎵 ${item.tema || 'Música'}
Voz: ${item.voz} | Ritmo: ${item.ritmo}
${new Date(item.created_at).toLocaleString()}
`;
estudioHistoryList.appendChild(div);
});
document.querySelectorAll('.btn-ver-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = e.target.dataset.id;
try {
const res = await fetch(`/api/music/${id}`);
const itemData = await res.json();
musicStudioLyricsArea.textContent = itemData.letra;
musicStudioAudioPlayer.src = itemData.audio_url;
musicStudioDownloadBtn.href = itemData.audio_url;
musicResultArea.style.display = 'flex';
estudioHistoryModal.style.display = 'none';
} catch (err) { alert('Erro ao carregar música do estúdio.'); }
});
});
document.querySelectorAll('.btn-del-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
if(!confirm('Tem certeza que deseja apagar esta canção?')) return;
const id = e.target.dataset.id;
try {
await fetch(`/api/music/${id}`, { method: 'DELETE' });
e.target.closest('div[style*="var(--bg-tertiary)"]').remove();
} catch (err) { alert('Erro ao deletar.'); }
});
});
} catch (err) {
estudioHistoryList.innerHTML = 'Erro ao carregar histórico.
';
}
});
}
if (btnCloseEstudioHistory) {
btnCloseEstudioHistory.addEventListener('click', () => {
estudioHistoryModal.style.display = 'none';
});
}
// ============================================================
// FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas)
// ============================================================
const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal');
const btnCloseComicsModal = document.getElementById('btnCloseComicsModal');
const barBtnComics = document.getElementById('barBtnComics');
const btnGenerateComics = document.getElementById('btnGenerateComics');
const comicsTemaInput = document.getElementById('comicsTemaInput');
const comicsGenerateLoader = document.getElementById('comicsGenerateLoader');
const comicsLoaderText = document.getElementById('comicsLoaderText');
const comicsLoaderProgress = document.getElementById('comicsLoaderProgress');
const comicsResultArea = document.getElementById('comicsResultArea');
const comicsResultTitle = document.getElementById('comicsResultTitle');
const comicsGridContainer = document.getElementById('comicsGridContainer');
const comicsCenarioSelect = document.getElementById('comicsCenarioSelect');
const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput');
// Export PDF / Vídeo
const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait');
const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape');
const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo');
const comicsVideoMusic = document.getElementById('comicsVideoMusic');
const comicsVideoDuration = document.getElementById('comicsVideoDuration');
const comicsVideoLoader = document.getElementById('comicsVideoLoader');
const comicsVideoResult = document.getElementById('comicsVideoResult');
const comicsVideoPlayer = document.getElementById('comicsVideoPlayer');
const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn');
const comicsProjectSelect = document.getElementById('comicsProjectSelect');
const btnNewComicsProject = document.getElementById('btnNewComicsProject');
const btnDeleteComicsProject = document.getElementById('btnDeleteComicsProject');
const comicsProjectTitleInput = document.getElementById('comicsProjectTitleInput');
const btnSaveComicsProject = document.getElementById('btnSaveComicsProject');
const btnSaveNewComicsProject = document.getElementById('btnSaveNewComicsProject');
let currentComicsProjectId = null;
let currentComicsCharDescEnglish = '';
let selectedComicsQty = 5;
let selectedComicsBubbles = true;
let selectedComicsRatio = '16:9';
let generatedPanelsData = [];
// Toggle ativo quantidade de quadros
document.querySelectorAll('.btn-comics-qty').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsQty = parseInt(btn.dataset.qty);
});
});
// Toggle ativo falas/diálogos
document.querySelectorAll('.btn-comics-bubbles').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsBubbles = btn.dataset.bubbles === 'true';
});
});
// Toggle ativo proporção visual
document.querySelectorAll('.btn-comics-ratio').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsRatio = btn.dataset.ratio;
});
});
// Gerenciador dinâmico de personagens
const comicsCharListContainer = document.getElementById('comicsCharListContainer');
const btnAddHumanChar = document.getElementById('btnAddHumanChar');
const btnAddAnimalChar = document.getElementById('btnAddAnimalChar');
const addCharacterRow = (type = '', name = '', isAnimal = false) => {
const row = document.createElement('div');
row.className = 'comic-char-row';
row.style.cssText = 'display: flex; gap: 8px; align-items: center; background: var(--bg-primary); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);';
const emoji = document.createElement('span');
emoji.style.fontSize = '1.1rem';
emoji.textContent = isAnimal ? '🐾' : '🧒';
row.appendChild(emoji);
const typeInput = document.createElement('input');
typeInput.type = 'text';
typeInput.className = 'obs-input char-type-input';
typeInput.placeholder = isAnimal ? 'Tipo (ex: Gatinho, Leão)' : 'Tipo (ex: Menino, Menina)';
typeInput.value = type;
typeInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
row.appendChild(typeInput);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'obs-input char-name-input';
nameInput.placeholder = 'Nome';
nameInput.value = name;
nameInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
row.appendChild(nameInput);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'btn-delete-char';
deleteBtn.innerHTML = '×';
deleteBtn.style.cssText = 'background: none; border: none; color: #ef4444; font-size: 1.3rem; cursor: pointer; padding: 0 4px; line-height: 1;';
deleteBtn.addEventListener('click', () => {
row.remove();
});
row.appendChild(deleteBtn);
comicsCharListContainer.appendChild(row);
};
if (btnAddHumanChar) {
btnAddHumanChar.addEventListener('click', () => {
addCharacterRow('', '', false);
});
}
if (btnAddAnimalChar) {
btnAddAnimalChar.addEventListener('click', () => {
addCharacterRow('', '', true);
});
}
// Exibir/ocultar cenário personalizado
if (comicsCenarioSelect) {
comicsCenarioSelect.addEventListener('change', () => {
if (comicsCenarioSelect.value === 'custom') {
comicsCenarioCustomInput.style.display = 'block';
} else {
comicsCenarioCustomInput.style.display = 'none';
}
});
}
// --- GERENCIADOR DE PROJETOS DE HQ ---
const startNewComicsProject = () => {
currentComicsProjectId = null;
currentComicsCharDescEnglish = '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'none';
const radioNew = document.querySelector('input[name="comicsGenMode"][value="new_story"]');
if (radioNew) radioNew.checked = true;
if (comicsProjectSelect) comicsProjectSelect.value = '';
if (comicsProjectTitleInput) comicsProjectTitleInput.value = '';
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'none';
comicsTemaInput.value = '';
comicsResultArea.style.display = 'none';
comicsGenerateLoader.style.display = 'none';
comicsVideoResult.style.display = 'none';
comicsVideoLoader.style.display = 'none';
btnGenerateComics.disabled = false;
if (comicsLoaderProgress) comicsLoaderProgress.style.width = '0%';
// Set active state for default quick options
selectedComicsQty = 5;
document.querySelectorAll('.btn-comics-qty').forEach(b => {
if (b.dataset.qty === '5') b.classList.add('active');
else b.classList.remove('active');
});
selectedComicsBubbles = true;
document.querySelectorAll('.btn-comics-bubbles').forEach(b => {
if (b.dataset.bubbles === 'true') b.classList.add('active');
else b.classList.remove('active');
});
selectedComicsRatio = '16:9';
document.querySelectorAll('.btn-comics-ratio').forEach(b => {
if (b.dataset.ratio === '16:9') b.classList.add('active');
else b.classList.remove('active');
});
if (comicsCenarioSelect) {
comicsCenarioSelect.value = 'no parque de diversões colorido';
if (comicsCenarioCustomInput) {
comicsCenarioCustomInput.style.display = 'none';
comicsCenarioCustomInput.value = '';
}
}
// Inicializar lista de personagens com padrão se estiver vazia
comicsCharListContainer.innerHTML = '';
addCharacterRow("Menino", "Lucas", false);
addCharacterRow("Menina", "Mariana", false);
generatedPanelsData = [];
comicsGridContainer.innerHTML = '';
};
const loadComicsProjectsList = async () => {
if (!comicsProjectSelect) return;
try {
const resp = await fetch('/api/comics/projects');
if (!resp.ok) throw new Error('Falha ao listar projetos');
const projects = await resp.json();
comicsProjectSelect.innerHTML = '';
projects.forEach(proj => {
const opt = document.createElement('option');
opt.value = proj.id;
opt.textContent = `${proj.titulo} (${new Date(proj.updated_at).toLocaleDateString('pt-BR')})`;
comicsProjectSelect.appendChild(opt);
});
if (currentComicsProjectId) {
comicsProjectSelect.value = currentComicsProjectId;
}
} catch (err) {
console.error('Erro ao listar projetos:', err);
}
};
const loadComicsProject = async (id) => {
if (!id) {
startNewComicsProject();
return;
}
try {
const resp = await fetch(`/api/comics/projects/${id}`);
if (!resp.ok) throw new Error('Falha ao carregar projeto');
const { project, panels } = await resp.json();
currentComicsProjectId = project.id;
currentComicsCharDescEnglish = project.character_description_english || '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'flex';
if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || '';
comicsTemaInput.value = project.tema || '';
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
// Carregar proporção
selectedComicsRatio = project.proporcao || '16:9';
document.querySelectorAll('.btn-comics-ratio').forEach(b => {
if (b.dataset.ratio === selectedComicsRatio) b.classList.add('active');
else b.classList.remove('active');
});
// Carregar cenário
if (comicsCenarioSelect) {
const standardOptions = ['no parque de diversões colorido', 'na sala de aula da escola infantil', 'em uma floresta encantada cheia de flores', 'no zoológico interagindo com animais', 'num jardim ensolarado de uma casa'];
if (standardOptions.includes(project.cenario)) {
comicsCenarioSelect.value = project.cenario;
if (comicsCenarioCustomInput) comicsCenarioCustomInput.style.display = 'none';
} else {
comicsCenarioSelect.value = 'custom';
if (comicsCenarioCustomInput) {
comicsCenarioCustomInput.style.display = 'block';
comicsCenarioCustomInput.value = project.cenario || '';
}
}
}
// Carregar quantidade e balões baseados nos painéis retornados
selectedComicsQty = panels.length || 5;
document.querySelectorAll('.btn-comics-qty').forEach(b => {
if (parseInt(b.dataset.qty) === selectedComicsQty) b.classList.add('active');
else b.classList.remove('active');
});
const hasDialogue = panels.some(p => p.dialogue);
selectedComicsBubbles = hasDialogue;
document.querySelectorAll('.btn-comics-bubbles').forEach(b => {
if ((b.dataset.bubbles === 'true') === selectedComicsBubbles) b.classList.add('active');
else b.classList.remove('active');
});
// Carregar personagens
comicsCharListContainer.innerHTML = '';
if (project.character_description_global) {
try {
const chars = JSON.parse(project.character_description_global);
if (Array.isArray(chars)) {
chars.forEach(c => {
addCharacterRow(c.type || '', c.name || '', c.isAnimal || false);
});
}
} catch (e) {
addCharacterRow("Menino", "Lucas", false);
addCharacterRow("Menina", "Mariana", false);
}
} else {
addCharacterRow("Menino", "Lucas", false);
addCharacterRow("Menina", "Mariana", false);
}
// Carregar os painéis
generatedPanelsData = panels.map(p => ({
panel_number: p.panel_number,
imageUrl: p.image_url,
dialogue: p.dialogue || '',
image_prompt: p.image_prompt || ''
}));
if (generatedPanelsData.length > 0) {
comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos';
comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach((panel, index) => {
const panelCard = document.createElement('div');
panelCard.className = 'comic-panel-card';
const imgContainer = document.createElement('div');
imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`;
const img = document.createElement('img');
img.className = 'comic-panel-image';
img.src = panel.imageUrl;
img.alt = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(img);
const badge = document.createElement('div');
badge.className = 'comic-panel-number-badge';
badge.textContent = panel.panel_number;
imgContainer.appendChild(badge);
panelCard.appendChild(imgContainer);
const caption = document.createElement('div');
caption.className = 'comic-caption-text';
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
const textarea = document.createElement('textarea');
textarea.className = 'comic-caption-input';
textarea.value = panel.dialogue || '';
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;';
textarea.addEventListener('input', (e) => {
panel.dialogue = e.target.value;
if (generatedPanelsData[index]) {
generatedPanelsData[index].dialogue = e.target.value;
}
});
caption.appendChild(textarea);
panelCard.appendChild(caption);
comicsGridContainer.appendChild(panelCard);
});
comicsResultArea.style.display = 'flex';
} else {
comicsResultArea.style.display = 'none';
}
comicsVideoResult.style.display = 'none';
comicsVideoLoader.style.display = 'none';
btnGenerateComics.disabled = false;
} catch (err) {
console.error(err);
await showCustomAlert('Erro', 'Erro ao carregar projeto: ' + err.message);
}
};
const saveComicsProjectFlow = async (forceNew = false) => {
const titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
if (!titulo) {
await showCustomAlert('Campo Obrigatório', 'Por favor, digite um título para salvar seu projeto.');
return;
}
const characters = [];
document.querySelectorAll('.comic-char-row').forEach(row => {
const typeInput = row.querySelector('.char-type-input');
const nameInput = row.querySelector('.char-name-input');
if (typeInput && nameInput) {
const type = typeInput.value.trim();
const name = nameInput.value.trim();
const isAnimal = row.querySelector('span').textContent === '🐾';
if (type || name) {
characters.push({ type, name, isAnimal });
}
}
});
let cenario = comicsCenarioSelect ? comicsCenarioSelect.value : 'no parque de diversões colorido';
if (cenario === 'custom' && comicsCenarioCustomInput) {
cenario = comicsCenarioCustomInput.value.trim();
}
const targetId = forceNew ? null : currentComicsProjectId;
const body = {
id: targetId,
titulo,
tema: comicsTemaInput.value.trim(),
cenario,
proporcao: selectedComicsRatio,
character_description_global: JSON.stringify(characters),
character_description_english: currentComicsCharDescEnglish || '',
panels: generatedPanelsData.map(p => ({
panel_number: p.panel_number,
image_url: p.imageUrl || p.image_url || '',
image_prompt: p.image_prompt || '',
dialogue: p.dialogue || ''
}))
};
try {
const resp = await fetch('/api/comics/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Erro ao salvar projeto');
}
const data = await resp.json();
currentComicsProjectId = data.id;
await showCustomAlert('Sucesso', 'Projeto salvo com sucesso!');
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
await loadComicsProjectsList();
} catch (err) {
console.error(err);
await showCustomAlert('Erro', 'Erro ao salvar projeto: ' + err.message);
}
};
const saveComicsProject = () => saveComicsProjectFlow(false);
const saveNewComicsProject = () => saveComicsProjectFlow(true);
const deleteComicsProject = async () => {
if (!currentComicsProjectId) return;
const confirmed = await showCustomConfirm(
'Confirmar Exclusão',
'Tem certeza de que deseja excluir este projeto permanentemente?',
true
);
if (!confirmed) return;
try {
const resp = await fetch(`/api/comics/projects/${currentComicsProjectId}`, {
method: 'DELETE'
});
if (!resp.ok) throw new Error('Falha ao excluir projeto');
await showCustomAlert('Excluído', 'Projeto excluído com sucesso!');
startNewComicsProject();
await loadComicsProjectsList();
} catch (err) {
console.error(err);
await showCustomAlert('Erro', 'Erro ao excluir projeto: ' + err.message);
}
};
// Abrir e fechar modal Fábrica de Quadrinhos
const openComicsModal = () => {
fabricaQuadrinhosModal.style.display = 'flex';
startNewComicsProject();
loadComicsProjectsList();
};
// Listeners do Gerenciador de Projetos
if (comicsProjectSelect) {
comicsProjectSelect.addEventListener('change', (e) => {
loadComicsProject(e.target.value);
});
}
if (btnNewComicsProject) {
btnNewComicsProject.addEventListener('click', async () => {
const confirmed = await showCustomConfirm(
'Novo Projeto',
'Deseja iniciar um novo projeto? Alterações não salvas serão perdidas.'
);
if (confirmed) {
startNewComicsProject();
}
});
}
if (btnSaveComicsProject) {
btnSaveComicsProject.addEventListener('click', saveComicsProject);
}
if (btnSaveNewComicsProject) {
btnSaveNewComicsProject.addEventListener('click', saveNewComicsProject);
}
if (btnDeleteComicsProject) {
btnDeleteComicsProject.addEventListener('click', deleteComicsProject);
}
if (barBtnComics) {
barBtnComics.addEventListener('click', openComicsModal);
}
if (btnCloseComicsModal) {
btnCloseComicsModal.addEventListener('click', () => {
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
});
}
// Fechar modal clicando fora
window.addEventListener('click', (e) => {
if (e.target === fabricaQuadrinhosModal) {
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
}
});
// Helper para converter URL de imagem local em base64
const getBase64Image = async (imgUrl) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
};
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
img.src = imgUrl;
});
};
// Ação de geração da história e imagens
if (btnGenerateComics) {
btnGenerateComics.addEventListener('click', async () => {
const tema = comicsTemaInput.value.trim();
if (!tema) {
await showCustomAlert('Campo Requerido', 'Por favor, digite um tema para a história.');
return;
}
// Reunir personagens dinamicamente
const personagens = [];
document.querySelectorAll('.comic-char-row').forEach(row => {
const type = row.querySelector('.char-type-input').value.trim();
const name = row.querySelector('.char-name-input').value.trim();
if (type && name) {
personagens.push(`${type} chamado(a) ${name}`);
}
});
if (personagens.length === 0) {
await showCustomAlert('Personagens Faltando', 'Adicione e preencha pelo menos um personagem principal.');
return;
}
// Definir cenário
let cenario = comicsCenarioSelect.value;
if (cenario === 'custom') {
cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito';
}
btnGenerateComics.disabled = true;
comicsGenerateLoader.style.display = 'flex';
comicsResultArea.style.display = 'none';
comicsVideoResult.style.display = 'none';
comicsLoaderProgress.style.width = '5%';
comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...';
try {
// 1. Gerar Roteiro
const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story';
const requestBody = {
tema,
quantidade: selectedComicsQty,
baloes: selectedComicsBubbles,
proporcao: selectedComicsRatio,
personagens,
cenario
};
if (currentComicsProjectId) {
requestBody.continuationType = genMode;
requestBody.character_description = currentComicsCharDescEnglish;
requestBody.existingPanels = generatedPanelsData.map(p => ({
panel_number: p.panel_number,
image_url: p.imageUrl || p.image_url || '',
image_prompt: p.image_prompt || '',
dialogue: p.dialogue || ''
}));
}
const scriptResp = await fetch('/api/comics/generate-script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
if (!scriptResp.ok) {
const err = await scriptResp.json();
throw new Error(err.error || 'Erro ao planejar roteiro');
}
const scriptData = await scriptResp.json();
comicsLoaderProgress.style.width = '15%';
if (scriptData.character_description) {
currentComicsCharDescEnglish = scriptData.character_description;
}
// 2. Gerar cada quadrinho sequencialmente
if (genMode !== 'extend') {
generatedPanelsData = [];
}
const totalPanels = scriptData.panels.length;
for (let i = 0; i < totalPanels; i++) {
const panel = scriptData.panels[i];
const progressPercent = 15 + Math.floor((i / totalPanels) * 80);
comicsLoaderProgress.style.width = `${progressPercent}%`;
comicsLoaderText.textContent = `Ilustrando quadrinho ${i + 1} de ${totalPanels}...`;
// Injetamos a proporção e estilo de forma reforçada no prompt
const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
const frameResp = await fetch('/api/comics/generate-frame', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: fullPrompt })
});
if (!frameResp.ok) {
const err = await frameResp.json();
throw new Error(`Erro no quadrinho ${i + 1}: ${err.error}`);
}
const frameData = await frameResp.json();
generatedPanelsData.push({
panel_number: panel.panel_number,
imageUrl: frameData.imageUrl,
dialogue: panel.dialogue || '',
image_prompt: panel.image_prompt || ''
});
}
// Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão)
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Camila';
comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach((panel, index) => {
const panelCard = document.createElement('div');
panelCard.className = 'comic-panel-card';
const imgContainer = document.createElement('div');
imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`;
const img = document.createElement('img');
img.className = 'comic-panel-image';
img.src = panel.imageUrl || panel.image_url;
img.alt = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(img);
// Selo do número
const badge = document.createElement('div');
badge.className = 'comic-panel-number-badge';
badge.textContent = panel.panel_number;
imgContainer.appendChild(badge);
panelCard.appendChild(imgContainer);
const caption = document.createElement('div');
caption.className = 'comic-caption-text';
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
const textarea = document.createElement('textarea');
textarea.className = 'comic-caption-input';
textarea.value = panel.dialogue || '';
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;';
textarea.addEventListener('input', (e) => {
panel.dialogue = e.target.value;
if (generatedPanelsData[index]) {
generatedPanelsData[index].dialogue = e.target.value;
}
});
caption.appendChild(textarea);
panelCard.appendChild(caption);
comicsGridContainer.appendChild(panelCard);
});
// Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar
if (genMode === 'new_story' && currentComicsProjectId) {
currentComicsProjectId = null;
if (comicsProjectSelect) comicsProjectSelect.value = '';
const modeContainer = document.getElementById('comicsGenerationModeContainer');
if (modeContainer) modeContainer.style.display = 'none';
if (comicsProjectTitleInput) {
comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)');
}
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
}
comicsGenerateLoader.style.display = 'none';
comicsResultArea.style.display = 'flex';
btnGenerateComics.disabled = false;
} catch (err) {
console.error('Erro ao fabricar quadrinhos:', err);
await showCustomAlert('Erro', 'Erro ao fabricar quadrinhos: ' + err.message);
comicsGenerateLoader.style.display = 'none';
btnGenerateComics.disabled = false;
}
});
}
// Exportar PDF Retrato (1 por página)
if (btnExportComicsPDFPortrait) {
btnExportComicsPDFPortrait.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnExportComicsPDFPortrait.disabled = true;
btnExportComicsPDFPortrait.textContent = '⏳ Montando PDF...';
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'pt', 'a4');
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
for (let i = 0; i < generatedPanelsData.length; i++) {
const panel = generatedPanelsData[i];
if (i > 0) doc.addPage();
// Título centralizado
doc.setFont('helvetica', 'bold');
doc.setFontSize(18);
doc.setTextColor(30, 41, 59);
doc.text(title, 297, 50, { align: 'center' });
// Imagem
const base64Img = await getBase64Image(panel.imageUrl);
const imgWidth = 515;
const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386;
doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight);
// Altura esticada para a caixa de diálogo (160pt de altura - aprox dobro)
const textHeight = 160;
const textY = 80 + imgHeight;
// Caixa de diálogo com fundo cinza claro e borda integrada
doc.setFillColor(248, 250, 252);
doc.rect(40, textY, imgWidth, textHeight, 'F');
// Moldura unificada ao redor de toda a imagem + texto
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(40, 80, imgWidth, imgHeight + textHeight, 'D');
// Linha divisória entre a imagem e a caixa de texto
doc.line(40, textY, 555, textY);
// Texto maior e mais legível (dobro do tamanho relativo - 15pt)
if (panel.dialogue) {
doc.setFont('helvetica', 'normal');
doc.setFontSize(15);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, imgWidth - 40);
doc.text(splitText, 60, textY + 35);
}
// Círculo discreto com o número do quadrinho no canto superior esquerdo da imagem
doc.setFillColor(254, 240, 138);
doc.circle(65, 105, 12, 'F');
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(1.5);
doc.circle(65, 105, 12, 'D');
doc.setFont('helvetica', 'bold');
doc.setFontSize(10);
doc.setTextColor(30, 41, 59);
doc.text(String(panel.panel_number), 65, 109, { align: 'center' });
// Rodapé
doc.setFont('helvetica', 'italic');
doc.setFontSize(9);
doc.setTextColor(148, 163, 184);
doc.text(`Criado com PedaGog - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 815, { align: 'center' });
}
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`);
} catch (error) {
console.error('Erro ao gerar PDF Retrato:', error);
alert('Erro ao gerar PDF: ' + error.message);
} finally {
btnExportComicsPDFPortrait.disabled = false;
btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)';
}
});
}
// Exportar PDF Paisagem (2 por página)
if (btnExportComicsPDFLandscape) {
btnExportComicsPDFLandscape.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnExportComicsPDFLandscape.disabled = true;
btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...';
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l', 'pt', 'a4');
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
const totalPanels = generatedPanelsData.length;
const totalPages = Math.ceil(totalPanels / 2);
for (let page = 0; page < totalPages; page++) {
if (page > 0) doc.addPage();
// Título centralizado no topo da página paisagem
doc.setFont('helvetica', 'bold');
doc.setFontSize(18);
doc.setTextColor(30, 41, 59);
doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' });
// Renderizar até dois quadrinhos lado a lado
for (let col = 0; col < 2; col++) {
const index = page * 2 + col;
if (index >= totalPanels) break;
const panel = generatedPanelsData[index];
const startX = col === 0 ? 40 : 426;
const cardWidth = 376;
// Proporções exatas mantidas
const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282;
const textHeight = 140; // Dobro de tamanho (antes era 65)
// Imagem do quadrinho
const base64Img = await getBase64Image(panel.imageUrl);
doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight);
// Fundo da caixa de texto
const textY = 70 + imgHeight;
doc.setFillColor(248, 250, 252);
doc.rect(startX, textY, cardWidth, textHeight, 'F');
// Moldura unificada ao redor do quadro (imagem + texto)
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(startX, 70, cardWidth, imgHeight + textHeight, 'D');
// Linha separando a imagem da caixa de texto
doc.line(startX, textY, startX + cardWidth, textY);
// Legenda formatada em tamanho maior (13pt)
if (panel.dialogue) {
doc.setFont('helvetica', 'normal');
doc.setFontSize(13);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 30);
doc.text(splitText, startX + 15, textY + 30);
}
// Círculo discreto com o numeral do quadro no canto superior esquerdo da imagem
doc.setFillColor(254, 240, 138);
doc.circle(startX + 20, 90, 11, 'F');
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(1.5);
doc.circle(startX + 20, 90, 11, 'D');
doc.setFont('helvetica', 'bold');
doc.setFontSize(9);
doc.setTextColor(30, 41, 59);
doc.text(String(panel.panel_number), startX + 20, 93, { align: 'center' });
}
}
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`);
} catch (error) {
console.error('Erro ao gerar PDF Paisagem:', error);
alert('Erro ao gerar PDF: ' + error.message);
} finally {
btnExportComicsPDFLandscape.disabled = false;
btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)';
}
});
}
// Compilar Vídeo de Apresentação (FFmpeg)
if (btnCompileComicsVideo) {
btnCompileComicsVideo.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnCompileComicsVideo.disabled = true;
comicsVideoLoader.style.display = 'flex';
comicsVideoResult.style.display = 'none';
try {
const response = await fetch('/api/comics/generate-video', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
frames: generatedPanelsData,
musicSelection: comicsVideoMusic.value,
frameDuration: comicsVideoDuration.value
})
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Erro na compilação do vídeo');
}
const data = await response.json();
comicsVideoPlayer.src = data.videoUrl;
comicsVideoDownloadBtn.href = data.videoUrl;
comicsVideoResult.style.display = 'flex';
comicsVideoLoader.style.display = 'none';
btnCompileComicsVideo.disabled = false;
} catch (err) {
console.error('Erro ao gerar vídeo dos quadrinhos:', err);
alert('Erro ao compilar o vídeo: ' + err.message);
comicsVideoLoader.style.display = 'none';
btnCompileComicsVideo.disabled = false;
}
});
}
// ============================================================
// VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO)
// ============================================================
const videoMindModal = document.getElementById('videoMindModal');
const btnCloseVideoMindModal = document.getElementById('btnCloseVideoMindModal');
const barBtnVideoMind = document.getElementById('barBtnVideoMind');
const btnGenerateVideoMind = document.getElementById('btnGenerateVideoMind');
const videoMindUrlInput = document.getElementById('videoMindUrlInput');
const videoMindLoader = document.getElementById('videoMindLoader');
const videoMindLoaderText = document.getElementById('videoMindLoaderText');
const videoMindResultArea = document.getElementById('videoMindResultArea');
const videoMindThumbnail = document.getElementById('videoMindThumbnail');
const videoMindTitle = document.getElementById('videoMindTitle');
const videoMindAuthor = document.getElementById('videoMindAuthor');
const videoMindReportArea = document.getElementById('videoMindReportArea');
const btnCopyVideoMindReport = document.getElementById('btnCopyVideoMindReport');
const btnDownloadVideoMindReport = document.getElementById('btnDownloadVideoMindReport');
const btnPdfVideoMindReport = document.getElementById('btnPdfVideoMindReport');
let selectedVideoMindLines = 20;
let activeVideoMindReport = null;
// Listener para selecionar o tamanho do resumo
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedVideoMindLines = parseInt(btn.dataset.lines) || 20;
});
});
// Abrir modal do VideoMind
if (barBtnVideoMind) {
barBtnVideoMind.addEventListener('click', () => {
videoMindModal.style.display = 'flex';
videoMindUrlInput.value = '';
videoMindResultArea.style.display = 'none';
videoMindReportArea.innerHTML = '';
if (videoMindLoader) videoMindLoader.style.display = 'none';
btnGenerateVideoMind.disabled = false;
activeVideoMindReport = null;
});
}
// Fechar modal
if (btnCloseVideoMindModal) {
btnCloseVideoMindModal.addEventListener('click', () => {
videoMindModal.style.display = 'none';
});
}
window.addEventListener('click', (e) => {
if (e.target === videoMindModal) {
videoMindModal.style.display = 'none';
}
});
// Ação de analisar vídeo
if (btnGenerateVideoMind) {
btnGenerateVideoMind.addEventListener('click', async () => {
const url = videoMindUrlInput.value.trim();
const pastedTranscriptInput = document.getElementById('videoMindPastedTranscriptInput');
const pastedTranscript = pastedTranscriptInput ? pastedTranscriptInput.value.trim() : '';
if (!url) {
alert('Por favor, insira o link de um vídeo do YouTube.');
return;
}
btnGenerateVideoMind.disabled = true;
videoMindLoader.style.display = 'flex';
videoMindResultArea.style.display = 'none';
videoMindLoaderText.textContent = 'Buscando metadados do vídeo...';
let statusMsgIdx = 0;
const statusMessages = [
'Buscando legenda automática do vídeo...',
'Carregando a transcrição completa...',
'Enviando transcrição para a IA...',
'Pedagoga IA analisando o conteúdo...',
'Alinhando temas do vídeo com a BNCC...',
'Gerando resumo de ' + selectedVideoMindLines + ' linhas...',
'Criando propostas de atividades pedagógicas...',
'Finalizando o parecer pedagógico...'
];
const statusInterval = setInterval(() => {
if (statusMsgIdx < statusMessages.length) {
videoMindLoaderText.textContent = statusMessages[statusMsgIdx];
statusMsgIdx++;
} else {
videoMindLoaderText.textContent = 'Ajustando formatação da análise...';
}
}, 4000);
try {
const response = await fetch('/api/videomind/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
lines: selectedVideoMindLines,
pastedTranscript
})
});
clearInterval(statusInterval);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error || 'Erro desconhecido');
}
const data = await response.json();
activeVideoMindReport = data;
// Preencher informações do vídeo
videoMindTitle.textContent = data.metadata.title;
videoMindAuthor.textContent = 'Canal: ' + data.metadata.author;
videoMindThumbnail.src = data.metadata.thumbnail;
// Renderizar Markdown
if (window.marked) {
videoMindReportArea.innerHTML = marked.parse(data.report);
} else {
videoMindReportArea.textContent = data.report;
}
videoMindLoader.style.display = 'none';
videoMindResultArea.style.display = 'flex';
} catch (err) {
clearInterval(statusInterval);
console.error('Erro no VideoMind:', err);
alert('Erro ao analisar vídeo: ' + err.message);
videoMindLoader.style.display = 'none';
btnGenerateVideoMind.disabled = false;
}
});
}
// Copiar Parecer
if (btnCopyVideoMindReport) {
btnCopyVideoMindReport.addEventListener('click', () => {
if (activeVideoMindReport && activeVideoMindReport.report) {
navigator.clipboard.writeText(activeVideoMindReport.report)
.then(() => {
btnCopyVideoMindReport.textContent = '✅ Copiado!';
setTimeout(() => {
btnCopyVideoMindReport.textContent = '📋 Copiar Parecer';
}, 2000);
})
.catch(err => {
console.error('Falha ao copiar:', err);
});
}
});
}
// Baixar relatório TXT
if (btnDownloadVideoMindReport) {
btnDownloadVideoMindReport.addEventListener('click', () => {
if (activeVideoMindReport && activeVideoMindReport.report) {
const title = activeVideoMindReport.metadata.title || 'analise_video';
const blob = new Blob([activeVideoMindReport.report], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `VideoMind_${title.replace(/\s+/g, '_').toLowerCase()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
}
// Baixar relatório PDF
if (btnPdfVideoMindReport) {
btnPdfVideoMindReport.addEventListener('click', () => {
if (!activeVideoMindReport) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
VideoMind - Parecer Pedagógico
🎬 VideoMind - Análise & Parecer Pedagógico
${window.marked ? marked.parse(activeVideoMindReport.report) : activeVideoMindReport.report}
`);
});
}
// ============================================================
// GARATUJAS (ANALISADOR DE TRAÇADO INFANTIL)
// ============================================================
const garatujasModal = document.getElementById('garatujasModal');
const btnCloseGaratujasModal = document.getElementById('btnCloseGaratujasModal');
const barBtnGaratujas = document.getElementById('barBtnGaratujas');
const tabGaratujasNova = document.getElementById('tabGaratujasNova');
const tabGaratujasHistorico = document.getElementById('tabGaratujasHistorico');
const contentGaratujasNova = document.getElementById('contentGaratujasNova');
const contentGaratujasHistorico = document.getElementById('contentGaratujasHistorico');
const garatujasAlunoInput = document.getElementById('garatujasAlunoInput');
const garatujasIdadeInput = document.getElementById('garatujasIdadeInput');
const garatujaFileInput = document.getElementById('garatujaFileInput');
const btnUploadGaratuja = document.getElementById('btnUploadGaratuja');
const garatujaFileStatus = document.getElementById('garatujaFileStatus');
const garatujaPreviewContainer = document.getElementById('garatujaPreviewContainer');
const garatujaImgPreview = document.getElementById('garatujaImgPreview');
const btnGenerateGaratuja = document.getElementById('btnGenerateGaratuja');
const garatujaLoader = document.getElementById('garatujaLoader');
const garatujaLoaderText = document.getElementById('garatujaLoaderText');
const garatujaResultArea = document.getElementById('garatujaResultArea');
const garatujaReportArea = document.getElementById('garatujaReportArea');
const btnCopyGaratujaReport = document.getElementById('btnCopyGaratujaReport');
const btnPdfGaratujaReport = document.getElementById('btnPdfGaratujaReport');
const garatujasListContainer = document.getElementById('garatujasListContainer');
const garatujasNoData = document.getElementById('garatujasNoData');
let activeGaratujaReport = null;
// Abas do Garatujas
if (tabGaratujasNova) {
tabGaratujasNova.addEventListener('click', () => {
tabGaratujasNova.classList.add('active');
tabGaratujasHistorico.classList.remove('active');
contentGaratujasNova.style.display = 'block';
contentGaratujasHistorico.style.display = 'none';
});
}
if (tabGaratujasHistorico) {
tabGaratujasHistorico.addEventListener('click', () => {
tabGaratujasHistorico.classList.add('active');
tabGaratujasNova.classList.remove('active');
contentGaratujasNova.style.display = 'none';
contentGaratujasHistorico.style.display = 'block';
loadGaratujasHistory();
});
}
// Abrir Modal
if (barBtnGaratujas) {
barBtnGaratujas.addEventListener('click', () => {
garatujasModal.style.display = 'flex';
garatujasAlunoInput.value = '';
garatujaFileInput.value = '';
garatujaFileStatus.textContent = 'Nenhum arquivo selecionado';
garatujaPreviewContainer.style.display = 'none';
garatujaResultArea.style.display = 'none';
garatujaLoader.style.display = 'none';
btnGenerateGaratuja.disabled = false;
activeGaratujaReport = null;
if (tabGaratujasNova) tabGaratujasNova.click();
});
}
// Fechar Modal
if (btnCloseGaratujasModal) {
btnCloseGaratujasModal.addEventListener('click', () => {
garatujasModal.style.display = 'none';
});
}
window.addEventListener('click', (e) => {
if (e.target === garatujasModal) {
garatujasModal.style.display = 'none';
}
});
// Upload trigger
if (btnUploadGaratuja) {
btnUploadGaratuja.addEventListener('click', () => {
garatujaFileInput.click();
});
}
if (garatujaFileInput) {
garatujaFileInput.addEventListener('change', () => {
const file = garatujaFileInput.files[0];
if (file) {
garatujaFileStatus.textContent = file.name;
const reader = new FileReader();
reader.onload = (e) => {
garatujaImgPreview.src = e.target.result;
garatujaPreviewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
garatujaFileStatus.textContent = 'Nenhum arquivo selecionado';
garatujaPreviewContainer.style.display = 'none';
}
});
}
// Gerar Análise
if (btnGenerateGaratuja) {
btnGenerateGaratuja.addEventListener('click', async () => {
const alunoName = garatujasAlunoInput.value.trim();
const alunoIdade = garatujasIdadeInput ? garatujasIdadeInput.value.trim() : '';
const file = garatujaFileInput.files[0];
if (!alunoName) {
alert('Por favor, informe o nome do aluno.');
return;
}
if (!file) {
alert('Por favor, selecione a foto do desenho.');
return;
}
btnGenerateGaratuja.disabled = true;
garatujaLoader.style.display = 'flex';
garatujaResultArea.style.display = 'none';
garatujaLoaderText.textContent = 'Enviando imagem pedagógica...';
const formData = new FormData();
formData.append('alunoName', alunoName);
if (alunoIdade) formData.append('alunoIdade', alunoIdade);
formData.append('image', file);
let statusMsgIdx = 0;
const statusMessages = [
'Carregando desenho infantil...',
'IA escaneando os traços e formas...',
'Analisando simetria e coordenação motora...',
'Definindo estágio evolutivo do desenho...',
'Criando sugestões de atividades formativas...',
'Finalizando laudo psicopedagógico...'
];
const statusInterval = setInterval(() => {
if (statusMsgIdx < statusMessages.length) {
garatujaLoaderText.textContent = statusMessages[statusMsgIdx];
statusMsgIdx++;
} else {
garatujaLoaderText.textContent = 'Formatando parecer...';
}
}, 4500);
try {
const response = await fetch('/api/garatujas/analyze', {
method: 'POST',
body: formData
});
clearInterval(statusInterval);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error || 'Erro desconhecido');
}
const data = await response.json();
activeGaratujaReport = data;
if (window.marked) {
garatujaReportArea.innerHTML = marked.parse(data.analise);
} else {
garatujaReportArea.textContent = data.analise;
}
garatujaLoader.style.display = 'none';
garatujaResultArea.style.display = 'flex';
btnGenerateGaratuja.disabled = false;
} catch (err) {
clearInterval(statusInterval);
console.error(err);
alert('Erro na análise: ' + err.message);
garatujaLoader.style.display = 'none';
btnGenerateGaratuja.disabled = false;
}
});
}
// Copiar Parecer Garatujas
if (btnCopyGaratujaReport) {
btnCopyGaratujaReport.addEventListener('click', () => {
if (activeGaratujaReport && activeGaratujaReport.analise) {
navigator.clipboard.writeText(activeGaratujaReport.analise)
.then(() => {
btnCopyGaratujaReport.textContent = '✅ Copiado!';
setTimeout(() => { btnCopyGaratujaReport.textContent = '📋 Copiar'; }, 2000);
});
}
});
}
// Baixar PDF Garatujas
if (btnPdfGaratujaReport) {
btnPdfGaratujaReport.addEventListener('click', () => {
if (!activeGaratujaReport) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
Garatujas - Laudo do Desenho
🎨 Garatujas - Parecer de Traçado Infantil
${window.marked ? marked.parse(activeGaratujaReport.analise) : activeGaratujaReport.analise}