import { useQuery } from "@tanstack/react-query"; // Icons import { AlignJustify, ArrowLeft, ArrowRight, BarChart4, BookMarked, Bookmark, BookOpen, Columns, Copy, Eye, Heart, Languages, LayoutGrid, LineChart, Link, Menu, MessageCircle, MoveHorizontal, Pencil, Settings, Share2, SlidersHorizontal, Sparkles, Volume2, Waves, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useLocation, useParams } from "wouter"; import { AuthorChip } from "@/components/common/AuthorChip"; import { PageLayout } from "@/components/layout/PageLayout"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { Skeleton } from "@/components/ui/skeleton"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; // UI Components import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { useMediaQuery } from "@/hooks/use-media-query"; import { toast } from "@/hooks/use-toast"; import type { TranslationWithDetails, WorkWithDetails } from "@/lib/types"; // 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< string | null >(null); const [selectedTranslationId, setSelectedTranslationId] = useState< number | undefined >(undefined); const [secondaryTranslationId, setSecondaryTranslationId] = useState< number | undefined >(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 } = useQuery< TranslationWithDetails[] >({ 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 demo linguistic analysis const generateLinguisticAnalysis = useCallback((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); }); }); Array.from(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 = Array.from( 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 }); }, []); // 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, generateLinguisticAnalysis]); // Get the selected translation content const getSelectedContent = useCallback(() => { if (!work) return ""; if (!selectedTranslationId) return work.content; const translation = translations?.find( (t) => t.id === String(selectedTranslationId), ); return translation?.content || work.content; }, [work, selectedTranslationId, translations]); // Get the secondary translation content (for parallel view) const getSecondaryContent = useCallback(() => { if (!work || !secondaryTranslationId) return ""; const translation = translations?.find( (t) => t.id === String(secondaryTranslationId), ); return translation?.content || ""; }, [work, secondaryTranslationId, translations]); // Split content into lines and pages for display const contentToLines = useCallback((content: string) => { return content.split("\n").filter((line) => line.length > 0); }, []); const getPagedContent = useCallback((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), Math.max(1, totalPages)); const startIdx = (safePage - 1) * linesPerPage; const endIdx = startIdx + linesPerPage; return { lines: lines.slice(startIdx, endIdx), page: safePage, totalPages, startLineNumber: startIdx + 1, }; }, [activePage, contentToLines]); // Add a separate effect to handle page bounds useEffect(() => { if (work) { const content = getSelectedContent(); const lines = contentToLines(content); const totalPages = Math.ceil(lines.length / 20); const safePage = Math.min( Math.max(1, activePage), Math.max(1, totalPages), ); if (safePage !== activePage) { setActivePage(safePage); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [work, activePage, getSelectedContent]); // 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 === String(selectedTranslationId), ); const secondaryTranslation = translations?.find( (t) => t.id === String(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 !== String(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 const 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 ?.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 && (
)}
); }