import { useQuery } from "@tanstack/react-query"; // Icons import { ArrowLeft, ArrowRight, BookMarked, Bookmark, BookOpen, Copy, Heart, Languages, LineChart, Link as LinkIcon, MessageCircle, Moon, Share2, Sparkles, Sun, Waves, } from "lucide-react"; import { 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 { Skeleton } from "@/components/ui/skeleton"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; // UI Components import { Tabs, TabsContent, 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"; // Define types 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 SyllableData { count: number; breakdown: string[]; } interface RhymeInfo { word: string; lineNumber: number; rhymeGroup: string; lineText: 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 themeLexicon: Record; // Theme to related words found readabilityScore: number; // Overall text readability (0-100) } export default function SimpleWorkReading() { const { slug } = useParams(); const [, navigate] = useLocation(); const _isMobile = useMediaQuery("(max-width: 768px)"); // Main content states const [activePage, setActivePage] = useState(1); 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 [isDarkMode, setIsDarkMode] = useState(false); const [fontSize, setFontSize] = useState(16); const [activeTab, setActiveTab] = useState("text"); const [viewMode, setViewMode] = useState< "traditional" | "enhanced" | "parallel" >("traditional"); const [selectedLineNumber, setSelectedLineNumber] = useState( null, ); const [highlightMode, setHighlightMode] = useState< "none" | "partOfSpeech" | "sentiment" | "meter" | "themes" >("none"); const [linguisticAnalysis, setLinguisticAnalysis] = useState(null); const [isAnalysisLoading, setIsAnalysisLoading] = 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< TranslationWithDetails[] >({ queryKey: [`/api/works/${slug}/translations`], enabled: !!work, }); // Page validation effect 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); } } }, [work, activePage, contentToLines, getSelectedContent]); // Effect to generate linguistic analysis when needed useEffect(() => { if (work && activeTab === "analysis" && !linguisticAnalysis) { setIsAnalysisLoading(true); // In a real implementation, this would be an API call to Claude 3 or another LLM setTimeout(() => { generateLinguisticAnalysis(getSelectedContent()); setIsAnalysisLoading(false); }, 1000); } }, [ work, activeTab, linguisticAnalysis, generateLinguisticAnalysis, getSelectedContent, ]); // Get the secondary translation content (for parallel view) function getSecondaryContent() { if (!work || !secondaryTranslationId) return ""; const translation = translations?.find( (t) => t.id === secondaryTranslationId, ); return translation?.content || ""; } // Generate demo linguistic analysis for the content function generateLinguisticAnalysis(content: string) { const lines = content.split("\n"); // Part of speech examples for lines const partOfSpeech: Record = {}; const entityRecognition: Record = {}; const sentiment: Record = {}; const meter: Record = {}; // Generate sample data for part of speech, entities, sentiment and meter lines.forEach((line, index) => { if (line.trim().length === 0) return; const lineNumber = index + 1; // Create sample POS data 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 sample entity recognition if (Math.random() > 0.7) { const _entities = [ "PERSON", "LOCATION", "ORGANIZATION", "TIME", "DATE", ]; entityRecognition[lineNumber] = [ words[Math.floor(Math.random() * words.length)], ]; } // Create sample 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 sample meter pattern meter[lineNumber] = Array(words.length) .fill("") .map(() => (Math.random() > 0.5 ? "/" : "\\")); }); // Create syllable count 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 sample rhyme data 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, }); }); // Create theme lexicon data const themes = ["love", "nature", "time", "death", "art"]; const themeLexicon: Record = {}; const allWords = lines .join(" ") .split(" ") .map((w) => w.replace(/[.,;!?]/g, "").toLowerCase()) .filter((w) => w.length > 0); 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, themeLexicon, readabilityScore: Math.floor(Math.random() * 40) + 60, // 60-100 }); }; // Get the selected translation content function getSelectedContent() { if (!work) return ""; if (!selectedTranslationId) return work.content; const translation = translations?.find( (t) => t.id === selectedTranslationId, ); return translation?.content || work.content; } // Split content into lines and pages for display function contentToLines(content: string) { return content.split("\n").filter((line) => line.length > 0); } function 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), 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, }; }; // Toggle bookmark status function handleBookmarkToggle() { setIsBookmarked(!isBookmarked); toast({ description: isBookmarked ? "Removed from your bookmarks" : "Added to your bookmarks", duration: 3000, }); } // Toggle like status function handleLikeToggle() { setIsLiked(!isLiked); toast({ description: isLiked ? "Removed from your favorites" : "Added to your favorites", duration: 3000, }); } // Share the work async function handleShare() { 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 function 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" }); } } } function handlePreviousPage() { if (activePage > 1) { setActivePage(activePage - 1); // Scroll to top of content area if (contentRef.current) { contentRef.current.scrollIntoView({ behavior: "smooth" }); } } } // 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()); // Get the selected translation details const selectedTranslation = translations?.find( (t) => t.id === selectedTranslationId, ); // Calculate reading time estimation const wordCount = getSelectedContent().split(/\s+/).length; const readingTimeMinutes = Math.ceil(wordCount / 200); // Average reading speed return (
{/* Work header */}

{work.title}

{work.year && ( Published: {work.year} )} Reading time: ~{readingTimeMinutes} min
{work.tags?.map((tag) => ( {tag.name} ))}
{/* Translation selector */} {translations && translations.length > 0 && (

Translations

{translations.map((translation) => ( ))}
)} {/* Reading content */}
{/* Reading settings */}

Reading Settings

Font Size:
setFontSize(values[0])} />
{fontSize}px
Dark Mode: {isDarkMode ? ( ) : ( )}
View Mode:
{/* Main tabs */} Text Annotations Analysis {translations && translations.length > 0 && ( Compare )} {/* Text tab content */} {viewMode === "traditional" && (
{mainContent.lines.map((line, i) => (
{line}
))}
)} {viewMode === "enhanced" && (
{mainContent.lines.map((line, i) => { const lineNumber = mainContent.startLineNumber + i; return (
setSelectedLineNumber(lineNumber)} >
{lineNumber}
{line} {/* Line actions - visible on hover */}

Copy line

Copy link to line

Add annotation

); })}
)} {viewMode === "parallel" && secondaryTranslationId === undefined && (

Parallel View

Select a second translation to enable parallel view

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

No translations available for this work.

Try another work or check back later as we add more translations.

)}
)} {viewMode === "parallel" && secondaryTranslationId !== undefined && (

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

{translations?.find( (t) => t.id === secondaryTranslationId, )?.language || "Translation"}

{/* First column */}
{mainContent.lines.map((line, i) => (
{line}
))}
{/* Second column */}
{getPagedContent(getSecondaryContent()).lines.map( (line, i) => (
{line}
), )}
)}
{/* Annotations tab content */} {selectedLineNumber ? (

Line {selectedLineNumber}

{getSelectedContent().split("\n")[ selectedLineNumber - 1 ] || ""}

Annotations

No annotations for this line yet.

Add your annotation