feat: sistema de observação por voz - fase 1

- FAB flutuante para gravar observação
- Modal com timer + waveform animado
- Botão sair para desistir
- MediaRecorder API (webm/opus)
- Transcrição via OpenAI Whisper
- Relatório estruturado via GPT
- Salva em .md com frontmatter (data/hora/timestamp)
- Índice JSON para consulta posterior
- Rotas: transcrever, salvar, listar observações
- multer para upload de áudio
This commit is contained in:
2026-05-27 15:46:03 +00:00
parent d309b03b55
commit ea8e33c891
72 changed files with 8531 additions and 4 deletions
+63 -2
View File
@@ -102,8 +102,15 @@
</div>
</aside>
<!-- Overlay para fechar sidebar clicando fora no mobile -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- FAB: Nova Observação por Voz -->
<button class="fab-record" id="fabRecord" title="Nova Observação (gravar)">
<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 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
</svg>
</button>
<!-- Overlay para fechar sidebar clicando fora no mobile -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- Área principal do Chat -->
<main class="chat-area">
@@ -247,6 +254,60 @@
</main>
</div>
<!-- Modal de Gravação de Observação -->
<div id="recordModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content record-modal-content">
<div class="record-modal-header">
<button id="btnExitRecord" class="btn-exit-record" title="Sair e descartar">
<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>
<span>Sair</span>
</button>
<h3>🎤 Nova Observação</h3>
<div style="width: 60px;"></div>
</div>
<div class="record-modal-body">
<!-- Estado: gravando -->
<div id="recordStateRecording" class="record-state" style="display: none;">
<div class="record-timer" id="recordTimer">00:00</div>
<div class="record-waveform" id="recordWaveform">
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
</div>
<p class="record-hint">Fale livremente sobre a observação...</p>
<button id="btnStopRecord" class="btn-stop-record">
<div class="stop-icon"></div>
<span>Parar</span>
</button>
</div>
<!-- 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>
</div>
<!-- Estado: prévia -->
<div id="recordStatePreview" class="record-state" style="display: none;">
<div class="preview-header">
<h4>📋 Relatório Gerado</h4>
<span id="previewDateTime"></span>
</div>
<div id="reportContent" class="report-content"></div>
<div class="preview-actions">
<button id="btnDiscardReport" class="btn-discard">Descartar</button>
<button id="btnSaveReport" class="btn-save-report">Salvar</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal de Configurações do Agente -->
<div id="settingsModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content settings-modal-large">