Adicionar gravação de voz no checklist

Implementei gravação de voz para itens reprovados do checklist usando Web Audio API. Adicionei áudio ao modelo de Checklist, atualizei o store para armazenar audioUrl, criei componente VoiceRecorder e integrei ao InspectionChecklist para gravar, reproduzir e excluir gravações, incluindo atualizações de UI e estado.

X-Lovable-Edit-ID: edt-6a84c183-2a41-4bc6-aa22-044b7a435a1b
This commit is contained in:
gpt-engineer-app[bot]
2026-02-27 01:04:27 +00:00
2 changed files with 115 additions and 7 deletions
+104 -3
View File
@@ -1,10 +1,110 @@
import { CheckCircle, XCircle, Clock, MessageSquare } from 'lucide-react';
import { CheckCircle, XCircle, Clock, MessageSquare, Mic, Square, Play, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { useModelStore, type ChecklistItem } from '@/stores/useModelStore';
import { useState } from 'react';
import { useState, useRef, useCallback } from 'react';
import { toast } from 'sonner';
function VoiceRecorder({ itemId, audioUrl }: { itemId: string; audioUrl: string | null }) {
const { setChecklistItemAudio } = useModelStore();
const [recording, setRecording] = useState(false);
const [playing, setPlaying] = useState(false);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const audioRef = useRef<HTMLAudioElement | null>(null);
const startRecording = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorderRef.current = mediaRecorder;
chunksRef.current = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunksRef.current.push(e.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
const url = URL.createObjectURL(blob);
setChecklistItemAudio(itemId, url);
stream.getTracks().forEach(t => t.stop());
toast.success('Anotação de voz gravada');
};
mediaRecorder.start();
setRecording(true);
} catch {
toast.error('Não foi possível acessar o microfone');
}
}, [itemId, setChecklistItemAudio]);
const stopRecording = useCallback(() => {
mediaRecorderRef.current?.stop();
setRecording(false);
}, []);
const playAudio = useCallback(() => {
if (!audioUrl) return;
const audio = new Audio(audioUrl);
audioRef.current = audio;
setPlaying(true);
audio.onended = () => setPlaying(false);
audio.play();
}, [audioUrl]);
const deleteAudio = useCallback(() => {
if (audioUrl) URL.revokeObjectURL(audioUrl);
setChecklistItemAudio(itemId, null);
setPlaying(false);
}, [itemId, audioUrl, setChecklistItemAudio]);
return (
<div className="flex items-center gap-1.5">
{!recording && !audioUrl && (
<Button variant="outline" size="sm" className="h-7 gap-1.5 text-[10px]" onClick={startRecording}>
<Mic className="h-3 w-3 text-destructive" />
Gravar Voz
</Button>
)}
{recording && (
<Button variant="destructive" size="sm" className="h-7 gap-1.5 text-[10px] animate-pulse" onClick={stopRecording}>
<Square className="h-3 w-3" />
Parar
</Button>
)}
{audioUrl && !recording && (
<>
<Button
variant="outline"
size="icon"
className="h-7 w-7"
onClick={playAudio}
disabled={playing}
title="Reproduzir"
>
<Play className="h-3 w-3 text-primary" />
</Button>
<span className="font-mono text-[10px] text-muted-foreground">
{playing ? '▶ Reproduzindo…' : '● Gravação salva'}
</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 ml-auto"
onClick={deleteAudio}
title="Excluir gravação"
>
<Trash2 className="h-3 w-3 text-destructive" />
</Button>
</>
)}
</div>
);
}
function ChecklistRow({ item }: { item: ChecklistItem }) {
const { setChecklistItemStatus, setChecklistItemNote } = useModelStore();
const [showNote, setShowNote] = useState(item.status === 'rejected');
@@ -61,7 +161,7 @@ function ChecklistRow({ item }: { item: ChecklistItem }) {
</div>
{(showNote || item.status === 'rejected') && (
<div className="space-y-1">
<div className="space-y-2">
<div className="flex items-center gap-1">
<MessageSquare className="h-3 w-3 text-muted-foreground" />
<span className="font-mono text-[10px] text-muted-foreground">Anotação</span>
@@ -72,6 +172,7 @@ function ChecklistRow({ item }: { item: ChecklistItem }) {
value={item.note}
onChange={(e) => setChecklistItemNote(item.id, e.target.value)}
/>
<VoiceRecorder itemId={item.id} audioUrl={item.audioUrl} />
</div>
)}
</div>
+11 -4
View File
@@ -22,13 +22,14 @@ export interface ChecklistItem {
label: string;
status: 'pending' | 'approved' | 'rejected';
note: string;
audioUrl: string | null;
}
const defaultChecklist: ChecklistItem[] = [
{ id: 'dimensions', label: 'Dimensões Gerais', status: 'pending', note: '' },
{ id: 'plates', label: 'Posição das Placas', status: 'pending', note: '' },
{ id: 'holes', label: 'Diâmetro dos Furos', status: 'pending', note: '' },
{ id: 'weld', label: 'Especificação da Solda', status: 'pending', note: '' },
{ id: 'dimensions', label: 'Dimensões Gerais', status: 'pending', note: '', audioUrl: null },
{ id: 'plates', label: 'Posição das Placas', status: 'pending', note: '', audioUrl: null },
{ id: 'holes', label: 'Diâmetro dos Furos', status: 'pending', note: '', audioUrl: null },
{ id: 'weld', label: 'Especificação da Solda', status: 'pending', note: '', audioUrl: null },
];
interface ModelStore {
@@ -57,6 +58,7 @@ interface ModelStore {
checklist: ChecklistItem[];
setChecklistItemStatus: (id: string, status: 'approved' | 'rejected') => void;
setChecklistItemNote: (id: string, note: string) => void;
setChecklistItemAudio: (id: string, audioUrl: string | null) => void;
resetChecklist: () => void;
}
@@ -96,5 +98,10 @@ export const useModelStore = create<ModelStore>((set) => ({
item.id === id ? { ...item, note } : item
),
})),
setChecklistItemAudio: (id, audioUrl) => set((state) => ({
checklist: state.checklist.map(item =>
item.id === id ? { ...item, audioUrl } : item
),
})),
resetChecklist: () => set({ checklist: defaultChecklist.map(i => ({ ...i })) }),
}));