feat: Adicionando separação de sílabas

This commit is contained in:
Lucas Santana 2025-01-23 16:49:12 -03:00
parent 229a1bffbb
commit ea5c5e87f1
12 changed files with 142 additions and 17 deletions

View File

@ -51,6 +51,9 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
### Modificado
- N/A (primeira versão)
- Todas as páginas principais para usar texto adaptativo
- Componentes de exercícios para suportar transformação de texto
- Movido controle de sílabas para a página de histórias
### Removido
- N/A (primeira versão)
@ -58,17 +61,16 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
## [1.1.0] - 2024-05-20
### Adicionado
- Novo componente `TextCaseToggle` para alternar entre maiúsculas/minúsculas
- Componentes de texto adaptativo (`AdaptiveText`, `AdaptiveTitle`, `AdaptiveParagraph`)
- Hook `useUppercasePreference` para gerenciar preferência do usuário
- Suporte a texto em maiúsculas para crianças em fase de alfabetização
- Suporte a texto maiúsculo para alfabetização infantil
- Componente de alternância de caixa de texto
- Sistema de persistência de preferências
- Destaque silábico interativo para apoio à decodificação
### Modificado
- Páginas de histórias e exercícios para usar o novo sistema de texto
- Cabeçalho das páginas principal com controle de caixa de texto
- Todas as páginas principais para usar texto adaptativo
- Componentes de exercícios para suportar transformação de texto
### Técnico
- Adicionada coluna `uppercase_text_preferences` na tabela students
- Sistema de persistência de preferências via Supabase
- Otimizações de performance com memoização de componentes
- Nova coluna na tabela students
- Hook para gerenciamento de estado
- Otimizações de performance

View File

@ -0,0 +1,15 @@
## Destaque Silábico
**Objetivo:**
Facilitar a identificação das sílabas durante a leitura
**Como usar:**
1. Clique no botão "Sílabas" ao lado do título da história
2. Todas as palavras serão divididas em sílabas
3. Sílabas destacadas em fundo amarelo
4. Clique novamente para desativar
**Benefícios:**
- Auxilia na decodificação fonêmica
- Promove consciência silábica
- Facilita a leitura de palavras complexas

View File

