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 ### Modificado
- N/A (primeira versão) - 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 ### Removido
- N/A (primeira versão) - 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 ## [1.1.0] - 2024-05-20
### Adicionado ### Adicionado
- Novo componente `TextCaseToggle` para alternar entre maiúsculas/minúsculas - Suporte a texto maiúsculo para alfabetização infantil
- Componentes de texto adaptativo (`AdaptiveText`, `AdaptiveTitle`, `AdaptiveParagraph`) - Componente de alternância de caixa de texto
- Hook `useUppercasePreference` para gerenciar preferência do usuário - Sistema de persistência de preferências
- Suporte a texto em maiúsculas para crianças em fase de alfabetização - Destaque silábico interativo para apoio à decodificação
### Modificado ### Modificado
- Páginas de histórias e exercícios para usar o novo sistema de texto - Todas as páginas principais para usar texto adaptativo
- Cabeçalho das páginas principal com controle de caixa de texto
- Componentes de exercícios para suportar transformação de texto - Componentes de exercícios para suportar transformação de texto
### Técnico ### Técnico
- Adicionada coluna `uppercase_text_preferences` na tabela students - Nova coluna na tabela students
- Sistema de persistência de preferências via Supabase - Hook para gerenciamento de estado
- Otimizações de performance com memoização de componentes - 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 { Timer } from "lucide-react";
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics"; import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AdaptiveText } from '../ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
interface ExercisePlayerProps { interface ExercisePlayerProps {
exercise: PhonicsExercise; exercise: PhonicsExercise;
@ -33,6 +35,7 @@ export function ExercisePlayer({
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null); const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
const updateProgress = useUpdatePhonicsProgress(); const updateProgress = useUpdatePhonicsProgress();
const { isUpperCase } = useUppercasePreference();
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
@ -150,6 +153,12 @@ export function ExercisePlayer({
Exercício {currentStep + 1} de {correctWords.length} Exercício {currentStep + 1} de {correctWords.length}
</div> </div>
<AdaptiveText
text={exercise.instructions}
isUpperCase={isUpperCase}
className="text-lg"
/>
<ExerciseFactory <ExerciseFactory
type_id={exercise.type_id} type_id={exercise.type_id}
currentWord={currentWord.word} currentWord={currentWord.word}

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> { interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
text: string; text: string;
isUpperCase: boolean; isUpperCase: boolean;
as?: keyof JSX.IntrinsicElements; as?: keyof JSX.IntrinsicElements;
preserveWhitespace?: boolean; preserveWhitespace?: boolean;
highlightSyllables?: boolean;
} }
export const AdaptiveText = React.memo(({ export const AdaptiveText = React.memo(({
@ -13,6 +15,7 @@ export const AdaptiveText = React.memo(({
isUpperCase, isUpperCase,
as: Component = 'span', as: Component = 'span',
preserveWhitespace = false, preserveWhitespace = false,
highlightSyllables = false,
className, className,
...props ...props
}: AdaptiveTextProps) => { }: AdaptiveTextProps) => {
@ -31,7 +34,9 @@ export const AdaptiveText = React.memo(({
), ),
...props ...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 && ( {error && (
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg"> <div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
{error} <AdaptiveText
text={error}
isUpperCase={isUpperCase}
/>
</div> </div>
)} )}

View File

@ -110,7 +110,11 @@ export function ExercisePage() {
return ( return (
<div className="container mx-auto p-6"> <div className="container mx-auto p-6">
<div className="text-center"> <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 <button
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
className="text-purple-600 hover:text-purple-700" className="text-purple-600 hover:text-purple-700"

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase'; import { supabase } from '../../lib/supabase';
import { AudioRecorder } from '../../components/story/AudioRecorder'; 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 { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components/ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference'; import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSession } from '../../hooks/useSession'; import { useSession } from '../../hooks/useSession';
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
interface StoryRecording { interface StoryRecording {
id: string; id: string;
@ -288,9 +289,11 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
<div className="mt-4"> <div className="mt-4">
<h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5> <h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5>
<div className="p-4 bg-gray-50 rounded-lg"> <div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 whitespace-pre-wrap"> <AdaptiveText
{recording.transcription || 'Transcrição não disponível'} text={recording.transcription || 'Transcrição não disponível'}
</p> isUpperCase={isUpperCase}
className="text-sm text-gray-600 whitespace-pre-wrap"
/>
</div> </div>
</div> </div>
@ -391,6 +394,7 @@ export function StoryPage() {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const { session } = useSession(); const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id); const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
const { isHighlighted, toggleHighlight } = useSyllables();
React.useEffect(() => { React.useEffect(() => {
const fetchStory = async () => { const fetchStory = async () => {
@ -617,6 +621,15 @@ export function StoryPage() {
onToggle={toggleUppercase} onToggle={toggleUppercase}
isLoading={isLoading} 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 <button
onClick={handleShare} onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900" 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 <AdaptiveParagraph
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'} text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
isUpperCase={isUpperCase} isUpperCase={isUpperCase}
highlightSyllables={isHighlighted}
className="text-xl leading-relaxed text-gray-700 mb-8" className="text-xl leading-relaxed text-gray-700 mb-8"
/> />

View File

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