import { useState, useEffect, useRef } from "react"; import { useParams, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { WorkWithDetails, TranslationWithDetails, Annotation } from "@/lib/types"; import { PageLayout } from "@/components/layout/PageLayout"; // UI Components import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from "@/hooks/use-toast"; import { AuthorChip } from "@/components/common/AuthorChip"; import { useMediaQuery } from "@/hooks/use-media-query"; // Icons import { BookOpen, Languages, Layers, MessageCircle, Heart, Share2, Bookmark, Sparkles, MoveHorizontal, LineChart, Waves, BookMarked, Pencil, Volume2, Copy, Link, BarChart4, ArrowLeft, ArrowRight, Menu, Settings, AlignJustify, LayoutGrid, SlidersHorizontal, Moon, Sun, MoveVertical, Search, HelpCircle, Save, Download, Eye, ExternalLink, Palette, Columns } from "lucide-react"; // Type definitions for linguistic analysis interface PartOfSpeech { term: string; tag: string; color: string; explanation: string; } interface Sentiment { score: number; // -1 to 1 label: string; // positive, negative, neutral intensity: string; // mild, moderate, strong } interface RhymeInfo { word: string; lineNumber: number; rhymeGroup: string; lineText: string; } interface SyllableData { count: number; breakdown: string[]; } interface LinguisticAnalysis { partOfSpeech: Record; // Line number to POS tags entityRecognition: Record; // Line number to entities (names, places) syllableCount: Record; // Word to syllable count sentiment: Record; // Line number to sentiment rhymes: RhymeInfo[]; // Identified rhymes meter: Record; // Line number to meter pattern complexTerms: string[]; // Words that might be difficult themeLexicon: Record; // Theme to related words found readabilityScore: number; // Overall text readability (0-100) } export default function NewWorkReading() { const { slug } = useParams(); const [, navigate] = useLocation(); const isMobile = useMediaQuery("(max-width: 768px)"); // Main content states const [activeTab, setActiveTab] = useState("read"); const [currentView, setCurrentView] = useState<"traditional" | "enhanced" | "parallel">("enhanced"); const [activePage, setActivePage] = useState(1); const [fontSize, setFontSize] = useState(18); const [lineHeight, setLineHeight] = useState(1.8); const [isDarkMode, setIsDarkMode] = useState(false); const [isFullWidth, setIsFullWidth] = useState(false); const [zenMode, setZenMode] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(!isMobile); const [selectedLanguageFilter, setSelectedLanguageFilter] = useState(null); const [selectedTranslationId, setSelectedTranslationId] = useState(undefined); const [secondaryTranslationId, setSecondaryTranslationId] = useState(undefined); const [isBookmarked, setIsBookmarked] = useState(false); const [isLiked, setIsLiked] = useState(false); const [readingProgress, setReadingProgress] = useState(0); const [selectedLineNumber, setSelectedLineNumber] = useState(null); // Linguistic analysis states const [highlightMode, setHighlightMode] = useState< "none" | "partOfSpeech" | "entities" | "sentiment" | "syllables" | "meter" | "themes" | "complexity" >("none"); const [showPhoneticTranscription, setShowPhoneticTranscription] = useState(false); const [showRhymePatterns, setShowRhymePatterns] = useState(false); const [showDefinitions, setShowDefinitions] = useState(false); const [autoPlay, setAutoPlay] = useState(false); const contentRef = useRef(null); // Queries const { data: work, isLoading: workLoading, error: workError } = useQuery({ queryKey: [`/api/works/${slug}`], }); const { data: translations, isLoading: translationsLoading } = useQuery({ queryKey: [`/api/works/${slug}/translations`], enabled: !!work, }); // Simulated linguistic analysis data (would be fetched from an API) const [linguisticAnalysis, setLinguisticAnalysis] = useState(null); const [isAnalysisLoading, setIsAnalysisLoading] = useState(false); // Generate simulated linguistic data when work loads useEffect(() => { if (work && activeTab === "analysis" && !linguisticAnalysis) { setIsAnalysisLoading(true); // In a real implementation, this would be an API call setTimeout(() => { generateLinguisticAnalysis(work.content); setIsAnalysisLoading(false); }, 1500); } }, [work, activeTab, linguisticAnalysis]); // Generate demo linguistic analysis const generateLinguisticAnalysis = (content: string) => { const lines = content.split('\n'); // Part of speech examples for the first 10 lines const partOfSpeech: Record = {}; const entityRecognition: Record = {}; const sentiment: Record = {}; const meter: Record = {}; // Sample data - in a real app, this would come from NLP API lines.forEach((line, index) => { if (line.trim().length === 0) return; const lineNumber = index + 1; // Create example POS data (randomly) const words = line.split(' ').filter(w => w.trim().length > 0); partOfSpeech[lineNumber] = words.map(word => { const tags = ['NOUN', 'VERB', 'ADJ', 'ADV', 'PRON', 'DET', 'ADP', 'CONJ', 'PRT']; const colors = ['#8884d8', '#83a6ed', '#8dd1e1', '#82ca9d', '#a4de6c', '#d0ed57', '#ffc658', '#ff8042', '#ff6361']; const explanations = ['Noun', 'Verb', 'Adjective', 'Adverb', 'Pronoun', 'Determiner', 'Adposition', 'Conjunction', 'Particle']; const randomIndex = Math.floor(Math.random() * tags.length); return { term: word.replace(/[.,;!?]/g, ''), tag: tags[randomIndex], color: colors[randomIndex], explanation: explanations[randomIndex] }; }); // Create example entity recognition if (Math.random() > 0.7) { const entities = ["PERSON", "LOCATION", "ORGANIZATION", "TIME", "DATE"]; entityRecognition[lineNumber] = [words[Math.floor(Math.random() * words.length)]]; } // Create example sentiment analysis const sentimentScore = Math.random() * 2 - 1; // -1 to 1 sentiment[lineNumber] = { score: sentimentScore, label: sentimentScore > 0.3 ? 'positive' : sentimentScore < -0.3 ? 'negative' : 'neutral', intensity: Math.abs(sentimentScore) > 0.7 ? 'strong' : Math.abs(sentimentScore) > 0.4 ? 'moderate' : 'mild' }; // Create example meter pattern const meterPatterns = ['iambic', 'trochaic', 'anapestic', 'dactylic']; const randomPattern = meterPatterns[Math.floor(Math.random() * meterPatterns.length)]; meter[lineNumber] = Array(words.length).fill('').map(() => Math.random() > 0.5 ? '/' : '\\' ); }); // Create example syllable data const syllableCount: Record = {}; const uniqueWords = new Set(); lines.forEach(line => { line.split(' ').forEach(word => { const cleanWord = word.replace(/[.,;!?]/g, '').toLowerCase(); if (cleanWord.length > 0) uniqueWords.add(cleanWord); }); }); uniqueWords.forEach(word => { const count = Math.ceil(word.length / 3); const breakdown = []; for (let i = 0; i < word.length; i += 3) { breakdown.push(word.substring(i, Math.min(i + 3, word.length))); } syllableCount[word] = { count, breakdown }; }); // Create example rhymes const rhymeGroups = ['A', 'B', 'C', 'D', 'E']; const rhymes: RhymeInfo[] = []; lines.forEach((line, index) => { if (line.trim().length === 0 || Math.random() > 0.3) return; const words = line.split(' '); if (words.length === 0) return; const lastWord = words[words.length - 1].replace(/[.,;!?]/g, ''); if (lastWord.length === 0) return; rhymes.push({ word: lastWord, lineNumber: index + 1, rhymeGroup: rhymeGroups[Math.floor(Math.random() * rhymeGroups.length)], lineText: line }); }); // Complex terms const allWords = lines.join(' ').split(' ') .map(w => w.replace(/[.,;!?]/g, '').toLowerCase()) .filter(w => w.length > 0); const complexTerms = [...new Set(allWords.filter(w => w.length > 8 || Math.random() > 0.9))]; // Theme lexicon const themes = ['love', 'nature', 'time', 'death', 'art']; const themeLexicon: Record = {}; themes.forEach(theme => { themeLexicon[theme] = allWords.filter(() => Math.random() > 0.95).slice(0, 10); }); // Set the linguistic analysis data setLinguisticAnalysis({ partOfSpeech, entityRecognition, syllableCount, sentiment, rhymes, meter, complexTerms, themeLexicon, readabilityScore: Math.floor(Math.random() * 40) + 60 // 60-100 }); }; // Get the selected translation content const getSelectedContent = () => { if (!work) return ""; if (!selectedTranslationId) return work.content; const translation = translations?.find(t => t.id === selectedTranslationId); return translation?.content || work.content; }; // Get the secondary translation content (for parallel view) const getSecondaryContent = () => { if (!work || !secondaryTranslationId) return ""; const translation = translations?.find(t => t.id === secondaryTranslationId); return translation?.content || ""; }; // Split content into lines and pages for display const contentToLines = (content: string) => { return content.split('\n').filter(line => line.length > 0); }; const getPagedContent = (content: string, linesPerPage = 20) => { const lines = contentToLines(content); const totalPages = Math.ceil(lines.length / linesPerPage); // Make sure active page is in bounds const safePage = Math.min(Math.max(1, activePage), totalPages); if (safePage !== activePage) { setActivePage(safePage); } const startIdx = (safePage - 1) * linesPerPage; const endIdx = startIdx + linesPerPage; return { lines: lines.slice(startIdx, endIdx), page: safePage, totalPages, startLineNumber: startIdx + 1 }; }; // Toggle bookmark status const handleBookmarkToggle = () => { setIsBookmarked(!isBookmarked); toast({ description: isBookmarked ? "Removed from your bookmarks" : "Added to your bookmarks", duration: 3000, }); }; // Toggle like status const handleLikeToggle = () => { setIsLiked(!isLiked); toast({ description: isLiked ? "Removed from your favorites" : "Added to your favorites", duration: 3000, }); }; // Share the work const handleShare = async () => { try { if (navigator.share) { await navigator.share({ title: work?.title || "Literary Work", text: `Reading ${work?.title || "a work"} on Tercul`, url: window.location.href }); } else { navigator.clipboard.writeText(window.location.href); toast({ description: "Link copied to clipboard", duration: 3000, }); } } catch (error) { console.error('Error sharing:', error); } }; // Handle navigation between pages const handleNextPage = () => { if (!work) return; const { totalPages } = getPagedContent(getSelectedContent()); if (activePage < totalPages) { setActivePage(activePage + 1); // Scroll to top of content area if (contentRef.current) { contentRef.current.scrollIntoView({ behavior: 'smooth' }); } } }; const handlePreviousPage = () => { if (activePage > 1) { setActivePage(activePage - 1); // Scroll to top of content area if (contentRef.current) { contentRef.current.scrollIntoView({ behavior: 'smooth' }); } } }; // Handle line selection for annotation or analysis const handleLineClick = (lineNumber: number) => { setSelectedLineNumber(lineNumber); // In a real implementation, this would: // 1. Open an annotation sidebar or modal // 2. Fetch any existing annotations for this line // 3. Allow adding new annotations }; // Loading state if (workLoading) { return (
{Array.from({ length: 15 }).map((_, i) => ( ))}
); } // Error state if (workError || !work) { return (

