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 turmaData = localStorage.getItem('pedagog_turma') || localStorage.getItem('camila_turma') || '[]';
const res = await fetch('/api/observacao/analisar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transcribedText: text, turmaContext: JSON.parse(turmaData) })
});
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);
}
// ============================================================
// 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';
});
}
// 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('pedagog_turma') || 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 (COM MÚLTIPLAS TURMAS E SUPABASE)
// ============================================================
const minhaTurmaModal = document.getElementById('minhaTurmaModal');
const btnMinhaTurma = document.getElementById('btnMinhaTurma');
const btnCloseMinhaTurmaModal = document.getElementById('btnCloseMinhaTurmaModal');
// Turmas List & Form
const turmasList = document.getElementById('turmasList');
const btnCreateTurma = document.getElementById('btnCreateTurma');
const btnCancelTurma = document.getElementById('btnCancelTurma');
const btnSaveTurmaConfig = document.getElementById('btnSaveTurmaConfig');
const configTurmaId = document.getElementById('configTurmaId');
const configTurmaNome = document.getElementById('configTurmaNome');
const configTurmaSala = document.getElementById('configTurmaSala');
const configTurmaPeriodo = document.getElementById('configTurmaPeriodo');
const configTurmaProfTitular = document.getElementById('configTurmaProfTitular');
const configTurmaProfAux1 = document.getElementById('configTurmaProfAux1');
const configTurmaProfAux2 = document.getElementById('configTurmaProfAux2');
const configTurmaCuidadora = document.getElementById('configTurmaCuidadora');
const turmaFormTitle = document.getElementById('turmaFormTitle');
// Alunos List & Form
const filterAlunosTurma = document.getElementById('filterAlunosTurma');
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');
const cFormId = document.getElementById('childFormId');
const cFormTurma = document.getElementById('childFormTurma');
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');
// Tabs
const tabTurmaConfig = document.getElementById('tabTurmaConfig');
const tabTurmaAlunos = document.getElementById('tabTurmaAlunos');
const turmaConfigSection = document.getElementById('turmaConfigSection');
const turmaAlunosSection = document.getElementById('turmaAlunosSection');
let currentTurmas = [];
let currentAlunos = [];
// API Calls
const fetchTurmas = async () => {
try {
const res = await fetch('/api/turmas');
currentTurmas = await res.json();
populateTurmaSelects();
return currentTurmas;
} catch(e) { console.error(e); return []; }
};
const fetchAlunos = async () => {
try {
const res = await fetch('/api/alunos');
currentAlunos = await res.json();
localStorage.setItem('pedagog_turma', JSON.stringify(currentAlunos));
return currentAlunos;
} catch(e) { console.error(e); return []; }
};
const populateTurmaSelects = () => {
const opts = '' + currentTurmas.map(t => ``).join('');
if(filterAlunosTurma) filterAlunosTurma.innerHTML = opts;
const formOpts = '' + currentTurmas.map(t => ``).join('');
if(cFormTurma) cFormTurma.innerHTML = formOpts;
if(document.getElementById('emitirFilterTurma')) {
document.getElementById('emitirFilterTurma').innerHTML = '' + currentTurmas.map(t => ``).join('');
}
};
// Tabs Logic
if (tabTurmaConfig) {
tabTurmaConfig.addEventListener('click', () => {
tabTurmaConfig.classList.add('active');
tabTurmaConfig.style.borderBottomColor = 'var(--brand-green)';
tabTurmaConfig.style.color = 'var(--brand-green)';
tabTurmaAlunos.classList.remove('active');
tabTurmaAlunos.style.borderBottomColor = 'transparent';
tabTurmaAlunos.style.color = 'var(--text-secondary)';
turmaConfigSection.style.display = 'flex';
turmaAlunosSection.style.display = 'none';
});
}
if (tabTurmaAlunos) {
tabTurmaAlunos.addEventListener('click', () => {
tabTurmaAlunos.classList.add('active');
tabTurmaAlunos.style.borderBottomColor = 'var(--brand-green)';
tabTurmaAlunos.style.color = 'var(--brand-green)';
tabTurmaConfig.classList.remove('active');
tabTurmaConfig.style.borderBottomColor = 'transparent';
tabTurmaConfig.style.color = 'var(--text-secondary)';
turmaAlunosSection.style.display = 'flex';
turmaConfigSection.style.display = 'none';
});
}
// Turmas Logic
const clearTurmaForm = () => {
configTurmaId.value = '';
configTurmaNome.value = '';
configTurmaSala.value = '';
configTurmaPeriodo.value = 'Integral';
configTurmaProfTitular.value = '';
configTurmaProfAux1.value = '';
configTurmaProfAux2.value = '';
configTurmaCuidadora.value = '';
turmaFormTitle.textContent = 'Cadastrar Turma';
};
const renderTurmasList = () => {
if(!turmasList) return;
if(currentTurmas.length === 0) {
turmasList.innerHTML = 'Nenhuma turma cadastrada.
';
return;
}
turmasList.innerHTML = currentTurmas.map(t => `
${escapeHtml(t.nome)}
${t.periodo || ''} ${t.sala ? '- ' + t.sala : ''}
`).join('');
};
window.editTurma = (id) => {
const t = currentTurmas.find(x => x.id === id);
if(!t) return;
configTurmaId.value = t.id;
configTurmaNome.value = t.nome || '';
configTurmaSala.value = t.sala || '';
configTurmaPeriodo.value = t.periodo || 'Integral';
configTurmaProfTitular.value = t.prof_titular || '';
configTurmaProfAux1.value = t.prof_aux1 || '';
configTurmaProfAux2.value = t.prof_aux2 || '';
configTurmaCuidadora.value = t.cuidadora || '';
turmaFormTitle.textContent = 'Editar Turma';
};
window.deleteTurma = async (id) => {
const isOk = await showCustomConfirm('Excluir Turma', 'Deseja excluir esta turma? Todos os alunos associados a ela também serão excluídos.', true);
if(isOk) {
await fetch('/api/turmas/' + id, { method: 'DELETE' });
await fetchTurmas();
await fetchAlunos();
renderTurmasList();
renderAlunosList();
clearTurmaForm();
}
};
if(btnCreateTurma) btnCreateTurma.addEventListener('click', clearTurmaForm);
if(btnCancelTurma) btnCancelTurma.addEventListener('click', clearTurmaForm);
if (btnSaveTurmaConfig) {
btnSaveTurmaConfig.addEventListener('click', async () => {
const nome = configTurmaNome.value.trim();
if(!nome) { showCustomAlert('Aviso', 'O Nome da Turma é obrigatório.'); return; }
const body = {
nome,
sala: configTurmaSala.value,
periodo: configTurmaPeriodo.value,
profTitular: configTurmaProfTitular.value,
profAux1: configTurmaProfAux1.value,
profAux2: configTurmaProfAux2.value,
cuidadora: configTurmaCuidadora.value
};
const id = configTurmaId.value;
const url = id ? '/api/turmas/' + id : '/api/turmas';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if(res.ok) {
showCustomAlert('Sucesso', 'Turma salva com sucesso!');
await fetchTurmas();
renderTurmasList();
clearTurmaForm();
} else {
showCustomAlert('Erro', 'Falha ao salvar turma.');
}
} catch(e) { console.error(e); }
});
}
// Alunos Logic
const clearChildForm = () => {
cFormId.value = '';
cFormTurma.value = filterAlunosTurma ? filterAlunosTurma.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';
};
if (cFormEspecial) {
cFormEspecial.addEventListener('change', () => {
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
});
}
const renderAlunosList = () => {
if (!childrenList) return;
const filterTurmaId = filterAlunosTurma ? filterAlunosTurma.value : '';
let filtered = currentAlunos;
if(filterTurmaId) {
filtered = currentAlunos.filter(a => a.turma_id === filterTurmaId);
}
if (filtered.length === 0) {
childrenList.innerHTML = 'Nenhuma criança encontrada.
';
return;
}
filtered.sort((a, b) => (a.nome || '').localeCompare(b.nome || ''));
childrenList.innerHTML = filtered.map(c => {
const turmaObj = currentTurmas.find(t => t.id === c.turma_id);
const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma';
return `
${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}
${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}
`}).join('');
};
if(filterAlunosTurma) {
filterAlunosTurma.addEventListener('change', () => {
renderAlunosList();
if(cFormTurma) cFormTurma.value = filterAlunosTurma.value;
});
}
window.editChild = (id) => {
const c = currentAlunos.find(x => x.id === id);
if (!c) return;
cFormId.value = c.id;
cFormTurma.value = c.turma_id || '';
cFormNome.value = c.nome || '';
cFormApelido.value = c.apelido || '';
if(c.data_nasc) {
cFormDataNasc.value = c.data_nasc.split('T')[0];
} else {
cFormDataNasc.value = '';
}
cFormEspecial.checked = !!c.especial;
cFormEspecialDetalhes.value = c.especial_detalhes || '';
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.', true);
if (isOk) {
await fetch('/api/alunos/' + id, { method: 'DELETE' });
await fetchAlunos();
renderAlunosList();
clearChildForm();
}
};
if (btnMinhaTurma) {
btnMinhaTurma.addEventListener('click', async () => {
minhaTurmaModal.style.display = 'flex';
await fetchTurmas();
await fetchAlunos();
renderTurmasList();
renderAlunosList();
clearTurmaForm();
clearChildForm();
});
}
if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none');
if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm);
if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm);
if (btnSaveChild) {
btnSaveChild.addEventListener('click', async () => {
if (!cFormNome.value.trim() || !cFormTurma.value) {
showCustomAlert('Aviso', 'O Nome da Criança e a Turma são obrigatórios.');
return;
}
const id = cFormId.value;
const body = {
turma_id: cFormTurma.value,
nome: cFormNome.value.trim(),
apelido: cFormApelido.value.trim(),
data_nasc: cFormDataNasc.value || null,
especial: cFormEspecial.checked,
especial_detalhes: cFormEspecialDetalhes.value.trim(),
pais: cFormPais.value.trim(),
autorizados: cFormAutorizados.value.trim()
};
const url = id ? '/api/alunos/' + id : '/api/alunos';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if(res.ok) {
await fetchAlunos();
renderAlunosList();
clearChildForm();
showCustomAlert('Sucesso', 'Criança salva com sucesso!');
} else {
showCustomAlert('Erro', 'Falha ao salvar criança.');
}
} catch(e) { console.error(e); }
});
}
// 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:
"nome", "apelido", "data_nasc" (formato YYYY-MM-DD), "especial" (boolean), "especial_detalhes", "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;
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.nome) cFormNome.value = parsed.nome;
if (parsed.apelido) cFormApelido.value = parsed.apelido;
if (parsed.data_nasc) cFormDataNasc.value = parsed.data_nasc;
if (parsed.especial) cFormEspecial.checked = parsed.especial;
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
if (parsed.especial_detalhes) cFormEspecialDetalhes.value = parsed.especial_detalhes;
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';
});
}
// 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';
});
}
// 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();
});
}
// 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.currentTarget.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.currentTarget.dataset.id;
try {
await fetch(`/api/music/${id}`, { method: 'DELETE' });
e.currentTarget.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);
};
const btnImportTurmaChars = document.getElementById('btnImportTurmaChars');
if (btnAddHumanChar) {
btnAddHumanChar.addEventListener('click', () => {
addCharacterRow('', '', false);
});
}
if (btnAddAnimalChar) {
btnAddAnimalChar.addEventListener('click', () => {
addCharacterRow('', '', true);
});
}
if (btnImportTurmaChars) {
btnImportTurmaChars.addEventListener('click', async () => {
try {
btnImportTurmaChars.disabled = true;
btnImportTurmaChars.textContent = '⏳ Carregando...';
const res = await fetch('/api/alunos');
if (!res.ok) throw new Error('Falha ao obter lista de alunos');
const alunos = await res.json();
if (!alunos || alunos.length === 0) {
await showCustomAlert('Nenhum Aluno', 'Cadastre alunos na seção "Minha Turma" para importá-los automaticamente.');
return;
}
// Adiciona os alunos como personagens (até 4 para manter a história focada)
const selecionados = alunos.slice(0, 4);
comicsCharListContainer.innerHTML = '';
selecionados.forEach(aluno => {
const generoProvavel = aluno.nome.trim().endsWith('a') ? 'Menina' : 'Menino';
addCharacterRow(generoProvavel, aluno.nome.trim(), false);
});
await showCustomAlert('Turma Importada!', `${selecionados.length} alunos foram adicionados como personagens da história.`);
} catch (err) {
console.error('Erro ao importar alunos:', err);
await showCustomAlert('Erro', 'Não foi possível carregar a lista de alunos: ' + err.message);
} finally {
btnImportTurmaChars.disabled = false;
btnImportTurmaChars.textContent = '+ 🏫 Importar da Turma';
}
});
}
// 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) {
console.warn('Projetos de quadrinhos temporariamente indisponíveis:', resp.status);
return;
}
const projects = await resp.json();
if (!Array.isArray(projects)) return;
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.warn('Aviso ao listar projetos:', err.message);
}
};
const loadComicsProject = async (id) => {
if (!id) {
startNewComicsProject();
return;
}
try {
const resp = await fetch(`/api/comics/projects/${id}`);
if (!resp.ok) {
const errMsg = await safeExtractError(resp, 'Falha ao carregar projeto');
throw new Error(errMsg);
}
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) {
if (comicsResultTitle) comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos';
renderComicsStoryboard();
if (comicsResultArea) comicsResultArea.style.display = 'flex';
}
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) => {
let titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
let tema = comicsTemaInput ? comicsTemaInput.value.trim() : '';
if (!titulo) {
if (comicsResultTitle && comicsResultTitle.textContent.trim() && comicsResultTitle.textContent.trim() !== 'Resultado da História em Quadrinhos') {
titulo = comicsResultTitle.textContent.trim();
} else if (tema) {
titulo = tema;
} else {
titulo = 'Minha História em Quadrinhos';
}
if (comicsProjectTitleInput) comicsProjectTitleInput.value = titulo;
}
if (!tema) {
tema = titulo;
}
const characters = [];
document.querySelectorAll('.comic-char-row').forEach(row => {
const typeInput = row.querySelector('.char-type-input');
const nameInput = row.querySelector('.char-name-input');
const animalToggle = row.querySelector('.comic-char-animal-toggle');
const type = typeInput ? typeInput.value.trim() : '';
const name = nameInput ? nameInput.value.trim() : '';
const isAnimal = animalToggle ? animalToggle.classList.contains('active') : false;
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 activeBtn = forceNew ? btnSaveNewComicsProject : btnSaveComicsProject;
const originalText = activeBtn ? activeBtn.textContent : '';
if (activeBtn) {
activeBtn.disabled = true;
activeBtn.textContent = '⏳ Salvando...';
}
const body = {
id: targetId,
titulo,
tema,
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 errMsg = await safeExtractError(resp, 'Erro ao salvar projeto');
throw new Error(errMsg);
}
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);
} finally {
if (activeBtn) {
activeBtn.disabled = false;
activeBtn.textContent = originalText;
}
}
};
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) {
const errMsg = await safeExtractError(resp, 'Falha ao excluir projeto');
throw new Error(errMsg);
}
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', async () => {
if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') {
const confirmed = await showCustomConfirm(
'Geração em Andamento',
'Sua história ainda está sendo gerada. Deseja realmente sair e cancelar a criação?'
);
if (!confirmed) return;
}
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
if (typeof stopComicsMusicPreview === 'function') stopComicsMusicPreview();
});
}
// Prevenir fechamento acidental ao clicar fora da janela do modal
window.addEventListener('click', (e) => {
if (e.target === fabricaQuadrinhosModal) {
if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') {
showCustomAlert('Aviso', 'Sua história em quadrinhos está sendo gerada. Por favor, aguarde a conclusão.');
}
// O modal permanece aberto para proteger o trabalho da professora contra cliques acidentais fora da janela
}
});
// Helper para converter URL de imagem local em base64 e obter dimensões reais sem deformar
const getBase64ImageDetails = async (imgUrl) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => {
const width = img.naturalWidth || img.width || 1280;
const height = img.naturalHeight || img.height || 720;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve({
dataUrl: canvas.toDataURL('image/jpeg'),
width: width,
height: height,
aspectRatio: width / height
});
};
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
img.src = imgUrl;
});
};
const getBase64Image = async (imgUrl) => {
const details = await getBase64ImageDetails(imgUrl);
return details.dataUrl;
};
// Helper para execução concorrente controlada
const mapConcurrent = async (items, limit, fn) => {
const results = new Array(items.length);
let currentIdx = 0;
const worker = async () => {
while (currentIdx < items.length) {
const i = currentIdx++;
results[i] = await fn(items[i], i);
}
};
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
await Promise.all(workers);
return results;
};
// 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 errMsg = await safeExtractError(scriptResp, 'Erro ao planejar roteiro');
throw new Error(errMsg);
}
const scriptData = await scriptResp.json();
comicsLoaderProgress.style.width = '20%';
if (scriptData.character_description) {
currentComicsCharDescEnglish = scriptData.character_description;
}
// 2. Gerar quadrinhos com concorrência balanceada (2 por vez) e tracking em tempo real
if (genMode !== 'extend') {
generatedPanelsData = [];
}
const totalPanels = scriptData.panels.length;
let completedCount = 0;
const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
const generateSinglePanel = async (panel) => {
let fullPrompt = panel.image_prompt || '';
if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
}
fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
let attempts = 0;
let frameData = null;
while (attempts < 3 && !frameData) {
attempts++;
try {
const frameResp = await fetch('/api/comics/generate-frame', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio })
});
if (frameResp.ok) {
frameData = await frameResp.json();
} else {
console.warn(`[Retry Comics] Tentativa ${attempts} para quadro ${panel.panel_number} respondeu ${frameResp.status}`);
if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts));
}
} catch (netErr) {
console.warn(`[Retry Comics] Tentativa ${attempts} erro de rede no quadro ${panel.panel_number}:`, netErr.message);
if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts));
}
}
completedCount++;
const progressPercent = 20 + Math.floor((completedCount / totalPanels) * 75);
if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`;
if (comicsLoaderText) comicsLoaderText.textContent = `🎨 Ilustrando quadrinhos: ${completedCount}/${totalPanels} quadros concluídos...`;
// Pequeno respiro entre gerações para respeitar a taxa da API
await new Promise(r => setTimeout(r, 600));
if (frameData && frameData.imageUrl) {
return {
panel_number: panel.panel_number,
imageUrl: frameData.imageUrl,
dialogue: panel.dialogue || '',
image_prompt: panel.image_prompt || '',
needsRegeneration: !!frameData.needsRegeneration
};
} else {
console.warn(`[Comics Resiliencia] Quadro ${panel.panel_number} em espera. Preservando a história.`);
return {
panel_number: panel.panel_number,
imageUrl: '/assets/img/placeholder_comic.png',
dialogue: panel.dialogue || '',
image_prompt: panel.image_prompt || '',
needsRegeneration: true
};
}
};
// Executa com 1 quadro por vez sequencial para estabilidade absoluta e sem estourar rate limit
const newPanels = await mapConcurrent(scriptData.panels, 1, generateSinglePanel);
// Ordena por panel_number para garantir a sequência correta
newPanels.sort((a, b) => a.panel_number - b.panel_number);
if (genMode === 'extend') {
generatedPanelsData = generatedPanelsData.concat(newPanels);
} else {
generatedPanelsData = newPanels;
}
// Atualizar cabeçalho e metadados
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Pedagog';
// Renderizar Storyboard Interativo
renderComicsStoryboard();
// 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;
// Salvamento automático imediato pós-geração para garantir que a história NUNCA se perca
try {
await saveComicsProjectFlow(false);
await showCustomAlert('🎉 História Gerada e Salva!', 'Sua história em quadrinhos foi concluída e salva automaticamente em "Meus Projetos"!');
} catch (autoSaveErr) {
console.warn('Falha no auto-salvamento pós geração:', autoSaveErr.message);
}
} 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;
}
});
}
// --- RENDERIZADOR DO STORYBOARD PROFISSIONAL DOS QUADRINHOS ---
const renderComicsStoryboard = () => {
if (!comicsGridContainer) return;
comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach((panel, index) => {
const panelCard = document.createElement('div');
panelCard.className = 'comic-panel-card';
panelCard.dataset.index = index;
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}`;
img.loading = 'lazy';
imgContainer.appendChild(img);
// Selo de número do quadrinho
const badge = document.createElement('div');
badge.className = 'comic-panel-number-badge';
badge.textContent = panel.panel_number;
imgContainer.appendChild(badge);
// Barra de Ações Rápidas por Quadro (Mobile & Desktop)
const actionsOverlay = document.createElement('div');
actionsOverlay.className = 'comic-panel-overlay-actions';
// Botão 🔍 Zoom
const btnZoom = document.createElement('button');
btnZoom.type = 'button';
btnZoom.className = 'btn-comic-action';
btnZoom.title = 'Ampliar imagem';
btnZoom.innerHTML = '🔍';
btnZoom.addEventListener('click', (e) => {
e.stopPropagation();
openComicsPresentation(index);
});
actionsOverlay.appendChild(btnZoom);
// Botão 🔄 Regenerar Imagem deste Quadro
const btnRegen = document.createElement('button');
btnRegen.type = 'button';
btnRegen.className = 'btn-comic-action';
btnRegen.title = 'Regenerar apenas este quadrinho';
btnRegen.innerHTML = '🔄';
btnRegen.addEventListener('click', async (e) => {
e.stopPropagation();
await regenerateSinglePanel(index, panelCard, img, btnRegen);
});
actionsOverlay.appendChild(btnRegen);
// Botão ✏️ Ver/Editar Prompt
const btnPromptToggle = document.createElement('button');
btnPromptToggle.type = 'button';
btnPromptToggle.className = 'btn-comic-action';
btnPromptToggle.title = 'Editar prompt visual';
btnPromptToggle.innerHTML = '✏️';
actionsOverlay.appendChild(btnPromptToggle);
// Botão 💾 Baixar Imagem Individual
const btnDownloadImg = document.createElement('button');
btnDownloadImg.type = 'button';
btnDownloadImg.className = 'btn-comic-action';
btnDownloadImg.title = 'Baixar esta imagem PNG';
btnDownloadImg.innerHTML = '💾';
btnDownloadImg.addEventListener('click', (e) => {
e.stopPropagation();
const a = document.createElement('a');
a.href = panel.imageUrl || panel.image_url;
a.download = `quadrinho_${panel.panel_number}.png`;
document.body.appendChild(a);
a.click();
a.remove();
});
actionsOverlay.appendChild(btnDownloadImg);
imgContainer.appendChild(actionsOverlay);
panelCard.appendChild(imgContainer);
// Área de Legenda / Diálogo
const caption = document.createElement('div');
caption.className = 'comic-caption-text';
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: 48px;';
textarea.addEventListener('input', (e) => {
panel.dialogue = e.target.value;
if (generatedPanelsData[index]) {
generatedPanelsData[index].dialogue = e.target.value;
}
});
caption.appendChild(textarea);
// Mini-Editor de Prompt Visual (Oculto por padrão, ativado no botão ✏️)
const promptBox = document.createElement('div');
promptBox.className = 'comic-prompt-details';
const promptInput = document.createElement('textarea');
promptInput.style.cssText = 'width: 100%; font-size: 0.72rem; padding: 4px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-light); border-radius: 4px; resize: vertical; min-height: 40px; box-sizing: border-box;';
promptInput.value = panel.image_prompt || '';
promptInput.addEventListener('input', (e) => {
panel.image_prompt = e.target.value;
});
promptBox.appendChild(document.createTextNode('Prompt Visual: '));
promptBox.appendChild(promptInput);
caption.appendChild(promptBox);
btnPromptToggle.addEventListener('click', (e) => {
e.stopPropagation();
promptBox.style.display = promptBox.style.display === 'block' ? 'none' : 'block';
});
panelCard.appendChild(caption);
comicsGridContainer.appendChild(panelCard);
});
};
// Regenerar um único painel inteligente (considera textos/legendas alterados e preserva âncora visual de personagens)
const regenerateSinglePanel = async (index, panelCard, imgElement, btnRegen) => {
const panel = generatedPanelsData[index];
if (!panel) return;
try {
btnRegen.disabled = true;
btnRegen.textContent = '⏳';
imgElement.style.opacity = '0.4';
// Captura o diálogo/texto ATUAL que está escrito na textarea do quadrinho (caso o usuário tenha editado)
const textarea = panelCard.querySelector('.comic-caption-input');
const currentDialogue = textarea ? textarea.value.trim() : (panel.dialogue || '');
panel.dialogue = currentDialogue;
// Captura o prompt visual se o mini-editor tiver sido alterado
const promptInput = panelCard.querySelector('.comic-prompt-details textarea');
let promptToUse = promptInput ? promptInput.value.trim() : (panel.image_prompt || '');
// Se o diálogo tiver sido preenchido/alterado, re-sincronizamos a cena visual com o novo texto
if (currentDialogue) {
try {
const resyncResp = await fetch('/api/comics/resync-frame-prompt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentDialogue,
oldPrompt: promptToUse,
character_description: currentComicsCharDescEnglish,
cenario: comicsCenarioSelect ? comicsCenarioSelect.value : ''
})
});
if (resyncResp.ok) {
const resyncData = await resyncResp.json();
if (resyncData.image_prompt) {
promptToUse = resyncData.image_prompt;
panel.image_prompt = promptToUse;
if (promptInput) promptInput.value = promptToUse;
}
}
} catch (resyncErr) {
console.warn('Re-sincronização de prompt ao regenerar:', resyncErr.message);
}
}
const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
let fullPrompt = promptToUse || '';
if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
}
fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
const resp = await fetch('/api/comics/generate-frame', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio })
});
if (!resp.ok) {
const errMsg = await safeExtractError(resp, 'Erro na regeneração');
throw new Error(errMsg);
}
const data = await resp.json();
panel.imageUrl = data.imageUrl;
imgElement.src = data.imageUrl + '?v=' + Date.now();
imgElement.style.opacity = '1';
// Auto-salva o projeto atualizado no banco
saveComicsProjectFlow(false).catch(e => console.warn('Auto-save pós regeneração:', e.message));
} catch (err) {
console.error('Erro ao regenerar painel:', err);
await showCustomAlert('Erro', 'Não foi possível regenerar o quadrinho: ' + err.message);
imgElement.style.opacity = '1';
} finally {
btnRegen.disabled = false;
btnRegen.innerHTML = '🔄';
}
};
// --- EXPORTAR TODAS AS IMAGENS EM ZIP ---
const btnExportComicsZIP = document.getElementById('btnExportComicsZIP');
if (btnExportComicsZIP) {
btnExportComicsZIP.addEventListener('click', async () => {
if (!generatedPanelsData || generatedPanelsData.length === 0) {
await showCustomAlert('Aviso', 'Nenhum quadrinho gerado para exportar.');
return;
}
try {
btnExportComicsZIP.disabled = true;
btnExportComicsZIP.textContent = '⏳ Criando ZIP...';
if (typeof JSZip === 'undefined') {
throw new Error('Biblioteca JSZip não carregada.');
}
const zip = new JSZip();
const folder = zip.folder('quadrinhos');
const title = (comicsResultTitle?.textContent || 'Historia_Quadrinhos').replace(/[^a-zA-Z0-9_-]/g, '_');
let roteiroTxt = `HISTÓRIA EM QUADRINHOS: ${comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'}\n`;
roteiroTxt += `Criado com PedagogIA em ${new Date().toLocaleDateString('pt-BR')}\n\n`;
for (let i = 0; i < generatedPanelsData.length; i++) {
const panel = generatedPanelsData[i];
const imgUrl = panel.imageUrl || panel.image_url;
roteiroTxt += `[QUADRO ${panel.panel_number}]\nFala/Legenda: ${panel.dialogue || '(Sem fala)'}\nPrompt Visual: ${panel.image_prompt || ''}\n\n`;
try {
const resp = await fetch(imgUrl);
const blob = await resp.blob();
folder.file(`quadrinho_${panel.panel_number}.jpg`, blob);
} catch (fetchErr) {
console.warn(`Erro ao incluir imagem ${panel.panel_number} no ZIP:`, fetchErr);
}
}
folder.file('roteiro.txt', roteiroTxt);
const content = await zip.generateAsync({ type: 'blob' });
const downloadUrl = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `${title}_pedagog.zip`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(downloadUrl);
} catch (err) {
console.error('Erro ao gerar ZIP:', err);
await showCustomAlert('Erro', 'Não foi possível gerar o ZIP: ' + err.message);
} finally {
btnExportComicsZIP.disabled = false;
btnExportComicsZIP.innerHTML = '📦 Baixar Todas as Imagens (ZIP)';
}
});
}
// --- MODO APRESENTAÇÃO EM TELA CHEIA (LEITURA INFANTIL / MOBILE) ---
const comicsPresModal = document.getElementById('comicsPresentationModal');
const comicsPresTitle = document.getElementById('comicsPresTitle');
const comicsPresImage = document.getElementById('comicsPresImage');
const comicsPresCaption = document.getElementById('comicsPresCaption');
const comicsPresCounter = document.getElementById('comicsPresCounter');
const btnPresPrev = document.getElementById('btnPresPrev');
const btnPresNext = document.getElementById('btnPresNext');
const btnCloseComicsPresModal = document.getElementById('btnCloseComicsPresModal');
const btnComicsPresentationMode = document.getElementById('btnComicsPresentationMode');
let presCurrentIndex = 0;
const openComicsPresentation = (index = 0) => {
if (!generatedPanelsData || generatedPanelsData.length === 0) return;
presCurrentIndex = Math.max(0, Math.min(index, generatedPanelsData.length - 1));
if (comicsPresModal) comicsPresModal.style.display = 'flex';
updateComicsPresentationView();
};
const closeComicsPresentation = () => {
if (comicsPresModal) comicsPresModal.style.display = 'none';
};
const updateComicsPresentationView = () => {
if (!generatedPanelsData || generatedPanelsData.length === 0) return;
const panel = generatedPanelsData[presCurrentIndex];
if (!panel) return;
if (comicsPresTitle) comicsPresTitle.textContent = comicsResultTitle?.textContent || 'Fábrica de Quadrinhos';
if (comicsPresCounter) comicsPresCounter.textContent = `${presCurrentIndex + 1} / ${generatedPanelsData.length}`;
if (comicsPresImage) comicsPresImage.src = panel.imageUrl || panel.image_url;
if (comicsPresCaption) comicsPresCaption.textContent = panel.dialogue || '(História silenciosa)';
if (btnPresPrev) btnPresPrev.disabled = presCurrentIndex === 0;
if (btnPresNext) btnPresNext.disabled = presCurrentIndex === generatedPanelsData.length - 1;
};
if (btnComicsPresentationMode) {
btnComicsPresentationMode.addEventListener('click', () => {
openComicsPresentation(0);
});
}
if (btnPresPrev) {
btnPresPrev.addEventListener('click', () => {
if (presCurrentIndex > 0) {
presCurrentIndex--;
updateComicsPresentationView();
}
});
}
if (btnPresNext) {
btnPresNext.addEventListener('click', () => {
if (presCurrentIndex < generatedPanelsData.length - 1) {
presCurrentIndex++;
updateComicsPresentationView();
}
});
}
if (btnCloseComicsPresModal) {
btnCloseComicsPresModal.addEventListener('click', closeComicsPresentation);
}
// Teclado (setas) para o modo apresentação
window.addEventListener('keydown', (e) => {
if (comicsPresModal && comicsPresModal.style.display === 'flex') {
if (e.key === 'ArrowRight' || e.key === 'Space') {
if (presCurrentIndex < generatedPanelsData.length - 1) {
presCurrentIndex++;
updateComicsPresentationView();
}
} else if (e.key === 'ArrowLeft') {
if (presCurrentIndex > 0) {
presCurrentIndex--;
updateComicsPresentationView();
}
} else if (e.key === 'Escape') {
closeComicsPresentation();
}
}
});
// Touch Swipe para smartphones no modo apresentação
let touchStartX = 0;
if (comicsPresModal) {
comicsPresModal.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
comicsPresModal.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].screenX;
const diff = touchEndX - touchStartX;
if (Math.abs(diff) > 50) {
if (diff < 0 && presCurrentIndex < generatedPanelsData.length - 1) {
// Swipe para a esquerda -> Próximo
presCurrentIndex++;
updateComicsPresentationView();
} else if (diff > 0 && presCurrentIndex > 0) {
// Swipe para a direita -> Anterior
presCurrentIndex--;
updateComicsPresentationView();
}
}
}, { passive: true });
}
// 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, 45, { align: 'center' });
const imgDetails = await getBase64ImageDetails(panel.imageUrl);
const boxWidth = 515;
const boxMaxHeight = 350;
const textHeight = 150;
// Cálculo exato mantendo a proporção real da imagem (sem esticar)
let renderW = boxWidth;
let renderH = renderW / imgDetails.aspectRatio;
if (renderH > boxMaxHeight) {
renderH = boxMaxHeight;
renderW = renderH * imgDetails.aspectRatio;
}
const imgX = 40 + (boxWidth - renderW) / 2;
const imgY = 70 + (boxMaxHeight - renderH) / 2;
const cardY = 70;
const textY = cardY + boxMaxHeight;
// Desenha a imagem centralizada e proporcional
doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH);
// Fundo da caixa de texto
doc.setFillColor(248, 250, 252);
doc.rect(40, textY, boxWidth, textHeight, 'F');
// Moldura unificada ao redor de todo o quadro
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(40, cardY, boxWidth, boxMaxHeight + textHeight, 'D');
// Linha divisória entre imagem e texto
doc.line(40, textY, 555, textY);
// Texto legível
if (panel.dialogue) {
doc.setFont('helvetica', 'normal');
doc.setFontSize(15);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 40);
doc.text(splitText, 60, textY + 35);
}
// Numeração do quadrinho
doc.setFillColor(254, 240, 138);
doc.circle(65, 95, 12, 'F');
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(1.5);
doc.circle(65, 95, 12, 'D');
doc.setFont('helvetica', 'bold');
doc.setFontSize(10);
doc.setTextColor(30, 41, 59);
doc.text(String(panel.panel_number), 65, 99, { 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
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 boxWidth = 376;
const boxMaxHeight = 230;
const textHeight = 140;
const imgDetails = await getBase64ImageDetails(panel.imageUrl);
// Cálculo rigoroso da dimensão sem deformar (preserva aspect ratio real)
let renderW = boxWidth;
let renderH = renderW / imgDetails.aspectRatio;
if (renderH > boxMaxHeight) {
renderH = boxMaxHeight;
renderW = renderH * imgDetails.aspectRatio;
}
const imgX = startX + (boxWidth - renderW) / 2;
const imgY = 70 + (boxMaxHeight - renderH) / 2;
const cardY = 70;
const textY = cardY + boxMaxHeight;
// Imagem do quadrinho centralizada e mantendo proporções exatas
doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH);
// Fundo da caixa de texto
doc.setFillColor(248, 250, 252);
doc.rect(startX, textY, boxWidth, textHeight, 'F');
// Moldura unificada ao redor do quadro (imagem + texto)
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(startX, cardY, boxWidth, boxMaxHeight + textHeight, 'D');
// Linha separadora entre imagem e caixa de texto
doc.line(startX, textY, startX + boxWidth, textY);
// Legenda formatada
if (panel.dialogue) {
doc.setFont('helvetica', 'normal');
doc.setFontSize(13);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 30);
doc.text(splitText, startX + 15, textY + 30);
}
// Numeral do quadro
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 com Narração IA)
if (btnCompileComicsVideo) {
btnCompileComicsVideo.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
stopComicsMusicPreview();
btnCompileComicsVideo.disabled = true;
comicsVideoLoader.style.display = 'flex';
comicsVideoResult.style.display = 'none';
const voice = document.getElementById('comicsVideoVoice')?.value || 'mulher';
try {
const response = await fetch('/api/comics/generate-video', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
frames: generatedPanelsData,
musicSelection: comicsVideoMusic ? comicsVideoMusic.value : 'alegre',
frameDuration: comicsVideoDuration ? comicsVideoDuration.value : 5,
narrationVoice: voice,
titulo: comicsResultTitle?.textContent || 'História em Quadrinhos'
})
});
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;
}
});
}
// --- PREVIEW INTERATIVO DE TRILHAS SONORAS (PLAY/STOP) ---
let comicsPreviewAudio = null;
const btnPreviewComicsMusic = document.getElementById('btnPreviewComicsMusic');
const comicsMusicPreviewIcon = document.getElementById('comicsMusicPreviewIcon');
const comicsMusicPreviewText = document.getElementById('comicsMusicPreviewText');
const comicsMusicPlayingTag = document.getElementById('comicsMusicPlayingTag');
function stopComicsMusicPreview() {
if (comicsPreviewAudio) {
comicsPreviewAudio.pause();
comicsPreviewAudio.currentTime = 0;
comicsPreviewAudio = null;
}
if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '▶️';
if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Ouvir';
if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'none';
if (btnPreviewComicsMusic) {
btnPreviewComicsMusic.style.background = 'rgba(219, 39, 119, 0.1)';
btnPreviewComicsMusic.style.borderColor = 'var(--brand-pink)';
btnPreviewComicsMusic.style.color = 'var(--brand-pink)';
}
}
if (btnPreviewComicsMusic && comicsVideoMusic) {
btnPreviewComicsMusic.addEventListener('click', () => {
const selectedTrack = comicsVideoMusic.value;
if (selectedTrack === 'sem_musica') {
showCustomAlert('Sem Trilha', 'A opção "Sem Música de Fundo" está selecionada.');
return;
}
if (comicsPreviewAudio && !comicsPreviewAudio.paused) {
stopComicsMusicPreview();
return;
}
stopComicsMusicPreview();
const audioUrl = `/assets/audio/${selectedTrack}.mp3`;
comicsPreviewAudio = new Audio(audioUrl);
comicsPreviewAudio.volume = 0.55;
comicsPreviewAudio.play().then(() => {
if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '⏹️';
if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Parar';
if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'inline';
if (btnPreviewComicsMusic) {
btnPreviewComicsMusic.style.background = 'rgba(16, 185, 129, 0.15)';
btnPreviewComicsMusic.style.borderColor = '#10b981';
btnPreviewComicsMusic.style.color = '#10b981';
}
}).catch(err => {
console.warn('Erro ao reproduzir prévia de áudio:', err.message);
stopComicsMusicPreview();
showCustomAlert('Áudio', 'Não foi possível carregar a prévia desta trilha.');
});
comicsPreviewAudio.onended = () => {
stopComicsMusicPreview();
};
});
comicsVideoMusic.addEventListener('change', () => {
stopComicsMusicPreview();
});
}
// ============================================================
// 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';
});
}
// 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';
});
}
// 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}