diff --git a/client/src/pages/works/SimpleWorkReading.tsx b/client/src/pages/works/SimpleWorkReading.tsx index 0a694b8..f188235 100644 --- a/client/src/pages/works/SimpleWorkReading.tsx +++ b/client/src/pages/works/SimpleWorkReading.tsx @@ -9,7 +9,24 @@ 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 { Card } from "@/components/ui/card"; +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"; @@ -22,9 +39,57 @@ import { Languages, Heart, Bookmark, - Share2 + 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(); @@ -33,8 +98,19 @@ export default function SimpleWorkReading() { // 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 @@ -60,6 +136,144 @@ export default function SimpleWorkReading() { } } }, [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 = () => { @@ -310,27 +524,596 @@ export default function SimpleWorkReading() { className="reading-content mb-8 prose dark:prose-invert max-w-none" ref={contentRef} > - + {/* 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 */} - {mainContent.lines.map((line, i) => ( -
- {line} + {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 */} -

No annotations yet.

+ {selectedLineNumber ? ( +
+
+

Line {selectedLineNumber}

+

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

+
+ +

Annotations

+

No annotations for this line yet.

+ +
+

Add your annotation

+