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;
+6
View File
@@ -255,6 +255,12 @@
</svg>
</button>
<input type="file" id="fileInput" accept="image/*,.pdf,.doc,.docx,.txt" multiple style="display:none">
<button type="button" id="btnToggleTools" class="btn-toggle-tools" aria-label="Alternar ferramentas" title="Mostrar/Ocultar ferramentas">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.098.04.197.09.294.13.38.197.839.226 1.22.062l.85-.36a1.125 1.125 0 011.37.49l.547.947a1.125 1.125 0 01-.26 1.43l-.708.583a1.125 1.125 0 00-.37.99c.004.12.006.242.006.364 0 .121-.002.242-.006.363a1.125 1.125 0 00.37.99l.708.582a1.125 1.125 0 01.26 1.43l-.547.947a1.125 1.125 0 01-1.37.491l-.85-.36a1.125 1.125 0 00-1.22.063c-.097.04-.196.09-.294.13a1.125 1.125 0 00-.78.93l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.02-.398-1.11-.94l-.148-.894a1.125 1.125 0 00-.78-.93c-.098-.04-.197-.09-.294-.13a1.125 1.125 0 00-1.22-.062l-.85.36a1.125 1.125 0 01-1.37-.49l-.546-.947a1.125 1.125 0 01.26-1.43l.707-.583a1.125 1.125 0 00.37-.99c-.003-.121-.005-.242-.005-.363 0-.122.002-.243.005-.364a1.125 1.125 0 00-.37-.99l-.707-.582a1.125 1.125 0 01-.26-1.43l.546-.947a1.125 1.125 0 011.37-.491l.85.36c.38.164.84.135 1.22-.063.097-.04.196-.09.294-.13.396-.166.71-.506.78-.93l.148-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<textarea id="userInput" placeholder="Mensagem Camila AI..." rows="1" autocomplete="off"></textarea>
<button type="button" id="btnVoice" class="btn-voice" aria-label="Usar microfone" title="Ditar mensagem">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke="currentColor">
+114
View File
@@ -3397,6 +3397,120 @@ code {
to { transform: scale(1); opacity: 1; }
}
/* Botão alternador de ferramentas (recursos) */
.btn-toggle-tools {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 8px;
color: var(--text-muted, #a3a3a3);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-toggle-tools svg {
width: 20px;
height: 20px;
}
.btn-toggle-tools:hover {
background: var(--bg-tertiary, #1a1a1a);
color: var(--text-primary, #ffffff);
}
.btn-toggle-tools.active {
color: var(--brand-green, #10a37f);
background: rgba(16, 163, 127, 0.1);
}
/* Ocultar barra de ferramentas no mobile por padrão */
@media (max-width: 768px) {
.prompt-modes-bar {
display: none !important;
width: 100%;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
opacity: 0;
}
.prompt-modes-bar.show {
display: flex !important;
max-height: 300px;
opacity: 1;
overflow-y: auto;
padding: 8px 4px;
}
}
/* Estilos de progresso da IA (Chain of Thought/Etapas) */
.ia-progress-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 18px;
background: var(--bg-secondary, #1a1a1a);
border: 1px solid var(--border-light, #262626);
border-radius: 12px;
max-width: 420px;
margin: 6px 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
animation: iaProgressPulse 2s infinite ease-in-out;
}
.ia-progress-header {
display: flex;
align-items: center;
gap: 10px;
}
.ia-progress-icon {
width: 16px;
height: 16px;
border: 2px solid rgba(16, 163, 127, 0.2);
border-radius: 50%;
border-top-color: var(--brand-green, #10a37f);
animation: iaProgressSpin 1s linear infinite;
flex-shrink: 0;
}
.ia-progress-step-text {
font-size: 13px;
color: var(--text-primary, #ffffff);
font-weight: 500;
font-family: var(--font-sans);
}
.ia-progress-bar {
width: 100%;
height: 5px;
background-color: var(--bg-tertiary, #121212);
border-radius: 3px;
overflow: hidden;
}
.ia-progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--brand-green, #10a37f) 0%, #06b6d4 100%);
border-radius: 3px;
width: 10%;
transition: width 0.5s ease-out;
}
@keyframes iaProgressSpin {
to { transform: rotate(360deg); }
}
@keyframes iaProgressPulse {
0% { border-color: var(--border-light, #262626); }
50% { border-color: rgba(16, 163, 127, 0.4); }
100% { border-color: var(--border-light, #262626); }
}