A React library for building and analyzing branching narrative story systems with interactive tree visualization and AI-powered quality analysis.
- 📊 Interactive Tree Visualization - Built on React Flow for smooth, interactive story tree diagrams
- 🤖 LLM-Powered Story Analysis - AI-driven critique of narrative logic, continuity, and coherence (Anthropic & OpenAI)
- 🎨 Customizable Theming - Flexible styling options for nodes, edges, and layout
- 🔍 Tree Traversal Utilities - Tools for navigating and analyzing story paths
- 🧪 Well-Tested - 94%+ test coverage with comprehensive edge case handling
- 📦 TypeScript First - Full type safety with exported TypeScript definitions
- 🎯 Zero Config - Works out of the box with sensible defaults
# npm
npm install react-story-tree
# pnpm
pnpm add react-story-tree
# yarn
yarn add react-story-treeThis library requires the following peer dependencies:
pnpm add react react-dom @emotion/react @emotion/styledimport { StoryTree } from 'react-story-tree';
import type { StoryNode, TreeStructure } from 'react-story-tree';
// Define your story nodes
const nodes = new Map<string, StoryNode>([
['start', {
id: 'start',
title: 'The Beginning',
content: 'Your adventure starts here...'
}],
['choice-a', {
id: 'choice-a',
title: 'Path A',
content: 'You chose the left path.'
}],
['choice-b', {
id: 'choice-b',
title: 'Path B',
content: 'You chose the right path.'
}],
]);
// Define the tree structure (parent -> children)
const structure: TreeStructure = {
'start': ['choice-a', 'choice-b'],
'choice-a': [],
'choice-b': [],
};
// Render the tree
function App() {
return (
<div style={{ height: '600px' }}>
<StoryTree
nodes={nodes}
structure={structure}
rootId="start"
/>
</div>
);
}Analyze your branching narratives for logical consistency, continuity errors, and narrative quality using AI models from Anthropic (Claude) or OpenAI (GPT).
- Catch Continuity Errors: Detect when objects disappear, characters appear after dying, or facts contradict
- Find Logic Issues: Identify impossible outcomes or contradictory story developments
- Improve Quality: Get suggestions for pacing, depth, clarity, and engagement
- Save Time: Automatically analyze all story paths instead of manually testing each branch
import { analyzeStoryPath, traverseTree } from 'react-story-tree';
import type { StoryNode, TreeStructure } from 'react-story-tree';
// Your story data
const nodes = new Map<string, StoryNode>([...]);
const structure: TreeStructure = {...};
// Get all paths through your story
const paths = traverseTree(nodes, structure, 'start');
// Analyze a specific path with Anthropic (Claude)
const result = await analyzeStoryPath(paths[0], {
provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY!,
rules: {
continuity: true, // Check for disappearing objects/facts
logic: true, // Check for contradictions
character: true, // Check character consistency
temporal: true, // Check time progression
}
});
console.log(`Found ${result.issues.length} issues`);
console.log(`Got ${result.suggestions.length} suggestions`);const result = await analyzeStoryPath(paths[0], {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY!,
modelName: 'gpt-5-mini', // Optional, this is the default
rules: {
continuity: true,
logic: true,
}
});import { analyzeStory } from 'react-story-tree';
// Analyze all paths in the tree
const result = await analyzeStory(nodes, structure, {
provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY!,
});
console.log(`Analyzed ${result.statistics.totalPaths} paths`);
console.log(`Found ${result.allIssues.length} unique issues across all paths`);
console.log(`Tree has ${result.statistics.totalNodes} nodes`);
console.log(`Average path length: ${result.statistics.averagePathLength.toFixed(1)} nodes`);
// Issues are deduplicated across paths
result.allIssues.forEach(issue => {
console.log(`[${issue.severity}] ${issue.type} in node ${issue.nodeId}:`);
console.log(` ${issue.message}`);
});continuity: Objects/facts disappear or contradict (e.g., "You picked up the sword" → "you have no weapon")logic: Impossible outcomes (e.g., "The wizard dies" → "the wizard thanks you")character: Character inconsistencies (e.g., appearing after death, contradicting own statements)temporal: Time progression errors (e.g., "It is 6 PM" → "noon meeting" without explanation)
error: Clear problems that break the storywarning: Potential issues that should be reviewedinfo: Minor observations or suggestions
{
path: StoryPath,
issues: [
{
severity: 'error',
type: 'character',
nodeId: 'node-5',
message: 'Character "Marcus" appears but died in node-3',
context: 'Marcus enters the room and greets you.'
}
],
suggestions: [
{
message: 'Add more sensory details to make the scene vivid',
nodeId: 'node-2',
category: 'depth'
}
]
}interface AnalysisOptions {
// LLM Provider (required)
provider: 'anthropic' | 'openai';
apiKey: string;
// Model selection (optional)
modelName?: string; // Defaults: claude-3-5-sonnet-20241022 or gpt-5-mini
// Analysis rules (optional - all default to true)
rules?: {
continuity?: boolean; // Check for disappearing objects/facts
logic?: boolean; // Check for contradictions
character?: boolean; // Check character consistency
temporal?: boolean; // Check time progression
};
// Custom analysis instructions (optional)
customInstructions?: string;
// Token limit for LLM response (optional)
maxTokens?: number;
// Root node for tree analysis (optional - auto-detected if omitted)
rootNodeId?: string;
}claude-3-5-sonnet-20241022(default) - Best balance of quality and speedclaude-3-5-haiku-20241022- Faster, more economicalclaude-3-opus-20240229- Most capable, slower
gpt-5-mini(default) - Latest, fast and capablegpt-4o- Multimodal supportgpt-4o-mini- Faster, more economical
- Analyze During Development: Run analysis as you write to catch issues early
- User-Selected Paths: For interactive tools, analyze specific paths users select
- Batch Analysis Sparingly:
analyzeStory()is expensive - use for final reports - Handle Partial Failures: The library continues on errors, providing partial results
- Custom Instructions: Add domain-specific requirements with
customInstructions
The library provides robust error handling:
try {
const result = await analyzeStoryPath(path, {
provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY!,
});
} catch (error) {
// Error includes helpful context:
// - Which path failed
// - What provider/model was used
// - Suggestions for fixing (check API key, network, etc.)
console.error(error.message);
}Partial success in batch operations:
// If analyzing 10 paths and path #7 fails, you still get results for the other 9
const result = await analyzeStory(nodes, structure, options);
// result.pathResults contains successful analyses
// Failed paths are logged to console.error"API key is required"
- Ensure your API key is not empty or whitespace-only
- For Anthropic: Get key from https://console.anthropic.com/
- For OpenAI: Get key from https://platform.openai.com/api-keys
"Failed to parse LLM response as JSON"
- The LLM didn't follow the JSON format instructions
- Try a different model (e.g., switch from haiku to sonnet)
- Check if
customInstructionsare conflicting with format requirements
"Rate limit exceeded"
- You've hit the API provider's rate limit
- Wait and retry, or upgrade your API plan
- For batch operations, consider adding delays between requests
"Model not found"
- Check the model name spelling
- Ensure the model is available in your API plan
- Try using the default model by omitting
modelName
Main component for rendering interactive story trees.
| Prop | Type | Default | Description |
|---|---|---|---|
nodes |
Map<string, StoryNode> |
required | Map of node IDs to story node data |
structure |
TreeStructure |
required | Tree structure defining parent-child relationships |
rootId |
string |
required | ID of the root node to start traversal from |
layoutOptions |
LayoutOptions |
{} |
Layout configuration (direction, spacing) |
theme |
VisualizationTheme |
{} |
Theme/styling options |
showNodeId |
boolean |
false |
Whether to show node IDs (customId) |
onNodeClick |
(nodeId: string) => void |
undefined |
Called when a node is clicked |
onEdgeClick |
(edgeId: string) => void |
undefined |
Called when an edge is clicked |
showBackground |
boolean |
true |
Show background grid |
showControls |
boolean |
true |
Show zoom/pan controls |
showMiniMap |
boolean |
true |
Show minimap |
className |
string |
undefined |
Custom CSS class for the container |
style |
React.CSSProperties |
undefined |
Custom inline styles for the container |
<StoryTree
nodes={nodes}
structure={structure}
rootId="start"
theme={{
leafBorderColor: '#f44336',
leafBackgroundColor: '#ffebee',
branchBorderColor: '#2196f3',
branchBackgroundColor: '#e3f2fd',
selectedBorderColor: '#ff9800',
}}
/><StoryTree
nodes={nodes}
structure={structure}
rootId="start"
layoutOptions={{
direction: 'TB', // 'TB' | 'BT' | 'LR' | 'RL'
nodeSpacing: 80, // Horizontal spacing between nodes
rankSpacing: 120, // Vertical spacing between ranks/levels
}}
/>Extract and navigate story paths programmatically:
import { traverseTree, concatenatePath } from 'react-story-tree';
// Get all possible story paths from root to leaves
const paths = traverseTree(nodes, structure, 'start');
console.log(`Found ${paths.length} different story paths`);
// Each path contains:
paths[0].nodeIds; // ['start', 'choice-a', 'ending-1']
paths[0].nodes; // [StoryNode, StoryNode, StoryNode]
paths[0].decisions; // ['Take the left path', 'Enter the cave']
// Concatenate a path into a single narrative
const fullStory = concatenatePath(paths[0]);
console.log(fullStory); // "Your adventure starts... You chose left... You enter the cave..."- Testing: Verify all paths lead to valid endings
- Analytics: Calculate average path length, identify orphaned nodes
- Content Generation: Export paths as linear stories for non-interactive formats
- LLM Analysis: Feed paths to
analyzeStoryPath()for quality checking
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests with coverage
pnpm test:coverage
# Run tests in watch mode
pnpm test:watch
# Type check
pnpm type-check
# Lint
pnpm lint
# Build
pnpm buildContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run tests (
pnpm test) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT © Thomas Shellberg
Built with:
- React Flow - Powerful library for building node-based editors
- Dagre - Graph layout algorithm
- Vercel AI SDK - Unified interface for LLM providers
- Anthropic - Claude AI models
- OpenAI - GPT models
- Vitest - Fast unit test framework