import { useState, useEffect, useRef } from "react"; import { useParams, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { WorkWithDetails, TranslationWithDetails } 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 { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { toast } from "@/hooks/use-toast"; import { AuthorChip } from "@/components/common/AuthorChip"; import { useMediaQuery } from "@/hooks/use-media-query"; // Icons import { BookOpen, ArrowLeft, ArrowRight, Languages, Heart, Bookmark, Share2, Sun, Moon, MessageCircle, Copy, Link as LinkIcon, LineChart, Sparkles, BookMarked, Waves, BarChart4, Columns } from "lucide-react"; // 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(undefined); const [secondaryTranslationId, setSecondaryTranslationId] = useState(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({ 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, selectedTranslationId]); // 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]); // 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 || ""; }; // Generate demo linguistic analysis for the content const 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 const 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 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), 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 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' }); } } }; // 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 => ( )) }
)}
)} {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