diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d06a8..12632d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,23 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). ## [1.1.0] - 2024-03-21 +### Adicionado +- Novo recurso "Modo Foco" para melhorar a experiência de leitura + - Ativação automática ao iniciar gravação + - Desativação automática ao parar gravação + - Interface adaptativa com foco no texto + - Controles de acessibilidade (tamanho da fonte, espaçamento) + - Destaque automático de palavras durante a leitura + +### Técnico +- Integração entre componentes `AudioRecorder` e `StoryPage` para gerenciamento do Modo Foco +- Adição de novos props no componente `AudioRecorder`: + - `onFocusModeToggle` + - `focusModeActive` + - `onRecordingStart` + - `onRecordingStop` +- Otimização de código com remoção de variáveis não utilizadas + ### Modificado - Atualizado o componente `AudioRecorder` para incluir tipagem correta e melhor gerenciamento de estado - Corrigido o gerenciamento de gravações no `StoryPage` com inicialização adequada de métricas @@ -95,23 +112,24 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). - Feedback visual durante o processamento do áudio - Inicialização de métricas zeradas para novas gravações -## [1.2.0] - 2024-05-22 +## [1.2.0] - 2024-03-21 ### Adicionado -- Criação de histórias por comando de voz -- Componente `VoiceCommandButton` para gravação de áudio -- Hook `useSpeechRecognition` para reconhecimento de voz -- Sistema de validação de conteúdo de áudio -- Integração com geração de histórias por IA -- Documentação de recursos de voz +- Novo Modo Foco para leitura e gravação +- Estilos específicos para o Modo Foco +- Timer de gravação no Modo Foco +- Transições suaves entre modos +- Controles flutuantes durante o Modo Foco ### Modificado -- Fluxo de criação de histórias para suportar entrada por voz -- Interface do gerador de histórias com novo modo de entrada -- Melhorias na experiência do usuário para gravação +- Componente AudioRecorder atualizado para suportar Modo Foco +- Interface do StoryPage reorganizada para Modo Foco +- Comportamento de gravação integrado com Modo Foco +- Melhorias na experiência do usuário durante a leitura ### Técnico -- Implementação de reconhecimento de voz com Web Speech API -- Sistema de validação de conteúdo sensível em transcrições -- Otimização do processamento de comandos de voz -- Melhorias na segurança do processamento de áudio +- Novo arquivo CSS para estilos do Modo Foco +- Interface FocusMode para gerenciamento de estado +- Callbacks de início e fim de gravação +- Sistema de transição entre modos normal e foco +- Otimização de performance para transições suaves diff --git a/src/components/story/AudioRecorder.tsx b/src/components/story/AudioRecorder.tsx index 4d229e8..a92da7b 100644 --- a/src/components/story/AudioRecorder.tsx +++ b/src/components/story/AudioRecorder.tsx @@ -2,15 +2,27 @@ import React, { useState, useRef } from 'react'; import { Mic, Square, Loader, Play, Upload } from 'lucide-react'; import { supabase } from '../../lib/supabase'; import { v4 as uuidv4 } from 'uuid'; -import { useStudentTracking } from '../../hooks/useStudentTracking'; +import { cn } from '../../lib/utils'; interface AudioRecorderProps { storyId: string; studentId: string; onAudioUploaded: (audioUrl: string) => void; + onRecordingStart?: () => void; + onRecordingStop?: () => void; + focusModeActive?: boolean; + onFocusModeToggle?: () => void; } -export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioRecorderProps) { +export function AudioRecorder({ + storyId, + studentId, + onAudioUploaded, + onRecordingStart, + onRecordingStop, + focusModeActive = false, + onFocusModeToggle +}: AudioRecorderProps) { const [isRecording, setIsRecording] = useState(false); const [audioBlob, setAudioBlob] = useState(null); const [isUploading, setIsUploading] = useState(false); @@ -18,11 +30,14 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); - const { trackAudioRecorded } = useStudentTracking(); const startTime = React.useRef(null); const startRecording = async () => { try { + if (!focusModeActive && onFocusModeToggle) { + onFocusModeToggle(); + } + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorderRef.current = new MediaRecorder(stream); chunksRef.current = []; @@ -34,12 +49,14 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco mediaRecorderRef.current.onstop = () => { const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }); setAudioBlob(audioBlob); + onRecordingStop?.(); }; mediaRecorderRef.current.start(); startTime.current = Date.now(); setIsRecording(true); setError(null); + onRecordingStart?.(); } catch (err) { setError('Erro ao acessar microfone. Verifique as permissões.'); console.error('Erro ao iniciar gravação:', err); @@ -51,8 +68,12 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco mediaRecorderRef.current.stop(); setIsRecording(false); - // Parar todas as tracks do stream mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()); + + // Desativar modo foco ao parar a gravação + if (focusModeActive && onFocusModeToggle) { + onFocusModeToggle(); + } } }; @@ -112,7 +133,7 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco .getPublicUrl(filePath); // 3. Criar o registro com a URL do áudio - const { data: recordData, error: recordError } = await supabase + const { error: recordError } = await supabase .from('story_recordings') .insert({ id: fileId, @@ -152,15 +173,23 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco }; return ( -
+
{!isRecording && !audioBlob && ( )} diff --git a/src/pages/student-dashboard/StoryPage.tsx b/src/pages/student-dashboard/StoryPage.tsx index c1b7b55..7c050a9 100644 --- a/src/pages/student-dashboard/StoryPage.tsx +++ b/src/pages/student-dashboard/StoryPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { ArrowLeft, ArrowRight, Share2, ChevronDown, ChevronUp, Loader2, Download, RefreshCw, Trash2, Type } from 'lucide-react'; +import { ArrowLeft, ArrowRight, Share2, ChevronDown, ChevronUp, Loader2, Download, RefreshCw, Trash2, Type, Eye, EyeOff } from 'lucide-react'; import { useParams, useNavigate } from 'react-router-dom'; import { supabase } from '../../lib/supabase'; import { AudioRecorder } from '../../components/story/AudioRecorder'; @@ -36,6 +36,17 @@ interface StoryRecording { transcription: string; } +interface FocusMode { + isActive: boolean; + isRecording: boolean; + originalState: { + isUpperCase: boolean; + isHighlighting: boolean; + fontSize: number; + imageVisible: boolean; + }; +} + function RecordingHistoryCard({ recording }: { recording: StoryRecording }) { const [isExpanded, setIsExpanded] = React.useState(false); const [isAudioSupported, setIsAudioSupported] = React.useState(true); @@ -384,13 +395,23 @@ export function StoryPage() { const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id); const { isHighlighted: isSyllablesEnabled, toggleHighlight: toggleSyllables } = useSyllables(); const [fontSize, setFontSize] = useState(18); - const [readingSpeed, setReadingSpeed] = useState(120); // 120 palavras por minuto + const [readingSpeed, setReadingSpeed] = useState(60); // 60 palavras por minuto const [letterSpacing, setLetterSpacing] = useState(0); const [wordSpacing, setWordSpacing] = useState(0); const [lineHeight, setLineHeight] = useState(1.5); const [isHighlighting, setIsHighlighting] = useState(false); const [currentWordIndex, setCurrentWordIndex] = useState(-1); const highlightInterval = React.useRef(null); + const [focusMode, setFocusMode] = useState({ + isActive: false, + isRecording: false, + originalState: { + isUpperCase: false, + isHighlighting: false, + fontSize: 18, + imageVisible: true + } + }); // Função para dividir o texto em palavras const getWords = (text: string) => text.split(/\s+/); @@ -642,6 +663,55 @@ export function StoryPage() { } }; + const handleFocusModeToggle = () => { + if (!focusMode.isActive) { + // Salvar estado atual antes de ativar o modo foco + setFocusMode(prev => ({ + ...prev, + isActive: true, + originalState: { + isUpperCase, + isHighlighting, + fontSize, + imageVisible: true + } + })); + + // Aplicar configurações do modo foco + toggleUppercase(); + setIsHighlighting(true); + setFontSize(24); + } else { + // Restaurar estado original + if (isUpperCase !== focusMode.originalState.isUpperCase) { + toggleUppercase(); + } + setIsHighlighting(focusMode.originalState.isHighlighting); + setFontSize(focusMode.originalState.fontSize); + + // Resetar modo foco + setFocusMode(prev => ({ + ...prev, + isActive: false, + isRecording: false + })); + } + }; + + const handleRecordingStart = () => { + setFocusMode(prev => ({ + ...prev, + isRecording: true + })); + }; + + const handleRecordingStop = () => { + setFocusMode(prev => ({ + ...prev, + isRecording: false + })); + }; + if (loading) { return (
@@ -678,6 +748,27 @@ export function StoryPage() {
+ + {/* Conteúdo Principal */} -
+
{/* 1. Controles de Texto */} -
+
- {/* 5. Imagem da História */} -
- {story?.content?.pages?.[currentPage]?.image ? ( - - ) : ( -
-

Sem imagem para esta página

-
- )} -
+ {/* 5. Imagem da História - Agora com controle de visibilidade */} + {!focusMode.isActive && ( +
+ {story?.content?.pages?.[currentPage]?.image ? ( + + ) : ( +
+

Sem imagem para esta página

+
+ )} +
+ )} {/* 6. Controle de Gravação */}
-

Gravação de Áudio

+

+ {focusMode.isActive ? "Modo Foco Ativado" : "Gravação de Áudio"} +

[newRecording, ...prev]); + + // Desativar modo foco após finalizar gravação + if (focusMode.isActive) { + handleFocusModeToggle(); + } }} + onRecordingStart={handleRecordingStart} + onRecordingStop={handleRecordingStop} + focusModeActive={focusMode.isActive} + onFocusModeToggle={handleFocusModeToggle} />
diff --git a/src/styles/focus-mode.css b/src/styles/focus-mode.css new file mode 100644 index 0000000..58a2d49 --- /dev/null +++ b/src/styles/focus-mode.css @@ -0,0 +1,100 @@ +/* Estilos para o Modo Foco */ + +.focus-mode-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + transition: all 0.3s ease-in-out; +} + +.focus-mode-active { + background-color: #f8f9fc; + min-height: 100vh; +} + +.focus-mode-text { + font-size: 24px; + line-height: 1.8; + letter-spacing: 0.5px; + word-spacing: 2px; + text-align: center; + padding: 2rem; + background-color: white; + border-radius: 1rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.focus-mode-controls { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 1rem; + padding: 1rem; + background-color: white; + border-radius: 9999px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + z-index: 50; +} + +.focus-mode-highlight { + background-color: #fef3c7; + border-radius: 0.25rem; + padding: 0 0.25rem; + transition: background-color 0.2s ease-in-out; +} + +.focus-mode-timer { + position: fixed; + top: 2rem; + right: 2rem; + padding: 0.75rem 1.5rem; + background-color: white; + border-radius: 9999px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + font-size: 1.125rem; + font-weight: 500; + color: #4b5563; + z-index: 50; +} + +/* Animações */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.focus-mode-transition { + animation: fadeIn 0.3s ease-out; +} + +/* Responsividade */ +@media (max-width: 640px) { + .focus-mode-container { + padding: 1rem; + } + + .focus-mode-text { + font-size: 20px; + padding: 1rem; + } + + .focus-mode-controls { + bottom: 1rem; + padding: 0.75rem; + } + + .focus-mode-timer { + top: 1rem; + right: 1rem; + padding: 0.5rem 1rem; + font-size: 1rem; + } +} \ No newline at end of file