Work not found

The literary work you're looking for could not be found.

); } // Get content for current view and page const mainContent = getPagedContent(getSelectedContent()); const secondaryContent = getPagedContent(getSecondaryContent()); // Get the selected translation details const selectedTranslation = translations?.find(t => t.id === selectedTranslationId); const secondaryTranslation = translations?.find(t => t.id === secondaryTranslationId); // Calculate reading time estimation const wordCount = getSelectedContent().split(/\s+/).length; const readingTimeMinutes = Math.ceil(wordCount / 200); // Average reading speed // Calculate unique language options from translations const languageOptions = translations ? Array.from(new Set(translations.map(t => t.language))) : []; return (
{/* Reading progress bar - fixed at the top */}
{/* Main content area with flexible layout */}
{/* Left sidebar - collapsible on mobile */} {/* Main reading area */}
{/* Mobile header with controls */} {!zenMode && (
{/* Same content as the desktop settings drawer */}
)} {/* Reading tabs - only visible when not in zen mode */} {!zenMode && (
Read Analysis Compare
)} {/* Content based on active tab */} {activeTab === "read" && (
{/* Reading header with work title and translation info */} {!zenMode && (

{work.title}

{selectedTranslation ? (
Translation in {selectedTranslation.language} {selectedTranslation.year && ( <> {selectedTranslation.year} )}
) : (
Original in {work.language} {work.year && ( <> {work.year} )}
)}
)} {/* Reading content */}
{/* Traditional view - simple text display */} {currentView === "traditional" && (
{mainContent.lines.map((line, i) => (
{line}
))}
)} {/* Enhanced view - line numbers and interaction */} {currentView === "enhanced" && (
{mainContent.lines.map((line, i) => { const lineNumber = mainContent.startLineNumber + i; return (
handleLineClick(lineNumber)} > {/* Line number */}
{lineNumber}
{/* Line content */}
{line} {/* Line actions - visible on hover */}

Copy line

Copy link to line

Annotate

); })}
)} {/* Parallel view - show two texts side by side */} {currentView === "parallel" && (
{!secondaryTranslationId ? (

Parallel View

Select a second translation to enable parallel view

{translations && translations.length > 0 ? ( translations .filter(t => t.id !== selectedTranslationId) .map(translation => ( )) ) : (

No other translations available

)} {selectedTranslationId && ( )}
) : (
{/* First column */}

{selectedTranslationId ? `${selectedTranslation?.language} Translation` : `Original (${work.language})`}

{mainContent.lines.map((line, i) => (
{line}
))}
{/* Second column */}

{secondaryTranslation ? `${secondaryTranslation.language} Translation` : `Original (${work.language})`}

{secondaryContent.lines.map((line, i) => (
{line}
))}
)}
)}
{/* Pagination controls */} {mainContent.totalPages > 1 && !zenMode && (
Page {activePage} of {mainContent.totalPages}
)}
)} {/* Linguistic Analysis Tab */} {activeTab === "analysis" && (

Linguistic Analysis

{isAnalysisLoading ? (

Analyzing text patterns...

) : ( <> {/* Analysis controls */}

Analysis Tools

{/* Analysis details */} {linguisticAnalysis && (
Readability
Score {linguisticAnalysis.readabilityScore}/100

{linguisticAnalysis.readabilityScore > 80 ? "Very accessible text with simple language and structure" : linguisticAnalysis.readabilityScore > 60 ? "Moderately complex text suitable for general readers" : "Complex text with sophisticated vocabulary and structure"}

Structure

Lines

{contentToLines(getSelectedContent()).length}

Words

{wordCount}

Syllables

{Object.values(linguisticAnalysis.syllableCount) .reduce((sum, data) => sum + data.count, 0)}

Characters

{getSelectedContent().length}

Themes
{Object.keys(linguisticAnalysis.themeLexicon).map(theme => ( {theme} ))}
)} {/* Text content with analysis overlay */}
{mainContent.lines.map((line, i) => { const lineNumber = mainContent.startLineNumber + i; // Handle different highlight modes let lineDisplay = line; let highlightInfo = null; if (linguisticAnalysis) { // POS highlighting if (highlightMode === "partOfSpeech" && linguisticAnalysis.partOfSpeech[lineNumber]) { highlightInfo = (
{linguisticAnalysis.partOfSpeech[lineNumber].map((pos, j) => ( {pos.term}: {pos.tag} ))}
); } // Sentiment highlighting if (highlightMode === "sentiment" && linguisticAnalysis.sentiment[lineNumber]) { const sentiment = linguisticAnalysis.sentiment[lineNumber]; const sentimentColor = sentiment.label === 'positive' ? 'border-green-500' : sentiment.label === 'negative' ? 'border-red-500' : 'border-gray-400'; highlightInfo = (
{sentiment.label.charAt(0).toUpperCase() + sentiment.label.slice(1)} ({sentiment.intensity})
); } // Meter highlighting if (highlightMode === "meter" && linguisticAnalysis.meter[lineNumber]) { const meter = linguisticAnalysis.meter[lineNumber]; highlightInfo = (
{meter.join(' ')}
); } } return (
{lineNumber}
{lineDisplay}
{highlightInfo}
); })}
)}
)} {/* Compare Tab */} {activeTab === "compare" && (

Compare Translations

{/* Language filter */}

Filter by Language

{languageOptions.map(language => ( ))}
{/* Translations Grid */}
{/* Original Version Card */} Original {work.language} • {work.year || 'Unknown year'}
{contentToLines(work.content).slice(0, 10).map((line, i) => (
{line}
))} {contentToLines(work.content).length > 10 && (
... {contentToLines(work.content).length - 10} more lines
)}
{/* Translation Cards */} {translations && translations .filter(t => selectedLanguageFilter === null || t.language === selectedLanguageFilter) .map(translation => ( {translation.language} Translation {translation.translator?.displayName || `Translator #${translation.translatorId}`} {translation.year && ` • ${translation.year}`}
{contentToLines(translation.content).slice(0, 10).map((line, i) => (
{line}
))} {contentToLines(translation.content).length > 10 && (
... {contentToLines(translation.content).length - 10} more lines
)}
))}
)} {/* Zen mode exit button - only visible in zen mode */} {zenMode && (
)}
); }