import React, { useMemo, ReactElement } from 'react'; import { TechnicalTerm } from '../types'; interface Props { text: string; bionicEnabled?: boolean; highlightTerms?: TechnicalTerm[]; currentTTSWord?: number; // Index of word being spoken for TTS highlighting } // Color palette for consistent term highlighting const TERM_COLORS = [ 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-200', 'bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-200', 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-200', 'bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200', 'bg-rose-100 dark:bg-rose-900/40 text-rose-800 dark:text-rose-200', 'bg-cyan-100 dark:bg-cyan-900/40 text-cyan-800 dark:text-cyan-200', 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-800 dark:text-indigo-200', 'bg-teal-100 dark:bg-teal-900/40 text-teal-800 dark:text-teal-200', ]; /** * Apply Bionic Reading formatting to a word * Bold the first portion of each word based on length */ const applyBionicReading = (word: string): ReactElement => { if (word.length <= 1) return <>{word}; // Calculate how many letters to bold // Short words (1-3): 1 letter // Medium words (4-6): 2 letters // Longer words (7+): ~40% of the word let boldCount: number; if (word.length <= 3) { boldCount = 1; } else if (word.length <= 6) { boldCount = 2; } else { boldCount = Math.ceil(word.length * 0.4); } const boldPart = word.slice(0, boldCount); const normalPart = word.slice(boldCount); return ( <> {boldPart} {normalPart} ); }; /** * Check if a word matches a technical term (case-insensitive) */ const findMatchingTerm = ( word: string, terms: TechnicalTerm[] ): { term: TechnicalTerm; colorClass: string } | null => { const cleanWord = word.toLowerCase().replace(/[^a-z0-9]/g, ''); for (let i = 0; i < terms.length; i++) { const termLower = terms[i].term.toLowerCase(); if (cleanWord === termLower || cleanWord.includes(termLower)) { return { term: terms[i], colorClass: terms[i].highlightColor || TERM_COLORS[i % TERM_COLORS.length] }; } } return null; }; const BionicText: React.FC = ({ text, bionicEnabled = false, highlightTerms = [], currentTTSWord }) => { const processedContent = useMemo(() => { // Split text into words while preserving whitespace and punctuation const words = text.split(/(\s+)/); let wordIndex = 0; return words.map((segment, idx) => { // If it's whitespace, just return it if (/^\s+$/.test(segment)) { return {segment}; } const currentWordIdx = wordIndex; wordIndex++; // Check if this word is currently being spoken (TTS) const isTTSActive = currentTTSWord !== undefined && currentWordIdx === currentTTSWord; // Check if this word matches a technical term const matchingTerm = highlightTerms.length > 0 ? findMatchingTerm(segment, highlightTerms) : null; // Base styling let className = ''; let content: ReactElement | string = segment; // Apply TTS highlighting (karaoke style) if (isTTSActive) { className = 'bg-brand-200 dark:bg-brand-700 px-1 rounded transition-colors duration-150'; } // Apply term highlighting if (matchingTerm) { className = `${matchingTerm.colorClass} px-1 py-0.5 rounded cursor-help transition-colors`; } // Apply bionic reading if (bionicEnabled) { content = applyBionicReading(segment); } // If it's a technical term, wrap with tooltip if (matchingTerm) { return ( {content} {/* Inline tooltip on hover */} {matchingTerm.term.term}:{' '} {matchingTerm.term.definition} ); } return ( {content} ); }); }, [text, bionicEnabled, highlightTerms, currentTTSWord]); return {processedContent}; }; /** * Hook for processing markdown content with bionic reading */ export const useBionicMarkdown = (content: string, enabled: boolean): string => { return useMemo(() => { if (!enabled) return content; // Process markdown while preserving syntax // We'll bold the first letters of words that aren't part of markdown syntax return content.replace( /(? { const boldCount = match.length <= 3 ? 1 : match.length <= 6 ? 2 : Math.ceil(match.length * 0.4); return `**${match.slice(0, boldCount)}**${match.slice(boldCount)}`; } ); }, [content, enabled]); }; export default BionicText;