From 39bbc2c827dadf4c3e8fadec5d18cf463368b36c Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 20 Dec 2024 10:06:24 -0300 Subject: [PATCH] =?UTF-8?q?Adiciona=20P=C3=A1gina=20Demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/demo/AudioRecorderDemo.tsx | 154 ++++++++++++++++++++ src/components/story/AudioRecorder.tsx | 168 ++++++++++++++++++++++ src/pages/demo/DemoPage.tsx | 96 +++++++++++++ src/pages/story/StoryPage.tsx | 39 +++++ src/routes/index.tsx | 3 +- 5 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 src/components/demo/AudioRecorderDemo.tsx create mode 100644 src/components/story/AudioRecorder.tsx create mode 100644 src/pages/demo/DemoPage.tsx create mode 100644 src/pages/story/StoryPage.tsx diff --git a/src/components/demo/AudioRecorderDemo.tsx b/src/components/demo/AudioRecorderDemo.tsx new file mode 100644 index 0000000..d65b737 --- /dev/null +++ b/src/components/demo/AudioRecorderDemo.tsx @@ -0,0 +1,154 @@ +import React, { useState, useRef } from 'react'; +import { Mic, Square, Loader, Play, RotateCcw } from 'lucide-react'; + +interface AudioRecorderDemoProps { + onAnalysisComplete: (result: { + fluency: number; + accuracy: number; + confidence: number; + feedback: string; + }) => void; +} + +export function AudioRecorderDemo({ onAnalysisComplete }: AudioRecorderDemoProps) { + const [isRecording, setIsRecording] = useState(false); + const [audioBlob, setAudioBlob] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [error, setError] = useState(null); + + const mediaRecorderRef = useRef(null); + const chunksRef = useRef([]); + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorderRef.current = new MediaRecorder(stream); + chunksRef.current = []; + + mediaRecorderRef.current.ondataavailable = (e) => { + chunksRef.current.push(e.data); + }; + + mediaRecorderRef.current.onstop = () => { + const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }); + setAudioBlob(audioBlob); + }; + + mediaRecorderRef.current.start(); + setIsRecording(true); + setError(null); + } catch (err) { + setError('Erro ao acessar microfone. Verifique as permissões.'); + console.error('Erro ao iniciar gravação:', err); + } + }; + + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + + mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()); + } + }; + + const analyzeAudio = async () => { + if (!audioBlob) return; + + setIsAnalyzing(true); + setError(null); + + try { + // Simulação de análise para demo + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Resultados simulados para demonstração + onAnalysisComplete({ + fluency: Math.floor(Math.random() * 20) + 80, // 80-100 + accuracy: Math.floor(Math.random() * 15) + 85, // 85-100 + confidence: Math.floor(Math.random() * 25) + 75, // 75-100 + feedback: "Excelente leitura! Sua fluência está muito boa e você demonstra confiança na pronúncia. Continue praticando para melhorar ainda mais." + }); + } catch (err) { + setError('Erro ao analisar áudio. Tente novamente.'); + console.error('Erro na análise:', err); + } finally { + setIsAnalyzing(false); + } + }; + + const resetRecording = () => { + setAudioBlob(null); + setError(null); + }; + + return ( +
+
+ {!isRecording && !audioBlob && ( + + )} + + {isRecording && ( + + )} + + {audioBlob && !isAnalyzing && ( + <> + + + + + + + )} + + {isAnalyzing && ( +
+ + Analisando sua leitura... +
+ )} +
+ + {error && ( +
+ {error} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/story/AudioRecorder.tsx b/src/components/story/AudioRecorder.tsx new file mode 100644 index 0000000..d3d706c --- /dev/null +++ b/src/components/story/AudioRecorder.tsx @@ -0,0 +1,168 @@ +import React, { useState, useRef } from 'react'; +import { Mic, Square, Loader, Play, Upload } from 'lucide-react'; +import { supabase } from '@/lib/supabase'; + +interface AudioRecorderProps { + storyId: string; + studentId: string; + classId: string; + schoolId: string; + onAudioUploaded: (audioUrl: string) => void; +} + +export function AudioRecorder({ storyId, studentId, classId, schoolId, onAudioUploaded }: AudioRecorderProps) { + const [isRecording, setIsRecording] = useState(false); + const [audioBlob, setAudioBlob] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + + const mediaRecorderRef = useRef(null); + const chunksRef = useRef([]); + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorderRef.current = new MediaRecorder(stream); + chunksRef.current = []; + + mediaRecorderRef.current.ondataavailable = (e) => { + chunksRef.current.push(e.data); + }; + + mediaRecorderRef.current.onstop = () => { + const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }); + setAudioBlob(audioBlob); + }; + + mediaRecorderRef.current.start(); + setIsRecording(true); + setError(null); + } catch (err) { + setError('Erro ao acessar microfone. Verifique as permissões.'); + console.error('Erro ao iniciar gravação:', err); + } + }; + + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + + // Parar todas as tracks do stream + mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()); + } + }; + + const uploadAudio = async () => { + if (!audioBlob) return; + + setIsUploading(true); + setError(null); + + try { + // Criar nome único para o arquivo usando uma estrutura de pastas organizada + const timestamp = new Date().getTime(); + const filePath = `${studentId}/${classId}/${storyId}/${timestamp}.webm`; + + // Upload do arquivo para o Supabase Storage + const { data, error: uploadError } = await supabase.storage + .from('recordings') + .upload(filePath, audioBlob, { + contentType: 'audio/webm', + cacheControl: '3600' + }); + + if (uploadError) throw uploadError; + + // Obter URL pública do arquivo + const { data: { publicUrl } } = supabase.storage + .from('recordings') + .getPublicUrl(filePath); + + // Salvar referência no banco com todas as relações + const { error: dbError } = await supabase + .from('story_recordings') + .insert({ + story_id: storyId, + student_id: studentId, + class_id: classId, + school_id: schoolId, + audio_url: publicUrl, + status: 'pending_analysis' + }); + + if (dbError) throw dbError; + + onAudioUploaded(publicUrl); + setAudioBlob(null); + } catch (err) { + setError('Erro ao enviar áudio. Tente novamente.'); + console.error('Erro no upload:', err); + } finally { + setIsUploading(false); + } + }; + + return ( +
+
+ {!isRecording && !audioBlob && ( + + )} + + {isRecording && ( + + )} + + {audioBlob && !isUploading && ( + <> + + + + + )} + + {isUploading && ( +
+ + Enviando áudio... +
+ )} +
+ + {error && ( +
+ {error} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/demo/DemoPage.tsx b/src/pages/demo/DemoPage.tsx new file mode 100644 index 0000000..2ce2ae1 --- /dev/null +++ b/src/pages/demo/DemoPage.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { AudioRecorderDemo } from '../../components/demo/AudioRecorderDemo'; +import { ArrowRight, Sparkles } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; + +export function DemoPage() { + const navigate = useNavigate(); + const [demoResult, setDemoResult] = useState<{ + fluency?: number; + accuracy?: number; + confidence?: number; + feedback?: string; + } | null>(null); + + const handleDemoComplete = (result: typeof demoResult) => { + setDemoResult(result); + }; + + return ( +
+
+
+

+ Experimente Agora! +

+

+ Grave um trecho de leitura e veja como nossa IA avalia seu desempenho +

+
+ +
+
+

Texto Sugerido para Leitura:

+
+ "O pequeno príncipe sentou-se numa pedra e levantou os olhos para o céu: + — Pergunto-me se as estrelas são iluminadas para que cada um possa um dia encontrar a sua." +
+
+ + +
+ + {demoResult && ( +
+

+ + Resultado da Análise +

+ +
+
+
+ {demoResult.fluency}% +
+
Fluência
+
+ +
+
+ {demoResult.accuracy}% +
+
Precisão
+
+ +
+
+ {demoResult.confidence}% +
+
Confiança
+
+
+ +
+

+ Feedback da IA +

+

+ {demoResult.feedback} +

+
+ +
+ +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/story/StoryPage.tsx b/src/pages/story/StoryPage.tsx new file mode 100644 index 0000000..36f1104 --- /dev/null +++ b/src/pages/story/StoryPage.tsx @@ -0,0 +1,39 @@ +import { AudioRecorder } from '@/components/story/AudioRecorder'; + +export function StoryPage() { + // ... outros códigos ... + + const handleAudioUploaded = async (audioUrl: string) => { + try { + // Salvar referência do áudio no banco de dados + const { error } = await supabase + .from('story_recordings') + .insert({ + story_id: storyId, + student_id: studentId, + audio_url: audioUrl, + status: 'pending_analysis' // será analisado pela IA posteriormente + }); + + if (error) throw error; + + // Aqui você pode adicionar a lógica para enviar o áudio para análise da IA + // Por exemplo, chamar uma função que envia o áudio para um endpoint de IA + + } catch (err) { + console.error('Erro ao salvar gravação:', err); + } + }; + + return ( +
+ {/* ... outros elementos da página ... */} + + +
+ ); +} \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 2d093fc..54751bb 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -13,6 +13,7 @@ import { TeachersPage } from '../pages/dashboard/teachers/TeachersPage'; import { InviteTeacherPage } from '../pages/dashboard/teachers/InviteTeacherPage'; import { StudentsPage } from '../pages/dashboard/students/StudentsPage'; import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage'; +import { DemoPage } from '../pages/demo/DemoPage'; import React from 'react'; export const router = createBrowserRouter([ @@ -106,7 +107,7 @@ export const router = createBrowserRouter([ }, { path: '/demo', - element: , + element: , }, { path: '/auth/callback',