Adicionado fluxo de revisão de transcrição de voz antes da análise de IA
This commit is contained in:
+115
-28
@@ -1993,6 +1993,14 @@ const initApp = () => {
|
||||
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');
|
||||
|
||||
let mediaRecorder = null;
|
||||
let audioChunks = [];
|
||||
let recordingStartTime = null;
|
||||
@@ -2002,6 +2010,40 @@ const initApp = () => {
|
||||
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);
|
||||
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 () => {
|
||||
@@ -2009,9 +2051,7 @@ const initApp = () => {
|
||||
generatedReport = null;
|
||||
|
||||
// Mostra estado inicial (gravando)
|
||||
recordStateRecording.style.display = 'flex';
|
||||
recordStateProcessing.style.display = 'none';
|
||||
recordStatePreview.style.display = 'none';
|
||||
showRecordState('recording');
|
||||
recordModal.style.display = 'flex';
|
||||
|
||||
// Inicia timer
|
||||
@@ -2056,20 +2096,25 @@ const initApp = () => {
|
||||
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 processar
|
||||
// Parar gravação e transcrever
|
||||
if (btnStopRecord) {
|
||||
btnStopRecord.addEventListener('click', async () => {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
||||
try {
|
||||
mediaRecorder.stop();
|
||||
} catch (e) {}
|
||||
if (mediaRecorder.stream) {
|
||||
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
||||
}
|
||||
}
|
||||
clearInterval(timerInterval);
|
||||
|
||||
// Mostra processando
|
||||
recordStateRecording.style.display = 'none';
|
||||
recordStateProcessing.style.display = 'flex';
|
||||
// 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' });
|
||||
@@ -2081,6 +2126,54 @@ const initApp = () => {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || 'Erro na transcrição');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Exibe o texto transcrito na área de texto para edição
|
||||
if (transcriptionTextarea) {
|
||||
transcriptionTextarea.value = data.transcribedText || '';
|
||||
}
|
||||
|
||||
showRecordState('transcribed');
|
||||
} catch (err) {
|
||||
console.error('Erro ao processar observação:', err);
|
||||
alert('Erro ao transcrever áudio: ' + err.message);
|
||||
closeRecordModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ação de analisar transcrição
|
||||
if (btnAnalyzeTranscription) {
|
||||
btnAnalyzeTranscription.addEventListener('click', async () => {
|
||||
const text = transcriptionTextarea ? transcriptionTextarea.value.trim() : '';
|
||||
if (!text || text.length < 5) {
|
||||
alert('Por favor, digite ou revise a transcrição (mínimo de 5 caracteres).');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostra processando com texto de análise
|
||||
if (recordProcessingText) recordProcessingText.textContent = 'Analisando observação...';
|
||||
if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Gerando relatório pedagógico estruturado';
|
||||
showRecordState('processing');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/observacao/analisar', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transcribedText: text })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || 'Erro na análise');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
generatedReport = data.relatorio || data.report;
|
||||
generatedCriancas = data.criancas;
|
||||
@@ -2088,17 +2181,23 @@ const initApp = () => {
|
||||
generatedTags = data.tags || [];
|
||||
showReportPreview(data);
|
||||
} catch (err) {
|
||||
console.error('Erro ao processar observação:', err);
|
||||
alert('Erro ao processar sua observação. Tente novamente.');
|
||||
closeRecordModal();
|
||||
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) {
|
||||
recordStateProcessing.style.display = 'none';
|
||||
recordStatePreview.style.display = 'flex';
|
||||
showRecordState('preview');
|
||||
|
||||
const now = new Date();
|
||||
previewDateTime.textContent = now.toLocaleString('pt-BR', {
|
||||
@@ -2123,7 +2222,7 @@ const initApp = () => {
|
||||
${marked.parse(generatedReport || 'Relatório não disponível.')}
|
||||
`;
|
||||
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
||||
};
|
||||
}
|
||||
|
||||
// Descartar relatório
|
||||
if (btnDiscardReport) {
|
||||
@@ -2173,18 +2272,6 @@ const initApp = () => {
|
||||
btnExitRecord.addEventListener('click', closeRecordModal);
|
||||
}
|
||||
|
||||
// Fechar modal
|
||||
function closeRecordModal() {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
||||
}
|
||||
clearInterval(timerInterval);
|
||||
audioChunks = [];
|
||||
generatedReport = null;
|
||||
recordModal.style.display = 'none';
|
||||
};
|
||||
|
||||
// Fechar clicando fora
|
||||
if (recordModal) {
|
||||
recordModal.addEventListener('click', (e) => {
|
||||
|
||||
+20
-2
@@ -296,8 +296,26 @@
|
||||
<!-- Estado: processando -->
|
||||
<div id="recordStateProcessing" class="record-state" style="display: none;">
|
||||
<div class="record-spinner"></div>
|
||||
<p>Analisando sua observação...</p>
|
||||
<small style="color: var(--text-muted);">Transcrevendo e gerando relatório</small>
|
||||
<p id="recordProcessingText">Analisando sua observação...</p>
|
||||
<small id="recordProcessingSubtext" style="color: var(--text-muted);">Transcrevendo e gerando relatório</small>
|
||||
</div>
|
||||
|
||||
<!-- Estado: Transcrição Concluída (Revisão) -->
|
||||
<div id="recordStateTranscribed" class="record-state" style="display: none; width: 100%;">
|
||||
<h4 style="margin-bottom: 8px; color: var(--text-main); display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
|
||||
<span>✍️ Revisar Transcrição</span>
|
||||
</h4>
|
||||
<p class="record-hint" style="margin-bottom: 12px; text-align: left; width: 100%;">Você pode ajustar o texto abaixo antes da análise da IA:</p>
|
||||
<textarea id="transcriptionTextarea" class="transcription-textarea" style="width: 100%; min-height: 120px; padding: 12px; border-radius: 8px; border: 1px solid var(--border-light); background-color: var(--bg-card); color: var(--text-main); font-family: inherit; font-size: 14px; line-height: 1.5; resize: vertical; margin-bottom: 16px;" placeholder="Digite ou ajuste o que foi falado..."></textarea>
|
||||
<div class="preview-actions" style="display: flex; gap: 12px; width: 100%; justify-content: flex-end;">
|
||||
<button id="btnDiscardTranscription" class="btn-discard" style="flex: 1; max-width: 150px;">Descartar</button>
|
||||
<button id="btnAnalyzeTranscription" class="btn-save-report" style="flex: 1; max-width: 200px; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||||
<span>Analisar com IA</span>
|
||||
<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="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: prévia -->
|
||||
|
||||
Reference in New Issue
Block a user