mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 02:31:34 +00:00
Enhance text reading experience with new display and analysis features
Implements dark mode, font size adjustment, linguistic analysis tab, and enhanced view modes in SimpleWorkReading.tsx. 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/f2be0b84-7f59-4eb8-b9d0-aa5e90b8b093.jpg
This commit is contained in:
parent
a67e7fc530
commit
1156b1b171
@ -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<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
|
||||
themeLexicon: Record<string, string[]>; // 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<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<number | null>(null);
|
||||
const [highlightMode, setHighlightMode] = useState<
|
||||
"none" | "partOfSpeech" | "sentiment" | "meter" | "themes"
|
||||
>("none");
|
||||
const [linguisticAnalysis, setLinguisticAnalysis] = useState<LinguisticAnalysis | null>(null);
|
||||
const [isAnalysisLoading, setIsAnalysisLoading] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(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<number, PartOfSpeech[]> = {};
|
||||
const entityRecognition: Record<number, string[]> = {};
|
||||
const sentiment: Record<number, Sentiment> = {};
|
||||
const meter: Record<number, string[]> = {};
|
||||
|
||||
// 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<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);
|
||||
});
|
||||
});
|
||||
|
||||
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<string, string[]> = {};
|
||||
|
||||
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}
|
||||
>
|
||||
<Tabs defaultValue="text">
|
||||
{/* Reading settings */}
|
||||
<div className="mb-6 flex flex-wrap justify-between items-center gap-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Reading Settings</h3>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Font Size:</span>
|
||||
<div className="w-32">
|
||||
<Slider
|
||||
value={[fontSize]}
|
||||
min={12}
|
||||
max={24}
|
||||
step={1}
|
||||
onValueChange={(values) => setFontSize(values[0])}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm">{fontSize}px</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Dark Mode:</span>
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onCheckedChange={setIsDarkMode}
|
||||
/>
|
||||
{isDarkMode ? (
|
||||
<Moon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Sun className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">View Mode:</span>
|
||||
<div className="flex border rounded-md">
|
||||
<Button
|
||||
variant={viewMode === "traditional" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("traditional")}
|
||||
className="rounded-r-none"
|
||||
>
|
||||
Traditional
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "enhanced" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("enhanced")}
|
||||
className="rounded-none border-x"
|
||||
>
|
||||
Enhanced
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "parallel" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("parallel")}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
Parallel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="text">Text</TabsTrigger>
|
||||
<TabsTrigger value="annotations">Annotations</TabsTrigger>
|
||||
<TabsTrigger value="analysis">Analysis</TabsTrigger>
|
||||
{translations && translations.length > 0 && (
|
||||
<TabsTrigger value="compare">Compare</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{/* Text tab content */}
|
||||
<TabsContent value="text">
|
||||
<Card className="p-6">
|
||||
{mainContent.lines.map((line, i) => (
|
||||
<div key={i} className="mb-3">
|
||||
{line}
|
||||
{viewMode === "traditional" && (
|
||||
<div style={{ fontSize: `${fontSize}px` }}>
|
||||
{mainContent.lines.map((line, i) => (
|
||||
<div key={i} className="mb-3">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{viewMode === "enhanced" && (
|
||||
<div style={{ fontSize: `${fontSize}px` }}>
|
||||
{mainContent.lines.map((line, i) => {
|
||||
const lineNumber = mainContent.startLineNumber + i;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex group hover:bg-muted/20 rounded transition-colors mb-2 py-1"
|
||||
onClick={() => setSelectedLineNumber(lineNumber)}
|
||||
>
|
||||
<div className="text-muted-foreground w-10 flex-shrink-0 text-right pr-3 select-none">
|
||||
{lineNumber}
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
{line}
|
||||
|
||||
{/* Line actions - visible on hover */}
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-card/80 backdrop-blur-sm rounded 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">
|
||||
<LinkIcon 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>Add annotation</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === "parallel" && secondaryTranslationId === undefined && (
|
||||
<div className="text-center py-6">
|
||||
<h3 className="text-lg font-medium mb-4">Parallel View</h3>
|
||||
<p className="text-muted-foreground mb-6">Select a second translation to enable parallel view</p>
|
||||
|
||||
{translations && translations.length > 0 && (
|
||||
<div className="flex flex-wrap justify-center gap-2 max-w-md mx-auto">
|
||||
{translations
|
||||
.filter(t => t.id !== selectedTranslationId)
|
||||
.map(translation => (
|
||||
<Button
|
||||
key={translation.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSecondaryTranslationId(translation.id)}
|
||||
>
|
||||
<Languages className="mr-2 h-4 w-4" />
|
||||
{translation.language}
|
||||
</Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === "parallel" && secondaryTranslationId !== undefined && (
|
||||
<div style={{ fontSize: `${fontSize}px` }}>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium">
|
||||
{selectedTranslationId
|
||||
? `${selectedTranslation?.language} Translation`
|
||||
: `Original (${work.language})`}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="w-8"></div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium">
|
||||
{translations?.find(t => t.id === secondaryTranslationId)?.language || "Translation"}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSecondaryTranslationId(undefined)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* First column */}
|
||||
<div>
|
||||
{mainContent.lines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="mb-3 py-1 px-2 hover:bg-muted/20 rounded transition-colors"
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Second column */}
|
||||
<div>
|
||||
{getPagedContent(getSecondaryContent()).lines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="mb-3 py-1 px-2 hover:bg-muted/20 rounded transition-colors"
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Annotations tab content */}
|
||||
<TabsContent value="annotations">
|
||||
<Card className="p-6">
|
||||
<p className="text-center text-muted-foreground">No annotations yet.</p>
|
||||
{selectedLineNumber ? (
|
||||
<div>
|
||||
<div className="bg-muted/30 p-3 rounded mb-4">
|
||||
<h3 className="text-sm font-medium mb-2">Line {selectedLineNumber}</h3>
|
||||
<p className="italic">
|
||||
{getSelectedContent().split('\n')[selectedLineNumber - 1] || ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium mb-4">Annotations</h3>
|
||||
<p className="text-center text-muted-foreground py-4">No annotations for this line yet.</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="text-sm font-medium mb-2">Add your annotation</h4>
|
||||
<textarea
|
||||
className="w-full min-h-[100px] p-3 border rounded-md"
|
||||
placeholder="Share your thoughts or insights about this line..."
|
||||
/>
|
||||
<div className="flex justify-end mt-2">
|
||||
<Button>Add Annotation</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<MessageCircle className="mx-auto h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">Line Annotations</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Select a line from the text to view or add annotations.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Analysis tab content */}
|
||||
<TabsContent value="analysis">
|
||||
{isAnalysisLoading ? (
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block h-12 w-12 border-4 border-primary/30 border-t-primary rounded-full animate-spin mb-4"></div>
|
||||
<p className="text-muted-foreground">Analyzing text patterns...</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : linguisticAnalysis ? (
|
||||
<div className="space-y-6">
|
||||
{/* Analysis tools */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Analysis Tools</CardTitle>
|
||||
<CardDescription>
|
||||
Select different views to explore the linguistic patterns
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md: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 === "sentiment" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setHighlightMode(
|
||||
highlightMode === "sentiment" ? "none" : "sentiment"
|
||||
)}
|
||||
>
|
||||
<LineChart className="mr-2 h-4 w-4" />
|
||||
Sentiment
|
||||
</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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Analysis summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<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>
|
||||
</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 with analysis overlay */}
|
||||
<Card className="p-6">
|
||||
<div style={{ fontSize: `${fontSize}px` }}>
|
||||
{mainContent.lines.map((line, i) => {
|
||||
const lineNumber = mainContent.startLineNumber + i;
|
||||
let highlightInfo = null;
|
||||
|
||||
// Handle different highlight modes
|
||||
if (highlightMode === "partOfSpeech" && linguisticAnalysis.partOfSpeech[lineNumber]) {
|
||||
highlightInfo = (
|
||||
<div className="text-xs text-muted-foreground mt-1 flex gap-1 flex-wrap">
|
||||
{linguisticAnalysis.partOfSpeech[lineNumber].map((pos, j) => (
|
||||
<Badge
|
||||
key={j}
|
||||
variant="outline"
|
||||
className="border-[0.5px]"
|
||||
style={{ borderColor: pos.color }}
|
||||
>
|
||||
{pos.term}: {pos.tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else 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>
|
||||
);
|
||||
} else if (highlightMode === "meter" && linguisticAnalysis.meter[lineNumber]) {
|
||||
highlightInfo = (
|
||||
<div className="text-xs font-mono mt-1">
|
||||
{linguisticAnalysis.meter[lineNumber].join(' ')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="mb-4 p-2 rounded hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-10 text-right pr-3 text-muted-foreground select-none">
|
||||
{lineNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div>{line}</div>
|
||||
{highlightInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-8">
|
||||
<p>Click to analyze the text</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => generateLinguisticAnalysis(getSelectedContent())}
|
||||
>
|
||||
Analyze Text
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Compare tab content */}
|
||||
{translations && translations.length > 0 && (
|
||||
<TabsContent value="compare">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-medium mb-4">Compare Translations</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md: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("text");
|
||||
}}
|
||||
>
|
||||
Read Original
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Translation cards */}
|
||||
{translations.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("text");
|
||||
}}
|
||||
>
|
||||
Read Translation
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
if (viewMode !== "parallel") setViewMode("parallel");
|
||||
setSelectedTranslationId(undefined);
|
||||
setSecondaryTranslationId(translation.id);
|
||||
setActiveTab("text");
|
||||
}}
|
||||
>
|
||||
Compare with Original
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user