feat: fase 2 - detecção de crianças, tags, filtros e histórico
- GPT retorna JSON estruturado: criancas, turma, tags, relatorio - Frontmatter agora inclui criancas[], turma, tags[] no .md - Índice JSON estruturado com metadados completos - Prévia do relatório mostra crianças detectadas + tags + turma - Modal 'Minhas Observações' na sidebar - Filtros por mês, tag e busca por criança - Lista de observações com preview e chips - Modal de detalhe com copiar texto - CSS para btn-observacoes, obs-modal, obs-items, tags/chips
This commit is contained in:
+174
-3
@@ -1990,6 +1990,9 @@ const initApp = () => {
|
|||||||
let recordingStartTime = null;
|
let recordingStartTime = null;
|
||||||
let timerInterval = null;
|
let timerInterval = null;
|
||||||
let generatedReport = null;
|
let generatedReport = null;
|
||||||
|
let generatedCriancas = null;
|
||||||
|
let generatedTurma = 'não informada';
|
||||||
|
let generatedTags = [];
|
||||||
|
|
||||||
// Abrir modal e iniciar gravação
|
// Abrir modal e iniciar gravação
|
||||||
if (fabRecord) {
|
if (fabRecord) {
|
||||||
@@ -2059,7 +2062,10 @@ const initApp = () => {
|
|||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
generatedReport = data.report;
|
generatedReport = data.relatorio || data.report;
|
||||||
|
generatedCriancas = data.criancas;
|
||||||
|
generatedTurma = data.turma || 'não informada';
|
||||||
|
generatedTags = data.tags || [];
|
||||||
showReportPreview(data);
|
showReportPreview(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao processar observação:', err);
|
console.error('Erro ao processar observação:', err);
|
||||||
@@ -2080,7 +2086,22 @@ const initApp = () => {
|
|||||||
hour: '2-digit', minute: '2-digit'
|
hour: '2-digit', minute: '2-digit'
|
||||||
});
|
});
|
||||||
|
|
||||||
reportContent.innerHTML = marked.parse(generatedReport || 'Relatório não disponível.');
|
// Monta HTML com metadados
|
||||||
|
const criancasHtml = data.criancas && data.criancas.length
|
||||||
|
? `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> ${data.criancas.join(', ')}</div>`
|
||||||
|
: `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> <em>não identificadas</em></div>`;
|
||||||
|
const tagsHtml = data.tags && data.tags.length
|
||||||
|
? `<div class="report-meta-item"><span class="meta-label">🏷️ Tags:</span> ${data.tags.map(t => `<span class="tag-chip">${t}</span>`).join(' ')}</div>`
|
||||||
|
: '';
|
||||||
|
const turmaHtml = data.turma && data.turma !== 'não informada'
|
||||||
|
? `<div class="report-meta-item"><span class="meta-label">🏫 Turma:</span> ${data.turma}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
reportContent.innerHTML = `
|
||||||
|
<div class="report-meta">${criancasHtml}${turmaHtml}${tagsHtml}</div>
|
||||||
|
<hr style="border-color: var(--border-light); margin: 12px 0;">
|
||||||
|
${marked.parse(generatedReport || 'Relatório não disponível.')}
|
||||||
|
`;
|
||||||
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2103,7 +2124,12 @@ const initApp = () => {
|
|||||||
const res = await fetch('/api/observacao/salvar', {
|
const res = await fetch('/api/observacao/salvar', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ report: generatedReport })
|
body: JSON.stringify({
|
||||||
|
report: generatedReport,
|
||||||
|
criancas: generatedCriancas,
|
||||||
|
turma: generatedTurma,
|
||||||
|
tags: generatedTags
|
||||||
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -2145,6 +2171,151 @@ const initApp = () => {
|
|||||||
if (e.target === recordModal) closeRecordModal();
|
if (e.target === recordModal) closeRecordModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MODAL: MINHAS OBSERVAÇÕES
|
||||||
|
// ============================================================
|
||||||
|
const obsModal = document.getElementById('obsModal');
|
||||||
|
const btnObservacoes = document.getElementById('btnObservacoes');
|
||||||
|
const btnCloseObsModal = document.getElementById('btnCloseObsModal');
|
||||||
|
const obsList = document.getElementById('obsList');
|
||||||
|
const obsFilterMes = document.getElementById('obsFilterMes');
|
||||||
|
const obsFilterTag = document.getElementById('obsFilterTag');
|
||||||
|
const obsSearchCrianca = document.getElementById('obsSearchCrianca');
|
||||||
|
|
||||||
|
let allObservations = [];
|
||||||
|
|
||||||
|
// Abrir modal de observações
|
||||||
|
if (btnObservacoes) {
|
||||||
|
btnObservacoes.addEventListener('click', async () => {
|
||||||
|
obsModal.style.display = 'flex';
|
||||||
|
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
|
||||||
|
|
||||||
|
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 = '<div class="obs-empty">Erro ao carregar observações.</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fechar modal
|
||||||
|
if (btnCloseObsModal) {
|
||||||
|
btnCloseObsModal.addEventListener('click', () => {
|
||||||
|
obsModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (obsModal) {
|
||||||
|
obsModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === obsModal) obsModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtros
|
||||||
|
if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList);
|
||||||
|
if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList);
|
||||||
|
if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList);
|
||||||
|
|
||||||
|
// Preenche select de meses
|
||||||
|
const populateMonthFilter = () => {
|
||||||
|
if (!obsFilterMes) return;
|
||||||
|
const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))]
|
||||||
|
.filter(Boolean).sort().reverse();
|
||||||
|
obsFilterMes.innerHTML = '<option value="">Todos os meses</option>' +
|
||||||
|
months.map(m => `<option value="${m}">${m.split('-')[1]}/${m.split('-')[0]}</option>`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Renderiza lista filtrada
|
||||||
|
const renderObsList = () => {
|
||||||
|
if (!obsList) return;
|
||||||
|
const mes = obsFilterMes?.value || '';
|
||||||
|
const tag = obsFilterTag?.value || '';
|
||||||
|
const search = obsSearchCrianca?.value.toLowerCase() || '';
|
||||||
|
|
||||||
|
const filtered = allObservations.filter(o => {
|
||||||
|
if (mes && !o.date?.startsWith(mes)) return false;
|
||||||
|
if (tag && !o.tags?.includes(tag)) return false;
|
||||||
|
if (search) {
|
||||||
|
const nomes = (o.criancas || []).join(' ').toLowerCase();
|
||||||
|
if (!nomes.includes(search) && !(o.preview || '').toLowerCase().includes(search)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
obsList.innerHTML = '<div class="obs-empty">Nenhuma observação encontrada.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obsList.innerHTML = filtered.map(o => {
|
||||||
|
const tagsHtml = (o.tags || []).map(t => `<span class="obs-tag">${t}</span>`).join('');
|
||||||
|
const criancasHtml = (o.criancas || []).map(c => `<span class="obs-crianca">${c}</span>`).join('');
|
||||||
|
const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : '';
|
||||||
|
return `
|
||||||
|
<div class="obs-item" data-filename="${o.filename}" data-yearmonth="${o.date?.slice(0, 7)}">
|
||||||
|
<div class="obs-item-header">
|
||||||
|
<strong>📋 ${dateFormatted}${o.time ? ' às ' + o.time : ''}</strong>
|
||||||
|
${o.turma && o.turma !== 'não informada' ? `<span class="obs-crianca">🏫 ${o.turma}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="obs-item-preview">${o.preview || 'Sem prévia disponível.'}</div>
|
||||||
|
<div class="obs-item-footer">${tagsHtml}${criancasHtml}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="settings-modal-content obs-modal-content" style="max-width: 680px;">
|
||||||
|
<div class="settings-modal-header">
|
||||||
|
<h3>📋 Observação</h3>
|
||||||
|
<button class="btn-close-modal" onclick="this.closest('.settings-modal').remove()">
|
||||||
|
<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="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 20px; overflow-y: auto; max-height: 70vh;">
|
||||||
|
<div class="obs-detail-content" style="color: var(--text-primary); line-height: 1.6;">
|
||||||
|
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||||||
|
<button class="btn-save-report" onclick="navigator.clipboard.writeText(this.closest('.settings-modal').querySelector('.obs-detail-content').innerText).then(()=>alert('Copiado!'))">Copiar Texto</button>
|
||||||
|
<button class="btn-discard" onclick="this.closest('.settings-modal').remove()">Fechar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) modal.remove();
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player de áudio personalizado global para as mídias geradas
|
// Player de áudio personalizado global para as mídias geradas
|
||||||
|
|||||||
@@ -65,6 +65,14 @@
|
|||||||
<button class="tag-filter" data-tag="estudos">📚 Estudos</button>
|
<button class="tag-filter" data-tag="estudos">📚 Estudos</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Botão: Minhas Observações -->
|
||||||
|
<button class="btn-observacoes" id="btnObservacoes">
|
||||||
|
<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="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
|
||||||
|
</svg>
|
||||||
|
<span>Minhas Observações</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Histórico de Conversas -->
|
<!-- Histórico de Conversas -->
|
||||||
<div class="chat-history" id="chatHistory">
|
<div class="chat-history" id="chatHistory">
|
||||||
<!-- Histórico filtrado/busca -->
|
<!-- Histórico filtrado/busca -->
|
||||||
@@ -386,6 +394,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Minhas Observações -->
|
||||||
|
<div id="obsModal" class="settings-modal" style="display: none;">
|
||||||
|
<div class="settings-modal-content obs-modal-content">
|
||||||
|
<div class="settings-modal-header">
|
||||||
|
<h3>📒 Minhas Observações</h3>
|
||||||
|
<button id="btnCloseObsModal" class="btn-close-modal">
|
||||||
|
<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="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="obs-modal-body">
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="obs-filters" id="obsFilters">
|
||||||
|
<select id="obsFilterMes" class="obs-select">
|
||||||
|
<option value="">Todos os meses</option>
|
||||||
|
</select>
|
||||||
|
<select id="obsFilterTag" class="obs-select">
|
||||||
|
<option value="">Todas as tags</option>
|
||||||
|
<option value="social">Social</option>
|
||||||
|
<option value="cognitivo">Cognitivo</option>
|
||||||
|
<option value="motor">Motor</option>
|
||||||
|
<option value="linguagem">Linguagem</option>
|
||||||
|
<option value="emocional">Emocional</option>
|
||||||
|
<option value="colaborativo">Colaborativo</option>
|
||||||
|
<option value="autónomo">Autónomo</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="obsSearchCrianca" class="obs-input" placeholder="Buscar por criança...">
|
||||||
|
</div>
|
||||||
|
<!-- Lista de observações -->
|
||||||
|
<div id="obsList" class="obs-list">
|
||||||
|
<div class="obs-empty">Carregando observações...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
|
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
|||||||
+171
-1
@@ -2804,7 +2804,177 @@ code {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animação fadeIn global */
|
/* Meta info do relatório na prévia */
|
||||||
|
.report-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--brand-green, #10a37f);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botão Minhas Observações na sidebar */
|
||||||
|
.btn-observacoes {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-secondary, #1a1a1a);
|
||||||
|
border: 1px solid var(--border-light, #262626);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-secondary, #a3a3a3);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-observacoes:hover {
|
||||||
|
background: var(--brand-green, #10a37f);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand-green, #10a37f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-observacoes svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal de Observações */
|
||||||
|
.obs-modal-content {
|
||||||
|
max-width: 640px !important;
|
||||||
|
max-height: 85vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-modal-body {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-light, #262626);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-select, .obs-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-light, #262626);
|
||||||
|
background: var(--bg-input, #121212);
|
||||||
|
color: var(--text-primary, #ffffff);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item {
|
||||||
|
background: var(--bg-secondary, #1a1a1a);
|
||||||
|
border: 1px solid var(--border-light, #262626);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item:hover {
|
||||||
|
border-color: var(--brand-green, #10a37f);
|
||||||
|
background: var(--bg-input, #121212);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item-preview {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-secondary, #a3a3a3);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-item-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(16, 163, 127, 0.2);
|
||||||
|
color: var(--brand-green, #10a37f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.obs-crianca {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-secondary, #a3a3a3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FAB: Floating Action Button para gravação */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
|
|||||||
@@ -252,24 +252,32 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio'
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gera relatório estruturado via GPT
|
// Gera relatório estruturado via GPT
|
||||||
|
// Envia transcrição + pedido de extrair: crianças, turma, tags, relatório
|
||||||
const reportPrompt = `Você é a Kemily, assistente pedagógica da professora Camila Martella Gasparini Reifonas.
|
const reportPrompt = `Você é a Kemily, assistente pedagógica da professora Camila Martella Gasparini Reifonas.
|
||||||
|
|
||||||
Ela acabou de gravar uma observação em voz sobre seus alunos da educação infantil. Sua tarefa é analisar a transcrição e gerar um RELATÓRIO ESTRUTURADO de observação pedagógica.
|
Ela acabou de gravar uma observação em voz sobre seus alunos da educação infantil. Sua tarefa é ANALISAR a transcrição e retornar UM OBJETO JSON (sem markdown, sem código, apenas o JSON puro) com:
|
||||||
|
|
||||||
REGRAS DO RELATÓRIO:
|
{
|
||||||
- Use linguagem formal-pedagógica com calor humano
|
"criancas": ["lista de nomes de crianças mencionadas ou 'null' se não identificar nenhuma"],
|
||||||
- Estruture em seções claras com_MARKDOWN
|
"turma": "turma mencionada ou 'não informada'",
|
||||||
- Identifique crianças mencionadas, comportamentos, interações, conquistas, dificuldades
|
"tags": ["tag1", "tag2"] (social, cognitivo, motor, linguagem, emocional,轮的哪个),
|
||||||
- Inclua sugestões de seguimento para o planejamento
|
"relatorio": "...markdown estruturado do relatório pedagógico..."
|
||||||
- Foque em evidências observáveis, não interpretções subjetivas
|
}
|
||||||
- Seja objetiva mas detalhista
|
|
||||||
|
|
||||||
TRANSCRIÇÃO DA OBSERVAÇÃO:
|
REGRAS:
|
||||||
|
- Identifique crianças pelo nome próprio mencionado na fala
|
||||||
|
- Se não conseguir identificar nomes, use "criancas": null
|
||||||
|
- Tags: escolha entre social, cognitivo, motor, linguagem, emocional, colaborativo, autónomo
|
||||||
|
- Relatório em português, linguagem formal-pedagógica, seções claras em **markdown**
|
||||||
|
- Foque em evidências observáveis
|
||||||
|
- Máximo 120 palavras no relatório
|
||||||
|
|
||||||
|
TRANSCRIÇÃO:
|
||||||
"""
|
"""
|
||||||
${transcribedText}
|
${transcribedText}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Gerando o relatório...`;
|
Retorne APENAS o JSON válido:`;
|
||||||
|
|
||||||
const reportRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const reportRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -290,11 +298,29 @@ Gerando o relatório...`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reportData = await reportRes.json();
|
const reportData = await reportRes.json();
|
||||||
const reportText = reportData.choices?.[0]?.message?.content || 'Relatório não pôde ser gerado.';
|
const rawContent = reportData.choices?.[0]?.message?.content || '';
|
||||||
|
|
||||||
|
// Parse JSON da resposta
|
||||||
|
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
|
||||||
|
try {
|
||||||
|
// Limpa markdown code blocks se houver
|
||||||
|
const cleanJson = rawContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||||
|
const parsed = JSON.parse(cleanJson);
|
||||||
|
parsedData = {
|
||||||
|
criancas: parsed.criancas || null,
|
||||||
|
turma: parsed.turma || 'não informada',
|
||||||
|
tags: parsed.tags || [],
|
||||||
|
relatorio: parsed.relatorio || rawContent
|
||||||
|
};
|
||||||
|
} catch (parseErr) {
|
||||||
|
// Se não conseguir parsear JSON, retorna o texto como relatório
|
||||||
|
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto');
|
||||||
|
parsedData.relatorio = rawContent;
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
transcribedText,
|
transcribedText,
|
||||||
report: reportText
|
...parsedData
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -306,7 +332,7 @@ Gerando o relatório...`;
|
|||||||
|
|
||||||
// Rota: POST /api/observacao/salvar — salva relatório em .md
|
// Rota: POST /api/observacao/salvar — salva relatório em .md
|
||||||
app.post('/api/observacao/salvar', requireAuth, (req, res) => {
|
app.post('/api/observacao/salvar', requireAuth, (req, res) => {
|
||||||
const { report } = req.body;
|
const { report, criancas, turma, tags } = req.body;
|
||||||
if (!report) return res.status(400).json({ error: 'Relatório vazio' });
|
if (!report) return res.status(400).json({ error: 'Relatório vazio' });
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -316,13 +342,7 @@ app.post('/api/observacao/salvar', requireAuth, (req, res) => {
|
|||||||
const timestamp = now.toISOString();
|
const timestamp = now.toISOString();
|
||||||
|
|
||||||
// Monta markdown com frontmatter
|
// Monta markdown com frontmatter
|
||||||
const mdContent = `---
|
const mdContent = `---\ndata: "${dateStr}"\nhora: "${timeStr}"\ntimestamp: "${timestamp}"\ncriancas: ${JSON.stringify(criancas)}\nturma: "${turma || 'não informada'}"\ntags: ${JSON.stringify(tags)}\n---\n\n${report}
|
||||||
data: "${dateStr}"
|
|
||||||
hora: "${timeStr}"
|
|
||||||
timestamp: "${timestamp}"
|
|
||||||
---
|
|
||||||
|
|
||||||
${report}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const filename = `${dateStr}_${timeStr}.md`;
|
const filename = `${dateStr}_${timeStr}.md`;
|
||||||
@@ -332,7 +352,7 @@ ${report}
|
|||||||
const filePath = path.join(monthDir, filename);
|
const filePath = path.join(monthDir, filename);
|
||||||
fs.writeFileSync(filePath, mdContent, 'utf8');
|
fs.writeFileSync(filePath, mdContent, 'utf8');
|
||||||
|
|
||||||
// Atualiza índice
|
// Atualiza índice com metadados estruturados
|
||||||
const idx = readObsIndex();
|
const idx = readObsIndex();
|
||||||
idx.observations.unshift({
|
idx.observations.unshift({
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
@@ -340,6 +360,9 @@ ${report}
|
|||||||
date: dateStr,
|
date: dateStr,
|
||||||
time: timeStr,
|
time: timeStr,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
criancas: criancas || null,
|
||||||
|
turma: turma || 'não informada',
|
||||||
|
tags: tags || [],
|
||||||
preview: report.slice(0, 120) + '...'
|
preview: report.slice(0, 120) + '...'
|
||||||
});
|
});
|
||||||
writeObsIndex(idx);
|
writeObsIndex(idx);
|
||||||
|
|||||||
Reference in New Issue
Block a user