Ajustes de UI: barra de progresso da IA e toggle de ferramentas no mobile

This commit is contained in:
2026-06-05 12:38:42 +00:00
parent 9d16105666
commit 68ad4907da
3 changed files with 251 additions and 12 deletions
+131 -12
View File
@@ -1087,6 +1087,17 @@ const initApp = () => {
fileInput.click();
});
// Botão alternador de ferramentas (recursos)
const btnToggleTools = document.getElementById('btnToggleTools');
const promptModesBar = document.querySelector('.prompt-modes-bar');
if (btnToggleTools && promptModesBar) {
btnToggleTools.addEventListener('click', () => {
promptModesBar.classList.toggle('show');
btnToggleTools.classList.toggle('active');
});
}
// Quando arquivos são selecionados
fileInput.addEventListener('change', () => {
const files = Array.from(fileInput.files);
@@ -1780,12 +1791,50 @@ const initApp = () => {
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;
@@ -1886,6 +1935,46 @@ const initApp = () => {
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 = `
<div class="ia-progress-container" id="iaProgressContainer">
<div class="ia-progress-header">
<div class="ia-progress-icon"></div>
<div class="ia-progress-step-text" id="iaProgressStepText">${progressSteps[0].text}</div>
</div>
<div class="ia-progress-bar">
<div class="ia-progress-bar-fill" id="iaProgressBarFill" style="width: ${progressSteps[0].progress}%;"></div>
</div>
</div>
`;
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('btnToggleTools');
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 => {
@@ -1939,17 +2028,13 @@ const initApp = () => {
if (parsed.content) {
assistantContent += parsed.content;
// Renderizar markdown em progresso (com o cursor no final)
botContentDiv.innerHTML = marked.parse(assistantContent) + '<span class="typing-cursor"></span>';
// Colorir códigos em tempo real
botContentDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
scrollToBottom();
// 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 = `
<div class="media-loading">
<div class="media-spinner"></div>
@@ -1959,6 +2044,10 @@ const initApp = () => {
scrollToBottom();
} else if (parsed.type === 'image') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
mediaMsgData = { type: 'image', url: parsed.url, originalPrompt: parsed.originalPrompt };
assistantContent = `
<div class="media-message-card">
@@ -1980,6 +2069,10 @@ const initApp = () => {
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 = `
@@ -2016,6 +2109,10 @@ const initApp = () => {
scrollToBottom();
} else if (parsed.type === 'video') {
isMediaResponse = true;
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
mediaMsgData = { type: 'video', url: parsed.url, originalPrompt: parsed.originalPrompt };
assistantContent = `
<div class="media-message-card">
@@ -2047,8 +2144,22 @@ const initApp = () => {
const cursor = botContentDiv.querySelector('.typing-cursor');
if (cursor) cursor.remove();
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
// 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);
@@ -2127,10 +2238,14 @@ const initApp = () => {
} 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 += `
botContentDiv.innerHTML = `
<p style="color: var(--error); margin-top: 8px; font-size: 13.5px; display: flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 16px; height: 16px;">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
@@ -2157,6 +2272,10 @@ const initApp = () => {
// 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 (btnToggleTools) btnToggleTools.classList.remove('active');
}
if (btn.dataset.prompt) {
handleSendMessage(btn.dataset.prompt);
return;