@ -7,6 +7,8 @@ import { ExerciseFactory } from "./exercises/ExerciseFactory";
import { Timer } from "lucide-react";
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
import { cn } from "@/lib/utils";
import { AdaptiveText } from '../ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
interface ExercisePlayerProps {
exercise: PhonicsExercise;
@ -33,6 +35,7 @@ export function ExercisePlayer({
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
const updateProgress = useUpdatePhonicsProgress();
const { isUpperCase } = useUppercasePreference();
useEffect(() => {
const timer = setInterval(() => {
@ -150,6 +153,12 @@ export function ExercisePlayer({
Exercício {currentStep + 1} de {correctWords.length}
</div>
<AdaptiveText
text={exercise.instructions}
isUpperCase={isUpperCase}
className="text-lg"
/>
<ExerciseFactory
type_id={exercise.type_id}
currentWord={currentWord.word}

View File

@ -1,11 +1,13 @@
import React from 'react';
import { cn } from '../../lib/utils';
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
text: string;
isUpperCase: boolean;
as?: keyof JSX.IntrinsicElements;
preserveWhitespace?: boolean;
highlightSyllables?: boolean;
}
export const AdaptiveText = React.memo(({
@ -13,6 +15,7 @@ export const AdaptiveText = React.memo(({
isUpperCase,
as: Component = 'span',
preserveWhitespace = false,
highlightSyllables = false,
className,
...props
}: AdaptiveTextProps) => {
@ -31,7 +34,9 @@ export const AdaptiveText = React.memo(({
),
...props
},
transformedText
highlightSyllables ? (
<SyllableHighlighter text={transformedText} />
) : transformedText
);
});

View File

@ -0,0 +1,29 @@
import React from 'react';
import { splitIntoSyllables } from '../utils/syllableSplitter';
interface SyllableHighlighterProps {
text: string;
highlightColor?: string;
className?: string;
}
export function SyllableHighlighter({
text,
highlightColor = 'bg-yellow-100',
className
}: SyllableHighlighterProps) {
const syllables = splitIntoSyllables(text);
return (
<div className={className}>
{syllables.map((syllable, index) => (
<span
key={index}
className={`px-1 rounded ${highlightColor}`}
>
{syllable}
</span>
))}
</div>
);
}

View File

@ -0,0 +1,10 @@
import { useState } from 'react';
export function useSyllables() {
const [isHighlighted, setIsHighlighted] = useState(false);
return {
isHighlighted,
toggleHighlight: () => setIsHighlighted(!isHighlighted)
};
}

View File

@ -0,0 +1,7 @@
import { splitIntoSyllables } from './syllableSplitter';
test('Separação silábica básica', () => {
expect(splitIntoSyllables('casa')).toEqual(['ca', 'sa']);
expect(splitIntoSyllables('banana')).toEqual(['ba', 'na', 'na']);
expect(splitIntoSyllables('computador')).toEqual(['com', 'pu', 'ta', 'dor']);
});

View File

@ -0,0 +1,22 @@
const VOWELS = new Set(['a', 'e', 'i', 'o', 'u', 'á', 'é', 'í', 'ó', 'ú', 'â', 'ê', 'ô', 'ã', 'õ']);
export function splitIntoSyllables(word: string): string[] {
const syllables: string[] = [];
let currentSyllable = '';
for (let i = 0; i < word.length; i++) {
const char = word[i].toLowerCase();
currentSyllable += word[i];
if (VOWELS.has(char)) {
if (i < word.length - 1 && !VOWELS.has(word[i+1].toLowerCase())) {
syllables.push(currentSyllable);
currentSyllable = '';
} else if (i === word.length - 1) {
syllables.push(currentSyllable);
}
}
}
return syllables.length > 0 ? syllables : [word];
}

View File

@ -65,7 +65,10 @@ export function CreateStoryPage() {
{error && (
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
<AdaptiveText
text={error}
isUpperCase={isUpperCase}
/>
</div>
)}

View File

@ -110,7 +110,11 @@ export function ExercisePage() {
return (
<div className="container mx-auto p-6">
<div className="text-center">
<p className="text-red-600 mb-4">{error}</p>
<AdaptiveText
text={error}
isUpperCase={isUpperCase}
className="text-red-600 mb-4"
/>
<button
onClick={() => navigate(-1)}
className="text-purple-600 hover:text-purple-700"

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { ArrowLeft, ArrowRight, Volume2, Share2, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2 } from 'lucide-react';
import { ArrowLeft, ArrowRight, Volume2, Share2, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2, TextSelect } from 'lucide-react';
import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import { AudioRecorder } from '../../components/story/AudioRecorder';
@ -12,6 +12,7 @@ import { TextCaseToggle } from '../../components/ui/text-case-toggle';
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components/ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSession } from '../../hooks/useSession';
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
interface StoryRecording {
id: string;
@ -288,9 +289,11 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
<div className="mt-4">
<h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5>
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 whitespace-pre-wrap">
{recording.transcription || 'Transcrição não disponível'}
</p>
<AdaptiveText
text={recording.transcription || 'Transcrição não disponível'}
isUpperCase={isUpperCase}
className="text-sm text-gray-600 whitespace-pre-wrap"
/>
</div>
</div>
@ -391,6 +394,7 @@ export function StoryPage() {
const [isDeleting, setIsDeleting] = useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
const { isHighlighted, toggleHighlight } = useSyllables();
React.useEffect(() => {
const fetchStory = async () => {
@ -617,6 +621,15 @@ export function StoryPage() {
onToggle={toggleUppercase}
isLoading={isLoading}
/>
<button
onClick={toggleHighlight}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors text-gray-600 hover:text-gray-900 hover:bg-gray-100 border border-gray-200"
>
<TextSelect className="h-4 w-4" />
<span className="text-sm font-medium">
{isHighlighted ? 'Desativar Sílabas' : 'Ativar Sílabas'}
</span>
</button>
<button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
@ -656,6 +669,7 @@ export function StoryPage() {
<AdaptiveParagraph
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
isUpperCase={isUpperCase}
highlightSyllables={isHighlighted}
className="text-xl leading-relaxed text-gray-700 mb-8"
/>

View File

@ -11,16 +11,21 @@ import {
Menu,
X,
ChevronLeft,
ChevronRight
ChevronRight,
} from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import * as Dialog from '@radix-ui/react-dialog';
import { TextCaseToggle } from '../../components/ui/text-case-toggle';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSession } from '../../hooks/useSession';
export function StudentDashboardLayout() {
const navigate = useNavigate();
const { signOut } = useAuth();
const [isCollapsed, setIsCollapsed] = React.useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
const handleLogout = async () => {
await signOut();