tercul-frontend/client/src/pages/works/NewWorkReading.tsx
mukimovd 1d35cf4e58 Revamp reading experience with enhanced text views and AI analysis
Replaces WorkReading page with NewWorkReading page, adds Anthropic AI SDK, and implements new UI components.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/4e478433-0dfc-490c-b4d2-644c52972301.jpg
2025-05-01 23:06:21 +00:00

1469 lines
62 KiB
TypeScript

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<number, PartOfSpeech[]>; // Line number to POS tags
entityRecognition: Record<number, string[]>; // Line number to entities (names, places)
syllableCount: Record<string, SyllableData>; // Word to syllable count
sentiment: Record<number, Sentiment>; // Line number to sentiment
rhymes: RhymeInfo[]; // Identified rhymes
meter: Record<number, string[]>; // Line number to meter pattern
complexTerms: string[]; // Words that might be difficult
themeLexicon: Record<string, string[]>; // 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<string>("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<number | null>(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<HTMLDivElement>(null);
// Queries
const { data: work, isLoading: workLoading, error: workError } = useQuery<WorkWithDetails>({
queryKey: [`/api/works/${slug}`],
});
const { data: translations, isLoading: translationsLoading } = useQuery<TranslationWithDetails[]>({
queryKey: [`/api/works/${slug}/translations`],
enabled: !!work,
});
// Simulated linguistic analysis data (would be fetched from an API)
const [linguisticAnalysis, setLinguisticAnalysis] = useState<LinguisticAnalysis | null>(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<number, PartOfSpeech[]> = {};
const entityRecognition: Record<number, string[]> = {};
const sentiment: Record<number, Sentiment> = {};
const meter: Record<number, string[]> = {};
// 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<string, SyllableData> = {};
const uniqueWords = new Set<string>();
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<string, string[]> = {};
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 (
<PageLayout>
<div className="max-w-full mx-auto px-4 py-8">
<div className="flex items-center space-x-4 mb-6">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<div className="flex flex-col lg:flex-row gap-8">
<div className="lg:w-64 flex-shrink-0">
<Skeleton className="h-64 w-full rounded-lg" />
</div>
<div className="flex-1">
<Skeleton className="h-8 w-3/4 mb-4" />
<div className="space-y-3">
{Array.from({ length: 15 }).map((_, i) => (
<Skeleton key={i} className="h-6 w-full" />
))}
</div>
</div>
</div>
</div>
</PageLayout>
);
}
// Error state
if (workError || !work) {
return (
<PageLayout>
<div className="max-w-[var(--content-width)] mx-auto px-4 py-16 text-center">
<h1 className="text-2xl font-bold mb-4 text-primary">Work not found</h1>
<p className="mb-6 text-muted-foreground">The literary work you're looking for could not be found.</p>
<div className="flex flex-col items-center gap-4">
<BookOpen className="h-16 w-16 text-primary/30" />
<Button onClick={() => navigate("/explore")} className="bg-primary hover:bg-primary/90 text-primary-foreground">
Explore Works
</Button>
</div>
</div>
</PageLayout>
);
}
// 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 (
<PageLayout>
<div className={`reading-experience relative ${zenMode ? 'zen-mode' : ''}`}>
{/* Reading progress bar - fixed at the top */}
<div className="sticky top-0 z-20 w-full h-1 bg-muted">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${readingProgress}%` }}
/>
</div>
{/* Main content area with flexible layout */}
<div className={`flex ${isFullWidth ? 'max-w-full' : 'max-w-[var(--content-width)]'} mx-auto transition-all duration-300`}>
{/* Left sidebar - collapsible on mobile */}
<aside className={`
context-sidebar
${sidebarOpen ? 'w-64 lg:w-72' : 'w-0 overflow-hidden'}
bg-card lg:bg-transparent
flex-shrink-0 transition-all duration-300
${!zenMode ? 'lg:sticky lg:top-16 lg:self-start lg:h-[calc(100vh-4rem)] lg:overflow-y-auto' : 'hidden'}
`}>
<div className="p-4 space-y-6">
{/* Work metadata */}
<div>
<h1 className="text-2xl font-serif font-bold text-primary mb-2">{work.title}</h1>
<div className="mb-4">
<AuthorChip author={work.author} withLifeDates />
</div>
<div className="flex flex-wrap gap-2 mb-4">
{work.tags?.map(tag => (
<Badge
key={tag.id}
variant="outline"
className="bg-primary/5 hover:bg-primary/10 text-primary/80"
>
{tag.name}
</Badge>
))}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<p>Original language: {work.language}</p>
{work.year && <p>Published: {work.year}</p>}
<p>Type: {work.type || "Unknown"}</p>
<p>Reading time: ~{readingTimeMinutes} min</p>
</div>
</div>
{/* Translation selector */}
<div>
<h3 className="text-sm font-medium mb-2 text-foreground">Translations</h3>
<div className="space-y-2">
<Button
variant={!selectedTranslationId ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedTranslationId(undefined)}
>
<Languages className="mr-2 h-4 w-4" />
Original ({work.language})
</Button>
{translations && translations.length > 0 ? (
translations.map(translation => (
<Button
key={translation.id}
variant={selectedTranslationId === translation.id ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedTranslationId(translation.id)}
>
<Languages className="mr-2 h-4 w-4" />
{translation.language}
</Button>
))
) : (
<p className="text-xs text-muted-foreground italic">No translations available</p>
)}
</div>
</div>
{/* Reading actions */}
<div>
<h3 className="text-sm font-medium mb-2 text-foreground">Actions</h3>
<div className="grid grid-cols-2 gap-2">
<Button
variant="outline"
size="sm"
className={`flex items-center justify-center gap-1 ${isLiked ? 'text-accent border-accent' : ''}`}
onClick={handleLikeToggle}
>
<Heart className={`h-4 w-4 ${isLiked ? 'fill-accent' : ''}`} />
<span className="sr-only lg:not-sr-only">{isLiked ? 'Unlike' : 'Like'}</span>
</Button>
<Button
variant="outline"
size="sm"
className={`flex items-center justify-center gap-1 ${isBookmarked ? 'text-accent border-accent' : ''}`}
onClick={handleBookmarkToggle}
>
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-accent' : ''}`} />
<span className="sr-only lg:not-sr-only">Bookmark</span>
</Button>
<Button
variant="outline"
size="sm"
className="flex items-center justify-center gap-1"
onClick={handleShare}
>
<Share2 className="h-4 w-4" />
<span className="sr-only lg:not-sr-only">Share</span>
</Button>
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center justify-center gap-1"
>
<MessageCircle className="h-4 w-4" />
<span className="sr-only lg:not-sr-only">Comments</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Work Comments</DialogTitle>
<DialogDescription>
View and add comments about the entire work.
</DialogDescription>
</DialogHeader>
<div className="h-96 overflow-y-auto">
<p className="text-center text-muted-foreground py-8">No comments yet</p>
</div>
</DialogContent>
</Dialog>
</div>
</div>
{/* Reading settings */}
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline" className="w-full">
<Settings className="mr-2 h-4 w-4" />
Reading Settings
</Button>
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-lg">
<DrawerHeader>
<DrawerTitle>Reading Settings</DrawerTitle>
<DrawerDescription>
Customize your reading experience
</DrawerDescription>
</DrawerHeader>
<div className="p-4 space-y-6">
{/* Font size control */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Font Size</span>
<span className="text-sm text-muted-foreground">{fontSize}px</span>
</div>
<Slider
value={[fontSize]}
min={14}
max={28}
step={1}
onValueChange={values => setFontSize(values[0])}
/>
</div>
{/* Line height control */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Line Spacing</span>
<span className="text-sm text-muted-foreground">{lineHeight.toFixed(1)}</span>
</div>
<Slider
value={[lineHeight * 10]}
min={10}
max={30}
step={1}
onValueChange={values => setLineHeight(values[0] / 10)}
/>
</div>
{/* View mode selector */}
<div className="space-y-2">
<span className="text-sm font-medium">View Mode</span>
<div className="grid grid-cols-3 gap-2">
<Button
variant={currentView === "traditional" ? "default" : "outline"}
size="sm"
onClick={() => setCurrentView("traditional")}
>
<AlignJustify className="mr-2 h-4 w-4" />
Traditional
</Button>
<Button
variant={currentView === "enhanced" ? "default" : "outline"}
size="sm"
onClick={() => setCurrentView("enhanced")}
>
<LayoutGrid className="mr-2 h-4 w-4" />
Enhanced
</Button>
<Button
variant={currentView === "parallel" ? "default" : "outline"}
size="sm"
onClick={() => setCurrentView("parallel")}
>
<Columns className="mr-2 h-4 w-4" />
Parallel
</Button>
</div>
</div>
{/* Other settings options */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<label className="text-sm font-medium">Dark Mode</label>
<p className="text-xs text-muted-foreground">
Reduce eye strain in low light
</p>
</div>
<Switch
checked={isDarkMode}
onCheckedChange={setIsDarkMode}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<label className="text-sm font-medium">Full Width</label>
<p className="text-xs text-muted-foreground">
Maximize reading space
</p>
</div>
<Switch
checked={isFullWidth}
onCheckedChange={setIsFullWidth}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<label className="text-sm font-medium">Zen Mode</label>
<p className="text-xs text-muted-foreground">
Hide all UI elements
</p>
</div>
<Switch
checked={zenMode}
onCheckedChange={setZenMode}
/>
</div>
</div>
</div>
<DrawerFooter>
<Button>Save Settings</Button>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
</div>
</aside>
{/* Main reading area */}
<main className="flex-1 py-6 px-4" ref={contentRef}>
{/* Mobile header with controls */}
{!zenMode && (
<div className="flex items-center justify-between mb-6 lg:hidden">
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(!sidebarOpen)}
>
<Menu className="h-5 w-5" />
</Button>
<div className="flex items-center gap-2">
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline" size="sm">
<SlidersHorizontal className="mr-2 h-4 w-4" />
Settings
</Button>
</DrawerTrigger>
<DrawerContent>
{/* Same content as the desktop settings drawer */}
</DrawerContent>
</Drawer>
<Button
variant="ghost"
size="icon"
onClick={() => setZenMode(true)}
>
<Eye className="h-5 w-5" />
</Button>
</div>
</div>
)}
{/* Reading tabs - only visible when not in zen mode */}
{!zenMode && (
<div className="mb-6">
<Tabs
defaultValue="read"
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="read">Read</TabsTrigger>
<TabsTrigger value="analysis">Analysis</TabsTrigger>
<TabsTrigger value="compare">Compare</TabsTrigger>
</TabsList>
</Tabs>
</div>
)}
{/* Content based on active tab */}
{activeTab === "read" && (
<div>
{/* Reading header with work title and translation info */}
{!zenMode && (
<div className="mb-6">
<h1 className="text-2xl lg:text-3xl font-serif font-bold text-primary mb-1">{work.title}</h1>
{selectedTranslation ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Translation in {selectedTranslation.language}</span>
{selectedTranslation.year && (
<>
<span>•</span>
<span>{selectedTranslation.year}</span>
</>
)}
</div>
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Original in {work.language}</span>
{work.year && (
<>
<span>•</span>
<span>{work.year}</span>
</>
)}
</div>
)}
</div>
)}
{/* Reading content */}
<div
className="reading-content"
style={{
fontSize: `${fontSize}px`,
lineHeight: lineHeight
}}
>
{/* Traditional view - simple text display */}
{currentView === "traditional" && (
<div className="mx-auto prose dark:prose-invert">
{mainContent.lines.map((line, i) => (
<div key={i} className="mb-1">
{line}
</div>
))}
</div>
)}
{/* Enhanced view - line numbers and interaction */}
{currentView === "enhanced" && (
<div className="mx-auto">
{mainContent.lines.map((line, i) => {
const lineNumber = mainContent.startLineNumber + i;
return (
<div
key={i}
className="flex group hover:bg-muted/30 rounded p-1 transition-colors"
onClick={() => handleLineClick(lineNumber)}
>
{/* Line number */}
<div className="text-muted-foreground w-12 text-right pr-4 select-none">
{lineNumber}
</div>
{/* Line content */}
<div className="flex-1 relative">
{line}
{/* Line actions - visible on hover */}
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-background/80 backdrop-blur-sm rounded-md flex items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy line</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Link className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy link to line</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MessageCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Annotate</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
);
})}
</div>
)}
{/* Parallel view - show two texts side by side */}
{currentView === "parallel" && (
<div>
{!secondaryTranslationId ? (
<div className="text-center py-8">
<h3 className="text-lg font-medium mb-2">Parallel View</h3>
<p className="text-muted-foreground mb-4">Select a second translation to enable parallel view</p>
<div className="space-y-2 max-w-md mx-auto">
{translations && translations.length > 0 ? (
translations
.filter(t => t.id !== selectedTranslationId)
.map(translation => (
<Button
key={translation.id}
variant="outline"
size="sm"
className="w-full justify-start"
onClick={() => setSecondaryTranslationId(translation.id)}
>
<Languages className="mr-2 h-4 w-4" />
{translation.language}
</Button>
))
) : (
<p className="text-xs text-muted-foreground italic">No other translations available</p>
)}
{selectedTranslationId && (
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={() => setSecondaryTranslationId(undefined)}
>
<Languages className="mr-2 h-4 w-4" />
Original ({work.language})
</Button>
)}
</div>
</div>
) : (
<div className="grid grid-cols-2 gap-8">
{/* First column */}
<div>
<h4 className="text-sm font-medium mb-2">
{selectedTranslationId
? `${selectedTranslation?.language} Translation`
: `Original (${work.language})`}
</h4>
<div>
{mainContent.lines.map((line, i) => (
<div
key={i}
className="mb-1 hover:bg-muted/30 p-1 rounded transition-colors"
>
{line}
</div>
))}
</div>
</div>
{/* Second column */}
<div>
<h4 className="text-sm font-medium mb-2 flex items-center justify-between">
<span>
{secondaryTranslation
? `${secondaryTranslation.language} Translation`
: `Original (${work.language})`}
</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setSecondaryTranslationId(undefined)}
>
<X className="h-4 w-4" />
</Button>
</h4>
<div>
{secondaryContent.lines.map((line, i) => (
<div
key={i}
className="mb-1 hover:bg-muted/30 p-1 rounded transition-colors"
>
{line}
</div>
))}
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Pagination controls */}
{mainContent.totalPages > 1 && !zenMode && (
<div className="flex items-center justify-between mt-8 border-t pt-4">
<Button
variant="outline"
onClick={handlePreviousPage}
disabled={activePage === 1}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<div className="text-sm text-muted-foreground">
Page {activePage} of {mainContent.totalPages}
</div>
<Button
variant="outline"
onClick={handleNextPage}
disabled={activePage === mainContent.totalPages}
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
)}
</div>
)}
{/* Linguistic Analysis Tab */}
{activeTab === "analysis" && (
<div>
<div className="mb-6">
<h2 className="text-2xl font-serif font-medium text-primary mb-4">
Linguistic Analysis
</h2>
{isAnalysisLoading ? (
<div className="space-y-4 text-center py-12">
<p className="text-muted-foreground">Analyzing text patterns...</p>
<div className="mx-auto w-24 h-24 rounded-full border-4 border-t-primary border-r-primary border-b-muted border-l-muted animate-spin" />
</div>
) : (
<>
{/* Analysis controls */}
<div className="mb-6">
<h3 className="text-sm font-medium mb-3">Analysis Tools</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<Button
variant={highlightMode === "partOfSpeech" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "partOfSpeech" ? "none" : "partOfSpeech"
)}
>
<BookMarked className="mr-2 h-4 w-4" />
Parts of Speech
</Button>
<Button
variant={highlightMode === "entities" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "entities" ? "none" : "entities"
)}
>
<Pencil className="mr-2 h-4 w-4" />
Named Entities
</Button>
<Button
variant={highlightMode === "sentiment" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "sentiment" ? "none" : "sentiment"
)}
>
<LineChart className="mr-2 h-4 w-4" />
Sentiment
</Button>
<Button
variant={highlightMode === "syllables" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "syllables" ? "none" : "syllables"
)}
>
<MoveHorizontal className="mr-2 h-4 w-4" />
Syllables
</Button>
<Button
variant={highlightMode === "meter" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "meter" ? "none" : "meter"
)}
>
<Waves className="mr-2 h-4 w-4" />
Meter
</Button>
<Button
variant={highlightMode === "themes" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "themes" ? "none" : "themes"
)}
>
<Sparkles className="mr-2 h-4 w-4" />
Themes
</Button>
<Button
variant={highlightMode === "complexity" ? "default" : "outline"}
size="sm"
onClick={() => setHighlightMode(
highlightMode === "complexity" ? "none" : "complexity"
)}
>
<BarChart4 className="mr-2 h-4 w-4" />
Complexity
</Button>
<Button
variant={showPhoneticTranscription ? "default" : "outline"}
size="sm"
onClick={() => setShowPhoneticTranscription(!showPhoneticTranscription)}
>
<Volume2 className="mr-2 h-4 w-4" />
Phonetics
</Button>
</div>
</div>
{/* Analysis details */}
{linguisticAnalysis && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Readability</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div>
<div className="flex justify-between text-sm">
<span>Score</span>
<span className="font-medium">{linguisticAnalysis.readabilityScore}/100</span>
</div>
<div className="h-2 bg-muted rounded-full mt-1">
<div
className="h-full bg-primary rounded-full"
style={{ width: `${linguisticAnalysis.readabilityScore}%` }}
/>
</div>
</div>
<p className="text-sm text-muted-foreground">
{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"}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Structure</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Lines</p>
<p className="text-2xl font-medium">{contentToLines(getSelectedContent()).length}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Words</p>
<p className="text-2xl font-medium">{wordCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Syllables</p>
<p className="text-2xl font-medium">
{Object.values(linguisticAnalysis.syllableCount)
.reduce((sum, data) => sum + data.count, 0)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Characters</p>
<p className="text-2xl font-medium">{getSelectedContent().length}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Themes</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-1">
{Object.keys(linguisticAnalysis.themeLexicon).map(theme => (
<Badge key={theme} variant="outline" className="capitalize">
{theme}
</Badge>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* Text content with analysis overlay */}
<div className="analysis-text-content mt-8">
{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 = (
<div className="text-xs text-muted-foreground mt-1 grid grid-cols-4 gap-1">
{linguisticAnalysis.partOfSpeech[lineNumber].map((pos, j) => (
<Badge key={j} variant="outline" className="justify-self-start" style={{ borderColor: pos.color }}>
{pos.term}: {pos.tag}
</Badge>
))}
</div>
);
}
// 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 = (
<div className="text-xs text-muted-foreground mt-1">
<Badge variant="outline" className={sentimentColor}>
{sentiment.label.charAt(0).toUpperCase() + sentiment.label.slice(1)}
({sentiment.intensity})
</Badge>
</div>
);
}
// Meter highlighting
if (highlightMode === "meter" && linguisticAnalysis.meter[lineNumber]) {
const meter = linguisticAnalysis.meter[lineNumber];
highlightInfo = (
<div className="text-xs mt-1 font-mono">
{meter.join(' ')}
</div>
);
}
}
return (
<div
key={i}
className="mb-4 p-2 rounded hover:bg-muted/30 transition-colors"
>
<div className="flex">
<div className="w-12 text-right pr-4 text-muted-foreground select-none">
{lineNumber}
</div>
<div className="flex-1">
<div>{lineDisplay}</div>
{highlightInfo}
</div>
</div>
</div>
);
})}
</div>
</>
)}
</div>
</div>
)}
{/* Compare Tab */}
{activeTab === "compare" && (
<div>
<div className="mb-6">
<h2 className="text-2xl font-serif font-medium text-primary mb-4">
Compare Translations
</h2>
{/* Language filter */}
<div className="mb-6">
<h3 className="text-sm font-medium mb-2">Filter by Language</h3>
<div className="flex flex-wrap gap-2">
<Button
variant={selectedLanguageFilter === null ? "default" : "outline"}
size="sm"
onClick={() => setSelectedLanguageFilter(null)}
>
All Languages
</Button>
{languageOptions.map(language => (
<Button
key={language}
variant={selectedLanguageFilter === language ? "default" : "outline"}
size="sm"
onClick={() => setSelectedLanguageFilter(language)}
>
{language}
</Button>
))}
</div>
</div>
{/* Translations Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Original Version Card */}
<Card>
<CardHeader className="pb-2">
<CardTitle>Original</CardTitle>
<CardDescription>{work.language} • {work.year || 'Unknown year'}</CardDescription>
</CardHeader>
<CardContent className="h-48 overflow-y-auto">
<div className="space-y-1 text-sm">
{contentToLines(work.content).slice(0, 10).map((line, i) => (
<div key={i} className="text-muted-foreground hover:text-foreground transition-colors">
{line}
</div>
))}
{contentToLines(work.content).length > 10 && (
<div className="italic text-muted-foreground">
... {contentToLines(work.content).length - 10} more lines
</div>
)}
</div>
</CardContent>
<CardFooter>
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
setSelectedTranslationId(undefined);
setActiveTab("read");
}}
>
Read Original
</Button>
</CardFooter>
</Card>
{/* Translation Cards */}
{translations && translations
.filter(t => selectedLanguageFilter === null || t.language === selectedLanguageFilter)
.map(translation => (
<Card key={translation.id}>
<CardHeader className="pb-2">
<CardTitle>{translation.language} Translation</CardTitle>
<CardDescription>
{translation.translator?.displayName || `Translator #${translation.translatorId}`}
{translation.year && `${translation.year}`}
</CardDescription>
</CardHeader>
<CardContent className="h-48 overflow-y-auto">
<div className="space-y-1 text-sm">
{contentToLines(translation.content).slice(0, 10).map((line, i) => (
<div key={i} className="text-muted-foreground hover:text-foreground transition-colors">
{line}
</div>
))}
{contentToLines(translation.content).length > 10 && (
<div className="italic text-muted-foreground">
... {contentToLines(translation.content).length - 10} more lines
</div>
)}
</div>
</CardContent>
<CardFooter className="flex flex-col gap-2">
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
setSelectedTranslationId(translation.id);
setActiveTab("read");
}}
>
Read Translation
</Button>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={() => {
if (currentView !== "parallel") setCurrentView("parallel");
setSelectedTranslationId(undefined);
setSecondaryTranslationId(translation.id);
setActiveTab("read");
}}
>
Compare with Original
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
</div>
)}
{/* Zen mode exit button - only visible in zen mode */}
{zenMode && (
<div className="fixed bottom-6 right-6 bg-background/80 backdrop-blur-sm rounded-full shadow-lg opacity-30 hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
onClick={() => setZenMode(false)}
className="h-12 w-12 rounded-full"
>
<X className="h-6 w-6" />
</Button>
</div>
)}
</main>
</div>
</div>
</PageLayout>
);
}