"use client"; import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { detectContentType, getContentTypeLabel } from './ContentDetector'; import RawFormatter from './formatters/RawFormatter'; import TranscriptFormatter from './formatters/TranscriptFormatter'; import BookFormatter from './formatters/BookFormatter'; import MarkdownFormatter from './formatters/MarkdownFormatter'; import SourceSearchBar from './SourceSearchBar'; interface SourceReaderProps { content: string; onTextSelect?: (text: string) => void; highlightedText?: string | null; } /** * Source Reader - Read-only formatted view of source content * * This is a pure presentation component that NEVER modifies the source data. * It detects content type and applies appropriate formatting for comfortable reading. */ export default function SourceReader({ content, onTextSelect, highlightedText, }: SourceReaderProps) { const [showSearch, setShowSearch] = useState(false); const [searchHighlight, setSearchHighlight] = useState(null); const [searchMatchIndex, setSearchMatchIndex] = useState(0); const [scrollTrigger, setScrollTrigger] = useState(0); const contentRef = useRef(null); // Detect content type (memoized for performance) const contentType = useMemo(() => detectContentType(content), [content]); const contentTypeLabel = getContentTypeLabel(contentType); // Combined highlight: search takes precedence over external highlight const activeHighlight = searchHighlight || highlightedText; // Keyboard shortcut for Cmd+F useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'f') { e.preventDefault(); setShowSearch(true); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); // Scroll to current match when search highlight changes useEffect(() => { if (!searchHighlight || !contentRef.current) return; // Small delay to let React render the mark element const timer = setTimeout(() => { const currentMatch = contentRef.current?.querySelector('mark[data-search-match="current"]'); if (currentMatch) { currentMatch.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 50); return () => clearTimeout(timer); }, [searchHighlight, scrollTrigger]); const handleSearchClose = useCallback(() => { setShowSearch(false); setSearchHighlight(null); setSearchMatchIndex(0); }, []); const handleHighlightChange = useCallback((text: string | null, matchIndex: number) => { setSearchHighlight(text); setSearchMatchIndex(matchIndex); // Trigger scroll even if same text (for navigating between matches) setScrollTrigger(prev => prev + 1); }, []); // Render appropriate formatter based on content type const renderContent = () => { const highlightIndex = showSearch ? searchMatchIndex : undefined; switch (contentType) { case 'transcript': return ; case 'book': case 'article': return ; case 'markdown': return ; default: return ; } }; return (
{/* Header with content type and search */} {content && content.length >= 50 && (
Detected: {contentTypeLabel}
)} {/* Search bar */} {showSearch && content && ( )} {/* Formatted content */}
{renderContent()}
); }