From 39943064a3d3a70fd4843e261472dcdeffde77af Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:04:27 +0000 Subject: [PATCH] Changes --- src/components/InspectionChecklist.tsx | 107 ++++++++++++++++++++++++- src/stores/useModelStore.ts | 15 +++- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/components/InspectionChecklist.tsx b/src/components/InspectionChecklist.tsx index 9705518..ca860e6 100644 --- a/src/components/InspectionChecklist.tsx +++ b/src/components/InspectionChecklist.tsx @@ -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(null); + const chunksRef = useRef([]); + const audioRef = useRef(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 ( +
+ {!recording && !audioUrl && ( + + )} + + {recording && ( + + )} + + {audioUrl && !recording && ( + <> + + + {playing ? '▶ Reproduzindo…' : '● Gravação salva'} + + + + )} +
+ ); +} + 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 }) { {(showNote || item.status === 'rejected') && ( -
+
Anotação @@ -72,6 +172,7 @@ function ChecklistRow({ item }: { item: ChecklistItem }) { value={item.note} onChange={(e) => setChecklistItemNote(item.id, e.target.value)} /> +
)}
diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 8130b8f..322058c 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -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((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 })) }), }));