improved literary analysis pgae and added compare works sub page

This commit is contained in:
Harivansh Rathi 2024-12-04 12:38:18 -05:00
parent 0134c81526
commit 5ca37d03f2
13 changed files with 3953 additions and 59 deletions

2488
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@mui/icons-material": "^6.1.9",
"@mui/material": "^6.1.9",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
@ -42,9 +46,11 @@
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@types/d3": "^7.4.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"d3": "^7.9.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
@ -53,6 +59,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-force-graph": "^1.44.7",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3", "react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",

View file

@ -7,6 +7,8 @@ import BlogPost from './pages/BlogPost/BlogPost';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
import SuccessStories from './pages/SuccessStories'; import SuccessStories from './pages/SuccessStories';
import Analysis from './pages/Analysis'; import Analysis from './pages/Analysis';
import ComparativeAnalysis from './pages/ComparativeAnalysis';
import NetworkVisualization from './pages/NetworkVisualization';
// Lazy load other pages // Lazy load other pages
const Quiz = React.lazy(() => import('./pages/Quiz')); const Quiz = React.lazy(() => import('./pages/Quiz'));
@ -16,7 +18,7 @@ const MarketCalculator = React.lazy(() => import('./pages/MarketCalculator'));
function App() { function App() {
return ( return (
<Router> <Router basename="/">
<MainLayout> <MainLayout>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
@ -30,6 +32,9 @@ function App() {
<Route path="/success-stories" element={<SuccessStories />} /> <Route path="/success-stories" element={<SuccessStories />} />
<Route path="/market-calculator" element={<MarketCalculator />} /> <Route path="/market-calculator" element={<MarketCalculator />} />
<Route path="/analysis" element={<Analysis />} /> <Route path="/analysis" element={<Analysis />} />
<Route path="/comparative" element={<ComparativeAnalysis />} />
<Route path="/network" element={<NetworkVisualization />} />
<Route path="*" element={<div>Page not found</div>} />
</Routes> </Routes>
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>

View file

@ -0,0 +1,19 @@
// For smaller network visualizations in subpages
const subGraphConfig = {
nodeRelSize: 4,
linkDistance: 80,
d3: {
alphaDecay: 0.05, // Faster decay for smaller graphs
alphaMin: 0.001,
velocityDecay: 1
},
cooldownTicks: 30,
cooldownTime: 1500,
};
// Use static positioning for very small networks
const staticLayout = {
enableNodeDrag: false,
staticGraph: true,
nodePosition: { x: node => node.initialX, y: node => node.initialY }
};

View file

@ -0,0 +1,132 @@
import { useRef, useCallback, useState, useEffect } from 'react';
const InteractiveGraph = ({ graphData, onNodeSelect }) => {
const graphRef = useRef();
const [selectedNode, setSelectedNode] = useState(null);
const clickTimeoutRef = useRef(null);
const [isProcessingClick, setIsProcessingClick] = useState(false);
// Cleanup function for the timeout
useEffect(() => {
return () => {
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
}
};
}, []);
const handleNodeClick = useCallback((node) => {
if (!node || isProcessingClick) return;
// Prevent multiple clicks
setIsProcessingClick(true);
// Clear any existing timeout
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
}
// Set the selected node immediately
setSelectedNode(node);
onNodeSelect(node);
// Reset the processing flag after a short delay
clickTimeoutRef.current = setTimeout(() => {
setIsProcessingClick(false);
}, 300); // 300ms debounce
}, [onNodeSelect, isProcessingClick]);
// Calculate static positions based on container size
const containerWidth = 600;
const containerHeight = 600;
const graphConfig = {
nodeRelSize: 8,
nodeVal: 30,
width: containerWidth,
height: containerHeight,
backgroundColor: "#ffffff",
nodeColor: node => node.type === 'protagonist' ? "#4CAF50" :
node.type === 'antagonist' ? "#f44336" : "#2196F3",
// Static layout configuration
staticGraph: true,
nodePosition: node => ({
x: containerWidth/2 + Math.cos(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerWidth/3),
y: containerHeight/2 + Math.sin(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerHeight/3)
}),
// Interaction settings
enableNodeDrag: false,
enableZoom: true,
minZoom: 0.5,
maxZoom: 2.5,
cooldownTime: 0, // Disable force simulation cooldown
warmupTicks: 0, // Disable initial ticks
// Node appearance
nodeResolution: 16,
// Completely disable all tooltips/labels
nodeLabel: null,
nodeCanvasObject: null,
nodeCanvasObjectMode: null,
enableNodeHover: false,
enableLinkHover: false,
// Link appearance
linkWidth: 2,
linkColor: () => "#999999",
linkOpacity: 0.6,
};
return (
<div style={{
display: 'flex',
gap: '20px',
width: '100%',
maxWidth: '1000px',
margin: '0 auto'
}}>
<div style={{
width: containerWidth,
height: containerHeight,
border: '1px solid #eee',
position: 'relative'
}}>
<ForceGraph
{...graphConfig}
ref={graphRef}
graphData={graphData}
onNodeClick={handleNodeClick}
/>
</div>
{/* Right side popup */}
<div style={{
width: '300px',
padding: '20px',
backgroundColor: '#fff',
border: '1px solid #eee',
borderRadius: '4px',
height: 'fit-content',
visibility: selectedNode ? 'visible' : 'hidden',
opacity: selectedNode ? 1 : 0,
transition: 'opacity 0.2s ease-in-out'
}}>
{selectedNode && (
<>
<h3 style={{ margin: '0 0 10px 0' }}>{selectedNode.name}</h3>
<p style={{ margin: '0 0 10px 0' }}>Type: {selectedNode.type}</p>
<p style={{ margin: '0 0 10px 0' }}>Novel: {selectedNode.novel}</p>
{selectedNode.class && (
<p style={{ margin: '0 0 10px 0' }}>Class: {selectedNode.class}</p>
)}
</>
)}
</div>
</div>
);
};
export default InteractiveGraph;

View file

@ -30,12 +30,20 @@ const Navbar = () => {
<Link to="/market-calculator" className="text-sage-700 hover:text-sage-900 transition-colors"> <Link to="/market-calculator" className="text-sage-700 hover:text-sage-900 transition-colors">
Market Value Market Value
</Link> </Link>
<Link <div className="flex items-center space-x-2">
to="/analysis" <Link
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors" to="/analysis"
> className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
Literary Analysis >
</Link> Literary Analysis
</Link>
<Link
to="/network"
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
>
Character Network
</Link>
</div>
</nav> </nav>
</div> </div>
</div> </div>

View file

@ -3,15 +3,19 @@ import Navbar from '../Navbar';
const MainLayout = ({ children }: { children: React.ReactNode }) => { const MainLayout = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div className="min-h-screen bg-cream-50"> <div className="min-h-screen bg-cream-50 flex flex-col">
<Navbar /> <Navbar />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="flex-1 flex flex-col">
{children} <div className="flex-1 container mx-auto px-4 sm:px-6 lg:px-8 py-8 flex flex-col">
<div className="flex-1 flex flex-col">
{children}
</div>
</div>
</main> </main>
<footer className="bg-sage-100 border-t border-sage-200 mt-12"> <footer className="bg-sage-100 border-t border-sage-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center font-cormorant text-sage-900"> <div className="text-center font-cormorant text-sage-900">
<p className="text-lg">"It is a truth universally acknowledged..."</p> <p className="text-lg">"It is a truth universally acknowledged..."</p>
<p className="mt-2">© {new Date().getFullYear()} Austen's Wedding Guide</p> <p className="mt-2">© {new Date().getFullYear()} Austen's Wedding Guide</p>

View file

@ -0,0 +1,461 @@
import { CharacterNetwork } from '../types/character-network';
export const characterNetwork: CharacterNetwork = {
books: [
{
id: "pride-and-prejudice",
name: "Pride and Prejudice",
type: "book",
color: "#e91e63",
description: "A masterpiece of wit and social commentary exploring marriage, class, and prejudice in Regency England.",
year: 1813
},
{
id: "sense-and-sensibility",
name: "Sense and Sensibility",
type: "book",
color: "#2196f3",
description: "A story of two sisters with contrasting temperaments navigating love and heartbreak.",
year: 1811
},
{
id: "northanger-abbey",
name: "Northanger Abbey",
type: "book",
color: "#4caf50",
description: "A coming-of-age story that parodies Gothic novels while exploring the perils of confusing fiction with reality.",
year: 1818
},
{
id: "mansfield-park",
name: "Mansfield Park",
type: "book",
color: "#9c27b0",
description: "A complex exploration of morality, social position, and personal integrity through the story of Fanny Price.",
year: 1814
},
{
id: "longbourn",
name: "Longbourn",
type: "book",
color: "#795548",
description: "A retelling of Pride and Prejudice from the servants' perspective, exploring class and social inequality.",
year: 2013
}
],
nodes: [
// Pride and Prejudice
{
id: "elizabeth-bennet",
name: "Elizabeth Bennet",
novel: "Pride and Prejudice",
class: "middle",
type: "protagonist",
description: "The witty and independent second daughter of the Bennet family, known for her intelligence and prejudiced first impressions."
},
{
id: "darcy",
name: "Fitzwilliam Darcy",
novel: "Pride and Prejudice",
class: "upper",
type: "protagonist",
description: "A wealthy and proud nobleman who learns to overcome his prejudices and rigid social expectations."
},
{
id: "jane-bennet",
name: "Jane Bennet",
novel: "Pride and Prejudice",
class: "middle",
type: "supporting",
description: "The beautiful and gentle eldest Bennet daughter, whose sweet nature leads her to see only the good in others."
},
{
id: "bingley",
name: "Charles Bingley",
novel: "Pride and Prejudice",
class: "upper",
type: "supporting",
description: "Darcy's wealthy and amiable friend who falls in love with Jane Bennet, easily influenced by others."
},
{
id: "lydia-bennet",
name: "Lydia Bennet",
novel: "Pride and Prejudice",
class: "middle",
type: "supporting",
description: "The youngest Bennet sister, whose reckless behavior threatens her family's reputation."
},
{
id: "wickham",
name: "George Wickham",
novel: "Pride and Prejudice",
class: "middle",
type: "antagonist",
description: "A charming but deceitful officer who causes trouble for both the Bennets and Darcy."
},
// Sense and Sensibility
{
id: "elinor-dashwood",
name: "Elinor Dashwood",
novel: "Sense and Sensibility",
class: "middle",
type: "protagonist",
description: "The sensible and reserved elder Dashwood sister, who represents sense and emotional restraint."
},
{
id: "marianne-dashwood",
name: "Marianne Dashwood",
novel: "Sense and Sensibility",
class: "middle",
type: "protagonist",
description: "The emotional and romantic younger Dashwood sister, who learns to balance sensibility with sense."
},
{
id: "edward-ferrars",
name: "Edward Ferrars",
novel: "Sense and Sensibility",
class: "upper",
type: "supporting",
description: "A gentle, reserved man who forms an attachment to Elinor despite his secret engagement."
},
{
id: "willoughby",
name: "John Willoughby",
novel: "Sense and Sensibility",
class: "upper",
type: "antagonist",
description: "A dashing young man who captures Marianne's heart but proves morally bankrupt."
},
{
id: "colonel-brandon",
name: "Colonel Brandon",
novel: "Sense and Sensibility",
class: "upper",
type: "supporting",
description: "A mature and honorable man who quietly loves Marianne and proves his worth through actions."
},
// Northanger Abbey
{
id: "catherine-morland",
name: "Catherine Morland",
novel: "Northanger Abbey",
class: "middle",
type: "protagonist",
description: "A naive but charming young woman whose love of Gothic novels colors her view of reality."
},
{
id: "henry-tilney",
name: "Henry Tilney",
novel: "Northanger Abbey",
class: "upper",
type: "protagonist",
description: "An intelligent and witty clergyman who guides Catherine's maturation while falling in love with her."
},
{
id: "isabella-thorpe",
name: "Isabella Thorpe",
novel: "Northanger Abbey",
class: "middle",
type: "antagonist",
description: "A manipulative friend who pretends affection for Catherine's brother while pursuing others."
},
{
id: "john-thorpe",
name: "John Thorpe",
novel: "Northanger Abbey",
class: "middle",
type: "antagonist",
description: "Isabella's boastful and dishonest brother who pursues Catherine for her perceived wealth."
},
// Mansfield Park
{
id: "fanny-price",
name: "Fanny Price",
novel: "Mansfield Park",
class: "lower",
type: "protagonist",
description: "A modest and moral young woman taken in by her wealthy relatives, steadfast in her principles."
},
{
id: "edmund-bertram",
name: "Edmund Bertram",
novel: "Mansfield Park",
class: "upper",
type: "protagonist",
description: "Fanny's cousin and friend, a would-be clergyman who is temporarily led astray by Mary Crawford."
},
{
id: "mary-crawford",
name: "Mary Crawford",
novel: "Mansfield Park",
class: "upper",
type: "antagonist",
description: "A charming but worldly woman who attracts Edmund while representing modern urban values."
},
{
id: "henry-crawford",
name: "Henry Crawford",
novel: "Mansfield Park",
class: "upper",
type: "antagonist",
description: "Mary's brother, a charismatic man who pursues Fanny after toying with her cousins' affections."
},
// Longbourn
{
id: "sarah",
name: "Sarah",
novel: "Longbourn",
class: "lower",
type: "protagonist",
description: "A hardworking housemaid at Longbourn with dreams beyond her station."
},
{
id: "james-smith",
name: "James Smith",
novel: "Longbourn",
class: "lower",
type: "protagonist",
description: "The mysterious new footman with a hidden past, who catches Sarah's attention."
},
{
id: "mrs-hill",
name: "Mrs. Hill",
novel: "Longbourn",
class: "lower",
type: "supporting",
description: "The housekeeper of Longbourn, who holds many secrets about the household."
},
{
id: "polly",
name: "Polly",
novel: "Longbourn",
class: "lower",
type: "supporting",
description: "The young housemaid learning her duties alongside Sarah."
}
],
relationships: [
// Pride and Prejudice Relationships
{
source: "elizabeth-bennet",
target: "jane-bennet",
type: "family",
description: "Sisters and closest confidantes",
development: [
"Share sisterly bond throughout the novel",
"Support each other through romantic trials",
"Maintain trust and confidence in each other"
]
},
{
source: "elizabeth-bennet",
target: "darcy",
type: "romance",
description: "Central romance overcoming pride and prejudice",
development: [
"Initial mutual dislike",
"Growing understanding through letters and visits",
"Eventual recognition of true character",
"Marriage"
]
},
{
source: "jane-bennet",
target: "bingley",
type: "romance",
description: "Love at first sight couple",
development: [
"Immediate mutual attraction",
"Separation due to social interference",
"Reunion and marriage"
]
},
{
source: "lydia-bennet",
target: "wickham",
type: "romance",
description: "Scandalous elopement",
development: [
"Flirtation and infatuation",
"Reckless elopement",
"Forced marriage through Darcy's intervention"
]
},
// Sense and Sensibility Relationships
{
source: "elinor-dashwood",
target: "marianne-dashwood",
type: "family",
description: "Sisters representing sense and sensibility",
development: [
"Contrasting approaches to emotion",
"Support through family difficulties",
"Mutual growth and understanding"
]
},
{
source: "elinor-dashwood",
target: "edward-ferrars",
type: "romance",
description: "Reserved romance complicated by duty",
development: [
"Quiet mutual attraction",
"Separation due to prior engagement",
"Eventual reunion and marriage"
]
},
{
source: "marianne-dashwood",
target: "willoughby",
type: "romance",
description: "Passionate but doomed romance",
development: [
"Intense initial attraction",
"Shared romantic sensibilities",
"Heartbreaking separation",
"Eventual disillusionment"
]
},
{
source: "marianne-dashwood",
target: "colonel-brandon",
type: "romance",
description: "Mature love developing from friendship",
development: [
"Initial indifference from Marianne",
"Growing appreciation through trials",
"Marriage based on deeper understanding"
]
},
// Northanger Abbey Relationships
{
source: "catherine-morland",
target: "henry-tilney",
type: "romance",
description: "Educational romance",
development: [
"Initial friendship and guidance",
"Growing romantic attachment",
"Overcoming misunderstandings",
"Marriage despite family opposition"
]
},
{
source: "catherine-morland",
target: "isabella-thorpe",
type: "friendship",
description: "False friendship",
development: [
"Initial close friendship",
"Growing awareness of Isabella's true nature",
"Betrayal and disillusionment"
]
},
// Mansfield Park Relationships
{
source: "fanny-price",
target: "edmund-bertram",
type: "romance",
description: "Constant love tested by rival",
development: [
"Childhood friendship",
"Fanny's secret love",
"Edmund's infatuation with Mary",
"Final recognition of true feelings"
]
},
{
source: "edmund-bertram",
target: "mary-crawford",
type: "romance",
description: "Tempting but unsuitable attachment",
development: [
"Initial attraction",
"Growing awareness of moral differences",
"Final disillusionment"
]
},
{
source: "fanny-price",
target: "henry-crawford",
type: "rivalry",
description: "Unwanted pursuit",
development: [
"Henry's initial indifference",
"Determined pursuit of Fanny",
"Fanny's steadfast refusal",
"Henry's moral failure"
]
},
// Longbourn Relationships
{
source: "sarah",
target: "james-smith",
type: "romance",
description: "A romance that crosses social boundaries",
development: [
"Initial curiosity about the mysterious new footman",
"Growing understanding of each other's struggles",
"Shared moments during daily tasks",
"Overcoming past secrets and social barriers"
]
},
{
source: "sarah",
target: "mrs-hill",
type: "family",
description: "Mentor-like relationship",
development: [
"Mrs. Hill's guidance in household duties",
"Growing trust and confidence",
"Sharing of household secrets",
"Maternal care and protection"
]
},
{
source: "sarah",
target: "polly",
type: "friendship",
description: "Fellow servants and friends",
development: [
"Teaching Polly household duties",
"Supporting each other through daily challenges",
"Sharing dreams and aspirations"
]
},
// Add connections between Longbourn and Pride and Prejudice characters
{
source: "sarah",
target: "elizabeth-bennet",
type: "friendship",
description: "Servant and mistress with mutual respect",
development: [
"Sarah's observations of Elizabeth's character",
"Growing understanding across social boundaries",
"Shared experiences from different perspectives"
]
},
{
source: "mrs-hill",
target: "elizabeth-bennet",
type: "family",
description: "Housekeeper's care for the Bennet family",
development: [
"Years of service to the Bennet family",
"Protection of family secrets",
"Maternal guidance from below stairs"
]
},
{
source: "james-smith",
target: "elizabeth-bennet",
type: "friendship",
description: "Distant but respectful relationship",
development: [
"Initial wariness",
"Growing trust through service",
"Understanding of social positions"
]
}
]
};

View file

@ -0,0 +1,163 @@
interface ThemeComparison {
theme: string;
description: string;
appearances: {
novel: string;
manifestation: string;
examples: {
quote: string;
context: string;
analysis: string;
}[];
significance: string;
}[];
}
interface CharacterType {
archetype: string;
description: string;
examples: {
novel: string;
character: string;
analysis: string;
evolution: string;
keyQuotes: {
quote: string;
context: string;
}[];
}[];
}
interface WritingStyleElement {
technique: string;
description: string;
evolution: {
novel: string;
usage: string;
examples: string[];
significance: string;
}[];
}
export const themeComparisons: ThemeComparison[] = [
{
theme: "Marriage and Economic Reality",
description: "The intersection of marriage, economic necessity, and social mobility across Austen's works",
appearances: [
{
novel: "Pride and Prejudice",
manifestation: "Marriage market and social climbing",
examples: [
{
quote: "It is a truth universally acknowledged...",
context: "Opening line",
analysis: "Establishes the economic premise of marriage in Regency society"
}
],
significance: "Direct examination of marriage as economic transaction"
},
{
novel: "Sense and Sensibility",
manifestation: "Financial vulnerability of women",
examples: [
{
quote: "What have wealth or grandeur to do with happiness?",
context: "Marianne's naive view",
analysis: "Contrasts romantic ideals with economic reality"
}
],
significance: "Explores the harsh realities of women's financial dependence"
}
]
},
{
theme: "Social Class and Mobility",
description: "The examination of class boundaries and social movement in Regency society",
appearances: [
{
novel: "Mansfield Park",
manifestation: "Class consciousness and moral worth",
examples: [
{
quote: "We have all a better guide in ourselves...",
context: "Fanny's moral stance",
analysis: "Links class position with moral character"
}
],
significance: "Explores the relationship between social position and moral integrity"
},
{
novel: "Northanger Abbey",
manifestation: "Social climbing and authenticity",
examples: [
{
quote: "No one who had ever seen Catherine Morland in her infancy...",
context: "Opening description",
analysis: "Subverts expectations of class and heroism"
}
],
significance: "Questions the relationship between social status and personal worth"
}
]
}
];
export const characterTypes: CharacterType[] = [
{
archetype: "The Witty Heroine",
description: "Intelligent, spirited female protagonists who challenge social norms",
examples: [
{
novel: "Pride and Prejudice",
character: "Elizabeth Bennet",
analysis: "Combines wit with social observation",
evolution: "Learns to balance judgment with understanding",
keyQuotes: [
{
quote: "I could easily forgive his pride, if he had not mortified mine",
context: "Early judgment of Darcy"
}
]
},
{
novel: "Emma",
character: "Emma Woodhouse",
analysis: "Uses wit but must learn its proper application",
evolution: "Develops from clever manipulation to genuine understanding",
keyQuotes: [
{
quote: "I seem to have been doomed to blindness",
context: "Moment of self-realization"
}
]
}
]
}
];
export const writingStyleEvolution: WritingStyleElement[] = [
{
technique: "Free Indirect Discourse",
description: "Narrative technique blending character and narrator perspectives",
evolution: [
{
novel: "Northanger Abbey",
usage: "Early experimentation with narrative voice",
examples: [
"Commentary on Gothic conventions",
"Catherine's naive perspectives"
],
significance: "Develops ironic distance while maintaining character sympathy"
},
{
novel: "Emma",
usage: "Sophisticated deployment for character insight",
examples: [
"Emma's self-deceptions",
"Social observations"
],
significance: "Achieves complex character psychology and social commentary"
}
]
}
];

View file

@ -1,5 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { novelAnalyses } from '../data/literary-analysis'; import { novelAnalyses } from '../data/literary-analysis';
import { useNavigate } from 'react-router-dom';
import type { NovelAnalysis, ThematicElement, CharacterAnalysis, SocialCommentary, LiteraryDevice } from '../data/literary-analysis';
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
@ -21,9 +23,13 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "../components/ui/select"; } from "../components/ui/select";
import { Button } from "../components/ui/button";
type NovelKey = keyof typeof novelAnalyses;
const Analysis = () => { const Analysis = () => {
const [selectedNovel, setSelectedNovel] = useState('prideAndPrejudice'); const [selectedNovel, setSelectedNovel] = useState<NovelKey>('prideAndPrejudice');
const navigate = useNavigate();
const analysis = novelAnalyses[selectedNovel]; const analysis = novelAnalyses[selectedNovel];
if (!analysis) { if (!analysis) {
@ -40,10 +46,10 @@ const Analysis = () => {
<h1 className="font-cormorant text-4xl text-sage-900"> <h1 className="font-cormorant text-4xl text-sage-900">
Literary Analysis Literary Analysis
</h1> </h1>
<div className="max-w-xs mx-auto"> <div className="max-w-xs mx-auto flex items-center gap-2">
<Select <Select
value={selectedNovel} value={selectedNovel}
onValueChange={setSelectedNovel} onValueChange={(value: NovelKey) => setSelectedNovel(value)}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Select a novel" /> <SelectValue placeholder="Select a novel" />
@ -55,6 +61,13 @@ const Analysis = () => {
<SelectItem value="mansfieldPark">Mansfield Park (1814)</SelectItem> <SelectItem value="mansfieldPark">Mansfield Park (1814)</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="outline"
className="whitespace-nowrap"
onClick={() => navigate('/comparative')}
>
Compare Works
</Button>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h2 className="font-cormorant text-2xl text-sage-900">{analysis.title}</h2> <h2 className="font-cormorant text-2xl text-sage-900">{analysis.title}</h2>
@ -73,7 +86,7 @@ const Analysis = () => {
</TabsList> </TabsList>
<TabsContent value="themes" className="space-y-4"> <TabsContent value="themes" className="space-y-4">
{analysis.mainThemes.map((theme, index) => ( {analysis.mainThemes.map((theme: ThematicElement, index: number) => (
<Card key={index}> <Card key={index}>
<CardHeader> <CardHeader>
<CardTitle>{theme.theme}</CardTitle> <CardTitle>{theme.theme}</CardTitle>
@ -82,7 +95,7 @@ const Analysis = () => {
<CardContent> <CardContent>
<ScrollArea className="h-[300px] rounded-md border p-4"> <ScrollArea className="h-[300px] rounded-md border p-4">
<div className="space-y-4"> <div className="space-y-4">
{theme.examples.map((example, i) => ( {theme.examples.map((example, i: number) => (
<div key={i} className="space-y-2"> <div key={i} className="space-y-2">
<blockquote className="border-l-2 border-sage-300 pl-4 italic"> <blockquote className="border-l-2 border-sage-300 pl-4 italic">
"{example.quote}" "{example.quote}"
@ -105,7 +118,7 @@ const Analysis = () => {
</TabsContent> </TabsContent>
<TabsContent value="characters" className="space-y-4"> <TabsContent value="characters" className="space-y-4">
{analysis.characterAnalysis.map((character, index) => ( {analysis.characterAnalysis.map((character: CharacterAnalysis, index: number) => (
<Card key={index}> <Card key={index}>
<CardHeader> <CardHeader>
<CardTitle>{character.character}</CardTitle> <CardTitle>{character.character}</CardTitle>
@ -124,7 +137,7 @@ const Analysis = () => {
</div> </div>
<div> <div>
<h4 className="font-semibold text-sage-900">Key Quotes</h4> <h4 className="font-semibold text-sage-900">Key Quotes</h4>
{character.keyQuotes.map((quote, i) => ( {character.keyQuotes.map((quote, i: number) => (
<div key={i} className="mt-2 space-y-2"> <div key={i} className="mt-2 space-y-2">
<blockquote className="border-l-2 border-sage-300 pl-4 italic"> <blockquote className="border-l-2 border-sage-300 pl-4 italic">
"{quote.quote}" "{quote.quote}"
@ -144,7 +157,7 @@ const Analysis = () => {
</TabsContent> </TabsContent>
<TabsContent value="social" className="space-y-4"> <TabsContent value="social" className="space-y-4">
{analysis.socialCommentary.map((topic, index) => ( {analysis.socialCommentary.map((topic: SocialCommentary, index: number) => (
<Card key={index}> <Card key={index}>
<CardHeader> <CardHeader>
<CardTitle>{topic.topic}</CardTitle> <CardTitle>{topic.topic}</CardTitle>
@ -163,7 +176,7 @@ const Analysis = () => {
<div> <div>
<h4 className="font-semibold text-sage-900">Examples</h4> <h4 className="font-semibold text-sage-900">Examples</h4>
<ul className="list-disc list-inside text-sage-700"> <ul className="list-disc list-inside text-sage-700">
{topic.examples.map((example, i) => ( {topic.examples.map((example: string, i: number) => (
<li key={i}>{example}</li> <li key={i}>{example}</li>
))} ))}
</ul> </ul>
@ -176,7 +189,7 @@ const Analysis = () => {
</TabsContent> </TabsContent>
<TabsContent value="literary" className="space-y-4"> <TabsContent value="literary" className="space-y-4">
{analysis.literaryDevices.map((device, index) => ( {analysis.literaryDevices.map((device: LiteraryDevice, index: number) => (
<Card key={index}> <Card key={index}>
<CardHeader> <CardHeader>
<CardTitle>{device.device}</CardTitle> <CardTitle>{device.device}</CardTitle>
@ -191,7 +204,7 @@ const Analysis = () => {
<div> <div>
<h4 className="font-semibold text-sage-900">Examples</h4> <h4 className="font-semibold text-sage-900">Examples</h4>
<ul className="list-disc list-inside text-sage-700"> <ul className="list-disc list-inside text-sage-700">
{device.examples.map((example, i) => ( {device.examples.map((example: string, i: number) => (
<li key={i}>{example}</li> <li key={i}>{example}</li>
))} ))}
</ul> </ul>

View file

@ -0,0 +1,174 @@
import { themeComparisons, characterTypes, writingStyleEvolution } from '../data/comparative-analysis';
import { useNavigate } from 'react-router-dom';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../components/ui/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../components/ui/card";
import { ScrollArea } from "../components/ui/scroll-area";
import { Button } from "../components/ui/button";
import { ArrowLeft } from "lucide-react";
const ComparativeAnalysis = () => {
const navigate = useNavigate();
return (
<div>
<div className="absolute top-24 left-8">
<Button
variant="ghost"
size="sm"
className="text-sage-700 hover:text-sage-900 hover:bg-sage-100"
onClick={() => navigate('/analysis')}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Analysis
</Button>
</div>
<div className="container mx-auto py-8 space-y-8">
<div className="text-center space-y-4">
<h1 className="font-cormorant text-4xl text-sage-900">
Comparative Analysis
</h1>
<p className="text-sage-600 max-w-2xl mx-auto">
Explore themes, character types, and writing techniques across Jane Austen's works
</p>
</div>
<Tabs defaultValue="themes" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="themes">Themes</TabsTrigger>
<TabsTrigger value="characters">Character Types</TabsTrigger>
<TabsTrigger value="style">Writing Style</TabsTrigger>
</TabsList>
<TabsContent value="themes" className="space-y-6">
{themeComparisons.map((theme, index) => (
<Card key={index}>
<CardHeader>
<CardTitle>{theme.theme}</CardTitle>
<CardDescription>{theme.description}</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] rounded-md border p-4">
<div className="space-y-6">
{theme.appearances.map((appearance, i) => (
<div key={i} className="border-l-2 border-sage-300 pl-4">
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
{appearance.novel}
</h3>
<p className="text-sage-700 mb-3">
{appearance.manifestation}
</p>
{appearance.examples.map((example, j) => (
<div key={j} className="mb-4">
<blockquote className="italic text-sage-800 mb-2">
"{example.quote}"
<footer className="text-sm text-sage-600">
- {example.context}
</footer>
</blockquote>
<p className="text-sage-700">{example.analysis}</p>
</div>
))}
<p className="text-sage-800 mt-2">
<span className="font-semibold">Significance: </span>
{appearance.significance}
</p>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="characters" className="space-y-6">
{characterTypes.map((type, index) => (
<Card key={index}>
<CardHeader>
<CardTitle>{type.archetype}</CardTitle>
<CardDescription>{type.description}</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] rounded-md border p-4">
<div className="space-y-6">
{type.examples.map((example, i) => (
<div key={i} className="border-l-2 border-sage-300 pl-4">
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
{example.character} in {example.novel}
</h3>
<p className="text-sage-700 mb-3">{example.analysis}</p>
<p className="text-sage-700 mb-3">
<span className="font-semibold">Character Evolution: </span>
{example.evolution}
</p>
<div className="space-y-2">
{example.keyQuotes.map((quote, j) => (
<blockquote key={j} className="italic text-sage-800">
"{quote.quote}"
<footer className="text-sm text-sage-600">
- {quote.context}
</footer>
</blockquote>
))}
</div>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="style" className="space-y-6">
{writingStyleEvolution.map((technique, index) => (
<Card key={index}>
<CardHeader>
<CardTitle>{technique.technique}</CardTitle>
<CardDescription>{technique.description}</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] rounded-md border p-4">
<div className="space-y-6">
{technique.evolution.map((stage, i) => (
<div key={i} className="border-l-2 border-sage-300 pl-4">
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
{stage.novel}
</h3>
<p className="text-sage-700 mb-3">{stage.usage}</p>
<ul className="list-disc list-inside text-sage-700 mb-3">
{stage.examples.map((example, j) => (
<li key={j}>{example}</li>
))}
</ul>
<p className="text-sage-800">
<span className="font-semibold">Significance: </span>
{stage.significance}
</p>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
))}
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default ComparativeAnalysis;

View file

@ -0,0 +1,461 @@
import React, { useState, useCallback, useRef } from 'react';
import { characterNetwork } from '../data/character-network';
import { CharacterNode, Relationship, BookNode } from '../types/character-network';
import { Box, Typography, Paper, Grid, Card, CardContent, IconButton, Tooltip, Popper } from '@mui/material';
import { ForceGraph2D } from 'react-force-graph';
import { ArrowBack, Help } from '@mui/icons-material';
import * as d3 from 'd3';
interface NetworkNode {
id: string;
name: string;
x?: number;
y?: number;
val: number;
color: string;
type: string;
description?: string;
novel?: string;
class?: string;
year?: number;
fx?: number;
fy?: number;
}
interface NetworkLink {
source: string;
target: string;
color: string;
}
interface GraphData {
nodes: NetworkNode[];
links: NetworkLink[];
}
interface TooltipState {
open: boolean;
content: string;
x: number;
y: number;
}
export default function NetworkVisualization() {
const [selectedNode, setSelectedNode] = useState<CharacterNode | BookNode | null>(null);
const [selectedRelationships, setSelectedRelationships] = useState<Relationship[]>([]);
const [selectedBook, setSelectedBook] = useState<string | null>(null);
const [tooltip, setTooltip] = useState<TooltipState>({ open: false, content: '', x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const handleNodeHover = useCallback((node: NetworkNode | null) => {
if (node) {
const rect = containerRef.current?.getBoundingClientRect();
if (rect) {
setTooltip({
open: true,
content: `${node.name}\n${node.description || ''}`,
x: rect.left + rect.width / 2, // Center of container
y: rect.top + 100 // Fixed position from top
});
}
} else {
setTooltip(prev => ({ ...prev, open: false }));
}
}, []);
const handleNodeClick = useCallback((node: NetworkNode) => {
if (!selectedBook && node.type === 'book') {
setSelectedBook(node.id);
setSelectedNode(characterNetwork.books.find(b => b.id === node.id) || null);
} else if (selectedBook) {
const characterNode = characterNetwork.nodes.find(n => n.id === node.id);
if (characterNode) {
setSelectedNode(characterNode);
const relations = characterNetwork.relationships.filter(
r => r.source === node.id || r.target === node.id
);
setSelectedRelationships(relations);
}
}
setTooltip(prev => ({ ...prev, open: false }));
}, [selectedBook]);
const handleBackClick = () => {
setSelectedBook(null);
setSelectedNode(null);
setSelectedRelationships([]);
};
const centerGraph = useCallback((graphData: GraphData) => {
const containerWidth = 800;
const containerHeight = 700;
const padding = 50;
const xExtent = d3.extent(graphData.nodes, d => d.x || 0) as [number, number];
const yExtent = d3.extent(graphData.nodes, d => d.y || 0) as [number, number];
const xRange = xExtent[1] - xExtent[0] || 1;
const yRange = yExtent[1] - yExtent[0] || 1;
const scale = Math.min(
(containerWidth - 2 * padding) / xRange,
(containerHeight - 2 * padding) / yRange
);
graphData.nodes.forEach(node => {
node.x = ((node.x || 0) - (xExtent[0] + xRange / 2)) * scale + containerWidth / 2;
node.y = ((node.y || 0) - (yExtent[0] + yRange / 2)) * scale + containerHeight / 2;
});
}, []);
const getGraphData = useCallback((): GraphData => {
if (!selectedBook) {
// Book-level view with fixed positions in a circle
const data: GraphData = {
nodes: characterNetwork.books.map((book, idx) => {
const angle = (idx / characterNetwork.books.length) * 2 * Math.PI;
const radius = 150; // Reduced radius for better visibility
return {
...book,
val: 20,
// Set initial positions in a circle around center
x: Math.cos(angle) * radius + 400,
y: Math.sin(angle) * radius + 350,
// Don't fix positions to allow force layout to adjust
fx: undefined,
fy: undefined
};
}) as NetworkNode[],
links: [] as NetworkLink[]
};
return data;
}
const bookName = characterNetwork.books.find(b => b.id === selectedBook)?.name;
const nodes = characterNetwork.nodes
.filter(node => node.novel === bookName)
.map((node, idx) => {
const angle = (idx / characterNetwork.nodes.length) * 2 * Math.PI;
const radius = 100; // Initial radius for character layout
return {
...node,
val: 10,
// Set initial positions in a circle
x: Math.cos(angle) * radius + 400,
y: Math.sin(angle) * radius + 350,
// Clear any fixed positions
fx: undefined,
fy: undefined,
color: node.type === 'protagonist' ? '#e91e63' :
node.type === 'antagonist' ? '#f44336' : '#2196f3'
};
}) as NetworkNode[];
const data: GraphData = {
nodes,
links: characterNetwork.relationships
.filter(rel =>
nodes.some((n: NetworkNode) => n.id === rel.source) &&
nodes.some((n: NetworkNode) => n.id === rel.target)
)
.map(rel => ({
source: rel.source,
target: rel.target,
color: rel.type === 'family' ? '#4caf50' :
rel.type === 'romance' ? '#e91e63' :
rel.type === 'friendship' ? '#2196f3' : '#ff9800'
})) as NetworkLink[]
};
return data;
}, [selectedBook]);
const renderNodeCanvas = useCallback((node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) => {
const size = node.val || 5;
const fontSize = Math.max(12 / scale, 2);
// Draw node
ctx.beginPath();
ctx.arc(node.x || 0, node.y || 0, size, 0, 2 * Math.PI);
ctx.fillStyle = node.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Only draw labels when zoomed in enough or for book nodes
if (scale > 0.7 || node.type === 'book') {
const labelDistance = size + fontSize;
ctx.font = `${fontSize}px Inter, Arial`;
const textWidth = ctx.measureText(node.name).width;
// Semi-transparent background for better readability
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
const padding = fontSize * 0.3;
const backgroundHeight = fontSize + padding * 2;
// Draw background with rounded corners
const cornerRadius = 4;
const backgroundY = (node.y || 0) + labelDistance - fontSize / 2 - padding;
roundRect(
ctx,
(node.x || 0) - textWidth / 2 - padding,
backgroundY,
textWidth + padding * 2,
backgroundHeight,
cornerRadius
);
// Draw text
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
node.name,
node.x || 0,
(node.y || 0) + labelDistance
);
}
}, []);
// Helper function for drawing rounded rectangles
const roundRect = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number
) => {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
};
const getLegendTooltip = () => (
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" gutterBottom>Color Legend:</Typography>
<Typography variant="body2">
Books:<br />
Pride and Prejudice - Pink<br />
Sense and Sensibility - Blue<br />
Northanger Abbey - Green<br />
Mansfield Park - Purple<br />
Longbourn - Brown
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Characters:<br />
Protagonists - Pink<br />
Antagonists - Red<br />
Supporting - Blue
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Relationships:<br />
Family - Green<br />
Romance - Pink<br />
Friendship - Blue<br />
Rivalry - Orange
</Typography>
</Box>
);
return (
<Box sx={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
mb: 2,
px: 3,
pt: 2
}}>
<Typography variant="h4">
Character Network & Timeline
</Typography>
<Tooltip title={getLegendTooltip()} arrow placement="right">
<IconButton size="small" sx={{ ml: 2 }}>
<Help />
</IconButton>
</Tooltip>
</Box>
<Grid container spacing={0} sx={{ flex: 1, minHeight: 0 }}>
<Grid item xs={12} md={8}>
<Paper
elevation={3}
sx={{
height: '100%',
minHeight: 700,
position: 'relative',
borderRadius: 0,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
ref={containerRef}
>
{selectedBook && (
<Tooltip title="Return to book overview" arrow>
<IconButton
onClick={handleBackClick}
sx={{ position: 'absolute', top: 8, left: 8, zIndex: 1 }}
>
<ArrowBack />
</IconButton>
</Tooltip>
)}
<Box sx={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 1,
bgcolor: 'rgba(255, 255, 255, 0.9)',
p: 1,
borderRadius: 1
}}>
<Typography variant="body2" color="textSecondary">
{!selectedBook ? 'Click a book to explore its characters' : 'Click characters to view relationships'}
</Typography>
</Box>
<ForceGraph2D
graphData={getGraphData()}
onNodeHover={handleNodeHover}
onNodeClick={handleNodeClick}
nodeCanvasObject={(node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) =>
renderNodeCanvas(node, ctx, scale)}
linkColor={(link: NetworkLink) => link.color}
linkWidth={2}
nodeRelSize={6}
width={800}
height={700}
cooldownTicks={50}
cooldownTime={3000}
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.003}
d3AlphaDecay={0.1}
d3VelocityDecay={0.4}
minZoom={0.5}
maxZoom={4}
dagMode={selectedBook ? undefined : 'radialin'}
dagLevelDistance={100}
enablePanInteraction={true}
enableZoomInteraction={true}
onEngineStop={() => {
if (!selectedBook) {
centerGraph(getGraphData());
}
}}
onNodeDragEnd={(node: NetworkNode) => {
if (node.x && node.y) {
node.fx = node.x;
node.fy = node.y;
}
}}
warmupTicks={100}
onZoom={() => centerGraph(getGraphData())}
centerAt={[400, 350]}
zoom={2}
enableNodeDrag={true}
enableZoomPanInteraction={true}
/>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{
height: '100%',
minHeight: 700,
overflow: 'auto',
borderLeft: '1px solid rgba(0, 0, 0, 0.12)'
}}>
{selectedNode && (
<Card sx={{ boxShadow: 'none', borderRadius: 0 }}>
<CardContent>
<Typography variant="h5" gutterBottom>
{selectedNode.name}
</Typography>
<Typography color="textSecondary" gutterBottom>
{selectedNode.type === 'book' ? `Published: ${(selectedNode as BookNode).year}` : selectedNode.novel}
</Typography>
<Typography variant="body1" paragraph>
{selectedNode.description}
</Typography>
{selectedNode.type !== 'book' && (
<>
<Typography variant="body2" sx={{ mt: 2 }} color="textSecondary">
Social Class: {(selectedNode as CharacterNode).class}
<br />
Character Role: {selectedNode.type}
</Typography>
<Typography variant="h6" sx={{ mt: 3, mb: 2 }}>
Relationships
</Typography>
{selectedRelationships.map((rel, index) => (
<Box key={index} sx={{ mt: 2, p: 2, bgcolor: 'rgba(0, 0, 0, 0.02)', borderRadius: 1 }}>
<Typography variant="subtitle1" color="primary">
{rel.description}
</Typography>
<Typography variant="body2" color="textSecondary" gutterBottom>
Type: {rel.type}
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Development:
</Typography>
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
{rel.development.map((step, i) => (
<li key={i}>
<Typography variant="body2">{step}</Typography>
</li>
))}
</ul>
</Box>
))}
</>
)}
</CardContent>
</Card>
)}
</Box>
</Grid>
</Grid>
<Popper
open={tooltip.open}
anchorEl={containerRef.current}
placement="top"
style={{
position: 'absolute',
left: `${tooltip.x}px`,
top: `${tooltip.y}px`,
zIndex: 1500
}}
>
<Paper sx={{
p: 2,
maxWidth: 300,
bgcolor: 'rgba(255, 255, 255, 0.95)',
boxShadow: 3,
borderRadius: 1
}}>
<Typography variant="body2" whiteSpace="pre-line">
{tooltip.content}
</Typography>
</Paper>
</Popper>
</Box>
);
}

View file

@ -0,0 +1,31 @@
export interface BookNode {
id: string;
name: string;
type: 'book';
color: string;
description: string;
year: number;
}
export interface CharacterNode {
id: string;
name: string;
novel: string;
class: string;
type: 'protagonist' | 'antagonist' | 'supporting';
description: string;
}
export interface Relationship {
source: string;
target: string;
type: 'family' | 'romance' | 'friendship' | 'rivalry';
description: string;
development: string[];
}
export interface CharacterNetwork {
books: BookNode[];
nodes: CharacterNode[];
relationships: Relationship[];
}