diff --git a/src/components/learning/WordHighlighter.tsx b/src/components/learning/WordHighlighter.tsx
index db13061..7ca5fbb 100644
--- a/src/components/learning/WordHighlighter.tsx
+++ b/src/components/learning/WordHighlighter.tsx
@@ -1,46 +1,129 @@
+import React, { useState, useEffect } from 'react';
+import { ChevronUp, ChevronDown, Play, Pause } from 'lucide-react';
+
interface WordHighlighterProps {
text: string; // Texto completo
highlightedWords: string[]; // Palavras para destacar (ex: palavras difíceis)
difficultWords: string[]; // Palavras que o aluno teve dificuldade
onWordClick: (word: string) => void; // Função para quando clicar na palavra
+ highlightSpeed?: number; // palavras por minuto
+ initialFontSize?: number;
}
export function WordHighlighter({
text,
highlightedWords,
difficultWords,
- onWordClick
+ onWordClick,
+ highlightSpeed = 60,
+ initialFontSize = 18
}: WordHighlighterProps) {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentWordIndex, setCurrentWordIndex] = useState(0);
+ const [fontSize, setFontSize] = useState(initialFontSize);
+
// Divide o texto em palavras mantendo a pontuação
const words = text.split(/(\s+)/).filter(word => word.trim().length > 0);
+ useEffect(() => {
+ if (!isPlaying) return;
+
+ const intervalTime = (60 / highlightSpeed) * 1000;
+
+ const interval = setInterval(() => {
+ setCurrentWordIndex((prevIndex) => {
+ if (prevIndex >= words.length - 1) {
+ setIsPlaying(false);
+ return prevIndex;
+ }
+ return prevIndex + 1;
+ });
+ }, intervalTime);
+
+ return () => clearInterval(interval);
+ }, [isPlaying, highlightSpeed, words.length]);
+
+ const handleFontSizeChange = (delta: number) => {
+ setFontSize(prev => Math.min(Math.max(12, prev + delta), 32));
+ };
+
+ const togglePlayPause = () => {
+ if (!isPlaying) {
+ setCurrentWordIndex(0);
+ }
+ setIsPlaying(!isPlaying);
+ };
+
return (
-
- {words.map((word, i) => {
- // Remove pontuação para comparação
- const cleanWord = word.toLowerCase().replace(/[.,!?;:]/, '');
-
- // Determina o estilo baseado no tipo da palavra
- const isHighlighted = highlightedWords.includes(cleanWord);
- const isDifficult = difficultWords.includes(cleanWord);
-
- return (
-
onWordClick(word)}
- className={`
- inline-block mx-1 px-1 rounded cursor-pointer transition-all
- hover:scale-110
- ${isHighlighted ? 'bg-yellow-200 hover:bg-yellow-300' : ''}
- ${isDifficult ? 'bg-red-100 hover:bg-red-200' : ''}
- hover:bg-gray-100
- `}
- title="Clique para ver mais informações"
+
+ {/* Controles */}
+
+
+
+ {fontSize}px
+
+
+
+
+
+
+ {/* Texto */}
+
+ {words.map((word, i) => {
+ const cleanWord = word.toLowerCase().replace(/[.,!?;:]/, '');
+ const isHighlighted = highlightedWords.includes(cleanWord);
+ const isDifficult = difficultWords.includes(cleanWord);
+ const isCurrentWord = i === currentWordIndex && isPlaying;
+
+ return (
+ onWordClick(word)}
+ className={`
+ inline-block mx-1 px-1 rounded cursor-pointer transition-all
+ hover:scale-110
+ ${isHighlighted ? 'bg-yellow-200 hover:bg-yellow-300' : ''}
+ ${isDifficult ? 'bg-red-100 hover:bg-red-200' : ''}
+ ${isCurrentWord ? 'bg-purple-200 scale-110' : ''}
+ hover:bg-gray-100
+ `}
+ title="Clique para ver mais informações"
+ >
+ {word}
+
+ );
+ })}
+
);
}
\ No newline at end of file
diff --git a/src/components/ui/adaptive-text.tsx b/src/components/ui/adaptive-text.tsx
index 2219666..1e1f6cd 100644
--- a/src/components/ui/adaptive-text.tsx
+++ b/src/components/ui/adaptive-text.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { cn } from '../../lib/utils';
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
+import { formatTextWithSyllables } from '../../features/syllables/utils/syllableSplitter';
interface AdaptiveTextProps extends React.HTMLAttributes {
text: string;
@@ -19,11 +20,8 @@ export const AdaptiveText = React.memo(({
className,
...props
}: AdaptiveTextProps) => {
- // Transformar o texto mantendo espaços em branco se necessário
- const transformedText = React.useMemo(() => {
- const transformed = isUpperCase ? text.toUpperCase() : text;
- return preserveWhitespace ? transformed : transformed.trim();
- }, [text, isUpperCase, preserveWhitespace]);
+ const formattedText = formatTextWithSyllables(text, highlightSyllables);
+ const finalText = isUpperCase ? formattedText.toUpperCase() : formattedText;
return React.createElement(
Component,
@@ -34,9 +32,7 @@ export const AdaptiveText = React.memo(({
),
...props
},
- highlightSyllables ? (
-
- ) : transformedText
+ finalText
);
});
diff --git a/src/components/ui/text-controls.tsx b/src/components/ui/text-controls.tsx
new file mode 100644
index 0000000..1c5638f
--- /dev/null
+++ b/src/components/ui/text-controls.tsx
@@ -0,0 +1,250 @@
+import React from 'react';
+import {
+ Type,
+ ChevronUp,
+ ChevronDown,
+ Play,
+ Pause,
+ Timer,
+ ArrowLeftRight,
+ MoveVertical
+} from 'lucide-react';
+import { cn } from '../../lib/utils';
+
+interface TextControlsProps {
+ isUpperCase: boolean;
+ onToggleUpperCase: () => void;
+ isSyllablesEnabled: boolean;
+ onToggleSyllables: () => void;
+ fontSize: number;
+ onFontSizeChange: (size: number) => void;
+ readingSpeed: number;
+ onReadingSpeedChange: (speed: number) => void;
+ letterSpacing: number;
+ onLetterSpacingChange: (spacing: number) => void;
+ wordSpacing: number;
+ onWordSpacingChange: (spacing: number) => void;
+ lineHeight: number;
+ onLineHeightChange: (height: number) => void;
+ isLoading?: boolean;
+ className?: string;
+ isHighlighting?: boolean;
+ onToggleHighlight?: () => void;
+}
+
+export function TextControls({
+ isUpperCase,
+ onToggleUpperCase,
+ isSyllablesEnabled,
+ onToggleSyllables,
+ fontSize,
+ onFontSizeChange,
+ readingSpeed,
+ onReadingSpeedChange,
+ letterSpacing,
+ onLetterSpacingChange,
+ wordSpacing,
+ onWordSpacingChange,
+ lineHeight,
+ onLineHeightChange,
+ isLoading = false,
+ className,
+ isHighlighting = false,
+ onToggleHighlight
+}: TextControlsProps) {
+ const handleFontSizeChange = (delta: number) => {
+ const newSize = Math.min(Math.max(12, fontSize + delta), 32);
+ onFontSizeChange(newSize);
+ };
+
+ const handleReadingSpeedChange = (delta: number) => {
+ const newSpeed = Math.min(Math.max(30, readingSpeed + delta), 300);
+ onReadingSpeedChange(newSpeed);
+ };
+
+ const handleLetterSpacingChange = (delta: number) => {
+ const newSpacing = Math.min(Math.max(0, letterSpacing + delta), 10);
+ onLetterSpacingChange(newSpacing);
+ };
+
+ const handleWordSpacingChange = (delta: number) => {
+ const newSpacing = Math.min(Math.max(0, wordSpacing + delta), 20);
+ onWordSpacingChange(newSpacing);
+ };
+
+ const handleLineHeightChange = (delta: number) => {
+ const newHeight = Math.min(Math.max(1, lineHeight + delta), 3);
+ onLineHeightChange(newHeight);
+ };
+
+ return (
+
+ {/* Primeira Seção: Controles Principais */}
+
+ {/* Controle de Maiúsculas */}
+
+
+ {/* Controle de Sílabas */}
+
+
+ {/* Word Highlighter */}
+
+
+
+ {/* Segunda Seção: Ajustes de Texto */}
+
+ {/* Controle de Tamanho da Fonte */}
+
+
+ {fontSize}px
+
+
+
+ {/* Controle de Velocidade de Leitura */}
+
+
+
+
+ {readingSpeed} ppm
+
+
+
+
+ {/* Controle de Espaçamento entre Letras */}
+
+
+
+
+ {letterSpacing}px
+
+
+
+
+ {/* Controle de Espaçamento entre Palavras */}
+
+
+
+
+
+
+ {/* Controle de Altura da Linha */}
+
+
+
+
+ {lineHeight.toFixed(1)}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/features/syllables/utils/syllableSplitter.ts b/src/features/syllables/utils/syllableSplitter.ts
index ebc571f..0cdb649 100644
--- a/src/features/syllables/utils/syllableSplitter.ts
+++ b/src/features/syllables/utils/syllableSplitter.ts
@@ -19,4 +19,17 @@ export function splitIntoSyllables(word: string): string[] {
}
return syllables.length > 0 ? syllables : [word];
+}
+
+export function highlightSyllables(text: string): string {
+ const words = text.split(/\s+/);
+ return words.map(word => {
+ const syllables = splitIntoSyllables(word);
+ return syllables.join('-');
+ }).join(' ');
+}
+
+export function formatTextWithSyllables(text: string, shouldHighlight: boolean): string {
+ if (!shouldHighlight) return text;
+ return highlightSyllables(text);
}
\ No newline at end of file
diff --git a/src/pages/student-dashboard/StoryPage.tsx b/src/pages/student-dashboard/StoryPage.tsx
index 40fdefa..7f70bee 100644
--- a/src/pages/student-dashboard/StoryPage.tsx
+++ b/src/pages/student-dashboard/StoryPage.tsx
@@ -13,6 +13,8 @@ import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components
import { useSession } from '../../hooks/useSession';
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
+import { TextControls } from '../../components/ui/text-controls';
+import { cn } from '../../lib/utils';
interface StoryRecording {
@@ -399,8 +401,83 @@ export function StoryPage() {
const [isDeleting, setIsDeleting] = useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
- const { isHighlighted, toggleHighlight } = useSyllables();
+ const { isHighlighted: isSyllablesEnabled, toggleHighlight: toggleSyllables } = useSyllables();
+ const [fontSize, setFontSize] = useState(18);
+ const [readingSpeed, setReadingSpeed] = useState(120); // 120 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);
+ // Função para dividir o texto em palavras
+ const getWords = (text: string) => text.split(/\s+/);
+
+ // Atualizar o useEffect do highlighting para usar readingSpeed
+ useEffect(() => {
+ if (isHighlighting) {
+ const words = getWords(story?.content?.pages?.[currentPage]?.text || '');
+ const intervalTime = (60 / readingSpeed) * 1000; // Converter palavras por minuto para milissegundos
+
+ highlightInterval.current = window.setInterval(() => {
+ setCurrentWordIndex(prev => {
+ if (prev >= words.length - 1) {
+ setIsHighlighting(false);
+ return -1;
+ }
+ return prev + 1;
+ });
+ }, intervalTime);
+
+ return () => {
+ if (highlightInterval.current) {
+ window.clearInterval(highlightInterval.current);
+ }
+ };
+ } else {
+ setCurrentWordIndex(-1);
+ if (highlightInterval.current) {
+ window.clearInterval(highlightInterval.current);
+ }
+ }
+ }, [isHighlighting, currentPage, story?.content?.pages, readingSpeed]);
+
+ // Função para renderizar o texto com destaque
+ const renderHighlightedText = (text: string) => {
+ if (!text || currentWordIndex === -1) {
+ return (
+
+ );
+ }
+
+ const words = getWords(text);
+ return (
+
+ {words.map((word, index) => (
+
+
+ {' '}
+
+ ))}
+
+ );
+ };
React.useEffect(() => {
const fetchStory = async () => {
@@ -447,8 +524,6 @@ export function StoryPage() {
fetchStory();
}, [id]);
-
-
React.useEffect(() => {
const fetchRecordings = async () => {
if (!story?.id) return;
@@ -628,12 +703,12 @@ export function StoryPage() {
isLoading={isLoading}
/>
Compartilhar
- {
- /*
- */}
- {/* História Principal */}
-
- {/* Imagem da página atual */}
- {story?.content?.pages?.[currentPage]?.image && (
-
- )}
-
-
-
+ {/* 1. Controles de Texto */}
+
+
-
- {/* Texto da página atual */}
-
setIsHighlighting(prev => !prev)}
/>
+
- {/* Gravador de áudio */}
- {
- console.log('Áudio gravado:', audioUrl);
+ {/* 2. Título da História */}
+
+
{story?.title}
+
+
+ {/* 3. Texto da História */}
+
+
+ >
+ {renderHighlightedText(story?.content?.pages?.[currentPage]?.text || '')}
+
+
- {/* Navegação entre páginas */}
-
+ {/* 4. Controles de Navegação */}
+
+
- Página {currentPage + 1} de {story.content.pages.length}
+ Página {currentPage + 1} de {story?.content?.pages?.length}
+
+ {/* 5. Imagem da História */}
+
+ {story?.content?.pages?.[currentPage]?.image ? (
+
+ ) : (
+
+
Sem imagem para esta página
+
+ )}
+
+
+ {/* 6. Controle de Gravação */}
+
+
+
Gravação de Áudio
+
{
+ setRecordings(prev => [recording, ...prev]);
+ }}
+ />
+
+
{/* Dashboard de métricas */}