mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
feat: add timeline system for multi-state constellation analysis
Implements a comprehensive timeline system that enables documents to contain multiple constellation states with branching timelines. This allows users to create different versions of their analysis for temporal evolution, alternative scenarios, or what-if analysis. Core Features: - Timeline store managing multiple states per document with branching structure - Visual timeline panel with React Flow-based state graph visualization - State management: create, switch, rename, duplicate (parallel/series), delete - Per-state undo/redo history (max 50 actions per state) - Context menu for timeline node operations - Collapsible timeline panel (always visible, moved toolbar to panel header) Architecture Changes: - Document structure: removed top-level graph field, states now only in timeline - Global types: nodeTypes and edgeTypes are now global per document, not per state - State graphs: only contain nodes and edges, types inherited from document - Persistence: full timeline serialization/deserialization with all states - History system: converted from document-level to per-state independent stacks Timeline Components: - TimelineView: main timeline visualization with state nodes and edges - BottomPanel: collapsible container with timeline controls in header - StateNode: custom node component showing state info and active indicator - CreateStateDialog: dialog for creating new timeline states - RenameStateDialog: dialog for renaming existing states - Context menu: right-click operations (rename, duplicate parallel/series, delete) Document Management: - Documents always have timeline (initialized with root state on creation) - Timeline persisted with document in localStorage - Export/import includes complete timeline with all states - Migration support for legacy single-state documents Store Updates: - timelineStore: manages timelines, states, and timeline operations - historyStore: per-state history with independent undo/redo stacks - workspaceStore: saves/loads timeline data, handles global types - panelStore: added timeline panel visibility state - useActiveDocument: syncs timeline state with graph editor Context Menu Improvements: - Smart viewport edge detection to prevent overflow - Click-outside detection for React Flow panes - Consistent styling across application Files Added: - src/types/timeline.ts - Timeline type definitions - src/stores/timelineStore.ts - Timeline state management - src/components/Timeline/TimelineView.tsx - Main timeline component - src/components/Timeline/BottomPanel.tsx - Timeline panel container - src/components/Timeline/StateNode.tsx - State node visualization - src/components/Timeline/CreateStateDialog.tsx - State creation dialog - src/components/Timeline/RenameStateDialog.tsx - State rename dialog Files Removed: - src/stores/persistence/middleware.ts - Obsolete persistence middleware Documentation: - Added comprehensive timeline feature documentation - Implementation checklists and quick reference guides - Temporal analysis concepts and UX guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2435c984ba
commit
28f8224284
29 changed files with 8978 additions and 499 deletions
755
IMPLEMENTATION_CHECKLIST.md
Normal file
755
IMPLEMENTATION_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,755 @@
|
||||||
|
# Temporal & Scenario Analysis - Implementation Checklist
|
||||||
|
|
||||||
|
## Phase 1: Core State Management (Week 1-2)
|
||||||
|
|
||||||
|
### Data Model Setup
|
||||||
|
- [ ] Create `/src/types/temporal.ts`
|
||||||
|
- [ ] Define `StateType` enum
|
||||||
|
- [ ] Define `TemporalMetadata` interface
|
||||||
|
- [ ] Define `ScenarioMetadata` interface
|
||||||
|
- [ ] Define `StateRelationship` interface
|
||||||
|
- [ ] Define `AnalysisState` interface
|
||||||
|
- [ ] Define `StateDiff` interface
|
||||||
|
- [ ] Define `ActorJourney` interface
|
||||||
|
- [ ] Define `Timeline` interface
|
||||||
|
- [ ] Define `ScenarioTree` interface
|
||||||
|
|
||||||
|
- [ ] Update `/src/stores/persistence/types.ts`
|
||||||
|
- [ ] Add `states` property to `ConstellationDocument`
|
||||||
|
- [ ] Add `supportsStates` to document metadata
|
||||||
|
- [ ] Ensure backward compatibility
|
||||||
|
|
||||||
|
- [ ] Create migration helper
|
||||||
|
- [ ] Function to enable states for existing documents
|
||||||
|
- [ ] Function to create initial state from current graph
|
||||||
|
- [ ] Test migration with sample documents
|
||||||
|
|
||||||
|
### State Store Implementation
|
||||||
|
- [ ] Create `/src/stores/stateStore.ts`
|
||||||
|
- [ ] Basic store structure with Zustand
|
||||||
|
- [ ] State CRUD operations
|
||||||
|
- [ ] `createState()`
|
||||||
|
- [ ] `loadState()`
|
||||||
|
- [ ] `deleteState()`
|
||||||
|
- [ ] `updateStateMetadata()`
|
||||||
|
- [ ] State retrieval
|
||||||
|
- [ ] `getState()`
|
||||||
|
- [ ] `getAllStates()`
|
||||||
|
- [ ] `getStatesByType()`
|
||||||
|
- [ ] Navigation helpers
|
||||||
|
- [ ] `getNextState()`
|
||||||
|
- [ ] `getPreviousState()`
|
||||||
|
|
||||||
|
### Snapshot Functionality
|
||||||
|
- [ ] Create `/src/utils/stateSnapshot.ts`
|
||||||
|
- [ ] `captureCurrentGraph()` - serialize current graph state
|
||||||
|
- [ ] `restoreGraphFromState()` - load state into graph
|
||||||
|
- [ ] `validateStateData()` - ensure data integrity
|
||||||
|
- [ ] Handle edge cases (empty graph, large graphs)
|
||||||
|
|
||||||
|
### Basic UI Integration
|
||||||
|
- [ ] Update `/src/components/Toolbar/Toolbar.tsx`
|
||||||
|
- [ ] Add "Capture State" button
|
||||||
|
- [ ] Add current state indicator
|
||||||
|
- [ ] Add loading/saving state indicators
|
||||||
|
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/StateSelector.tsx`
|
||||||
|
- [ ] Basic dropdown UI
|
||||||
|
- [ ] List all states
|
||||||
|
- [ ] Click to load state
|
||||||
|
- [ ] Show current state indicator
|
||||||
|
- [ ] Search/filter functionality
|
||||||
|
|
||||||
|
- [ ] Create hooks
|
||||||
|
- [ ] `/src/hooks/useStateManagement.ts`
|
||||||
|
- [ ] `useCurrentState()`
|
||||||
|
- [ ] `useCaptureState()`
|
||||||
|
- [ ] `useLoadState()`
|
||||||
|
|
||||||
|
### Integration with Workspace
|
||||||
|
- [ ] Update `/src/stores/workspaceStore.ts`
|
||||||
|
- [ ] Add `captureCurrentState()` action
|
||||||
|
- [ ] Add `restoreState()` action
|
||||||
|
- [ ] Ensure states saved with document
|
||||||
|
- [ ] Handle state data in export/import
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test state creation
|
||||||
|
- [ ] Test state loading
|
||||||
|
- [ ] Test state persistence
|
||||||
|
- [ ] Test with empty graphs
|
||||||
|
- [ ] Test with large graphs (100+ nodes)
|
||||||
|
- [ ] Test edge cases (missing data, corrupted states)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Temporal Analysis (Week 3-4)
|
||||||
|
|
||||||
|
### Timeline Data Management
|
||||||
|
- [ ] Extend `/src/stores/stateStore.ts`
|
||||||
|
- [ ] Timeline CRUD operations
|
||||||
|
- [ ] `createTimeline()`
|
||||||
|
- [ ] `deleteTimeline()`
|
||||||
|
- [ ] `updateTimeline()`
|
||||||
|
- [ ] Timeline state management
|
||||||
|
- [ ] `addStateToTimeline()`
|
||||||
|
- [ ] `removeStateFromTimeline()`
|
||||||
|
- [ ] `reorderTimeline()`
|
||||||
|
- [ ] Timeline queries
|
||||||
|
- [ ] `getStatesByTimeline()`
|
||||||
|
- [ ] `getTimelineStates()` (ordered)
|
||||||
|
|
||||||
|
### Temporal Metadata
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/StateMetadataEditor.tsx`
|
||||||
|
- [ ] Temporal metadata form
|
||||||
|
- [ ] Date/time picker
|
||||||
|
- [ ] Sequence number input
|
||||||
|
- [ ] Label input
|
||||||
|
- [ ] Period range inputs
|
||||||
|
- [ ] Display format selector
|
||||||
|
- [ ] Notes/description textarea
|
||||||
|
- [ ] Tags input
|
||||||
|
|
||||||
|
### Timeline Panel UI
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/TimelinePanel.tsx`
|
||||||
|
- [ ] Horizontal timeline visualization
|
||||||
|
- [ ] State markers on timeline
|
||||||
|
- [ ] Click to load state
|
||||||
|
- [ ] Drag to scrub timeline
|
||||||
|
- [ ] Zoom in/out on timeline
|
||||||
|
- [ ] Pan timeline horizontally
|
||||||
|
- [ ] State creation controls
|
||||||
|
- [ ] Timeline selector dropdown (multiple timelines)
|
||||||
|
|
||||||
|
- [ ] Timeline styling
|
||||||
|
- [ ] Color-coded state markers
|
||||||
|
- [ ] Current state highlight
|
||||||
|
- [ ] Hover effects
|
||||||
|
- [ ] Responsive design
|
||||||
|
- [ ] Collapsible panel
|
||||||
|
|
||||||
|
### Timeline Navigation
|
||||||
|
- [ ] Add keyboard shortcuts
|
||||||
|
- [ ] `←` Previous state in timeline
|
||||||
|
- [ ] `→` Next state in timeline
|
||||||
|
- [ ] `Home` First state
|
||||||
|
- [ ] `End` Last state
|
||||||
|
|
||||||
|
- [ ] Navigation buttons
|
||||||
|
- [ ] Previous/Next buttons
|
||||||
|
- [ ] Jump to start/end
|
||||||
|
- [ ] State counter (e.g., "3 of 12")
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [ ] Update `/src/App.tsx`
|
||||||
|
- [ ] Add TimelinePanel to layout
|
||||||
|
- [ ] Handle panel visibility toggle
|
||||||
|
- [ ] Handle panel resize
|
||||||
|
|
||||||
|
- [ ] Update `/src/components/Menu/MenuBar.tsx`
|
||||||
|
- [ ] Add "States" menu
|
||||||
|
- [ ] "View Timeline" action
|
||||||
|
- [ ] "Create Timeline" action
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test timeline creation
|
||||||
|
- [ ] Test state ordering
|
||||||
|
- [ ] Test timeline navigation
|
||||||
|
- [ ] Test keyboard shortcuts
|
||||||
|
- [ ] Test with multiple timelines
|
||||||
|
- [ ] Test timeline visualization with many states (20+)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Comparison & Diff Analysis (Week 5-6)
|
||||||
|
|
||||||
|
### Diff Calculation Engine
|
||||||
|
- [ ] Create `/src/utils/stateDiff.ts`
|
||||||
|
- [ ] `calculateStateDiff()` function
|
||||||
|
- [ ] Actor comparison logic
|
||||||
|
- [ ] Detect added actors
|
||||||
|
- [ ] Detect removed actors
|
||||||
|
- [ ] Detect modified actors (label, type, position, metadata)
|
||||||
|
- [ ] Relation comparison logic
|
||||||
|
- [ ] Detect added relations
|
||||||
|
- [ ] Detect removed relations
|
||||||
|
- [ ] Detect modified relations (type, directionality, strength)
|
||||||
|
- [ ] Summary statistics calculation
|
||||||
|
- [ ] Performance optimization for large graphs
|
||||||
|
|
||||||
|
- [ ] Create `/src/hooks/useStateDiff.ts`
|
||||||
|
- [ ] `useComparison()` hook
|
||||||
|
- [ ] `useDiffCalculation()` hook
|
||||||
|
- [ ] Memoization for expensive calculations
|
||||||
|
|
||||||
|
### Visual Diff on Graph
|
||||||
|
- [ ] Create `/src/components/Editor/DiffOverlay.tsx`
|
||||||
|
- [ ] Overlay mode component
|
||||||
|
- [ ] Color coding for changes
|
||||||
|
- [ ] Green for added (actors/relations)
|
||||||
|
- [ ] Red for removed (actors/relations)
|
||||||
|
- [ ] Yellow/orange for modified
|
||||||
|
- [ ] Change badges/icons on nodes
|
||||||
|
- [ ] Toggle overlay on/off
|
||||||
|
- [ ] Opacity control for overlay
|
||||||
|
|
||||||
|
- [ ] Update `/src/components/Editor/GraphEditor.tsx`
|
||||||
|
- [ ] Integrate DiffOverlay
|
||||||
|
- [ ] Apply diff styling to nodes/edges
|
||||||
|
- [ ] Animated transitions for diff highlights
|
||||||
|
|
||||||
|
### Comparison View UI
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/ComparisonView.tsx`
|
||||||
|
- [ ] Modal/panel for comparison
|
||||||
|
- [ ] State selectors (From/To)
|
||||||
|
- [ ] Comparison mode selector
|
||||||
|
- [ ] Side-by-side
|
||||||
|
- [ ] Overlay
|
||||||
|
- [ ] Tabbed
|
||||||
|
- [ ] Graph visualization for both states
|
||||||
|
- [ ] Synchronized panning/zooming
|
||||||
|
- [ ] Difference highlighting
|
||||||
|
- [ ] Summary statistics panel
|
||||||
|
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/StateDiffViewer.tsx`
|
||||||
|
- [ ] List of changes (actors/relations)
|
||||||
|
- [ ] Filter by change type (added/removed/modified)
|
||||||
|
- [ ] Click to highlight on graph
|
||||||
|
- [ ] Export changes as report
|
||||||
|
|
||||||
|
### Change Summary
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/ChangeSummary.tsx`
|
||||||
|
- [ ] Statistics dashboard
|
||||||
|
- [ ] Total actors (before/after)
|
||||||
|
- [ ] Total relations (before/after)
|
||||||
|
- [ ] Added/removed/modified counts
|
||||||
|
- [ ] Change breakdown by type
|
||||||
|
- [ ] Visual charts (pie chart, bar chart)
|
||||||
|
- [ ] Export to CSV/JSON
|
||||||
|
|
||||||
|
### Quick Compare
|
||||||
|
- [ ] Add to TimelinePanel
|
||||||
|
- [ ] "Compare" button on state markers
|
||||||
|
- [ ] Right-click context menu "Compare with..."
|
||||||
|
- [ ] Select two states to compare
|
||||||
|
|
||||||
|
- [ ] Add to state selector
|
||||||
|
- [ ] Checkbox mode to select multiple states
|
||||||
|
- [ ] "Compare Selected" button
|
||||||
|
|
||||||
|
### Export Comparison Report
|
||||||
|
- [ ] Report generation
|
||||||
|
- [ ] PDF export with diff summary
|
||||||
|
- [ ] JSON export of diff data
|
||||||
|
- [ ] HTML report with interactive visualization
|
||||||
|
- [ ] Include screenshots of both states
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test diff calculation accuracy
|
||||||
|
- [ ] Test with various graph sizes
|
||||||
|
- [ ] Test comparison view UI
|
||||||
|
- [ ] Test visual diff overlay
|
||||||
|
- [ ] Test export functionality
|
||||||
|
- [ ] Performance test with large diffs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Scenario Branching (Week 7-8)
|
||||||
|
|
||||||
|
### Scenario Data Model
|
||||||
|
- [ ] Extend `/src/stores/stateStore.ts`
|
||||||
|
- [ ] Scenario tree management
|
||||||
|
- [ ] `createScenarioBranch()`
|
||||||
|
- [ ] `addStateToScenario()`
|
||||||
|
- [ ] `deleteScenarioBranch()`
|
||||||
|
- [ ] Scenario queries
|
||||||
|
- [ ] `getStatesByScenario()`
|
||||||
|
- [ ] `getScenarioTree()`
|
||||||
|
- [ ] `getScenarioBranches()`
|
||||||
|
|
||||||
|
### Scenario Creation UI
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/ScenarioCreator.tsx`
|
||||||
|
- [ ] "Branch from here" button/dialog
|
||||||
|
- [ ] Scenario metadata form
|
||||||
|
- [ ] Label input
|
||||||
|
- [ ] Description textarea
|
||||||
|
- [ ] Assumptions list input
|
||||||
|
- [ ] Probability/confidence selector
|
||||||
|
- [ ] Color picker for branch
|
||||||
|
- [ ] Parent state selection
|
||||||
|
- [ ] Create and switch to scenario
|
||||||
|
|
||||||
|
### Scenario Tree Visualization
|
||||||
|
- [ ] Extend TimelinePanel for scenarios
|
||||||
|
- [ ] Vertical branching layout
|
||||||
|
- [ ] Branch lines/connectors
|
||||||
|
- [ ] Branch labels
|
||||||
|
- [ ] Branch color coding
|
||||||
|
- [ ] Collapse/expand branches
|
||||||
|
- [ ] Scenario navigation controls
|
||||||
|
|
||||||
|
- [ ] Alternative: Tree view component
|
||||||
|
- [ ] Hierarchical tree visualization
|
||||||
|
- [ ] Collapsible nodes
|
||||||
|
- [ ] Click to load state
|
||||||
|
- [ ] Branch context menu (edit, delete, compare)
|
||||||
|
|
||||||
|
### Scenario Metadata Editor
|
||||||
|
- [ ] Update StateMetadataEditor
|
||||||
|
- [ ] Scenario-specific fields
|
||||||
|
- [ ] Assumptions editor (add/remove/edit)
|
||||||
|
- [ ] Probability slider
|
||||||
|
- [ ] Confidence level selector
|
||||||
|
- [ ] Branch color picker
|
||||||
|
|
||||||
|
### Scenario Navigation
|
||||||
|
- [ ] Scenario switcher
|
||||||
|
- [ ] Dropdown to select branch
|
||||||
|
- [ ] Filter timeline by scenario
|
||||||
|
- [ ] Visual indicator of current branch
|
||||||
|
|
||||||
|
- [ ] Branch comparison
|
||||||
|
- [ ] "Compare branches" action
|
||||||
|
- [ ] Select multiple scenarios
|
||||||
|
- [ ] Side-by-side comparison
|
||||||
|
- [ ] Outcome analysis
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [ ] Update menu bar
|
||||||
|
- [ ] "Create Scenario" menu item
|
||||||
|
- [ ] "Manage Scenarios" menu item
|
||||||
|
|
||||||
|
- [ ] Update state selector
|
||||||
|
- [ ] Group states by scenario
|
||||||
|
- [ ] Scenario branch indicators
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test scenario creation
|
||||||
|
- [ ] Test branch visualization
|
||||||
|
- [ ] Test scenario navigation
|
||||||
|
- [ ] Test with multiple branches
|
||||||
|
- [ ] Test nested scenarios (if supported)
|
||||||
|
- [ ] Test scenario deletion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Actor Tracking & Journeys (Week 9-10)
|
||||||
|
|
||||||
|
### Journey Calculation
|
||||||
|
- [ ] Create `/src/utils/actorJourney.ts`
|
||||||
|
- [ ] `getActorJourney()` function
|
||||||
|
- [ ] Track actor across states
|
||||||
|
- [ ] Detect first/last appearance
|
||||||
|
- [ ] Calculate property changes
|
||||||
|
- [ ] Track relationship evolution
|
||||||
|
- [ ] Performance optimization
|
||||||
|
|
||||||
|
- [ ] Create `/src/hooks/useActorJourney.ts`
|
||||||
|
- [ ] `useActorJourney()` hook
|
||||||
|
- [ ] `useMultiActorJourney()` hook
|
||||||
|
- [ ] Memoization
|
||||||
|
|
||||||
|
### Journey Viewer UI
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/ActorJourneyViewer.tsx`
|
||||||
|
- [ ] Actor selection interface
|
||||||
|
- [ ] Dropdown or autocomplete
|
||||||
|
- [ ] Search by label
|
||||||
|
- [ ] Select from graph click
|
||||||
|
- [ ] Timeline visualization
|
||||||
|
- [ ] Horizontal timeline
|
||||||
|
- [ ] Actor appearance markers
|
||||||
|
- [ ] Property change indicators
|
||||||
|
- [ ] Property evolution display
|
||||||
|
- [ ] Label changes
|
||||||
|
- [ ] Type changes
|
||||||
|
- [ ] Position changes
|
||||||
|
- [ ] Relationship changes display
|
||||||
|
- [ ] Relations added/removed
|
||||||
|
- [ ] Relation type changes
|
||||||
|
- [ ] Relation strength changes
|
||||||
|
|
||||||
|
### Multi-Actor Comparison
|
||||||
|
- [ ] Select multiple actors
|
||||||
|
- [ ] Overlay journeys on same timeline
|
||||||
|
- [ ] Compare property evolution
|
||||||
|
- [ ] Compare relationship dynamics
|
||||||
|
- [ ] Identify interaction points
|
||||||
|
|
||||||
|
### Journey Export
|
||||||
|
- [ ] Export journey data
|
||||||
|
- [ ] CSV export
|
||||||
|
- [ ] JSON export
|
||||||
|
- [ ] PDF report with visualizations
|
||||||
|
- [ ] Include:
|
||||||
|
- [ ] Actor metadata
|
||||||
|
- [ ] State sequence
|
||||||
|
- [ ] Property changes
|
||||||
|
- [ ] Relationship changes
|
||||||
|
- [ ] Summary statistics
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [ ] Add to right panel
|
||||||
|
- [ ] "View Journey" button on actor selection
|
||||||
|
- [ ] Quick journey view
|
||||||
|
|
||||||
|
- [ ] Add to menu bar
|
||||||
|
- [ ] "Actor Journeys" menu item
|
||||||
|
- [ ] Keyboard shortcut (Ctrl+J)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test journey calculation
|
||||||
|
- [ ] Test with various actor types
|
||||||
|
- [ ] Test with actors that appear/disappear
|
||||||
|
- [ ] Test multi-actor comparison
|
||||||
|
- [ ] Test export functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Animation & Presentation (Week 11-12)
|
||||||
|
|
||||||
|
### Animation Engine
|
||||||
|
- [ ] Create `/src/utils/stateAnimation.ts`
|
||||||
|
- [ ] `interpolateStates()` function
|
||||||
|
- [ ] Position interpolation
|
||||||
|
- [ ] Opacity interpolation (fade in/out)
|
||||||
|
- [ ] Size interpolation
|
||||||
|
- [ ] Color interpolation
|
||||||
|
- [ ] Easing functions (linear, ease-in-out, etc.)
|
||||||
|
|
||||||
|
- [ ] Create `/src/hooks/useStateAnimation.ts`
|
||||||
|
- [ ] `useAnimation()` hook
|
||||||
|
- [ ] Animation state management
|
||||||
|
- [ ] Frame rate control
|
||||||
|
- [ ] Performance optimization
|
||||||
|
|
||||||
|
### Animation Controls
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/StateAnimator.tsx`
|
||||||
|
- [ ] Play/pause button
|
||||||
|
- [ ] Step forward/backward buttons
|
||||||
|
- [ ] Speed control slider
|
||||||
|
- [ ] Loop toggle
|
||||||
|
- [ ] Progress bar
|
||||||
|
- [ ] Frame counter
|
||||||
|
- [ ] Quality settings (performance vs. smoothness)
|
||||||
|
|
||||||
|
- [ ] Integrate with TimelinePanel
|
||||||
|
- [ ] Animation controls bar
|
||||||
|
- [ ] Visual playhead on timeline
|
||||||
|
- [ ] Click timeline to jump to state
|
||||||
|
|
||||||
|
### Animation Modes
|
||||||
|
- [ ] Sequential (state A → B → C)
|
||||||
|
- [ ] Comparison (fade between two states)
|
||||||
|
- [ ] Journey (follow actor across states)
|
||||||
|
- [ ] Custom sequence (user-selected states)
|
||||||
|
|
||||||
|
### Presentation Mode
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/PresentationMode.tsx`
|
||||||
|
- [ ] Full-screen mode
|
||||||
|
- [ ] Slideshow interface
|
||||||
|
- [ ] State sequence selector
|
||||||
|
- [ ] Auto-advance with timer
|
||||||
|
- [ ] Manual navigation (arrow keys)
|
||||||
|
- [ ] Annotation overlays
|
||||||
|
- [ ] Title for each state
|
||||||
|
- [ ] Notes/narration text
|
||||||
|
- [ ] Key insights callouts
|
||||||
|
- [ ] Exit presentation (Esc key)
|
||||||
|
|
||||||
|
### Export Capabilities (Stretch Goal)
|
||||||
|
- [ ] Export animation
|
||||||
|
- [ ] Animated GIF export
|
||||||
|
- [ ] Video export (WebM/MP4) - may require server-side
|
||||||
|
- [ ] Frame sequence export (PNG images)
|
||||||
|
- [ ] Interactive HTML export
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [ ] Add to menu bar
|
||||||
|
- [ ] "Animate Timeline" menu item
|
||||||
|
- [ ] "Presentation Mode" menu item
|
||||||
|
- [ ] Keyboard shortcut (Ctrl+Shift+P)
|
||||||
|
|
||||||
|
- [ ] Add to timeline panel
|
||||||
|
- [ ] Play button
|
||||||
|
- [ ] Presentation mode button
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test animation smoothness
|
||||||
|
- [ ] Test with various animation speeds
|
||||||
|
- [ ] Test with large state transitions
|
||||||
|
- [ ] Test presentation mode navigation
|
||||||
|
- [ ] Performance test with complex graphs
|
||||||
|
- [ ] Test on different screen sizes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: ChromaDB Integration (Week 13-14)
|
||||||
|
|
||||||
|
### ChromaDB Setup
|
||||||
|
- [ ] Install ChromaDB dependencies
|
||||||
|
- [ ] Add to package.json
|
||||||
|
- [ ] Configure ChromaDB client
|
||||||
|
|
||||||
|
- [ ] Create `/src/utils/chromaIntegration.ts`
|
||||||
|
- [ ] Initialize ChromaDB client
|
||||||
|
- [ ] Connection management
|
||||||
|
- [ ] Error handling
|
||||||
|
|
||||||
|
### Collection Setup
|
||||||
|
- [ ] Create collections
|
||||||
|
- [ ] `constellation_states` - state metadata
|
||||||
|
- [ ] `actor_journeys` - actor trajectories
|
||||||
|
- [ ] `state_comparisons` - cached comparisons
|
||||||
|
- [ ] `annotations` - user notes and insights
|
||||||
|
|
||||||
|
- [ ] Define schemas
|
||||||
|
- [ ] Metadata fields
|
||||||
|
- [ ] Embedding strategies
|
||||||
|
- [ ] Query filters
|
||||||
|
|
||||||
|
### State Indexing
|
||||||
|
- [ ] Create indexing functions
|
||||||
|
- [ ] `indexState()` - index single state
|
||||||
|
- [ ] `batchIndexStates()` - index multiple states
|
||||||
|
- [ ] `updateStateIndex()` - update existing index
|
||||||
|
- [ ] `removeStateIndex()` - remove from index
|
||||||
|
|
||||||
|
- [ ] Generate embeddings
|
||||||
|
- [ ] Combine label, description, notes, assumptions
|
||||||
|
- [ ] Use ChromaDB's built-in embedding
|
||||||
|
- [ ] Handle long text (truncation/summarization)
|
||||||
|
|
||||||
|
### Semantic Search
|
||||||
|
- [ ] Create search functions
|
||||||
|
- [ ] `searchStates()` - general search
|
||||||
|
- [ ] `findSimilarStates()` - similarity search
|
||||||
|
- [ ] `searchByTags()` - tag-based search
|
||||||
|
- [ ] `searchByTimeRange()` - temporal search
|
||||||
|
|
||||||
|
- [ ] Create `/src/components/TemporalAnalysis/StateSearch.tsx`
|
||||||
|
- [ ] Search input
|
||||||
|
- [ ] Search filters (type, timeline, scenario, tags)
|
||||||
|
- [ ] Results list
|
||||||
|
- [ ] Click to load state
|
||||||
|
- [ ] Relevance scoring display
|
||||||
|
|
||||||
|
### Pattern Recognition
|
||||||
|
- [ ] Implement pattern detection
|
||||||
|
- [ ] Identify similar network structures
|
||||||
|
- [ ] Find recurring patterns
|
||||||
|
- [ ] Detect anomalies
|
||||||
|
- [ ] Trend analysis
|
||||||
|
|
||||||
|
- [ ] Create pattern visualization
|
||||||
|
- [ ] Display pattern clusters
|
||||||
|
- [ ] Highlight similar states
|
||||||
|
- [ ] Generate insights
|
||||||
|
|
||||||
|
### Annotation Storage
|
||||||
|
- [ ] Store annotations in ChromaDB
|
||||||
|
- [ ] Link to specific states
|
||||||
|
- [ ] Link to specific changes
|
||||||
|
- [ ] Support rich text
|
||||||
|
- [ ] Support tags
|
||||||
|
|
||||||
|
- [ ] Create annotation search
|
||||||
|
- [ ] Search within annotations
|
||||||
|
- [ ] Find states by annotation content
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
- [ ] Cache diff calculations
|
||||||
|
- [ ] Store diff results in ChromaDB
|
||||||
|
- [ ] Retrieve cached diffs
|
||||||
|
- [ ] Invalidate cache on state changes
|
||||||
|
|
||||||
|
- [ ] Cache journey calculations
|
||||||
|
- [ ] Store journey data
|
||||||
|
- [ ] Update on relevant state changes
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [ ] Add to state store
|
||||||
|
- [ ] `indexStateForSearch()` action
|
||||||
|
- [ ] `searchStates()` action
|
||||||
|
|
||||||
|
- [ ] Add to UI
|
||||||
|
- [ ] Search box in state selector
|
||||||
|
- [ ] "Find similar" button on states
|
||||||
|
- [ ] Pattern insights panel
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test indexing performance
|
||||||
|
- [ ] Test search accuracy
|
||||||
|
- [ ] Test similarity detection
|
||||||
|
- [ ] Test caching effectiveness
|
||||||
|
- [ ] Test with large state collections (100+ states)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Advanced Features (Week 15-16)
|
||||||
|
|
||||||
|
### Automatic State Capture
|
||||||
|
- [ ] Implement auto-capture
|
||||||
|
- [ ] Periodic snapshots (e.g., every 10 minutes)
|
||||||
|
- [ ] Significant change detection (threshold-based)
|
||||||
|
- [ ] User-configurable triggers
|
||||||
|
- [ ] Auto-cleanup of old snapshots
|
||||||
|
|
||||||
|
- [ ] Settings UI
|
||||||
|
- [ ] Enable/disable auto-capture
|
||||||
|
- [ ] Configure frequency
|
||||||
|
- [ ] Configure retention policy
|
||||||
|
|
||||||
|
### State Templates
|
||||||
|
- [ ] Template creation
|
||||||
|
- [ ] Save state as template
|
||||||
|
- [ ] Template metadata (name, description, category)
|
||||||
|
- [ ] Template preview
|
||||||
|
|
||||||
|
- [ ] Template application
|
||||||
|
- [ ] Browse template library
|
||||||
|
- [ ] Apply template to create new state
|
||||||
|
- [ ] Customize template on application
|
||||||
|
|
||||||
|
- [ ] Template management
|
||||||
|
- [ ] Edit templates
|
||||||
|
- [ ] Delete templates
|
||||||
|
- [ ] Import/export templates
|
||||||
|
|
||||||
|
### Collaborative Features (Stretch Goal)
|
||||||
|
- [ ] Sharing
|
||||||
|
- [ ] Export shareable link to timeline
|
||||||
|
- [ ] Export shareable link to scenario
|
||||||
|
- [ ] Embed code for presentations
|
||||||
|
|
||||||
|
- [ ] Comments
|
||||||
|
- [ ] Comment on specific states
|
||||||
|
- [ ] Reply to comments
|
||||||
|
- [ ] @mentions
|
||||||
|
- [ ] Resolve comments
|
||||||
|
|
||||||
|
- [ ] Approvals (Stretch Goal)
|
||||||
|
- [ ] Submit state for review
|
||||||
|
- [ ] Approve/reject states
|
||||||
|
- [ ] Review workflow
|
||||||
|
|
||||||
|
### Advanced Analytics
|
||||||
|
- [ ] Network metrics over time
|
||||||
|
- [ ] Density evolution
|
||||||
|
- [ ] Centrality changes
|
||||||
|
- [ ] Clustering coefficient
|
||||||
|
- [ ] Path lengths
|
||||||
|
|
||||||
|
- [ ] Statistical analysis
|
||||||
|
- [ ] Correlation analysis
|
||||||
|
- [ ] Regression models
|
||||||
|
- [ ] Predictive analytics
|
||||||
|
|
||||||
|
- [ ] Export to analysis tools
|
||||||
|
- [ ] Export time-series data
|
||||||
|
- [ ] Export to CSV for Excel/R/Python
|
||||||
|
- [ ] API for external tools
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test auto-capture functionality
|
||||||
|
- [ ] Test template system
|
||||||
|
- [ ] Test collaborative features
|
||||||
|
- [ ] Test analytics calculations
|
||||||
|
- [ ] Integration testing with all phases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Polish & Documentation
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
- [ ] Profile performance bottlenecks
|
||||||
|
- [ ] Optimize diff calculation
|
||||||
|
- [ ] Optimize rendering for many states
|
||||||
|
- [ ] Lazy loading for large timelines
|
||||||
|
- [ ] Implement virtualization where needed
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- [ ] Graceful degradation for missing data
|
||||||
|
- [ ] User-friendly error messages
|
||||||
|
- [ ] Rollback on failed operations
|
||||||
|
- [ ] Data validation throughout
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- [ ] Keyboard navigation for all features
|
||||||
|
- [ ] Screen reader support
|
||||||
|
- [ ] High contrast mode support
|
||||||
|
- [ ] Focus indicators
|
||||||
|
- [ ] ARIA labels
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] User guide (see TEMPORAL_QUICK_START.md)
|
||||||
|
- [ ] Getting started tutorial
|
||||||
|
- [ ] Feature walkthroughs
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] FAQ
|
||||||
|
|
||||||
|
- [ ] Developer documentation
|
||||||
|
- [ ] API reference
|
||||||
|
- [ ] Type definitions
|
||||||
|
- [ ] Architecture overview
|
||||||
|
- [ ] Extension guide
|
||||||
|
|
||||||
|
- [ ] Video tutorials
|
||||||
|
- [ ] Overview video
|
||||||
|
- [ ] Feature-specific videos
|
||||||
|
- [ ] Advanced use cases
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Unit tests for all utilities
|
||||||
|
- [ ] Integration tests for stores
|
||||||
|
- [ ] Component tests for UI
|
||||||
|
- [ ] E2E tests for workflows
|
||||||
|
- [ ] Performance benchmarks
|
||||||
|
- [ ] User acceptance testing
|
||||||
|
|
||||||
|
### Release
|
||||||
|
- [ ] Version bump
|
||||||
|
- [ ] Changelog
|
||||||
|
- [ ] Migration guide for existing users
|
||||||
|
- [ ] Announcement post/blog
|
||||||
|
- [ ] Update README
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
Track these metrics post-release:
|
||||||
|
|
||||||
|
- [ ] Feature adoption rate
|
||||||
|
- [ ] % of users who enable states
|
||||||
|
- [ ] % of documents with states
|
||||||
|
- [ ] Average states per document
|
||||||
|
|
||||||
|
- [ ] Usage patterns
|
||||||
|
- [ ] Temporal vs. scenario usage
|
||||||
|
- [ ] Most used features (comparison, animation, journeys)
|
||||||
|
- [ ] Average session time with states
|
||||||
|
|
||||||
|
- [ ] Performance
|
||||||
|
- [ ] State creation time
|
||||||
|
- [ ] Diff calculation time
|
||||||
|
- [ ] Animation frame rate
|
||||||
|
- [ ] ChromaDB query latency
|
||||||
|
|
||||||
|
- [ ] User satisfaction
|
||||||
|
- [ ] User feedback/ratings
|
||||||
|
- [ ] Support tickets related to states
|
||||||
|
- [ ] Feature requests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Each phase builds on the previous one
|
||||||
|
- Test thoroughly before moving to next phase
|
||||||
|
- Gather user feedback early and often
|
||||||
|
- Iterate based on actual usage patterns
|
||||||
|
- Keep performance in mind throughout
|
||||||
|
- Document as you go
|
||||||
|
|
||||||
|
**Current Status**: Not started
|
||||||
|
**Next Step**: Begin Phase 1 - Core State Management
|
||||||
443
QUICK_REFERENCE.md
Normal file
443
QUICK_REFERENCE.md
Normal file
|
|
@ -0,0 +1,443 @@
|
||||||
|
# Temporal & Scenario Analysis - Quick Reference Card
|
||||||
|
|
||||||
|
## At a Glance
|
||||||
|
|
||||||
|
**What is this?**
|
||||||
|
A tool for temporal evolution analysis and scenario exploration of constellation graphs.
|
||||||
|
|
||||||
|
**What it's NOT:**
|
||||||
|
Version control (Git), undo/redo, or collaborative editing.
|
||||||
|
|
||||||
|
**Key Idea:**
|
||||||
|
Capture snapshots of your graph at different times or scenarios, then compare and analyze them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Concepts (5-Second Summary)
|
||||||
|
|
||||||
|
| Concept | What It Is | Example |
|
||||||
|
|---------|-----------|---------|
|
||||||
|
| **State** | Snapshot of graph at specific moment | "Q1 2023", "Session 5", "Strategy A" |
|
||||||
|
| **Timeline** | Ordered sequence of states | Jan → Feb → Mar → Apr |
|
||||||
|
| **Scenario** | Alternative branch from a point | Current → Strategy A vs Strategy B |
|
||||||
|
| **Comparison** | Visual diff between two states | What changed from Q1 to Q4? |
|
||||||
|
| **Journey** | Track one actor across states | How did Alice's role evolve? |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### 1. Historical Tracking
|
||||||
|
Track how your network changed over time
|
||||||
|
- **Example**: Company org chart 2020-2024
|
||||||
|
- **Action**: Capture state at each quarter/year
|
||||||
|
|
||||||
|
### 2. Therapeutic Progress
|
||||||
|
Show relationship evolution across sessions
|
||||||
|
- **Example**: Family therapy sessions 1-10
|
||||||
|
- **Action**: Capture state after each session
|
||||||
|
|
||||||
|
### 3. Strategic Planning
|
||||||
|
Explore different future scenarios
|
||||||
|
- **Example**: 3 different growth strategies
|
||||||
|
- **Action**: Branch scenarios from current state
|
||||||
|
|
||||||
|
### 4. Project Evolution
|
||||||
|
Show stakeholder changes through project phases
|
||||||
|
- **Example**: Kickoff → Planning → Execution → Closure
|
||||||
|
- **Action**: Capture state at each phase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Actions
|
||||||
|
|
||||||
|
### Capture a State
|
||||||
|
1. Work on your graph
|
||||||
|
2. Click "Capture State" button (toolbar)
|
||||||
|
3. Label it (e.g., "Q1 2023")
|
||||||
|
4. Add notes (optional)
|
||||||
|
5. Done!
|
||||||
|
|
||||||
|
### Load a State
|
||||||
|
1. Click state selector dropdown (toolbar)
|
||||||
|
2. Choose state from list
|
||||||
|
3. Graph updates to that state
|
||||||
|
|
||||||
|
### Compare Two States
|
||||||
|
1. Select state A
|
||||||
|
2. Click "Compare" button
|
||||||
|
3. Select state B
|
||||||
|
4. View differences (side-by-side or overlay)
|
||||||
|
|
||||||
|
### Create Timeline
|
||||||
|
1. Capture several states
|
||||||
|
2. Open timeline panel (bottom)
|
||||||
|
3. States auto-appear in order
|
||||||
|
4. Use scrubber to navigate
|
||||||
|
|
||||||
|
### Create Scenario Branch
|
||||||
|
1. Load the branching point state
|
||||||
|
2. Menu → States → Create Scenario Branch
|
||||||
|
3. Name it and add description
|
||||||
|
4. Modify graph for this scenario
|
||||||
|
5. Capture states along the scenario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|----------|--------|
|
||||||
|
| `Ctrl+Shift+S` | Capture current state |
|
||||||
|
| `Ctrl+Shift+T` | Toggle timeline panel |
|
||||||
|
| `Ctrl+Shift+C` | Open comparison view |
|
||||||
|
| `←` / `→` | Navigate timeline (when focused) |
|
||||||
|
| `Space` | Play/pause animation (when focused) |
|
||||||
|
| `Ctrl+J` | View actor journeys |
|
||||||
|
| `Ctrl+Shift+P` | Presentation mode |
|
||||||
|
| `Esc` | Exit modal/presentation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### Toolbar (Top)
|
||||||
|
```
|
||||||
|
Current State: Q3 2023 ▼ [📸 Capture] [🔍 Compare]
|
||||||
|
```
|
||||||
|
- **State Selector**: Dropdown to switch states
|
||||||
|
- **Capture Button**: Create new state
|
||||||
|
- **Compare Button**: Compare two states
|
||||||
|
|
||||||
|
### Timeline Panel (Bottom)
|
||||||
|
```
|
||||||
|
●═══●═══●═══●═══●
|
||||||
|
Q1 Q2 Q3 Q4 Now
|
||||||
|
```
|
||||||
|
- **Markers**: Click to load state
|
||||||
|
- **Scrubber**: Drag to animate through timeline
|
||||||
|
- **Controls**: Navigate, play, compare
|
||||||
|
|
||||||
|
### Right Panel
|
||||||
|
When state is selected, shows:
|
||||||
|
- State metadata (date, label, notes)
|
||||||
|
- Quick stats (actors, relations)
|
||||||
|
- Navigation (previous/next)
|
||||||
|
- Actions (edit, compare, delete)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison Modes
|
||||||
|
|
||||||
|
### Side-by-Side
|
||||||
|
Two graphs shown next to each other
|
||||||
|
- **Best for**: Overall comparison
|
||||||
|
- **Pros**: Clear separation
|
||||||
|
- **Cons**: Takes more screen space
|
||||||
|
|
||||||
|
### Overlay
|
||||||
|
Changes highlighted on single graph
|
||||||
|
- **Best for**: Detailed change analysis
|
||||||
|
- **Pros**: Shows changes in context
|
||||||
|
- **Cons**: Can be cluttered with many changes
|
||||||
|
|
||||||
|
### Diff List
|
||||||
|
Text list of all changes
|
||||||
|
- **Best for**: Systematic review
|
||||||
|
- **Pros**: Comprehensive, exportable
|
||||||
|
- **Cons**: Less visual
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change Indicators
|
||||||
|
|
||||||
|
### Visual Coding
|
||||||
|
- 🟢 **Green**: Added (new actors/relations)
|
||||||
|
- 🔴 **Red**: Removed (deleted actors/relations)
|
||||||
|
- 🟡 **Yellow**: Modified (changed properties)
|
||||||
|
- ⚪ **Gray**: Unchanged
|
||||||
|
|
||||||
|
### Change Types
|
||||||
|
- **Actor Added**: New person/entity joined
|
||||||
|
- **Actor Removed**: Person/entity left
|
||||||
|
- **Actor Modified**: Role, name, or properties changed
|
||||||
|
- **Relation Added**: New connection formed
|
||||||
|
- **Relation Removed**: Connection broken
|
||||||
|
- **Relation Modified**: Relationship type or strength changed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Naming States
|
||||||
|
✅ **Good**: "Q3 2023: Post-Merger Integration"
|
||||||
|
❌ **Bad**: "State 3"
|
||||||
|
|
||||||
|
✅ **Good**: "Session 5: Breakthrough Session"
|
||||||
|
❌ **Bad**: "May 15"
|
||||||
|
|
||||||
|
✅ **Good**: "Strategy A: Aggressive Growth (Optimistic)"
|
||||||
|
❌ **Bad**: "Option 1"
|
||||||
|
|
||||||
|
### When to Capture States
|
||||||
|
✅ Capture at **significant milestones**
|
||||||
|
✅ Capture at **regular intervals** (quarterly, sessions)
|
||||||
|
✅ Capture **before major changes**
|
||||||
|
|
||||||
|
❌ Don't capture for every tiny edit
|
||||||
|
❌ Don't create states "just in case"
|
||||||
|
❌ Don't capture without context/labels
|
||||||
|
|
||||||
|
### Organizing States
|
||||||
|
✅ **Use timelines** for temporal sequences
|
||||||
|
✅ **Use scenarios** for alternatives
|
||||||
|
✅ **Add descriptions** explaining what changed
|
||||||
|
✅ **Tag states** for easy finding
|
||||||
|
|
||||||
|
❌ Don't mix temporal and scenario in same timeline
|
||||||
|
❌ Don't create orphaned states without context
|
||||||
|
❌ Don't forget to clean up old/unused states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Workflow: Temporal Analysis
|
||||||
|
```
|
||||||
|
1. Start with current graph
|
||||||
|
2. Capture state: "Jan 2024"
|
||||||
|
3. Make changes to graph
|
||||||
|
4. Capture state: "Feb 2024"
|
||||||
|
5. Repeat monthly
|
||||||
|
6. View timeline
|
||||||
|
7. Compare Jan vs Dec
|
||||||
|
8. Animate evolution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow: Scenario Exploration
|
||||||
|
```
|
||||||
|
1. Create current state: "Current Reality"
|
||||||
|
2. Branch scenario: "Strategy A"
|
||||||
|
3. Modify graph for Strategy A
|
||||||
|
4. Capture: "Strategy A - Year 1"
|
||||||
|
5. Return to "Current Reality"
|
||||||
|
6. Branch scenario: "Strategy B"
|
||||||
|
7. Develop Strategy B
|
||||||
|
8. Compare Strategy A vs B
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow: Actor Journey
|
||||||
|
```
|
||||||
|
1. Ensure multiple states captured
|
||||||
|
2. Select actor on graph
|
||||||
|
3. Click "View Journey" (right panel)
|
||||||
|
4. See actor's timeline
|
||||||
|
5. Review changes over time
|
||||||
|
6. Export journey report
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Summary
|
||||||
|
|
||||||
|
### State
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
stateId: "unique-id",
|
||||||
|
stateType: "temporal" | "scenario",
|
||||||
|
snapshot: { nodes, edges },
|
||||||
|
temporal: { label, timestamp },
|
||||||
|
// OR
|
||||||
|
scenario: { label, description, assumptions },
|
||||||
|
notes: "What changed and why"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
timelineId: "unique-id",
|
||||||
|
label: "Project Evolution",
|
||||||
|
states: ["state1", "state2", "state3"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario Branch
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
branchId: "unique-id",
|
||||||
|
label: "Strategy A",
|
||||||
|
states: ["stateA1", "stateA2"],
|
||||||
|
color: "#3b82f6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison Output
|
||||||
|
|
||||||
|
### Summary Statistics
|
||||||
|
- Total actors: Before → After (Δ)
|
||||||
|
- Total relations: Before → After (Δ)
|
||||||
|
- Network density change
|
||||||
|
- Centrality changes
|
||||||
|
|
||||||
|
### Detailed Changes
|
||||||
|
- Actors added: [list]
|
||||||
|
- Actors removed: [list]
|
||||||
|
- Actors modified: [list with changes]
|
||||||
|
- Relations added: [list]
|
||||||
|
- Relations removed: [list]
|
||||||
|
- Relations modified: [list with changes]
|
||||||
|
|
||||||
|
### Export Formats
|
||||||
|
- PDF report with graphs
|
||||||
|
- JSON data for analysis
|
||||||
|
- CSV for spreadsheet
|
||||||
|
- HTML interactive report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
### For Large Graphs (100+ actors)
|
||||||
|
- Capture states selectively
|
||||||
|
- Use state pagination
|
||||||
|
- Enable caching
|
||||||
|
- Reduce animation quality if needed
|
||||||
|
|
||||||
|
### For Many States (50+ states)
|
||||||
|
- Organize into multiple timelines
|
||||||
|
- Use semantic search (ChromaDB)
|
||||||
|
- Archive old states
|
||||||
|
- Export/backup regularly
|
||||||
|
|
||||||
|
### For Smooth Animation
|
||||||
|
- Limit number of frames
|
||||||
|
- Use simplified rendering
|
||||||
|
- Adjust animation speed
|
||||||
|
- Close other applications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: State won't load
|
||||||
|
**Solution**: Check if state data is corrupted, try restarting app
|
||||||
|
|
||||||
|
### Problem: Comparison is slow
|
||||||
|
**Solution**: Large graph - reduce comparison mode quality or use diff list
|
||||||
|
|
||||||
|
### Problem: Animation is choppy
|
||||||
|
**Solution**: Reduce animation speed or quality setting
|
||||||
|
|
||||||
|
### Problem: Can't find a state
|
||||||
|
**Solution**: Use search function or check timeline filters
|
||||||
|
|
||||||
|
### Problem: Timeline is cluttered
|
||||||
|
**Solution**: Create multiple timelines, archive old states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
| Phase | Feature | Status | Priority |
|
||||||
|
|-------|---------|--------|----------|
|
||||||
|
| 1 | Core State Management | 🔲 Not Started | HIGH |
|
||||||
|
| 2 | Temporal Analysis | 🔲 Not Started | HIGH |
|
||||||
|
| 3 | Comparison & Diff | 🔲 Not Started | HIGH |
|
||||||
|
| 4 | Scenario Branching | 🔲 Not Started | MEDIUM |
|
||||||
|
| 5 | Actor Journeys | 🔲 Not Started | MEDIUM |
|
||||||
|
| 6 | Animation & Presentation | 🔲 Not Started | MEDIUM |
|
||||||
|
| 7 | ChromaDB Integration | 🔲 Not Started | MEDIUM |
|
||||||
|
| 8 | Advanced Features | 🔲 Not Started | LOW |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- **Summary**: `TEMPORAL_ANALYSIS_SUMMARY.md`
|
||||||
|
- **Full Plan**: `TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md`
|
||||||
|
- **User Guide**: `TEMPORAL_QUICK_START.md`
|
||||||
|
- **Examples**: `VISUAL_EXAMPLES.md`
|
||||||
|
- **Checklist**: `IMPLEMENTATION_CHECKLIST.md`
|
||||||
|
- **This Card**: `QUICK_REFERENCE.md`
|
||||||
|
|
||||||
|
### Key Files (To Be Created)
|
||||||
|
- Types: `/src/types/temporal.ts`
|
||||||
|
- Store: `/src/stores/stateStore.ts`
|
||||||
|
- Components: `/src/components/TemporalAnalysis/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQs
|
||||||
|
|
||||||
|
**Q: Will this replace normal editing?**
|
||||||
|
A: No, it's optional. You can ignore states and use app normally.
|
||||||
|
|
||||||
|
**Q: Can I undo after capturing a state?**
|
||||||
|
A: Yes, undo/redo is separate. States don't affect edit history.
|
||||||
|
|
||||||
|
**Q: How many states can I create?**
|
||||||
|
A: No hard limit, but recommend <100 per document for performance.
|
||||||
|
|
||||||
|
**Q: Can I delete a state?**
|
||||||
|
A: Yes, but be careful - this can't be undone.
|
||||||
|
|
||||||
|
**Q: Can I rename states?**
|
||||||
|
A: Yes, edit state metadata anytime.
|
||||||
|
|
||||||
|
**Q: Can states be shared?**
|
||||||
|
A: Yes, they're included in document export/import.
|
||||||
|
|
||||||
|
**Q: What's the difference between temporal and scenario?**
|
||||||
|
A: Temporal = time progression. Scenario = alternative branches.
|
||||||
|
|
||||||
|
**Q: Can I merge scenarios?**
|
||||||
|
A: No, scenarios are independent explorations for comparison.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Tips
|
||||||
|
|
||||||
|
💡 **Tip 1**: Label states descriptively - your future self will thank you
|
||||||
|
|
||||||
|
💡 **Tip 2**: Use comparison view liberally - it's the most powerful feature
|
||||||
|
|
||||||
|
💡 **Tip 3**: Animate timelines for presentations - it's impressive!
|
||||||
|
|
||||||
|
💡 **Tip 4**: Track key actors across states to tell their story
|
||||||
|
|
||||||
|
💡 **Tip 5**: Capture states BEFORE making major changes (safety net)
|
||||||
|
|
||||||
|
💡 **Tip 6**: Use scenarios to explore "what if" without commitment
|
||||||
|
|
||||||
|
💡 **Tip 7**: Export comparison reports for documentation
|
||||||
|
|
||||||
|
💡 **Tip 8**: Clean up old/unused states periodically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
This is about **storytelling and analysis**, not version control!
|
||||||
|
|
||||||
|
Think: "How did this network evolve?" not "What edits did I make?"
|
||||||
|
|
||||||
|
Use states to:
|
||||||
|
- ✅ Show temporal evolution
|
||||||
|
- ✅ Explore scenarios
|
||||||
|
- ✅ Compare alternatives
|
||||||
|
- ✅ Track actor journeys
|
||||||
|
- ✅ Present findings
|
||||||
|
|
||||||
|
Not to:
|
||||||
|
- ❌ Undo/redo edits
|
||||||
|
- ❌ Track every change
|
||||||
|
- ❌ Collaborate on editing
|
||||||
|
- ❌ Version control your work
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy analyzing!** 🎉
|
||||||
602
TEMPORAL_ANALYSIS_SUMMARY.md
Normal file
602
TEMPORAL_ANALYSIS_SUMMARY.md
Normal file
|
|
@ -0,0 +1,602 @@
|
||||||
|
# Temporal & Scenario Analysis - Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document provides a high-level summary of the revised multi-graph implementation plan, now correctly framed as a **temporal and scenario analysis** tool for constellation analyses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Correction: NOT Version Control
|
||||||
|
|
||||||
|
### Previous Misunderstanding
|
||||||
|
The initial approach treated this as a version control system (like Git for graphs), focusing on:
|
||||||
|
- Commits and checkouts
|
||||||
|
- Branching for collaboration
|
||||||
|
- Merge operations
|
||||||
|
- Edit history
|
||||||
|
|
||||||
|
### Corrected Understanding
|
||||||
|
This is actually a **temporal and scenario analysis tool** focused on:
|
||||||
|
- **Temporal evolution**: How constellations change over time
|
||||||
|
- **Scenario exploration**: Comparing alternative futures
|
||||||
|
- **Comparison analysis**: Visual diff and change tracking
|
||||||
|
- **Storytelling**: Presenting network dynamics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Use Cases
|
||||||
|
|
||||||
|
### 1. Historical/Temporal Analysis
|
||||||
|
**Example**: Track how a team evolved from 2020 to 2024
|
||||||
|
- Capture states at key time points (quarters, years, milestones)
|
||||||
|
- Navigate through timeline to see evolution
|
||||||
|
- Compare early vs. late states
|
||||||
|
- Identify inflection points and trends
|
||||||
|
|
||||||
|
### 2. Therapeutic/Session-Based Tracking
|
||||||
|
**Example**: Family therapist tracking constellation across 10 sessions
|
||||||
|
- Capture state after each therapy session
|
||||||
|
- Track relationship changes over time
|
||||||
|
- Visualize progress and breakthroughs
|
||||||
|
- Compare initial vs. final states
|
||||||
|
|
||||||
|
### 3. Strategic Scenario Planning
|
||||||
|
**Example**: Explore three different organizational strategies
|
||||||
|
- Start from current state
|
||||||
|
- Branch into multiple scenarios (Strategy A, B, C)
|
||||||
|
- Develop each scenario independently
|
||||||
|
- Compare outcomes side-by-side
|
||||||
|
- Present findings to stakeholders
|
||||||
|
|
||||||
|
### 4. Project Evolution
|
||||||
|
**Example**: Stakeholder network from project kickoff to closure
|
||||||
|
- Capture states at project phases
|
||||||
|
- Track key stakeholders across phases
|
||||||
|
- Animate evolution for presentations
|
||||||
|
- Generate actor journey reports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Phase 1: Core State Management (Weeks 1-2)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: HIGH - Foundation
|
||||||
|
|
||||||
|
- Capture current graph as a "state" (snapshot)
|
||||||
|
- Load states to view at different points
|
||||||
|
- Basic state metadata (label, notes)
|
||||||
|
- Simple state selector dropdown
|
||||||
|
|
||||||
|
**Deliverable**: Users can create and switch between states
|
||||||
|
|
||||||
|
### Phase 2: Temporal Analysis (Weeks 3-4)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: HIGH - Core use case
|
||||||
|
|
||||||
|
- Timeline management (ordered sequence of states)
|
||||||
|
- Timeline panel UI with scrubber
|
||||||
|
- Temporal metadata (dates, sequence numbers)
|
||||||
|
- Timeline navigation (previous/next)
|
||||||
|
|
||||||
|
**Deliverable**: Users can create temporal sequences and navigate through time
|
||||||
|
|
||||||
|
### Phase 3: Comparison & Diff (Weeks 5-6)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: HIGH - Key analytical feature
|
||||||
|
|
||||||
|
- Diff calculation engine
|
||||||
|
- Visual diff overlay on graph
|
||||||
|
- Comparison view (side-by-side)
|
||||||
|
- Change summary panel
|
||||||
|
- Export comparison reports
|
||||||
|
|
||||||
|
**Deliverable**: Users can compare states and see visual differences
|
||||||
|
|
||||||
|
### Phase 4: Scenario Branching (Weeks 7-8)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
|
||||||
|
- Scenario data model and tree structure
|
||||||
|
- "Branch from here" UI
|
||||||
|
- Scenario tree visualization
|
||||||
|
- Scenario comparison
|
||||||
|
|
||||||
|
**Deliverable**: Users can create and explore alternative scenarios
|
||||||
|
|
||||||
|
### Phase 5: Actor Journeys (Weeks 9-10)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
|
||||||
|
- Track specific actors across states
|
||||||
|
- Journey visualization
|
||||||
|
- Property and relationship evolution
|
||||||
|
- Export actor journey reports
|
||||||
|
|
||||||
|
**Deliverable**: Users can follow individual actors through time
|
||||||
|
|
||||||
|
### Phase 6: Animation & Presentation (Weeks 11-12)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
|
||||||
|
- Smooth transitions between states
|
||||||
|
- Animation controls (play/pause/speed)
|
||||||
|
- Presentation mode (full-screen slideshow)
|
||||||
|
- Export animations (stretch goal)
|
||||||
|
|
||||||
|
**Deliverable**: Users can animate evolution and present findings
|
||||||
|
|
||||||
|
### Phase 7: ChromaDB Integration (Weeks 13-14)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
|
||||||
|
- Index states in ChromaDB
|
||||||
|
- Semantic search for states
|
||||||
|
- Pattern recognition
|
||||||
|
- Annotation storage and search
|
||||||
|
|
||||||
|
**Deliverable**: Users can search and analyze state history semantically
|
||||||
|
|
||||||
|
### Phase 8: Advanced Features (Weeks 15-16)
|
||||||
|
**Status**: Not started
|
||||||
|
**Priority**: LOW
|
||||||
|
|
||||||
|
- Auto-capture states
|
||||||
|
- State templates
|
||||||
|
- Collaborative features (stretch)
|
||||||
|
- Advanced analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
### New Type: `AnalysisState`
|
||||||
|
Represents the constellation at a specific moment:
|
||||||
|
```typescript
|
||||||
|
interface AnalysisState {
|
||||||
|
stateId: string;
|
||||||
|
stateType: 'temporal' | 'scenario';
|
||||||
|
snapshot: {
|
||||||
|
nodes: SerializedActor[];
|
||||||
|
edges: SerializedRelation[];
|
||||||
|
};
|
||||||
|
temporal?: TemporalMetadata; // For time-based states
|
||||||
|
scenario?: ScenarioMetadata; // For scenario branches
|
||||||
|
relationships: StateRelationship[]; // Links to other states
|
||||||
|
notes?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updated: `ConstellationDocument`
|
||||||
|
Existing documents get optional state support:
|
||||||
|
```typescript
|
||||||
|
interface ConstellationDocument {
|
||||||
|
metadata: { ... };
|
||||||
|
graph: { ... }; // Current working graph
|
||||||
|
states?: {
|
||||||
|
stateList: AnalysisState[];
|
||||||
|
currentStateId: string | null;
|
||||||
|
timelines: Timeline[];
|
||||||
|
scenarioTrees: ScenarioTree[];
|
||||||
|
settings: { ... };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Type: `Timeline`
|
||||||
|
Ordered sequence of temporal states:
|
||||||
|
```typescript
|
||||||
|
interface Timeline {
|
||||||
|
timelineId: string;
|
||||||
|
label: string;
|
||||||
|
states: string[]; // Ordered state IDs
|
||||||
|
displaySettings: { ... };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Type: `ScenarioTree`
|
||||||
|
Hierarchical structure of branched scenarios:
|
||||||
|
```typescript
|
||||||
|
interface ScenarioTree {
|
||||||
|
rootStateId: string;
|
||||||
|
branches: Array<{
|
||||||
|
branchId: string;
|
||||||
|
label: string;
|
||||||
|
states: string[];
|
||||||
|
color?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Changes
|
||||||
|
|
||||||
|
### New Store: `stateStore.ts`
|
||||||
|
Manages all state-related operations:
|
||||||
|
- Create/read/update/delete states
|
||||||
|
- Timeline management
|
||||||
|
- Scenario branch management
|
||||||
|
- State comparison
|
||||||
|
- Actor journey tracking
|
||||||
|
- ChromaDB integration
|
||||||
|
|
||||||
|
### New Components: `TemporalAnalysis/`
|
||||||
|
```
|
||||||
|
/src/components/TemporalAnalysis/
|
||||||
|
├── TimelinePanel.tsx # Bottom panel with timeline
|
||||||
|
├── StateSelector.tsx # Dropdown to select states
|
||||||
|
├── ComparisonView.tsx # Side-by-side comparison
|
||||||
|
├── StateDiffViewer.tsx # List of changes
|
||||||
|
├── StateMetadataEditor.tsx # Edit state metadata
|
||||||
|
├── ActorJourneyViewer.tsx # Track actor across states
|
||||||
|
├── StateAnimator.tsx # Animation controls
|
||||||
|
└── PresentationMode.tsx # Full-screen slideshow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updated Components
|
||||||
|
- **Toolbar**: Add state controls (capture, current state indicator)
|
||||||
|
- **BottomPanel**: Integrate TimelinePanel
|
||||||
|
- **RightPanel**: Add state history section
|
||||||
|
- **MenuBar**: Add "States" menu
|
||||||
|
- **GraphEditor**: Support diff overlay and state loading
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminology Changes
|
||||||
|
|
||||||
|
| Old Term (Version Control) | New Term (Temporal/Scenario) |
|
||||||
|
|----------------------------|------------------------------|
|
||||||
|
| Version | State / Timepoint / Scenario |
|
||||||
|
| Commit | Capture State / Create Snapshot |
|
||||||
|
| Checkout | Load State / View State |
|
||||||
|
| Branch | Create Scenario Branch |
|
||||||
|
| Version History | Timeline / State History |
|
||||||
|
| Version Graph | Timeline / Scenario Tree |
|
||||||
|
| Diff | Comparison / Change Analysis |
|
||||||
|
| Merge | N/A (not applicable) |
|
||||||
|
| Revert | Restore State |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Workflows
|
||||||
|
|
||||||
|
### Workflow 1: Create Temporal Sequence
|
||||||
|
1. Work on graph normally
|
||||||
|
2. At key milestone, click "Capture State"
|
||||||
|
3. Label it (e.g., "Q1 2023")
|
||||||
|
4. Continue editing graph
|
||||||
|
5. Capture next state (e.g., "Q2 2023")
|
||||||
|
6. Repeat for all time points
|
||||||
|
7. View timeline panel to see sequence
|
||||||
|
8. Use scrubber to navigate
|
||||||
|
|
||||||
|
### Workflow 2: Compare Two States
|
||||||
|
1. Select first state (e.g., "Q1 2023")
|
||||||
|
2. Click "Compare" button
|
||||||
|
3. Select second state (e.g., "Q4 2023")
|
||||||
|
4. View side-by-side comparison
|
||||||
|
5. See highlighted changes
|
||||||
|
6. Review change summary
|
||||||
|
7. Export comparison report
|
||||||
|
|
||||||
|
### Workflow 3: Create Scenario Branch
|
||||||
|
1. Load the state to branch from (e.g., "Current")
|
||||||
|
2. Click "Create Scenario Branch"
|
||||||
|
3. Name it (e.g., "Strategy A")
|
||||||
|
4. Add description and assumptions
|
||||||
|
5. Modify graph for this scenario
|
||||||
|
6. Capture states along scenario
|
||||||
|
7. Return to branching point
|
||||||
|
8. Create alternative scenario (e.g., "Strategy B")
|
||||||
|
9. Compare scenarios
|
||||||
|
|
||||||
|
### Workflow 4: Track Actor Journey
|
||||||
|
1. Select actor on graph
|
||||||
|
2. Click "View Journey" in right panel
|
||||||
|
3. See timeline of actor appearances
|
||||||
|
4. Review property changes over time
|
||||||
|
5. Examine relationship evolution
|
||||||
|
6. Export journey report
|
||||||
|
|
||||||
|
### Workflow 5: Animate & Present
|
||||||
|
1. Create timeline with multiple states
|
||||||
|
2. Click "Animate" button
|
||||||
|
3. Adjust animation speed
|
||||||
|
4. Play animation (smooth transitions)
|
||||||
|
5. Enter presentation mode (full-screen)
|
||||||
|
6. Navigate through slideshow
|
||||||
|
7. Present to stakeholders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChromaDB Integration Strategy
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
|
||||||
|
**1. State Metadata Collection**
|
||||||
|
- Index state descriptions, notes, assumptions
|
||||||
|
- Enable semantic search ("Find states about merger")
|
||||||
|
- Support tag-based filtering
|
||||||
|
|
||||||
|
**2. Actor Journey Collection**
|
||||||
|
- Store actor trajectories
|
||||||
|
- Enable actor-centric queries
|
||||||
|
- Track relationship evolution
|
||||||
|
|
||||||
|
**3. Comparison Cache Collection**
|
||||||
|
- Cache expensive diff calculations
|
||||||
|
- Speed up repeated comparisons
|
||||||
|
- Store change summaries
|
||||||
|
|
||||||
|
**4. Annotation Collection**
|
||||||
|
- Store user notes and insights
|
||||||
|
- Link to specific states or changes
|
||||||
|
- Enable annotation search
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
1. **Semantic Search**: "Find all states related to organizational restructuring"
|
||||||
|
2. **Pattern Recognition**: "Find states similar to current state"
|
||||||
|
3. **Actor Tracking**: "Find all states where Alice appears"
|
||||||
|
4. **Change Analysis**: "Find states with significant network changes"
|
||||||
|
5. **Insight Discovery**: "Search annotations for mentions of 'conflict'"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
### Must-Have (MVP)
|
||||||
|
1. Phase 1: Core State Management
|
||||||
|
2. Phase 2: Temporal Analysis
|
||||||
|
3. Phase 3: Comparison & Diff
|
||||||
|
|
||||||
|
These three phases provide core value:
|
||||||
|
- Users can capture states at different times
|
||||||
|
- Navigate through temporal sequences
|
||||||
|
- Compare and analyze differences
|
||||||
|
|
||||||
|
### Should-Have
|
||||||
|
4. Phase 4: Scenario Branching
|
||||||
|
5. Phase 5: Actor Journeys
|
||||||
|
6. Phase 6: Animation & Presentation
|
||||||
|
|
||||||
|
These add significant analytical and storytelling power.
|
||||||
|
|
||||||
|
### Nice-to-Have
|
||||||
|
7. Phase 7: ChromaDB Integration
|
||||||
|
8. Phase 8: Advanced Features
|
||||||
|
|
||||||
|
These enhance but aren't essential for core functionality.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Feature Adoption
|
||||||
|
- Percentage of documents with states enabled
|
||||||
|
- Average number of states per document
|
||||||
|
- Temporal vs. scenario usage ratio
|
||||||
|
|
||||||
|
### User Engagement
|
||||||
|
- Time spent in comparison view
|
||||||
|
- Number of comparisons per session
|
||||||
|
- Animation playback frequency
|
||||||
|
- Actor journey queries
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- State creation time < 500ms
|
||||||
|
- Diff calculation time < 1s
|
||||||
|
- Animation frame rate > 30fps
|
||||||
|
- ChromaDB query latency < 200ms
|
||||||
|
|
||||||
|
### User Satisfaction
|
||||||
|
- Qualitative feedback
|
||||||
|
- Feature requests
|
||||||
|
- Support tickets
|
||||||
|
- User testimonials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Existing Documents
|
||||||
|
- All existing documents continue to work
|
||||||
|
- States are optional (`states?` property)
|
||||||
|
- No breaking changes
|
||||||
|
|
||||||
|
### Enabling States
|
||||||
|
Users can enable temporal analysis for any document:
|
||||||
|
1. Click "Enable Temporal Analysis" in menu
|
||||||
|
2. System captures current graph as initial state
|
||||||
|
3. Creates default timeline
|
||||||
|
4. User can now create additional states
|
||||||
|
|
||||||
|
### Export/Import
|
||||||
|
- Export includes all states (if present)
|
||||||
|
- Import preserves state structure
|
||||||
|
- Backward compatible with old exports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Plan
|
||||||
|
|
||||||
|
### User Documentation
|
||||||
|
- **Quick Start Guide**: `TEMPORAL_QUICK_START.md` (created)
|
||||||
|
- **Use Case Examples**: Included in quick start
|
||||||
|
- **Best Practices**: Included in quick start
|
||||||
|
- **Video Tutorials**: To be created
|
||||||
|
|
||||||
|
### Developer Documentation
|
||||||
|
- **Implementation Plan**: `TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md` (created)
|
||||||
|
- **Implementation Checklist**: `IMPLEMENTATION_CHECKLIST.md` (created)
|
||||||
|
- **Visual Examples**: `VISUAL_EXAMPLES.md` (created)
|
||||||
|
- **API Reference**: To be created with code
|
||||||
|
- **Type Definitions**: To be created in code
|
||||||
|
|
||||||
|
### Design Documentation
|
||||||
|
- **Visual wireframes**: Included in plan and examples
|
||||||
|
- **Interaction patterns**: Included in examples
|
||||||
|
- **Component hierarchy**: Included in plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls to Avoid
|
||||||
|
|
||||||
|
### 1. Creating Too Many States
|
||||||
|
**Problem**: State for every tiny change
|
||||||
|
**Solution**: Capture states only at significant milestones
|
||||||
|
|
||||||
|
### 2. Poor State Naming
|
||||||
|
**Problem**: "State 1", "State 2" with no context
|
||||||
|
**Solution**: Use descriptive labels with date/context
|
||||||
|
|
||||||
|
### 3. Not Using Comparison
|
||||||
|
**Problem**: Just switching between states without analysis
|
||||||
|
**Solution**: Actively use comparison view to identify changes
|
||||||
|
|
||||||
|
### 4. Mixing Temporal and Scenario
|
||||||
|
**Problem**: Scenarios and time in same timeline
|
||||||
|
**Solution**: Keep temporal timelines and scenario branches separate
|
||||||
|
|
||||||
|
### 5. Neglecting Metadata
|
||||||
|
**Problem**: States without descriptions or notes
|
||||||
|
**Solution**: Always add context (date, description, key changes)
|
||||||
|
|
||||||
|
### 6. Performance Issues with Large Graphs
|
||||||
|
**Problem**: Slow diff calculation or animation
|
||||||
|
**Solution**: Implement caching, Web Workers, progressive rendering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate (This Week)
|
||||||
|
1. Review and approve this revised plan
|
||||||
|
2. Set up project board/tracker
|
||||||
|
3. Create initial type definitions
|
||||||
|
4. Begin Phase 1 implementation
|
||||||
|
|
||||||
|
### Short-Term (Next 2-4 Weeks)
|
||||||
|
1. Complete Phase 1 (Core State Management)
|
||||||
|
2. Begin Phase 2 (Temporal Analysis)
|
||||||
|
3. Create example documents for testing
|
||||||
|
4. Gather early user feedback
|
||||||
|
|
||||||
|
### Medium-Term (Next 2-3 Months)
|
||||||
|
1. Complete Phases 2-3 (Temporal + Comparison)
|
||||||
|
2. Begin Phase 4 (Scenario Branching)
|
||||||
|
3. User testing with real use cases
|
||||||
|
4. Iterate based on feedback
|
||||||
|
|
||||||
|
### Long-Term (3-6 Months)
|
||||||
|
1. Complete Phases 4-6 (Scenarios, Journeys, Animation)
|
||||||
|
2. ChromaDB integration (Phase 7)
|
||||||
|
3. Advanced features (Phase 8)
|
||||||
|
4. Production release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
This revision has created the following documentation:
|
||||||
|
|
||||||
|
1. **TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md** (24KB)
|
||||||
|
- Complete technical implementation plan
|
||||||
|
- Data models and type definitions
|
||||||
|
- Component architecture
|
||||||
|
- Phase-by-phase breakdown
|
||||||
|
- ChromaDB integration details
|
||||||
|
- Algorithms and utilities
|
||||||
|
|
||||||
|
2. **TEMPORAL_QUICK_START.md** (16KB)
|
||||||
|
- User-focused guide
|
||||||
|
- Core concepts explained
|
||||||
|
- Common use cases with examples
|
||||||
|
- Workflow walkthroughs
|
||||||
|
- Best practices
|
||||||
|
- FAQ
|
||||||
|
|
||||||
|
3. **IMPLEMENTATION_CHECKLIST.md** (15KB)
|
||||||
|
- Granular task breakdown
|
||||||
|
- Phase-by-phase checklist
|
||||||
|
- Testing requirements
|
||||||
|
- Documentation tasks
|
||||||
|
- Success metrics
|
||||||
|
|
||||||
|
4. **VISUAL_EXAMPLES.md** (20KB)
|
||||||
|
- Concrete visual examples
|
||||||
|
- 8 detailed scenarios
|
||||||
|
- UI mockups and wireframes
|
||||||
|
- Interaction patterns
|
||||||
|
- Before/after visualizations
|
||||||
|
|
||||||
|
5. **TEMPORAL_ANALYSIS_SUMMARY.md** (This document)
|
||||||
|
- High-level overview
|
||||||
|
- Quick reference
|
||||||
|
- Links to detailed docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions & Answers
|
||||||
|
|
||||||
|
### Q: Is this replacing the existing document system?
|
||||||
|
**A**: No, it's an optional enhancement. Documents work fine without states.
|
||||||
|
|
||||||
|
### Q: Can I use this for undo/redo?
|
||||||
|
**A**: No, states are for temporal/scenario analysis, not edit history. Undo/redo is separate.
|
||||||
|
|
||||||
|
### Q: How many states can a document have?
|
||||||
|
**A**: No hard limit, but recommend <100 for performance. We'll implement pagination for large collections.
|
||||||
|
|
||||||
|
### Q: Will this work with existing documents?
|
||||||
|
**A**: Yes, fully backward compatible. Enable states when you need them.
|
||||||
|
|
||||||
|
### Q: Can I export just the timeline?
|
||||||
|
**A**: Yes, you can export specific timelines or scenario branches independently.
|
||||||
|
|
||||||
|
### Q: How does this differ from version control?
|
||||||
|
**A**: Version control tracks edit history for recovery. This tracks temporal evolution and scenarios for analysis and storytelling.
|
||||||
|
|
||||||
|
### Q: What about collaborative editing?
|
||||||
|
**A**: That's a separate feature. States can be shared but editing is still single-user.
|
||||||
|
|
||||||
|
### Q: Can I animate between any two states?
|
||||||
|
**A**: Yes, animation works between any pair of states, not just sequential ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Main plan: `TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md`
|
||||||
|
- User guide: `TEMPORAL_QUICK_START.md`
|
||||||
|
- Checklist: `IMPLEMENTATION_CHECKLIST.md`
|
||||||
|
- Examples: `VISUAL_EXAMPLES.md`
|
||||||
|
|
||||||
|
### Code (To Be Created)
|
||||||
|
- Types: `/src/types/temporal.ts`
|
||||||
|
- Store: `/src/stores/stateStore.ts`
|
||||||
|
- Components: `/src/components/TemporalAnalysis/`
|
||||||
|
- Utils: `/src/utils/stateDiff.ts`, `stateAnimation.ts`, etc.
|
||||||
|
|
||||||
|
### External Resources
|
||||||
|
- ChromaDB docs: https://docs.trychroma.com/
|
||||||
|
- React Flow (for graph): https://reactflow.dev/
|
||||||
|
- Zustand (state management): https://github.com/pmndrs/zustand
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This revised implementation plan transforms Constellation Analyzer from a static graph editor into a powerful temporal and scenario analysis tool. By correctly framing this as storytelling and analysis (not version control), we enable users to:
|
||||||
|
|
||||||
|
1. **Understand change**: Track how networks evolve over time
|
||||||
|
2. **Explore alternatives**: Compare different possible futures
|
||||||
|
3. **Analyze dynamics**: Identify patterns, trends, and inflection points
|
||||||
|
4. **Communicate insights**: Present findings with animation and comparison
|
||||||
|
|
||||||
|
The phased approach ensures we deliver value incrementally while building toward a comprehensive solution. Starting with core state management and temporal analysis provides immediate utility, while later phases add sophisticated analytical and presentation capabilities.
|
||||||
|
|
||||||
|
**Ready to begin implementation!**
|
||||||
462
TEMPORAL_QUICK_START.md
Normal file
462
TEMPORAL_QUICK_START.md
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
# Temporal & Scenario Analysis - Quick Start Guide
|
||||||
|
|
||||||
|
## What This Is (And Isn't)
|
||||||
|
|
||||||
|
### This IS:
|
||||||
|
- A tool for **temporal evolution analysis** (how constellations change over time)
|
||||||
|
- A tool for **scenario exploration** (exploring alternative futures)
|
||||||
|
- A **comparison and analysis** framework (visualizing differences)
|
||||||
|
- A **storytelling platform** (presenting network dynamics)
|
||||||
|
|
||||||
|
### This Is NOT:
|
||||||
|
- Version control for your work (not Git for graphs)
|
||||||
|
- Undo/redo functionality (that's separate)
|
||||||
|
- Collaborative editing (that's separate)
|
||||||
|
- A backup system (save your documents!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### 1. States
|
||||||
|
A **state** is a snapshot of your constellation at a specific moment in time or scenario. Think of it as a photograph of your network.
|
||||||
|
|
||||||
|
**Two types:**
|
||||||
|
- **Temporal States**: Time-based snapshots (e.g., "Q1 2023", "Session 5", "Post-Merger")
|
||||||
|
- **Scenario States**: Alternative futures (e.g., "Strategy A", "Pessimistic Case", "Option 2")
|
||||||
|
|
||||||
|
### 2. Timelines
|
||||||
|
A **timeline** is an ordered sequence of temporal states showing evolution over time.
|
||||||
|
|
||||||
|
**Example Timeline:**
|
||||||
|
```
|
||||||
|
2020 → 2021 → 2022 → 2023 → Projected 2024
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Scenarios
|
||||||
|
A **scenario** is a branch from a specific point to explore "what if" alternatives.
|
||||||
|
|
||||||
|
**Example Scenario Tree:**
|
||||||
|
```
|
||||||
|
Current State
|
||||||
|
├→ Strategy A → Quarter 2 → Quarter 3
|
||||||
|
├→ Strategy B → Quarter 2
|
||||||
|
└→ Strategy C → Quarter 2 → Quarter 3 → Quarter 4
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Comparisons
|
||||||
|
**Comparison** shows the differences between any two states with visual highlighting:
|
||||||
|
- Green: Added actors/relations
|
||||||
|
- Red: Removed actors/relations
|
||||||
|
- Yellow: Modified actors/relations
|
||||||
|
|
||||||
|
### 5. Actor Journeys
|
||||||
|
An **actor journey** tracks a specific actor across multiple states to see how they evolve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Use Case 1: Historical Analysis
|
||||||
|
**Scenario**: You want to show how a team's structure evolved over a year.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Create states for each quarter: Q1, Q2, Q3, Q4
|
||||||
|
2. Set temporal metadata (dates or sequence)
|
||||||
|
3. Add these states to a timeline
|
||||||
|
4. Use timeline scrubber to navigate through time
|
||||||
|
5. Compare Q1 vs Q4 to see total change
|
||||||
|
|
||||||
|
**Result**: You can present the evolution story and identify key inflection points.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Use Case 2: Therapeutic Progress
|
||||||
|
**Scenario**: A therapist tracking a patient's family constellation across sessions.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Capture state after each session: "Session 1", "Session 5", "Session 10"
|
||||||
|
2. Track specific family members (actors) across sessions
|
||||||
|
3. Compare early vs. late sessions to show progress
|
||||||
|
4. Create animation showing relationship evolution
|
||||||
|
|
||||||
|
**Result**: Visual evidence of therapeutic progress and relationship changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Use Case 3: Strategic Planning
|
||||||
|
**Scenario**: Exploring three different organizational restructuring options.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Capture current state: "Current Org Structure"
|
||||||
|
2. Create three scenario branches:
|
||||||
|
- "Option A: Consolidation"
|
||||||
|
- "Option B: Decentralization"
|
||||||
|
- "Option C: Hybrid"
|
||||||
|
3. Develop each scenario with different actor configurations
|
||||||
|
4. Compare all three scenarios side-by-side
|
||||||
|
5. Present findings to leadership
|
||||||
|
|
||||||
|
**Result**: Clear visual comparison of strategic alternatives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Use Case 4: Project Evolution
|
||||||
|
**Scenario**: Tracking how a project's stakeholder network changes from kickoff to completion.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Create timeline: "Kickoff" → "Planning" → "Execution" → "Closure"
|
||||||
|
2. Capture state at each phase
|
||||||
|
3. Track key stakeholders across all phases
|
||||||
|
4. Generate actor journey reports for executives
|
||||||
|
5. Animate the evolution for presentation
|
||||||
|
|
||||||
|
**Result**: Compelling narrative of project dynamics and stakeholder engagement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Feature Matrix
|
||||||
|
|
||||||
|
| Feature | Phase | Priority | Use Case |
|
||||||
|
|---------|-------|----------|----------|
|
||||||
|
| **Capture State** | 1 | HIGH | Create snapshots of current graph |
|
||||||
|
| **Load State** | 1 | HIGH | Switch between different states |
|
||||||
|
| **Timeline View** | 2 | HIGH | Navigate temporal sequences |
|
||||||
|
| **Temporal Metadata** | 2 | HIGH | Label states with dates/periods |
|
||||||
|
| **Compare States** | 3 | HIGH | Side-by-side comparison |
|
||||||
|
| **Visual Diff** | 3 | HIGH | Highlight changes on graph |
|
||||||
|
| **Change Summary** | 3 | HIGH | Statistics and change lists |
|
||||||
|
| **Scenario Branching** | 4 | MEDIUM | Create alternative futures |
|
||||||
|
| **Scenario Tree** | 4 | MEDIUM | Visualize branches |
|
||||||
|
| **Actor Journey** | 5 | MEDIUM | Track actors across states |
|
||||||
|
| **State Animation** | 6 | MEDIUM | Smooth transitions |
|
||||||
|
| **Presentation Mode** | 6 | MEDIUM | Full-screen slideshow |
|
||||||
|
| **Semantic Search** | 7 | MEDIUM | Find states by description |
|
||||||
|
| **Pattern Recognition** | 7 | MEDIUM | Identify similar states |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Interface Overview
|
||||||
|
|
||||||
|
### 1. Main Toolbar
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ [File] [Edit] [View] [States] [Help] │
|
||||||
|
│ │
|
||||||
|
│ Current State: Q3 2023 ▼ [📸 Capture] [🔍 Compare] │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Controls:**
|
||||||
|
- **State Selector Dropdown**: Quick switch between states
|
||||||
|
- **Capture Button**: Create new state from current graph
|
||||||
|
- **Compare Button**: Open comparison view
|
||||||
|
|
||||||
|
### 2. Bottom Timeline Panel
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Timeline: Project Evolution [+] State [≡] View │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ●═══●═══●═══●═══●════┬══●═══● │
|
||||||
|
│ Q1 Q2 Q3 Q4 Now │ S1 S2 (Branch A) │
|
||||||
|
│ 2023 2023 2023 2023 │ │
|
||||||
|
│ └──●═══● (Branch B) │
|
||||||
|
│ S1 S2 │
|
||||||
|
│ │
|
||||||
|
│ [◀] [▶] Navigate [⏯] Animate [⚖] Compare │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Horizontal timeline with state markers
|
||||||
|
- Vertical scenario branches
|
||||||
|
- Navigation controls
|
||||||
|
- Animation playback
|
||||||
|
- Quick compare access
|
||||||
|
|
||||||
|
### 3. Right Panel (When State Selected)
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ State: Q3 2023 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ Type: Temporal │
|
||||||
|
│ Date: 2023-09-30 │
|
||||||
|
│ Sequence: 3 of 4 │
|
||||||
|
│ │
|
||||||
|
│ Actors: 12 │
|
||||||
|
│ Relations: 18 │
|
||||||
|
│ │
|
||||||
|
│ Notes: │
|
||||||
|
│ "Significant restructuring after │
|
||||||
|
│ merger announcement..." │
|
||||||
|
│ │
|
||||||
|
│ [Edit Metadata] [Compare] [Delete] │
|
||||||
|
│ │
|
||||||
|
│ Navigation: │
|
||||||
|
│ ← Q2 2023 | Q4 2023 → │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|----------|--------|
|
||||||
|
| `Ctrl+Shift+S` | Capture current state |
|
||||||
|
| `Ctrl+Shift+T` | Open timeline panel |
|
||||||
|
| `Ctrl+Shift+C` | Open comparison view |
|
||||||
|
| `←` / `→` | Navigate timeline (when focused) |
|
||||||
|
| `Space` | Play/pause animation (when focused) |
|
||||||
|
| `Ctrl+J` | View actor journeys |
|
||||||
|
| `Ctrl+Shift+P` | Presentation mode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### Workflow 1: Creating a Temporal Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Start with your current graph
|
||||||
|
└→ [Capture State] → "January 2025"
|
||||||
|
|
||||||
|
2. Make changes to graph (add/remove/modify actors)
|
||||||
|
└→ [Capture State] → "February 2025"
|
||||||
|
|
||||||
|
3. Continue for each time period
|
||||||
|
└→ [Capture State] → "March 2025"
|
||||||
|
|
||||||
|
4. View timeline panel
|
||||||
|
└→ See all states in sequence
|
||||||
|
└→ Use scrubber to navigate
|
||||||
|
|
||||||
|
5. Compare any two states
|
||||||
|
└→ [Compare] → Select "January" vs "March"
|
||||||
|
└→ See visual diff
|
||||||
|
|
||||||
|
6. Animate evolution
|
||||||
|
└→ [Play] → Watch smooth transition
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow 2: Exploring Scenarios
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Load the state you want to branch from
|
||||||
|
└→ Select "Current State" from dropdown
|
||||||
|
|
||||||
|
2. Create scenario branch
|
||||||
|
└→ [States Menu] → [Create Scenario Branch]
|
||||||
|
└→ Name: "Strategy A"
|
||||||
|
└→ Description: "Aggressive expansion"
|
||||||
|
└→ Assumptions: ["Funding secured", "Market growth"]
|
||||||
|
|
||||||
|
3. Modify graph for this scenario
|
||||||
|
└→ Add new actors, relations
|
||||||
|
|
||||||
|
4. Capture states along this scenario
|
||||||
|
└→ [Capture State] → "Strategy A - Q2"
|
||||||
|
└→ [Capture State] → "Strategy A - Q3"
|
||||||
|
|
||||||
|
5. Return to branching point
|
||||||
|
└→ Select "Current State" again
|
||||||
|
|
||||||
|
6. Create alternative scenario
|
||||||
|
└→ [Create Scenario Branch] → "Strategy B"
|
||||||
|
└→ Different modifications
|
||||||
|
|
||||||
|
7. Compare scenarios
|
||||||
|
└→ [Compare] → "Strategy A - Q3" vs "Strategy B - Q3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow 3: Actor Journey Analysis
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Ensure you have multiple states captured
|
||||||
|
|
||||||
|
2. Select an actor on the graph
|
||||||
|
└→ Click actor node
|
||||||
|
|
||||||
|
3. View actor journey
|
||||||
|
└→ [Right Panel] → [View Journey]
|
||||||
|
OR
|
||||||
|
└→ [Ctrl+J] → Select actor from list
|
||||||
|
|
||||||
|
4. Journey viewer shows:
|
||||||
|
└→ All states where actor appears
|
||||||
|
└→ Property changes over time
|
||||||
|
└→ Relationship changes
|
||||||
|
└→ Position evolution
|
||||||
|
|
||||||
|
5. Export actor report
|
||||||
|
└→ [Export Journey] → PDF or JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. State Naming
|
||||||
|
- **Use descriptive labels**: "Q3 2023 Post-Restructuring" not just "State 3"
|
||||||
|
- **Include context**: "Strategy A: Optimistic Scenario - Year 2"
|
||||||
|
- **Be consistent**: Use same format for similar states
|
||||||
|
|
||||||
|
### 2. Timeline Organization
|
||||||
|
- **One timeline per narrative**: Don't mix different stories
|
||||||
|
- **Logical sequencing**: Ensure temporal order makes sense
|
||||||
|
- **Manageable length**: Consider breaking very long timelines
|
||||||
|
|
||||||
|
### 3. Scenario Branching
|
||||||
|
- **Clear branching points**: Choose meaningful divergence points
|
||||||
|
- **Document assumptions**: Always explain what makes scenarios different
|
||||||
|
- **Parallel development**: Develop scenario branches to similar time horizons
|
||||||
|
- **Color coding**: Use colors to distinguish branches visually
|
||||||
|
|
||||||
|
### 4. Comparison Analysis
|
||||||
|
- **Compare meaningful pairs**: Adjacent states or alternative scenarios
|
||||||
|
- **Focus on key changes**: Filter by change type if needed
|
||||||
|
- **Document insights**: Add notes about significant differences
|
||||||
|
- **Export reports**: Save comparison results for reference
|
||||||
|
|
||||||
|
### 5. Presentation
|
||||||
|
- **Start with context**: Begin with overview state
|
||||||
|
- **Show progression**: Use animation for temporal sequences
|
||||||
|
- **Highlight key changes**: Use comparison view for dramatic differences
|
||||||
|
- **Tell a story**: Sequence states to create narrative flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls to Avoid
|
||||||
|
|
||||||
|
### 1. Too Many States
|
||||||
|
**Problem**: Creating state for every tiny change clutters timeline
|
||||||
|
**Solution**: Capture states at significant milestones only
|
||||||
|
|
||||||
|
### 2. Inconsistent Labeling
|
||||||
|
**Problem**: "Jan", "February 2023", "2023-03-15" in same timeline
|
||||||
|
**Solution**: Choose format and stick with it
|
||||||
|
|
||||||
|
### 3. Forgetting Metadata
|
||||||
|
**Problem**: States labeled "State 1", "State 2" with no context
|
||||||
|
**Solution**: Always add description, date, or sequence info
|
||||||
|
|
||||||
|
### 4. Not Using Comparison
|
||||||
|
**Problem**: Just switching between states without analyzing differences
|
||||||
|
**Solution**: Use comparison view to identify and document changes
|
||||||
|
|
||||||
|
### 5. Orphaned Scenarios
|
||||||
|
**Problem**: Creating scenario branches but not developing them
|
||||||
|
**Solution**: Either fully develop scenarios or delete incomplete branches
|
||||||
|
|
||||||
|
### 6. Mixing Temporal and Scenario
|
||||||
|
**Problem**: Putting scenarios and time progression in same timeline
|
||||||
|
**Solution**: Keep temporal timelines and scenario branches separate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
### For Large Graphs (100+ actors)
|
||||||
|
- Capture states selectively (not every change)
|
||||||
|
- Use diff caching (automatic in ChromaDB integration)
|
||||||
|
- Limit animation quality for smooth playback
|
||||||
|
- Consider pagination for very long timelines
|
||||||
|
|
||||||
|
### For Many States (50+ states)
|
||||||
|
- Organize into multiple timelines by theme
|
||||||
|
- Use semantic search to find relevant states
|
||||||
|
- Archive old/unused states
|
||||||
|
- Export and backup state data regularly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Existing Features
|
||||||
|
|
||||||
|
### Document System
|
||||||
|
- Each document can have its own state history
|
||||||
|
- States are document-specific (not shared across documents)
|
||||||
|
- Duplicate document includes all states
|
||||||
|
|
||||||
|
### Export/Import
|
||||||
|
- Export document includes all states
|
||||||
|
- Import preserves state structure
|
||||||
|
- Can export specific timeline or scenario branch
|
||||||
|
|
||||||
|
### Undo/Redo
|
||||||
|
- Undo/redo works on current working graph
|
||||||
|
- Does NOT affect captured states
|
||||||
|
- Capturing state does not add to undo history
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Phase 1: Getting Started (Week 1-2)
|
||||||
|
1. Implement basic state capture and loading
|
||||||
|
2. Create simple state selector
|
||||||
|
3. Test with example document
|
||||||
|
|
||||||
|
### Phase 2: Temporal Features (Week 3-4)
|
||||||
|
1. Build timeline panel UI
|
||||||
|
2. Add temporal metadata editor
|
||||||
|
3. Implement timeline navigation
|
||||||
|
|
||||||
|
### Phase 3: Comparison (Week 5-6)
|
||||||
|
1. Develop diff algorithm
|
||||||
|
2. Create comparison view UI
|
||||||
|
3. Add visual diff overlay
|
||||||
|
|
||||||
|
**Continue with remaining phases as outlined in main implementation plan**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions to Consider
|
||||||
|
|
||||||
|
Before implementing, discuss:
|
||||||
|
|
||||||
|
1. **State Limit**: Should we limit number of states per document?
|
||||||
|
2. **Storage**: IndexedDB for states or localStorage? (Recommend IndexedDB)
|
||||||
|
3. **Persistence**: Auto-save states or explicit save?
|
||||||
|
4. **Naming**: Should users be prompted to name states or auto-generate?
|
||||||
|
5. **Default State**: What happens when document opens - load latest state or working graph?
|
||||||
|
6. **Branching UI**: Tree view or timeline with vertical branches?
|
||||||
|
7. **Animation**: Default animation duration and easing function?
|
||||||
|
8. **Export**: Include states in normal JSON export or separate?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Main Implementation Plan**: See `TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md` for complete technical details
|
||||||
|
- **Type Definitions**: `/src/types/temporal.ts` (to be created)
|
||||||
|
- **Store Implementation**: `/src/stores/stateStore.ts` (to be created)
|
||||||
|
- **UI Components**: `/src/components/TemporalAnalysis/` (to be created)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
You'll know the implementation is successful when users can:
|
||||||
|
|
||||||
|
1. Capture a state in < 3 clicks
|
||||||
|
2. Navigate timeline intuitively
|
||||||
|
3. Immediately see differences when comparing states
|
||||||
|
4. Create and understand scenario branches
|
||||||
|
5. Animate evolution smoothly
|
||||||
|
6. Find specific states quickly (with ChromaDB)
|
||||||
|
7. Present findings effectively to stakeholders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions about this implementation:
|
||||||
|
- Review the main implementation plan for technical details
|
||||||
|
- Check type definitions for data structures
|
||||||
|
- Examine workflow examples for common patterns
|
||||||
|
- Test with real use cases early and often
|
||||||
|
|
||||||
|
**Remember**: This is about storytelling and analysis, not version control!
|
||||||
1311
TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md
Normal file
1311
TEMPORAL_SCENARIO_IMPLEMENTATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load diff
1149
TEMPORAL_SCENARIO_UX_CONCEPT.md
Normal file
1149
TEMPORAL_SCENARIO_UX_CONCEPT.md
Normal file
File diff suppressed because it is too large
Load diff
1318
UX_CONCEPT_MULTI_VERSION_GRAPH.md
Normal file
1318
UX_CONCEPT_MULTI_VERSION_GRAPH.md
Normal file
File diff suppressed because it is too large
Load diff
916
VISUAL_EXAMPLES.md
Normal file
916
VISUAL_EXAMPLES.md
Normal file
|
|
@ -0,0 +1,916 @@
|
||||||
|
# Temporal & Scenario Analysis - Visual Examples
|
||||||
|
|
||||||
|
This document provides concrete visual examples of how the temporal and scenario analysis features should look and behave.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 1: Organizational Evolution (Temporal Analysis)
|
||||||
|
|
||||||
|
### Scenario
|
||||||
|
A company tracking how its organizational structure changed during a merger over 12 months.
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
```
|
||||||
|
2023 January → April → July → October → 2024 January
|
||||||
|
(Pre-Merger) (Integration) (Consolidation) (Restructuring) (New Structure)
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 1: January 2023 (Pre-Merger)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Company A: 25 employees
|
||||||
|
- CEO (Alice)
|
||||||
|
- 3 Department Heads
|
||||||
|
- 21 Team Members
|
||||||
|
|
||||||
|
Company B: 18 employees
|
||||||
|
- CEO (Bob)
|
||||||
|
- 2 Department Heads
|
||||||
|
- 15 Team Members
|
||||||
|
|
||||||
|
No relations between companies yet
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 2: April 2023 (Integration)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Merged Company: 43 employees
|
||||||
|
- Co-CEOs (Alice + Bob)
|
||||||
|
- 5 Department Heads (3 from A, 2 from B)
|
||||||
|
- Joint steering committee (6 people)
|
||||||
|
- 36 Team Members
|
||||||
|
|
||||||
|
New relations:
|
||||||
|
- Cross-company collaboration edges
|
||||||
|
- Reporting structure changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 3: July 2023 (Consolidation)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Merged Company: 40 employees (-3 departures)
|
||||||
|
- Single CEO (Alice, Bob moves to advisory)
|
||||||
|
- 4 Department Heads (1 department merged)
|
||||||
|
- 35 Team Members
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Removed: 3 actors (departures)
|
||||||
|
- Modified: Bob's role and relations
|
||||||
|
- Added: Advisory board node
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 4: October 2023 (Restructuring)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Merged Company: 42 employees (+2 new hires)
|
||||||
|
- CEO (Alice)
|
||||||
|
- 4 Department Heads (reshuffled)
|
||||||
|
- 2 new leadership roles
|
||||||
|
- 36 Team Members
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Added: 2 new strategic roles
|
||||||
|
- Modified: Several reporting relationships
|
||||||
|
- Removed: Steering committee (integration complete)
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 5: January 2024 (New Structure)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Merged Company: 45 employees (+3 new hires)
|
||||||
|
- CEO (Alice)
|
||||||
|
- 4 Department Heads (stable)
|
||||||
|
- Established matrix structure
|
||||||
|
- 40 Team Members
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Added: Cross-functional teams (new edge types)
|
||||||
|
- Added: 3 new hires
|
||||||
|
- Stabilized structure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparison: State 1 vs State 5
|
||||||
|
|
||||||
|
**Visual Diff:**
|
||||||
|
```
|
||||||
|
Green (Added):
|
||||||
|
- 2 new leadership roles
|
||||||
|
- 5 new team members
|
||||||
|
- 30+ cross-functional collaboration edges
|
||||||
|
- Matrix structure edges
|
||||||
|
|
||||||
|
Red (Removed):
|
||||||
|
- Bob as Co-CEO (moved to advisory)
|
||||||
|
- 1 department head (consolidation)
|
||||||
|
- 3 departed employees
|
||||||
|
- Company B as separate entity
|
||||||
|
|
||||||
|
Yellow (Modified):
|
||||||
|
- Alice: Title change (Co-CEO → CEO)
|
||||||
|
- Bob: Role change (Co-CEO → Advisor)
|
||||||
|
- Multiple reporting relationship changes
|
||||||
|
- 5 department heads repositioned in hierarchy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Summary Statistics:**
|
||||||
|
- Total actors: 43 → 45 (+2, +4.7%)
|
||||||
|
- Total relations: 68 → 112 (+44, +64.7%)
|
||||||
|
- Network density: 0.037 → 0.056 (+51%)
|
||||||
|
- Average connections per person: 3.2 → 5.0 (+56%)
|
||||||
|
|
||||||
|
### Actor Journey: Bob
|
||||||
|
```
|
||||||
|
Timeline visualization:
|
||||||
|
|
||||||
|
Jan 2023 Apr 2023 Jul 2023 Oct 2023 Jan 2024
|
||||||
|
● ● ● ● ●
|
||||||
|
CEO Co-CEO Advisor Advisor Advisor
|
||||||
|
Company B Merged Co Merged Co Merged Co Merged Co
|
||||||
|
|
||||||
|
Relations:
|
||||||
|
17 direct reports → 20 → 4 → 2 → 2
|
||||||
|
Type: Leadership → Leadership → Advisory → Advisory → Advisory
|
||||||
|
Position: Center → Center → Periphery → Periphery → Periphery
|
||||||
|
|
||||||
|
Key Changes:
|
||||||
|
Apr 2023: Became Co-CEO, gained cross-company relations
|
||||||
|
Jul 2023: Transitioned to advisor, lost most direct reports
|
||||||
|
Oct 2023+: Stable advisory role
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 2: Therapeutic Progress (Temporal Analysis)
|
||||||
|
|
||||||
|
### Scenario
|
||||||
|
Family therapist tracking a family constellation across 10 therapy sessions.
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
```
|
||||||
|
Session 1 → Session 3 → Session 5 → Session 7 → Session 10
|
||||||
|
(Intake) (Early Work) (Breakthrough) (Integration) (Closure)
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 1: Session 1 (Intake)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Family Members:
|
||||||
|
- Mother (Sarah)
|
||||||
|
- Father (John)
|
||||||
|
- Daughter (Emma, 16)
|
||||||
|
- Son (Michael, 12)
|
||||||
|
|
||||||
|
Relations:
|
||||||
|
- Sarah ←conflict→ John (high intensity, red)
|
||||||
|
- Sarah ←protective→ Emma (strong, dashed)
|
||||||
|
- John ←distant→ Michael (weak, dotted)
|
||||||
|
- Emma ←tension→ Michael (medium, orange)
|
||||||
|
|
||||||
|
Notes: "High conflict between parents, children taking sides,
|
||||||
|
triangulation patterns evident"
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 2: Session 3 (Early Work)
|
||||||
|
```
|
||||||
|
Changes from Session 1:
|
||||||
|
Modified:
|
||||||
|
- Sarah ↔ John: Conflict intensity reduced (high → medium)
|
||||||
|
- Sarah → Emma: Protective edge slightly weakened
|
||||||
|
|
||||||
|
Added:
|
||||||
|
- John ↔ Emma: New communication edge (weak)
|
||||||
|
|
||||||
|
Notes: "Parents beginning to communicate more directly,
|
||||||
|
Emma less involved in parental conflict"
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 3: Session 5 (Breakthrough)
|
||||||
|
```
|
||||||
|
Changes from Session 3:
|
||||||
|
Modified:
|
||||||
|
- Sarah ↔ John: Conflict edge changed to "communication" type
|
||||||
|
- John → Michael: Distant edge strengthened (engagement improving)
|
||||||
|
|
||||||
|
Added:
|
||||||
|
- Family unit node (representing whole family identity)
|
||||||
|
- All members connected to family unit
|
||||||
|
|
||||||
|
Notes: "Major breakthrough - parents able to discuss issues
|
||||||
|
without involving children. Family identity emerging."
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 4: Session 7 (Integration)
|
||||||
|
```
|
||||||
|
Changes from Session 5:
|
||||||
|
Modified:
|
||||||
|
- Sarah ↔ John: Communication edge strengthened
|
||||||
|
- Emma ↔ Michael: Tension edge changed to "sibling bond"
|
||||||
|
|
||||||
|
Added:
|
||||||
|
- John → Emma: Communication edge strengthened
|
||||||
|
- Sarah → Michael: New supportive edge
|
||||||
|
|
||||||
|
Notes: "Parents functioning as parental team. Sibling
|
||||||
|
relationship improving. Cross-generational boundaries
|
||||||
|
clearer."
|
||||||
|
```
|
||||||
|
|
||||||
|
### State 5: Session 10 (Closure)
|
||||||
|
```
|
||||||
|
Changes from Session 7:
|
||||||
|
Modified:
|
||||||
|
- Sarah ↔ John: Strong partnership edge (blue, solid)
|
||||||
|
- All parent-child edges balanced and healthy
|
||||||
|
- Sibling edge strong and positive
|
||||||
|
|
||||||
|
Removed:
|
||||||
|
- No conflict edges remaining
|
||||||
|
- Protective/distant edges normalized
|
||||||
|
|
||||||
|
Notes: "Family system stabilized. Healthy boundaries,
|
||||||
|
effective communication, age-appropriate relationships.
|
||||||
|
Ready for termination."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animated Visualization
|
||||||
|
|
||||||
|
**Frame-by-frame description:**
|
||||||
|
```
|
||||||
|
Frame 1 (Session 1):
|
||||||
|
- Actors positioned with visible tension
|
||||||
|
- Conflict edge pulsing in red
|
||||||
|
- Protective edge thick and binding
|
||||||
|
|
||||||
|
Frame 10 (Session 3):
|
||||||
|
- Conflict edge fading to orange
|
||||||
|
- New communication line appearing (fade in)
|
||||||
|
- Emma moving slightly away from protective orbit
|
||||||
|
|
||||||
|
Frame 20 (Session 5):
|
||||||
|
- Family unit node appearing in center (fade in)
|
||||||
|
- Connections to family unit growing out
|
||||||
|
- All actors shifting toward center
|
||||||
|
|
||||||
|
Frame 30 (Session 7):
|
||||||
|
- Sibling edge morphing from orange to blue
|
||||||
|
- Cross-connections strengthening
|
||||||
|
- Network becoming more interconnected
|
||||||
|
|
||||||
|
Frame 40 (Session 10):
|
||||||
|
- All edges now healthy colors (blue, green)
|
||||||
|
- Balanced positioning
|
||||||
|
- Strong sense of unity and connection
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Journey: Emma
|
||||||
|
```
|
||||||
|
Timeline:
|
||||||
|
|
||||||
|
Session 1 Session 3 Session 5 Session 7 Session 10
|
||||||
|
● ● ● ● ●
|
||||||
|
|
||||||
|
Role:
|
||||||
|
Parentified Transitioning De-triangulated Teen Member Healthy Teen
|
||||||
|
|
||||||
|
Position:
|
||||||
|
Between Moving out Periphery Appropriate Age-appropriate
|
||||||
|
parents of middle of conflict teen role teen role
|
||||||
|
|
||||||
|
Key Relations:
|
||||||
|
Mother: Protective, enmeshed → Lessening → Normal parental → Healthy
|
||||||
|
Father: Distant → Emerging → Communicating → Connected
|
||||||
|
Brother: Tension → Neutral → Improving → Sibling bond
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Session 1-3: Caught in parental conflict, taking mother's side
|
||||||
|
Session 5: Breakthrough allowed her to step out of middle
|
||||||
|
Session 7+: Re-established as teenager, not parent-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 3: Strategic Planning (Scenario Analysis)
|
||||||
|
|
||||||
|
### Scenario
|
||||||
|
Tech startup exploring three growth strategies over 2 years.
|
||||||
|
|
||||||
|
### Current State (Branching Point)
|
||||||
|
```
|
||||||
|
Graph:
|
||||||
|
Team: 15 people
|
||||||
|
- 1 CEO
|
||||||
|
- 2 Co-founders (CTO, CPO)
|
||||||
|
- 3 Engineers
|
||||||
|
- 2 Designers
|
||||||
|
- 3 Sales
|
||||||
|
- 2 Marketing
|
||||||
|
- 2 Support
|
||||||
|
|
||||||
|
Product: Single core product
|
||||||
|
Market: Single vertical
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario A: Rapid Expansion
|
||||||
|
```
|
||||||
|
Branch: "Strategy A - Rapid Expansion"
|
||||||
|
Assumptions:
|
||||||
|
- $5M Series A funding secured
|
||||||
|
- Aggressive hiring
|
||||||
|
- Market demand high
|
||||||
|
- Risk: Operational complexity
|
||||||
|
|
||||||
|
Year 1:
|
||||||
|
Team: 35 people (+20)
|
||||||
|
- Added: 2 managers, 8 engineers, 4 sales, 3 marketing, 3 support
|
||||||
|
- Added: Product expansion team (5 people)
|
||||||
|
- Added: VP Sales node
|
||||||
|
|
||||||
|
Year 2:
|
||||||
|
Team: 65 people (+30)
|
||||||
|
- Added: 3 managers, 15 engineers, 7 sales, 5 marketing
|
||||||
|
- Added: International team (8 people)
|
||||||
|
- Added: VP Engineering, VP Marketing nodes
|
||||||
|
- Network density: High complexity
|
||||||
|
|
||||||
|
Outcome Analysis:
|
||||||
|
Strengths: Market capture, rapid growth, multiple products
|
||||||
|
Risks: Management overhead, coordination challenges, burn rate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario B: Focused Growth
|
||||||
|
```
|
||||||
|
Branch: "Strategy B - Focused Growth"
|
||||||
|
Assumptions:
|
||||||
|
- $2M seed extension
|
||||||
|
- Selective hiring
|
||||||
|
- Deep vertical penetration
|
||||||
|
- Risk: Market saturation
|
||||||
|
|
||||||
|
Year 1:
|
||||||
|
Team: 22 people (+7)
|
||||||
|
- Added: 3 engineers, 2 sales, 2 support
|
||||||
|
- Focus: Core product improvement
|
||||||
|
- Strengthened: Sales/customer relations
|
||||||
|
|
||||||
|
Year 2:
|
||||||
|
Team: 30 people (+8)
|
||||||
|
- Added: 4 engineers, 2 sales, 2 marketing
|
||||||
|
- Added: Customer success team (3 people)
|
||||||
|
- Network density: Moderate, well-connected
|
||||||
|
|
||||||
|
Outcome Analysis:
|
||||||
|
Strengths: Product excellence, customer loyalty, sustainable growth
|
||||||
|
Risks: Slower growth, single product dependency
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario C: Pivot to Platform
|
||||||
|
```
|
||||||
|
Branch: "Strategy C - Platform Pivot"
|
||||||
|
Assumptions:
|
||||||
|
- $3M funding
|
||||||
|
- Product architecture change
|
||||||
|
- Partner ecosystem
|
||||||
|
- Risk: Technical debt, market confusion
|
||||||
|
|
||||||
|
Year 1:
|
||||||
|
Team: 28 people (+13)
|
||||||
|
- Added: Platform team (8 people: 5 engineers, 2 product, 1 architect)
|
||||||
|
- Added: Partner relations (2 people)
|
||||||
|
- Added: Developer advocacy (2 people)
|
||||||
|
- Restructured: Product org → Platform + Ecosystem
|
||||||
|
|
||||||
|
Year 2:
|
||||||
|
Team: 42 people (+14)
|
||||||
|
- Added: Partner ecosystem (external nodes)
|
||||||
|
- Added: Developer community node
|
||||||
|
- Added: Integration team (5 people)
|
||||||
|
- Network: Extended beyond company (partners)
|
||||||
|
|
||||||
|
Outcome Analysis:
|
||||||
|
Strengths: Ecosystem leverage, network effects, scalability
|
||||||
|
Risks: Complex coordination, dependency on partners
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparison: Scenario A vs B vs C (Year 2)
|
||||||
|
|
||||||
|
**Side-by-side visualization:**
|
||||||
|
```
|
||||||
|
┌──────────────────┬──────────────────┬──────────────────┐
|
||||||
|
│ Scenario A │ Scenario B │ Scenario C │
|
||||||
|
│ Rapid Expansion │ Focused Growth │ Platform Pivot │
|
||||||
|
├──────────────────┼──────────────────┼──────────────────┤
|
||||||
|
│ Team: 65 people │ Team: 30 people │ Team: 42 people │
|
||||||
|
│ 5 VPs │ 3 Managers │ 4 Managers │
|
||||||
|
│ High complexity │ Moderate density │ Extended network │
|
||||||
|
│ │ │ │
|
||||||
|
│ [Dense graph] │ [Tight graph] │ [Extended graph] │
|
||||||
|
│ Many nodes │ Fewer nodes │ External nodes │
|
||||||
|
│ Hierarchical │ Flat structure │ Hub-and-spoke │
|
||||||
|
│ │ │ │
|
||||||
|
│ Burn: High │ Burn: Low │ Burn: Medium │
|
||||||
|
│ Revenue: High │ Revenue: Medium │ Revenue: Variable│
|
||||||
|
│ Risk: Medium │ Risk: Low │ Risk: High │
|
||||||
|
└──────────────────┴──────────────────┴──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comparison metrics:**
|
||||||
|
```
|
||||||
|
Metric Scenario A Scenario B Scenario C
|
||||||
|
─────────────────────────────────────────────────────────────
|
||||||
|
Team Size 65 30 42
|
||||||
|
Management Layers 4 2 3
|
||||||
|
Network Density 0.089 0.156 0.112*
|
||||||
|
Avg Connections/Person 5.8 4.7 6.3*
|
||||||
|
External Connections 5 8 24
|
||||||
|
Products 3 1 Platform
|
||||||
|
Revenue Potential $15M $5M $8M
|
||||||
|
Risk Score 7/10 3/10 8/10
|
||||||
|
|
||||||
|
* Including external partner nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Journey: CTO (Across All Scenarios)
|
||||||
|
|
||||||
|
**Scenario A (Rapid Expansion):**
|
||||||
|
```
|
||||||
|
Current → Year 1 → Year 2
|
||||||
|
|
||||||
|
Role:
|
||||||
|
CTO (Hands-on) → VP Engineering → CTO (Strategic)
|
||||||
|
|
||||||
|
Direct Reports:
|
||||||
|
3 → 8 → 23
|
||||||
|
|
||||||
|
Focus:
|
||||||
|
Architecture → Team building → Organization leadership
|
||||||
|
|
||||||
|
Network Position:
|
||||||
|
Central-technical → Central-management → Central-strategic
|
||||||
|
|
||||||
|
Note: Increasingly removed from code, focus on scaling organization
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scenario B (Focused Growth):**
|
||||||
|
```
|
||||||
|
Current → Year 1 → Year 2
|
||||||
|
|
||||||
|
Role:
|
||||||
|
CTO (Hands-on) → CTO (Hands-on) → CTO (Technical Lead)
|
||||||
|
|
||||||
|
Direct Reports:
|
||||||
|
3 → 6 → 10
|
||||||
|
|
||||||
|
Focus:
|
||||||
|
Architecture → Product excellence → Technical depth
|
||||||
|
|
||||||
|
Network Position:
|
||||||
|
Central-technical → Central-technical → Central-technical
|
||||||
|
|
||||||
|
Note: Remains hands-on, deep technical involvement, leads by example
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scenario C (Platform Pivot):**
|
||||||
|
```
|
||||||
|
Current → Year 1 → Year 2
|
||||||
|
|
||||||
|
Role:
|
||||||
|
CTO (Hands-on) → CTO + Chief Architect → CTO (Ecosystem)
|
||||||
|
|
||||||
|
Direct Reports:
|
||||||
|
3 → 8 → 12
|
||||||
|
|
||||||
|
Focus:
|
||||||
|
Product → Platform architecture → External integrations
|
||||||
|
|
||||||
|
Network Position:
|
||||||
|
Central-technical → Central-hub → Hub-to-external
|
||||||
|
|
||||||
|
Note: Shifts to platform thinking, manages internal + external relations
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comparison visualization:**
|
||||||
|
```
|
||||||
|
Scenario A Scenario B Scenario C
|
||||||
|
Year 2: (Strategic) (Technical) (Ecosystem)
|
||||||
|
|
||||||
|
Hands-on ●──────────●──────────●──────────● ●──────────●
|
||||||
|
Code: None Moderate Some
|
||||||
|
|
||||||
|
Team Size: ●──────────●──────────●──────────● ●──────────●
|
||||||
|
Huge (23) Small (10) Medium (12)
|
||||||
|
|
||||||
|
External ●──────────●──────────●──────────● ●──────────●
|
||||||
|
Focus: Low Low High
|
||||||
|
|
||||||
|
Stress: ●──────────●──────────●──────────● ●──────────●
|
||||||
|
High Low Medium
|
||||||
|
|
||||||
|
Job ●──────────●──────────●──────────● ●──────────●
|
||||||
|
Satisfaction: Medium High Medium
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 4: Timeline Scrubber Interaction
|
||||||
|
|
||||||
|
### Visual Design
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Timeline: Company Evolution │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ↓ (You are here) │
|
||||||
|
│ ●═══●═══●═══●═══●═══●═══●═══●═══●═══●═══●═══● │
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │ │ │ │
|
||||||
|
│ J F M A M J J A S O N D │
|
||||||
|
│ a e a p a u u u e c o e │
|
||||||
|
│ n b r r y n l g p t v c │
|
||||||
|
│ │
|
||||||
|
│ ◀───────────────────────────────────────────────────────▶ │
|
||||||
|
│ [Drag to scrub through timeline] │
|
||||||
|
│ │
|
||||||
|
│ Speed: [◀] [1x] [▶] Loop: [ ] Auto-play: [ ] │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interaction States
|
||||||
|
|
||||||
|
**Hover over state marker:**
|
||||||
|
```
|
||||||
|
● ← Marker highlights
|
||||||
|
╱╲
|
||||||
|
╱ ╲ Tooltip appears:
|
||||||
|
┌────────────────┐
|
||||||
|
│ April 2023 │
|
||||||
|
│ Q2 Review │
|
||||||
|
│ 12 actors │
|
||||||
|
│ 18 relations │
|
||||||
|
│ Click to view │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Click state marker:**
|
||||||
|
```
|
||||||
|
Before: After:
|
||||||
|
●───●───●───● ●───●───●───●
|
||||||
|
↑ ↑
|
||||||
|
(You are here) (You are here)
|
||||||
|
|
||||||
|
Graph updates with transition animation:
|
||||||
|
- Actors fade out/in
|
||||||
|
- Actors move to new positions
|
||||||
|
- Relations appear/disappear
|
||||||
|
- Duration: 500ms
|
||||||
|
```
|
||||||
|
|
||||||
|
**Drag scrubber:**
|
||||||
|
```
|
||||||
|
Dragging:
|
||||||
|
●═══●═══●═══●═══●
|
||||||
|
↑ ↑ ↑ ↑
|
||||||
|
(scrubbing through intermediate frames)
|
||||||
|
|
||||||
|
Graph continuously updates
|
||||||
|
Shows interpolated states
|
||||||
|
Smooth animation at 30fps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-select for comparison:**
|
||||||
|
```
|
||||||
|
Click first state (Shift+Click):
|
||||||
|
●═══●═══●═══●═══●
|
||||||
|
✓ (selected)
|
||||||
|
|
||||||
|
Click second state:
|
||||||
|
●═══●═══●═══●═══●
|
||||||
|
✓ ✓
|
||||||
|
└───────┘
|
||||||
|
(Range highlighted)
|
||||||
|
|
||||||
|
Compare button appears:
|
||||||
|
[⚖ Compare Selected]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 5: Diff Visualization Modes
|
||||||
|
|
||||||
|
### Mode 1: Overlay Mode
|
||||||
|
```
|
||||||
|
Original graph (State A) shown in gray/muted colors
|
||||||
|
Changes overlaid with highlighting:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Alice │
|
||||||
|
│ (gray) │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─┴─┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Bob Carol │
|
||||||
|
│ (gray)(gray) │
|
||||||
|
│ │
|
||||||
|
│ NEW! │
|
||||||
|
│ ┌─────┐ │
|
||||||
|
│ │ Dave │ ← Green border │
|
||||||
|
│ └─────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ │ ← Green edge │
|
||||||
|
│ │ │
|
||||||
|
│ Carol │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
■ Green = Added
|
||||||
|
■ Red = Removed (shown faded)
|
||||||
|
■ Yellow = Modified
|
||||||
|
■ Gray = Unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode 2: Side-by-Side Mode
|
||||||
|
```
|
||||||
|
┌──────────────────────┬──────────────────────┐
|
||||||
|
│ State A (Before) │ State B (After) │
|
||||||
|
├──────────────────────┼──────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ Alice │ Alice │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌─┴─┐ │ ┌─┴─┐ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ Bob Carol │ Bob Carol │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Dave │
|
||||||
|
│ │ (green) │
|
||||||
|
│ │ │
|
||||||
|
│ Actors: 3 │ Actors: 4 │
|
||||||
|
│ Relations: 2 │ Relations: 3 │
|
||||||
|
└──────────────────────┴──────────────────────┘
|
||||||
|
|
||||||
|
Synchronized: Zoom and pan linked between both views
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode 3: Diff List View
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Changes: State A → State B │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ADDED (1 actor, 1 relation) │
|
||||||
|
│ ✓ Dave (Person) │
|
||||||
|
│ ✓ Carol → Dave (Collaboration) │
|
||||||
|
│ │
|
||||||
|
│ REMOVED (0) │
|
||||||
|
│ (none) │
|
||||||
|
│ │
|
||||||
|
│ MODIFIED (1 actor) │
|
||||||
|
│ ○ Carol │
|
||||||
|
│ • Position: (120, 80) → (180, 100) │
|
||||||
|
│ • Connections: 1 → 2 │
|
||||||
|
│ │
|
||||||
|
│ UNCHANGED (2 actors, 2 relations) │
|
||||||
|
│ [Collapse to hide] │
|
||||||
|
│ │
|
||||||
|
│ Summary: │
|
||||||
|
│ Total changes: 3 │
|
||||||
|
│ Actors affected: 2 (50%) │
|
||||||
|
│ Relations affected: 1 (33%) │
|
||||||
|
│ Network density change: +15% │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode 4: Animated Diff
|
||||||
|
```
|
||||||
|
Animation sequence (10 frames, 2 seconds total):
|
||||||
|
|
||||||
|
Frame 0 (State A):
|
||||||
|
Alice, Bob, Carol visible
|
||||||
|
|
||||||
|
Frame 3:
|
||||||
|
Dave fades in (opacity 0.3)
|
||||||
|
|
||||||
|
Frame 5:
|
||||||
|
Dave fully visible
|
||||||
|
New edge starts growing from Carol
|
||||||
|
|
||||||
|
Frame 7:
|
||||||
|
Edge fully connected
|
||||||
|
Carol moves to new position (interpolated)
|
||||||
|
|
||||||
|
Frame 10 (State B):
|
||||||
|
Final state
|
||||||
|
Highlighting fades out over 1 second
|
||||||
|
|
||||||
|
Visual cues during animation:
|
||||||
|
- New elements pulse briefly
|
||||||
|
- Removed elements fade with red glow
|
||||||
|
- Modified elements highlighted during change
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 6: Actor Journey Visualization
|
||||||
|
|
||||||
|
### Journey Timeline View
|
||||||
|
```
|
||||||
|
Actor: Sarah Chen
|
||||||
|
Timeframe: January 2023 - December 2023
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Jan Feb Mar Apr May Jun Jul Aug Sep │
|
||||||
|
│ ●──────●──────●──────●──────●──────●──────●──────●──────● │
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │
|
||||||
|
│ Eng Eng Sr.Eng Sr.Eng Lead Lead Manager Manager Manager│
|
||||||
|
│ │
|
||||||
|
│ Connections: 3→3→4→5→6→7→8→10→12 │
|
||||||
|
│ │
|
||||||
|
│ Key Events: │
|
||||||
|
│ Mar: Promotion to Senior Engineer │
|
||||||
|
│ May: Promoted to Team Lead │
|
||||||
|
│ Jul: Became Engineering Manager │
|
||||||
|
│ Sep: Team expanded significantly │
|
||||||
|
│ │
|
||||||
|
│ Property Changes: │
|
||||||
|
│ • Title changed: 3 times │
|
||||||
|
│ • Direct reports: 0→0→2→2→4→4→8→8→8 │
|
||||||
|
│ • Position: Center-left → Center (more central) │
|
||||||
|
│ │
|
||||||
|
│ Relationship Evolution: │
|
||||||
|
│ • Peer relationships: 3→3→4→4→3→3→2→2→1 (declining) │
|
||||||
|
│ • Managerial relations: 0→0→0→2→4→4→8→10→12 (growing) │
|
||||||
|
│ • Cross-team relations: 0→0→1→2→3→5→6→8→10 (growing) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Journey Graph View
|
||||||
|
```
|
||||||
|
Visual representation of Sarah's network evolution:
|
||||||
|
|
||||||
|
January (Starting point):
|
||||||
|
[Sarah]
|
||||||
|
│
|
||||||
|
┌────┼────┐
|
||||||
|
│ │ │
|
||||||
|
[Tom][Ann][Lee]
|
||||||
|
(peers)
|
||||||
|
|
||||||
|
May (Became Team Lead):
|
||||||
|
[Sarah] ← Now has direct reports
|
||||||
|
│
|
||||||
|
┌────┼────┬────┐
|
||||||
|
│ │ │ │
|
||||||
|
[Tom][Ann][Lee][New]
|
||||||
|
↓ ↓
|
||||||
|
[Jr1][Jr2]
|
||||||
|
|
||||||
|
September (Engineering Manager):
|
||||||
|
[Sarah]
|
||||||
|
│
|
||||||
|
┌────────┼────────┐
|
||||||
|
│ │ │
|
||||||
|
[Lead1][Lead2][Lead3]
|
||||||
|
│ │ │
|
||||||
|
┌─┼─┐ ┌─┼─┐ ┌─┼─┐
|
||||||
|
[T][T] [T][T] [T][T]
|
||||||
|
|
||||||
|
Network metrics:
|
||||||
|
Betweenness centrality: 0.05 → 0.15 → 0.42 (×8.4 increase)
|
||||||
|
Degree centrality: 0.12 → 0.18 → 0.35 (×2.9 increase)
|
||||||
|
Closeness centrality: 0.25 → 0.32 → 0.48 (×1.9 increase)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 7: Scenario Branching Visualization
|
||||||
|
|
||||||
|
### Tree View
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Scenario Tree: Strategic Planning │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Current State │
|
||||||
|
│ (Jan 2024) │
|
||||||
|
│ ● │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────┼──────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ Scenario A Scenario B Scenario C │
|
||||||
|
│ (Rapid Expand) (Focused) (Platform) │
|
||||||
|
│ ● ● ● │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌────┼────┐ ┌────┴────┐ ┌────┴────┐ │
|
||||||
|
│ │ │ │ │ │ │ │ │
|
||||||
|
│ Q2 Q3 Q4 Q2 Q3 Q2 Q3 │
|
||||||
|
│ ● ● ● ● ● ● ● │
|
||||||
|
│ │ │ │
|
||||||
|
│ Q4-A1 │ │
|
||||||
|
│ ● ┌────┴────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ Q4-A2 Q4-C1 Q4-C2 │
|
||||||
|
│ ● ● ● │
|
||||||
|
│ │
|
||||||
|
│ Colors: │
|
||||||
|
│ ● Blue = Scenario A branch │
|
||||||
|
│ ● Green = Scenario B branch │
|
||||||
|
│ ● Purple = Scenario C branch │
|
||||||
|
│ ● Gray = Current reality │
|
||||||
|
│ │
|
||||||
|
│ [Expand All] [Collapse All] [Compare Scenarios] │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline View with Branches
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ●─────●─────● Scenario A │
|
||||||
|
│ / Q2 Q3 Q4 (Rapid) │
|
||||||
|
│ / │
|
||||||
|
│ ●────────Current────────● │
|
||||||
|
│ Jan 2024 │ │
|
||||||
|
│ \ │
|
||||||
|
│ ●────● Scenario B │
|
||||||
|
│ Q2 Q3 (Focused) │
|
||||||
|
│ │
|
||||||
|
│ ●─────●─────●─────● Scenario C │
|
||||||
|
│ / Q2 Q3 Q4-C1 Q4-C2 (Platform) │
|
||||||
|
│ / │
|
||||||
|
│ / │
|
||||||
|
│ ● │
|
||||||
|
│ │
|
||||||
|
│ Hover on branch to see: │
|
||||||
|
│ • Scenario assumptions │
|
||||||
|
│ • Key metrics comparison │
|
||||||
|
│ • Probability/confidence │
|
||||||
|
│ • Notes and rationale │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 8: Presentation Mode
|
||||||
|
|
||||||
|
### Full-Screen Slideshow
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ [F to toggle fullscreen] │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Organizational Evolution │ │
|
||||||
|
│ │ 2023-2024 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Graph Visual] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Key Insights: │ │
|
||||||
|
│ │ • Team grew by 80% over 12 months │ │
|
||||||
|
│ │ • Network density increased significantly │ │
|
||||||
|
│ │ • Leadership structure matured │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ State 3 of 5 │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [◀ Previous] [⏸ Pause] [▶ Next] [Esc Exit] │
|
||||||
|
│ │
|
||||||
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Navigation:
|
||||||
|
- Arrow keys to navigate
|
||||||
|
- Space to play/pause
|
||||||
|
- Esc to exit presentation mode
|
||||||
|
- Number keys to jump to slide
|
||||||
|
- 'R' to restart from beginning
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
These examples demonstrate:
|
||||||
|
|
||||||
|
1. **Temporal Evolution**: How organizations, families, and systems change over time
|
||||||
|
2. **Scenario Exploration**: Comparing alternative futures with different assumptions
|
||||||
|
3. **Visual Comparison**: Multiple ways to see and understand differences
|
||||||
|
4. **Actor Tracking**: Following individuals through changes
|
||||||
|
5. **Interactive Timeline**: Scrubbing, clicking, and animating through states
|
||||||
|
6. **Presentation**: Telling compelling stories with data
|
||||||
|
|
||||||
|
All visualizations should be:
|
||||||
|
- **Interactive**: Click, drag, hover for more information
|
||||||
|
- **Animated**: Smooth transitions between states
|
||||||
|
- **Informative**: Rich metadata and context
|
||||||
|
- **Exportable**: Save as images, videos, or reports
|
||||||
|
- **Responsive**: Work on different screen sizes
|
||||||
|
|
||||||
|
The goal is to make temporal and scenario analysis intuitive, visual, and powerful for storytelling and analysis.
|
||||||
119
src/App.tsx
119
src/App.tsx
|
|
@ -3,6 +3,7 @@ import { ReactFlowProvider, useReactFlow } from "reactflow";
|
||||||
import GraphEditor from "./components/Editor/GraphEditor";
|
import GraphEditor from "./components/Editor/GraphEditor";
|
||||||
import LeftPanel from "./components/Panels/LeftPanel";
|
import LeftPanel from "./components/Panels/LeftPanel";
|
||||||
import RightPanel from "./components/Panels/RightPanel";
|
import RightPanel from "./components/Panels/RightPanel";
|
||||||
|
import BottomPanel from "./components/Timeline/BottomPanel";
|
||||||
import DocumentTabs from "./components/Workspace/DocumentTabs";
|
import DocumentTabs from "./components/Workspace/DocumentTabs";
|
||||||
import Toolbar from "./components/Toolbar/Toolbar";
|
import Toolbar from "./components/Toolbar/Toolbar";
|
||||||
import MenuBar from "./components/Menu/MenuBar";
|
import MenuBar from "./components/Menu/MenuBar";
|
||||||
|
|
@ -41,7 +42,7 @@ import type { ExportOptions } from "./utils/graphExport";
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const { undo, redo } = useDocumentHistory();
|
const { undo, redo } = useDocumentHistory();
|
||||||
const { activeDocumentId } = useWorkspaceStore();
|
const { activeDocumentId } = useWorkspaceStore();
|
||||||
const { leftPanelVisible, rightPanelVisible } = usePanelStore();
|
const { leftPanelVisible, rightPanelVisible, bottomPanelVisible } = usePanelStore();
|
||||||
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||||
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
||||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||||
|
|
@ -141,63 +142,71 @@ function AppContent() {
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
{activeDocumentId && <Toolbar />}
|
{activeDocumentId && <Toolbar />}
|
||||||
|
|
||||||
{/* Main content area with side panels */}
|
{/* Main content area with side panels and bottom panel */}
|
||||||
<main className="flex-1 overflow-hidden flex">
|
<main className="flex-1 overflow-hidden flex flex-col">
|
||||||
{/* Left Panel */}
|
{/* Top section: Left panel, graph editor, right panel */}
|
||||||
{leftPanelVisible && activeDocumentId && (
|
<div className="flex-1 overflow-hidden flex">
|
||||||
<LeftPanel
|
{/* Left Panel */}
|
||||||
onDeselectAll={() => {
|
{leftPanelVisible && activeDocumentId && (
|
||||||
setSelectedNode(null);
|
<LeftPanel
|
||||||
setSelectedEdge(null);
|
onDeselectAll={() => {
|
||||||
}}
|
|
||||||
onAddNode={addNodeCallback || undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Center: Graph Editor */}
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
<GraphEditor
|
|
||||||
selectedNode={selectedNode}
|
|
||||||
selectedEdge={selectedEdge}
|
|
||||||
onNodeSelect={(node) => {
|
|
||||||
setSelectedNode(node);
|
|
||||||
// Only clear edge if we're setting a node (not clearing)
|
|
||||||
if (node) {
|
|
||||||
setSelectedEdge(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onEdgeSelect={(edge) => {
|
|
||||||
setSelectedEdge(edge);
|
|
||||||
// Only clear node if we're setting an edge (not clearing)
|
|
||||||
if (edge) {
|
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
}
|
setSelectedEdge(null);
|
||||||
}}
|
}}
|
||||||
onAddNodeRequest={(
|
onAddNode={addNodeCallback || undefined}
|
||||||
callback: (
|
/>
|
||||||
nodeTypeId: string,
|
)}
|
||||||
position?: { x: number; y: number },
|
|
||||||
) => void,
|
{/* Center: Graph Editor */}
|
||||||
) => setAddNodeCallback(() => callback)}
|
<div className="flex-1 overflow-hidden">
|
||||||
onExportRequest={(
|
<GraphEditor
|
||||||
callback: (
|
selectedNode={selectedNode}
|
||||||
format: "png" | "svg",
|
selectedEdge={selectedEdge}
|
||||||
options?: ExportOptions,
|
onNodeSelect={(node) => {
|
||||||
) => Promise<void>,
|
setSelectedNode(node);
|
||||||
) => setExportCallback(() => callback)}
|
// Only clear edge if we're setting a node (not clearing)
|
||||||
/>
|
if (node) {
|
||||||
|
setSelectedEdge(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEdgeSelect={(edge) => {
|
||||||
|
setSelectedEdge(edge);
|
||||||
|
// Only clear node if we're setting an edge (not clearing)
|
||||||
|
if (edge) {
|
||||||
|
setSelectedNode(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onAddNodeRequest={(
|
||||||
|
callback: (
|
||||||
|
nodeTypeId: string,
|
||||||
|
position?: { x: number; y: number },
|
||||||
|
) => void,
|
||||||
|
) => setAddNodeCallback(() => callback)}
|
||||||
|
onExportRequest={(
|
||||||
|
callback: (
|
||||||
|
format: "png" | "svg",
|
||||||
|
options?: ExportOptions,
|
||||||
|
) => Promise<void>,
|
||||||
|
) => setExportCallback(() => callback)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel */}
|
||||||
|
{rightPanelVisible && activeDocumentId && (
|
||||||
|
<RightPanel
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
selectedEdge={selectedEdge}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedNode(null);
|
||||||
|
setSelectedEdge(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel */}
|
{/* Bottom Panel (Timeline) - show when bottomPanelVisible and there's an active document */}
|
||||||
{rightPanelVisible && activeDocumentId && (
|
{bottomPanelVisible && activeDocumentId && (
|
||||||
<RightPanel
|
<BottomPanel />
|
||||||
selectedNode={selectedNode}
|
|
||||||
selectedEdge={selectedEdge}
|
|
||||||
onClose={() => {
|
|
||||||
setSelectedNode(null);
|
|
||||||
setSelectedEdge(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,44 @@ const ContextMenu = ({ x, y, sections, onClose }: Props) => {
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
// Adjust position to prevent overflow
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuRef.current) {
|
||||||
|
const rect = menuRef.current.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
let adjustedX = x;
|
||||||
|
let adjustedY = y;
|
||||||
|
|
||||||
|
// Check right edge overflow
|
||||||
|
if (rect.right > viewportWidth) {
|
||||||
|
adjustedX = viewportWidth - rect.width - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bottom edge overflow
|
||||||
|
if (rect.bottom > viewportHeight) {
|
||||||
|
adjustedY = viewportHeight - rect.height - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check left edge overflow
|
||||||
|
if (adjustedX < 10) {
|
||||||
|
adjustedX = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check top edge overflow
|
||||||
|
if (adjustedY < 10) {
|
||||||
|
adjustedY = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply adjusted position
|
||||||
|
if (adjustedX !== x || adjustedY !== y) {
|
||||||
|
menuRef.current.style.left = `${adjustedX}px`;
|
||||||
|
menuRef.current.style.top = `${adjustedY}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [x, y]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
|
|
|
||||||
151
src/components/Timeline/BottomPanel.tsx
Normal file
151
src/components/Timeline/BottomPanel.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
|
import { usePanelStore, PANEL_CONSTANTS } from '../../stores/panelStore';
|
||||||
|
import { IconButton, Tooltip, Divider } from '@mui/material';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import TimelineView from './TimelineView';
|
||||||
|
import CreateStateDialog from './CreateStateDialog';
|
||||||
|
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||||
|
import { useTimelineStore } from '../../stores/timelineStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BottomPanel - Timeline visualization and state management
|
||||||
|
*/
|
||||||
|
const BottomPanel: React.FC = () => {
|
||||||
|
const {
|
||||||
|
bottomPanelHeight,
|
||||||
|
bottomPanelCollapsed,
|
||||||
|
setBottomPanelHeight,
|
||||||
|
collapseBottomPanel,
|
||||||
|
expandBottomPanel,
|
||||||
|
} = usePanelStore();
|
||||||
|
|
||||||
|
const { activeDocumentId } = useWorkspaceStore();
|
||||||
|
const { timelines, getAllStates } = useTimelineStore();
|
||||||
|
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const [showCreateState, setShowCreateState] = useState(false);
|
||||||
|
|
||||||
|
const hasTimeline = activeDocumentId ? timelines.has(activeDocumentId) : false;
|
||||||
|
const currentState = hasTimeline ? getAllStates().find(s => {
|
||||||
|
const timeline = timelines.get(activeDocumentId!);
|
||||||
|
return timeline && s.id === timeline.currentStateId;
|
||||||
|
}) : null;
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsResizing(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const newHeight = windowHeight - e.clientY;
|
||||||
|
|
||||||
|
const clampedHeight = Math.max(
|
||||||
|
PANEL_CONSTANTS.MIN_BOTTOM_HEIGHT,
|
||||||
|
Math.min(PANEL_CONSTANTS.MAX_BOTTOM_HEIGHT, newHeight)
|
||||||
|
);
|
||||||
|
|
||||||
|
setBottomPanelHeight(clampedHeight);
|
||||||
|
},
|
||||||
|
[isResizing, setBottomPanelHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add/remove event listeners for resize
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResizing) {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isResizing, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
const displayHeight = bottomPanelCollapsed
|
||||||
|
? PANEL_CONSTANTS.COLLAPSED_BOTTOM_HEIGHT
|
||||||
|
: bottomPanelHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white border-t border-gray-300 relative flex flex-col"
|
||||||
|
style={{ height: `${displayHeight}px` }}
|
||||||
|
>
|
||||||
|
{/* Resize Handle */}
|
||||||
|
{!bottomPanelCollapsed && (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 right-0 h-1 cursor-ns-resize hover:bg-blue-500 z-10 transition-colors"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ cursor: isResizing ? 'ns-resize' : 'ns-resize' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm">Timeline</h3>
|
||||||
|
|
||||||
|
{/* Current State Indicator */}
|
||||||
|
{currentState && !bottomPanelCollapsed && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||||
|
<div className="flex items-center gap-2 px-2 py-0.5 bg-blue-50 rounded border border-blue-200">
|
||||||
|
<span className="text-xs font-medium text-blue-700">Current:</span>
|
||||||
|
<span className="text-sm font-semibold text-blue-900">{currentState.label}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* Timeline Controls - Only show when expanded */}
|
||||||
|
{!bottomPanelCollapsed && activeDocumentId && hasTimeline && (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Create New State" arrow>
|
||||||
|
<IconButton onClick={() => setShowCreateState(true)} size="small">
|
||||||
|
<AddIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Collapse/Expand Button */}
|
||||||
|
<Tooltip title={bottomPanelCollapsed ? 'Expand Timeline' : 'Collapse Timeline'} arrow>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={bottomPanelCollapsed ? expandBottomPanel : collapseBottomPanel}
|
||||||
|
>
|
||||||
|
{bottomPanelCollapsed ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create State Dialog */}
|
||||||
|
<CreateStateDialog
|
||||||
|
open={showCreateState}
|
||||||
|
onClose={() => setShowCreateState(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content - Only show when not collapsed */}
|
||||||
|
{!bottomPanelCollapsed && (
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TimelineView />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomPanel;
|
||||||
99
src/components/Timeline/CreateStateDialog.tsx
Normal file
99
src/components/Timeline/CreateStateDialog.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTimelineStore } from '../../stores/timelineStore';
|
||||||
|
|
||||||
|
interface CreateStateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for creating a new constellation state
|
||||||
|
*/
|
||||||
|
const CreateStateDialog: React.FC<CreateStateDialogProps> = ({ open, onClose }) => {
|
||||||
|
const [label, setLabel] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [cloneFromCurrent, setCloneFromCurrent] = useState(true);
|
||||||
|
|
||||||
|
const { createState } = useTimelineStore();
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (!label.trim()) return;
|
||||||
|
|
||||||
|
createState(label.trim(), description.trim() || undefined, cloneFromCurrent);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setLabel('');
|
||||||
|
setDescription('');
|
||||||
|
setCloneFromCurrent(true);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Reset form on cancel
|
||||||
|
setLabel('');
|
||||||
|
setDescription('');
|
||||||
|
setCloneFromCurrent(true);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Create New State</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<div className="space-y-4 mt-2">
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
label="State Label"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
placeholder="e.g., 'January 2024' or 'Strategy A'"
|
||||||
|
helperText="Give this state a descriptive name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
label="Description (optional)"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Optional notes about this state..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={cloneFromCurrent}
|
||||||
|
onChange={(e) => setCloneFromCurrent(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Clone current graph (uncheck for empty state)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreate}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!label.trim()}
|
||||||
|
>
|
||||||
|
Create State
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateStateDialog;
|
||||||
79
src/components/Timeline/RenameStateDialog.tsx
Normal file
79
src/components/Timeline/RenameStateDialog.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
interface RenameStateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
currentLabel: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onRename: (newLabel: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RenameStateDialog - Dialog for renaming timeline states
|
||||||
|
*/
|
||||||
|
const RenameStateDialog: React.FC<RenameStateDialogProps> = ({
|
||||||
|
open,
|
||||||
|
currentLabel,
|
||||||
|
onClose,
|
||||||
|
onRename,
|
||||||
|
}) => {
|
||||||
|
const [label, setLabel] = useState(currentLabel);
|
||||||
|
|
||||||
|
// Update label when currentLabel changes
|
||||||
|
useEffect(() => {
|
||||||
|
setLabel(currentLabel);
|
||||||
|
}, [currentLabel]);
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
if (label.trim()) {
|
||||||
|
onRename(label.trim());
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRename();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Rename State</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="State Label"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter state label"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRename}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!label.trim()}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RenameStateDialog;
|
||||||
99
src/components/Timeline/StateNode.tsx
Normal file
99
src/components/Timeline/StateNode.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position, NodeProps } from 'reactflow';
|
||||||
|
import type { ConstellationState } from '../../types/timeline';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
|
||||||
|
interface StateNodeData {
|
||||||
|
state: ConstellationState;
|
||||||
|
isCurrent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StateNode - Custom node for timeline visualization
|
||||||
|
*/
|
||||||
|
const StateNode: React.FC<NodeProps<StateNodeData>> = ({ data, selected }) => {
|
||||||
|
const { state, isCurrent } = data;
|
||||||
|
|
||||||
|
// Format date if present
|
||||||
|
const dateStr = state.metadata?.date
|
||||||
|
? new Date(state.metadata.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Get custom color or default
|
||||||
|
const color = state.metadata?.color || '#3b82f6';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
px-3 py-2 rounded-lg border-2 bg-white shadow-sm
|
||||||
|
transition-all cursor-pointer
|
||||||
|
${selected ? 'border-blue-500 shadow-md' : 'border-gray-300'}
|
||||||
|
${isCurrent ? 'ring-2 ring-green-400' : ''}
|
||||||
|
hover:shadow-lg
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
minWidth: '120px',
|
||||||
|
maxWidth: '200px',
|
||||||
|
borderColor: selected ? '#3b82f6' : isCurrent ? '#10b981' : '#d1d5db',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Handles for connections */}
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
{isCurrent && (
|
||||||
|
<CheckCircleIcon
|
||||||
|
className="text-green-500 flex-shrink-0"
|
||||||
|
style={{ fontSize: '16px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-semibold text-sm truncate" title={state.label}>
|
||||||
|
{state.label}
|
||||||
|
</div>
|
||||||
|
{dateStr && (
|
||||||
|
<div className="text-xs text-gray-500">{dateStr}</div>
|
||||||
|
)}
|
||||||
|
{state.description && (
|
||||||
|
<div className="text-xs text-gray-600 truncate mt-1" title={state.description}>
|
||||||
|
{state.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state.metadata?.tags && state.metadata.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 mt-1 flex-wrap">
|
||||||
|
{state.metadata.tags.slice(0, 2).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-xs bg-blue-100 text-blue-700 px-1 rounded"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{state.metadata.tags.length > 2 && (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
+{state.metadata.tags.length - 2}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StateNode;
|
||||||
371
src/components/Timeline/TimelineView.tsx
Normal file
371
src/components/Timeline/TimelineView.tsx
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
|
import ReactFlow, {
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
NodeTypes,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
BackgroundVariant,
|
||||||
|
ReactFlowProvider,
|
||||||
|
} from 'reactflow';
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
|
import { useTimelineStore } from '../../stores/timelineStore';
|
||||||
|
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||||
|
import StateNode from './StateNode';
|
||||||
|
import ContextMenu from '../Editor/ContextMenu';
|
||||||
|
import RenameStateDialog from './RenameStateDialog';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import FileCopyIcon from '@mui/icons-material/FileCopy';
|
||||||
|
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import type { ConstellationState, StateId } from '../../types/timeline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout states in a horizontal timeline with branches
|
||||||
|
*/
|
||||||
|
function layoutStates(
|
||||||
|
states: ConstellationState[],
|
||||||
|
currentStateId: StateId,
|
||||||
|
rootStateId: StateId
|
||||||
|
): { nodes: Node[]; edges: Edge[] } {
|
||||||
|
const horizontalSpacing = 200;
|
||||||
|
const verticalSpacing = 100;
|
||||||
|
|
||||||
|
// Build parent-child relationships
|
||||||
|
const children = new Map<StateId, StateId[]>();
|
||||||
|
states.forEach((state) => {
|
||||||
|
if (state.parentStateId) {
|
||||||
|
if (!children.has(state.parentStateId)) {
|
||||||
|
children.set(state.parentStateId, []);
|
||||||
|
}
|
||||||
|
children.get(state.parentStateId)!.push(state.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// BFS to assign horizontal positions (level in tree)
|
||||||
|
const levels = new Map<StateId, number>();
|
||||||
|
const queue: StateId[] = [rootStateId];
|
||||||
|
levels.set(rootStateId, 0);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const stateId = queue.shift()!;
|
||||||
|
const level = levels.get(stateId)!;
|
||||||
|
|
||||||
|
const childStates = children.get(stateId) || [];
|
||||||
|
childStates.forEach((childId) => {
|
||||||
|
if (!levels.has(childId)) {
|
||||||
|
levels.set(childId, level + 1);
|
||||||
|
queue.push(childId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign vertical lanes for branches
|
||||||
|
const lanes = new Map<StateId, number>();
|
||||||
|
let currentLane = 0;
|
||||||
|
|
||||||
|
function assignLanes(stateId: StateId, lane: number) {
|
||||||
|
lanes.set(stateId, lane);
|
||||||
|
|
||||||
|
const childStates = children.get(stateId) || [];
|
||||||
|
if (childStates.length === 0) return;
|
||||||
|
|
||||||
|
// First child continues in same lane
|
||||||
|
assignLanes(childStates[0], lane);
|
||||||
|
|
||||||
|
// Additional children get new lanes
|
||||||
|
for (let i = 1; i < childStates.length; i++) {
|
||||||
|
currentLane++;
|
||||||
|
assignLanes(childStates[i], currentLane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assignLanes(rootStateId, 0);
|
||||||
|
|
||||||
|
// Create nodes
|
||||||
|
const nodes: Node[] = states.map((state) => {
|
||||||
|
const level = levels.get(state.id) || 0;
|
||||||
|
const lane = lanes.get(state.id) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: state.id,
|
||||||
|
type: 'stateNode',
|
||||||
|
position: {
|
||||||
|
x: level * horizontalSpacing,
|
||||||
|
y: lane * verticalSpacing,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
state,
|
||||||
|
isCurrent: state.id === currentStateId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create edges
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
states.forEach((state) => {
|
||||||
|
if (state.parentStateId) {
|
||||||
|
edges.push({
|
||||||
|
id: `${state.parentStateId}-${state.id}`,
|
||||||
|
source: state.parentStateId,
|
||||||
|
target: state.id,
|
||||||
|
type: 'smoothstep',
|
||||||
|
animated: state.id === currentStateId,
|
||||||
|
style: {
|
||||||
|
strokeWidth: state.id === currentStateId ? 3 : 2,
|
||||||
|
stroke: state.id === currentStateId ? '#10b981' : '#9ca3af',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimelineViewInner - Inner component with React Flow
|
||||||
|
*/
|
||||||
|
const TimelineViewInner: React.FC = () => {
|
||||||
|
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
||||||
|
const { timelines, switchToState, updateState, duplicateState, duplicateStateAsChild, deleteState } = useTimelineStore();
|
||||||
|
|
||||||
|
const timeline = activeDocumentId ? timelines.get(activeDocumentId) : null;
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
stateId: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Rename dialog state
|
||||||
|
const [renameDialog, setRenameDialog] = useState<{
|
||||||
|
stateId: string;
|
||||||
|
currentLabel: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Get all states
|
||||||
|
const states = useMemo(() => {
|
||||||
|
if (!timeline) return [];
|
||||||
|
return Array.from(timeline.states.values());
|
||||||
|
}, [timeline]);
|
||||||
|
|
||||||
|
// Layout nodes and edges
|
||||||
|
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(() => {
|
||||||
|
if (!timeline || states.length === 0) {
|
||||||
|
return { nodes: [], edges: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return layoutStates(states, timeline.currentStateId, timeline.rootStateId);
|
||||||
|
}, [states, timeline]);
|
||||||
|
|
||||||
|
// React Flow state
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);
|
||||||
|
|
||||||
|
// Update when layout changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
setNodes(layoutNodes);
|
||||||
|
setEdges(layoutEdges);
|
||||||
|
}, [layoutNodes, layoutEdges, setNodes, setEdges]);
|
||||||
|
|
||||||
|
// Handle node click - switch to state
|
||||||
|
const handleNodeClick = useCallback(
|
||||||
|
(_event: React.MouseEvent, node: Node) => {
|
||||||
|
switchToState(node.id);
|
||||||
|
setContextMenu(null); // Close context menu if open
|
||||||
|
},
|
||||||
|
[switchToState]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle pane click - close context menu
|
||||||
|
const handlePaneClick = useCallback(() => {
|
||||||
|
if (contextMenu) {
|
||||||
|
setContextMenu(null);
|
||||||
|
}
|
||||||
|
}, [contextMenu]);
|
||||||
|
|
||||||
|
// Handle node context menu
|
||||||
|
const handleNodeContextMenu = useCallback(
|
||||||
|
(event: React.MouseEvent, node: Node) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenu({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
stateId: node.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Context menu actions
|
||||||
|
const handleRenameFromMenu = useCallback(() => {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
const state = timeline?.states.get(contextMenu.stateId);
|
||||||
|
if (state) {
|
||||||
|
setRenameDialog({
|
||||||
|
stateId: contextMenu.stateId,
|
||||||
|
currentLabel: state.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setContextMenu(null);
|
||||||
|
}, [contextMenu, timeline]);
|
||||||
|
|
||||||
|
// Duplicate (Parallel): Creates sibling state with same parent
|
||||||
|
const handleDuplicateParallelFromMenu = useCallback(() => {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
duplicateState(contextMenu.stateId);
|
||||||
|
setContextMenu(null);
|
||||||
|
}, [contextMenu, duplicateState]);
|
||||||
|
|
||||||
|
// Duplicate (Series): Creates child state with original as parent
|
||||||
|
const handleDuplicateSeriesFromMenu = useCallback(() => {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
duplicateStateAsChild(contextMenu.stateId);
|
||||||
|
setContextMenu(null);
|
||||||
|
}, [contextMenu, duplicateStateAsChild]);
|
||||||
|
|
||||||
|
const handleDeleteFromMenu = useCallback(() => {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
deleteState(contextMenu.stateId);
|
||||||
|
setContextMenu(null);
|
||||||
|
}, [contextMenu, deleteState]);
|
||||||
|
|
||||||
|
// Rename dialog actions
|
||||||
|
const handleRename = useCallback(
|
||||||
|
(newLabel: string) => {
|
||||||
|
if (renameDialog) {
|
||||||
|
updateState(renameDialog.stateId, { label: newLabel });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[renameDialog, updateState]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom node types
|
||||||
|
const nodeTypes: NodeTypes = useMemo(
|
||||||
|
() => ({
|
||||||
|
stateNode: StateNode,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!timeline) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
<div className="text-center">
|
||||||
|
<p>No timeline for this document.</p>
|
||||||
|
<p className="text-sm mt-1">Create a timeline to manage multiple states.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
No states in timeline
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onNodeClick={handleNodeClick}
|
||||||
|
onNodeContextMenu={handleNodeContextMenu}
|
||||||
|
onPaneClick={handlePaneClick}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
minZoom={0.3}
|
||||||
|
maxZoom={1.5}
|
||||||
|
nodesDraggable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
elementsSelectable={true}
|
||||||
|
panOnDrag={true}
|
||||||
|
zoomOnScroll={true}
|
||||||
|
zoomOnPinch={true}
|
||||||
|
preventScrolling={false}
|
||||||
|
panOnScroll={false}
|
||||||
|
selectionOnDrag={false}
|
||||||
|
selectNodesOnDrag={false}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
||||||
|
<Controls showInteractive={false} />
|
||||||
|
</ReactFlow>
|
||||||
|
|
||||||
|
{/* Context Menu */}
|
||||||
|
{contextMenu && (
|
||||||
|
<ContextMenu
|
||||||
|
x={contextMenu.x}
|
||||||
|
y={contextMenu.y}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Rename',
|
||||||
|
icon: <EditIcon fontSize="small" />,
|
||||||
|
onClick: handleRenameFromMenu,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Duplicate',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Duplicate (Parallel)',
|
||||||
|
icon: <FileCopyIcon fontSize="small" />,
|
||||||
|
onClick: handleDuplicateParallelFromMenu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Duplicate (Series)',
|
||||||
|
icon: <CallSplitIcon fontSize="small" />,
|
||||||
|
onClick: handleDuplicateSeriesFromMenu,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <DeleteIcon fontSize="small" />,
|
||||||
|
onClick: handleDeleteFromMenu,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onClose={() => setContextMenu(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rename Dialog */}
|
||||||
|
{renameDialog && (
|
||||||
|
<RenameStateDialog
|
||||||
|
open={true}
|
||||||
|
currentLabel={renameDialog.currentLabel}
|
||||||
|
onClose={() => setRenameDialog(null)}
|
||||||
|
onRename={handleRename}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimelineView - Wrapped with its own ReactFlowProvider to avoid conflicts
|
||||||
|
*/
|
||||||
|
const TimelineView: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<TimelineViewInner />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimelineView;
|
||||||
|
|
@ -2,23 +2,22 @@ import { useCallback, useEffect } from 'react';
|
||||||
import { useWorkspaceStore } from '../stores/workspaceStore';
|
import { useWorkspaceStore } from '../stores/workspaceStore';
|
||||||
import { useHistoryStore } from '../stores/historyStore';
|
import { useHistoryStore } from '../stores/historyStore';
|
||||||
import { useGraphStore } from '../stores/graphStore';
|
import { useGraphStore } from '../stores/graphStore';
|
||||||
import type { ConstellationDocument } from '../stores/persistence/types';
|
import { useTimelineStore } from '../stores/timelineStore';
|
||||||
import { createDocument } from '../stores/persistence/saver';
|
import type { GraphSnapshot } from '../stores/historyStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useDocumentHistory Hook
|
* useDocumentHistory Hook
|
||||||
*
|
*
|
||||||
* Provides undo/redo functionality for the active document.
|
* Provides undo/redo functionality for the active timeline state.
|
||||||
* Each document has its own independent history stack (max 50 actions).
|
* Each timeline state has its own independent history stack (max 50 actions).
|
||||||
*
|
*
|
||||||
* IMPORTANT: History is per-document. Switching documents maintains separate undo/redo stacks.
|
* IMPORTANT: History is per-timeline-state. Each state in a document's timeline has completely separate undo/redo stacks.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* const { undo, redo, canUndo, canRedo, pushToHistory } = useDocumentHistory();
|
* const { undo, redo, canUndo, canRedo, pushToHistory } = useDocumentHistory();
|
||||||
*/
|
*/
|
||||||
export function useDocumentHistory() {
|
export function useDocumentHistory() {
|
||||||
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
||||||
const getActiveDocument = useWorkspaceStore((state) => state.getActiveDocument);
|
|
||||||
const markDocumentDirty = useWorkspaceStore((state) => state.markDocumentDirty);
|
const markDocumentDirty = useWorkspaceStore((state) => state.markDocumentDirty);
|
||||||
|
|
||||||
const setNodes = useGraphStore((state) => state.setNodes);
|
const setNodes = useGraphStore((state) => state.setNodes);
|
||||||
|
|
@ -28,175 +27,144 @@ export function useDocumentHistory() {
|
||||||
|
|
||||||
const historyStore = useHistoryStore();
|
const historyStore = useHistoryStore();
|
||||||
|
|
||||||
// Initialize history for active document
|
// Get current timeline state ID
|
||||||
useEffect(() => {
|
const currentStateId = useTimelineStore((state) => {
|
||||||
if (!activeDocumentId) return;
|
if (!activeDocumentId) return null;
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
return timeline?.currentStateId || null;
|
||||||
|
});
|
||||||
|
|
||||||
const history = historyStore.histories.get(activeDocumentId);
|
// Initialize history for active timeline state
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentStateId) return;
|
||||||
|
|
||||||
|
const history = historyStore.histories.get(currentStateId);
|
||||||
if (!history) {
|
if (!history) {
|
||||||
const currentDoc = getActiveDocument();
|
historyStore.initializeHistory(currentStateId);
|
||||||
if (currentDoc) {
|
|
||||||
historyStore.initializeHistory(activeDocumentId, currentDoc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [activeDocumentId, historyStore, getActiveDocument]);
|
}, [currentStateId, historyStore]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push current graph state to history
|
* Push current graph state to history
|
||||||
*/
|
*/
|
||||||
const pushToHistory = useCallback(
|
const pushToHistory = useCallback(
|
||||||
(description: string) => {
|
(description: string) => {
|
||||||
if (!activeDocumentId) {
|
if (!currentStateId) {
|
||||||
console.warn('No active document to record action');
|
console.warn('No active timeline state to record action');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read current state directly from store (not from React hooks which might be stale)
|
// Read current state directly from store (not from React hooks which might be stale)
|
||||||
const currentState = useGraphStore.getState();
|
const currentState = useGraphStore.getState();
|
||||||
|
|
||||||
const currentDoc = getActiveDocument();
|
// Create a snapshot of the current graph state
|
||||||
if (!currentDoc) {
|
const snapshot: GraphSnapshot = {
|
||||||
console.warn('Active document not loaded, attempting to use current graph state');
|
nodes: currentState.nodes,
|
||||||
// If document isn't loaded yet, create a minimal snapshot from current state
|
edges: currentState.edges,
|
||||||
const snapshot: ConstellationDocument = createDocument(
|
nodeTypes: currentState.nodeTypes,
|
||||||
currentState.nodes as never[],
|
edgeTypes: currentState.edgeTypes,
|
||||||
currentState.edges as never[],
|
|
||||||
currentState.nodeTypes,
|
|
||||||
currentState.edgeTypes
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use minimal metadata
|
|
||||||
snapshot.metadata = {
|
|
||||||
documentId: activeDocumentId,
|
|
||||||
title: 'Untitled',
|
|
||||||
version: '1.0.0',
|
|
||||||
appName: 'Constellation Analyzer',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
lastSavedBy: 'user',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Push to history
|
|
||||||
historyStore.pushAction(activeDocumentId, {
|
|
||||||
description,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
documentState: snapshot,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a snapshot of the current state
|
|
||||||
const snapshot: ConstellationDocument = createDocument(
|
|
||||||
currentState.nodes as never[],
|
|
||||||
currentState.edges as never[],
|
|
||||||
currentState.nodeTypes,
|
|
||||||
currentState.edgeTypes
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy metadata from current document
|
|
||||||
snapshot.metadata = {
|
|
||||||
...currentDoc.metadata,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Push to history
|
// Push to history
|
||||||
historyStore.pushAction(activeDocumentId, {
|
historyStore.pushAction(currentStateId, {
|
||||||
description,
|
description,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
documentState: snapshot,
|
graphState: snapshot,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[activeDocumentId, historyStore, getActiveDocument]
|
[currentStateId, historyStore]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo the last action for the active document
|
* Undo the last action for the active timeline state
|
||||||
*/
|
*/
|
||||||
const undo = useCallback(() => {
|
const undo = useCallback(() => {
|
||||||
if (!activeDocumentId) {
|
if (!currentStateId || !activeDocumentId) {
|
||||||
console.warn('No active document to undo');
|
console.warn('No active timeline state to undo');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoredState = historyStore.undo(activeDocumentId);
|
const restoredState = historyStore.undo(currentStateId);
|
||||||
if (restoredState) {
|
if (restoredState) {
|
||||||
// Update graph store with restored state
|
// Update graph store with restored state
|
||||||
setNodes(restoredState.graph.nodes as never[]);
|
setNodes(restoredState.nodes as never[]);
|
||||||
setEdges(restoredState.graph.edges as never[]);
|
setEdges(restoredState.edges as never[]);
|
||||||
setNodeTypes(restoredState.graph.nodeTypes);
|
setNodeTypes(restoredState.nodeTypes as never[]);
|
||||||
setEdgeTypes(restoredState.graph.edgeTypes);
|
setEdgeTypes(restoredState.edgeTypes as never[]);
|
||||||
|
|
||||||
// Update workspace document
|
// Update the timeline's current state with the restored graph (nodes and edges only)
|
||||||
const { documents, saveDocument } = useWorkspaceStore.getState();
|
useTimelineStore.getState().saveCurrentGraph({
|
||||||
const newDocuments = new Map(documents);
|
nodes: restoredState.nodes as never[],
|
||||||
newDocuments.set(activeDocumentId, restoredState);
|
edges: restoredState.edges as never[],
|
||||||
useWorkspaceStore.setState({ documents: newDocuments });
|
});
|
||||||
|
|
||||||
// Mark document as dirty and trigger auto-save
|
// Mark document as dirty and trigger auto-save
|
||||||
markDocumentDirty(activeDocumentId);
|
markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
// Auto-save after a short delay
|
// Auto-save after a short delay
|
||||||
|
const { saveDocument } = useWorkspaceStore.getState();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveDocument(activeDocumentId);
|
saveDocument(activeDocumentId);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, [activeDocumentId, historyStore, setNodes, setEdges, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
}, [currentStateId, activeDocumentId, historyStore, setNodes, setEdges, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redo the last undone action for the active document
|
* Redo the last undone action for the active timeline state
|
||||||
*/
|
*/
|
||||||
const redo = useCallback(() => {
|
const redo = useCallback(() => {
|
||||||
if (!activeDocumentId) {
|
if (!currentStateId || !activeDocumentId) {
|
||||||
console.warn('No active document to redo');
|
console.warn('No active timeline state to redo');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoredState = historyStore.redo(activeDocumentId);
|
const restoredState = historyStore.redo(currentStateId);
|
||||||
if (restoredState) {
|
if (restoredState) {
|
||||||
// Update graph store with restored state
|
// Update graph store with restored state
|
||||||
setNodes(restoredState.graph.nodes as never[]);
|
setNodes(restoredState.nodes as never[]);
|
||||||
setEdges(restoredState.graph.edges as never[]);
|
setEdges(restoredState.edges as never[]);
|
||||||
setNodeTypes(restoredState.graph.nodeTypes);
|
setNodeTypes(restoredState.nodeTypes as never[]);
|
||||||
setEdgeTypes(restoredState.graph.edgeTypes);
|
setEdgeTypes(restoredState.edgeTypes as never[]);
|
||||||
|
|
||||||
// Update workspace document
|
// Update the timeline's current state with the restored graph (nodes and edges only)
|
||||||
const { documents, saveDocument } = useWorkspaceStore.getState();
|
useTimelineStore.getState().saveCurrentGraph({
|
||||||
const newDocuments = new Map(documents);
|
nodes: restoredState.nodes as never[],
|
||||||
newDocuments.set(activeDocumentId, restoredState);
|
edges: restoredState.edges as never[],
|
||||||
useWorkspaceStore.setState({ documents: newDocuments });
|
});
|
||||||
|
|
||||||
// Mark document as dirty and trigger auto-save
|
// Mark document as dirty and trigger auto-save
|
||||||
markDocumentDirty(activeDocumentId);
|
markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
// Auto-save after a short delay
|
// Auto-save after a short delay
|
||||||
|
const { saveDocument } = useWorkspaceStore.getState();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveDocument(activeDocumentId);
|
saveDocument(activeDocumentId);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, [activeDocumentId, historyStore, setNodes, setEdges, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
}, [currentStateId, activeDocumentId, historyStore, setNodes, setEdges, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if undo is available for the active document
|
* Check if undo is available for the active timeline state
|
||||||
*/
|
*/
|
||||||
const canUndo = activeDocumentId ? historyStore.canUndo(activeDocumentId) : false;
|
const canUndo = currentStateId ? historyStore.canUndo(currentStateId) : false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if redo is available for the active document
|
* Check if redo is available for the active timeline state
|
||||||
*/
|
*/
|
||||||
const canRedo = activeDocumentId ? historyStore.canRedo(activeDocumentId) : false;
|
const canRedo = currentStateId ? historyStore.canRedo(currentStateId) : false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the description of the next undo action
|
* Get the description of the next undo action
|
||||||
*/
|
*/
|
||||||
const undoDescription = activeDocumentId
|
const undoDescription = currentStateId
|
||||||
? historyStore.getUndoDescription(activeDocumentId)
|
? historyStore.getUndoDescription(currentStateId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the description of the next redo action
|
* Get the description of the next redo action
|
||||||
*/
|
*/
|
||||||
const redoDescription = activeDocumentId
|
const redoDescription = currentStateId
|
||||||
? historyStore.getRedoDescription(activeDocumentId)
|
? historyStore.getRedoDescription(currentStateId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import type {
|
||||||
RelationData,
|
RelationData,
|
||||||
GraphActions
|
GraphActions
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { persistenceMiddleware } from './persistence/middleware';
|
|
||||||
import { loadGraphState } from './persistence/loader';
|
import { loadGraphState } from './persistence/loader';
|
||||||
import { exportGraphToFile, selectFileForImport } from './persistence/fileIO';
|
import { exportGraphToFile, selectFileForImport } from './persistence/fileIO';
|
||||||
|
|
||||||
|
|
@ -72,12 +71,11 @@ const loadInitialState = (): GraphStore => {
|
||||||
|
|
||||||
const initialState = loadInitialState();
|
const initialState = loadInitialState();
|
||||||
|
|
||||||
export const useGraphStore = create<GraphStore & GraphActions>(
|
export const useGraphStore = create<GraphStore & GraphActions>((set) => ({
|
||||||
persistenceMiddleware((set) => ({
|
nodes: initialState.nodes,
|
||||||
nodes: initialState.nodes,
|
edges: initialState.edges,
|
||||||
edges: initialState.edges,
|
nodeTypes: initialState.nodeTypes,
|
||||||
nodeTypes: initialState.nodeTypes,
|
edgeTypes: initialState.edgeTypes,
|
||||||
edgeTypes: initialState.edgeTypes,
|
|
||||||
|
|
||||||
// Node operations
|
// Node operations
|
||||||
addNode: (node: Actor) =>
|
addNode: (node: Actor) =>
|
||||||
|
|
@ -224,4 +222,4 @@ export const useGraphStore = create<GraphStore & GraphActions>(
|
||||||
nodeTypes: data.nodeTypes,
|
nodeTypes: data.nodeTypes,
|
||||||
edgeTypes: data.edgeTypes,
|
edgeTypes: data.edgeTypes,
|
||||||
}),
|
}),
|
||||||
})));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,75 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { ConstellationDocument } from "./persistence/types";
|
|
||||||
import { useGraphStore } from "./graphStore";
|
import { useGraphStore } from "./graphStore";
|
||||||
|
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* History Store - Per-Document Undo/Redo System
|
* History Store - Per-Timeline-State Undo/Redo System
|
||||||
*
|
*
|
||||||
* Each document maintains its own independent history stack with a maximum of 50 actions.
|
* Each timeline state maintains its own independent history stack with a maximum of 50 actions.
|
||||||
* Tracks all reversible operations: node add/delete/move, edge add/delete/edit, type changes.
|
* Tracks all reversible operations: node add/delete/move, edge add/delete/edit, type changes.
|
||||||
*
|
*
|
||||||
* IMPORTANT: History is per-document. Each document has completely separate undo/redo stacks.
|
* IMPORTANT: History is per-timeline-state. Each state in a document's timeline has completely separate undo/redo stacks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface GraphSnapshot {
|
||||||
|
nodes: Actor[];
|
||||||
|
edges: Relation[];
|
||||||
|
nodeTypes: NodeTypeConfig[];
|
||||||
|
edgeTypes: EdgeTypeConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface HistoryAction {
|
export interface HistoryAction {
|
||||||
description: string; // Human-readable description (e.g., "Add Person Actor", "Delete Collaborates Relation")
|
description: string; // Human-readable description (e.g., "Add Person Actor", "Delete Collaborates Relation")
|
||||||
timestamp: number; // When the action occurred
|
timestamp: number; // When the action occurred
|
||||||
documentState: ConstellationDocument; // Complete document state after this action
|
graphState: GraphSnapshot; // Graph state snapshot (not full document)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentHistory {
|
export interface StateHistory {
|
||||||
undoStack: HistoryAction[]; // Past states to restore (most recent at end)
|
undoStack: HistoryAction[]; // Past states to restore (most recent at end)
|
||||||
redoStack: HistoryAction[]; // Future states to restore (most recent at end)
|
redoStack: HistoryAction[]; // Future states to restore (most recent at end)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryStore {
|
interface HistoryStore {
|
||||||
// Map of documentId -> history (each document has its own independent history)
|
// Map of stateId -> history (each timeline state has its own independent history)
|
||||||
histories: Map<string, DocumentHistory>;
|
histories: Map<string, StateHistory>;
|
||||||
|
|
||||||
// Max number of actions to keep in history per document
|
// Max number of actions to keep in history per state
|
||||||
maxHistorySize: number;
|
maxHistorySize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryActions {
|
interface HistoryActions {
|
||||||
// Initialize history for a document
|
// Initialize history for a timeline state
|
||||||
initializeHistory: (
|
initializeHistory: (stateId: string) => void;
|
||||||
documentId: string,
|
|
||||||
initialState: ConstellationDocument,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Push a new action onto the document's history stack
|
// Push a new action onto the state's history stack
|
||||||
pushAction: (documentId: string, action: HistoryAction) => void;
|
pushAction: (stateId: string, action: HistoryAction) => void;
|
||||||
|
|
||||||
// Undo the last action for a specific document
|
// Undo the last action for a specific state
|
||||||
undo: (documentId: string) => ConstellationDocument | null;
|
undo: (stateId: string) => GraphSnapshot | null;
|
||||||
|
|
||||||
// Redo the last undone action for a specific document
|
// Redo the last undone action for a specific state
|
||||||
redo: (documentId: string) => ConstellationDocument | null;
|
redo: (stateId: string) => GraphSnapshot | null;
|
||||||
|
|
||||||
// Check if undo is available for a document
|
// Check if undo is available for a state
|
||||||
canUndo: (documentId: string) => boolean;
|
canUndo: (stateId: string) => boolean;
|
||||||
|
|
||||||
// Check if redo is available for a document
|
// Check if redo is available for a state
|
||||||
canRedo: (documentId: string) => boolean;
|
canRedo: (stateId: string) => boolean;
|
||||||
|
|
||||||
// Get the description of the next undo action for a document
|
// Get the description of the next undo action for a state
|
||||||
getUndoDescription: (documentId: string) => string | null;
|
getUndoDescription: (stateId: string) => string | null;
|
||||||
|
|
||||||
// Get the description of the next redo action for a document
|
// Get the description of the next redo action for a state
|
||||||
getRedoDescription: (documentId: string) => string | null;
|
getRedoDescription: (stateId: string) => string | null;
|
||||||
|
|
||||||
// Clear history for a specific document
|
// Clear history for a specific state
|
||||||
clearHistory: (documentId: string) => void;
|
clearHistory: (stateId: string) => void;
|
||||||
|
|
||||||
// Remove history for a document (when document is deleted)
|
// Remove history for a state (when state is deleted)
|
||||||
removeHistory: (documentId: string) => void;
|
removeHistory: (stateId: string) => void;
|
||||||
|
|
||||||
// Get history stats for debugging
|
// Get history stats for debugging
|
||||||
getHistoryStats: (documentId: string) => {
|
getHistoryStats: (stateId: string) => {
|
||||||
undoCount: number;
|
undoCount: number;
|
||||||
redoCount: number;
|
redoCount: number;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
@ -78,13 +82,13 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
histories: new Map(),
|
histories: new Map(),
|
||||||
maxHistorySize: MAX_HISTORY_SIZE,
|
maxHistorySize: MAX_HISTORY_SIZE,
|
||||||
|
|
||||||
initializeHistory: (documentId: string) => {
|
initializeHistory: (stateId: string) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newHistories = new Map(state.histories);
|
const newHistories = new Map(state.histories);
|
||||||
|
|
||||||
// Only initialize if not already present
|
// Only initialize if not already present
|
||||||
if (!newHistories.has(documentId)) {
|
if (!newHistories.has(stateId)) {
|
||||||
newHistories.set(documentId, {
|
newHistories.set(stateId, {
|
||||||
undoStack: [],
|
undoStack: [],
|
||||||
redoStack: [],
|
redoStack: [],
|
||||||
});
|
});
|
||||||
|
|
@ -94,30 +98,30 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
pushAction: (documentId: string, action: HistoryAction) => {
|
pushAction: (stateId: string, action: HistoryAction) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newHistories = new Map(state.histories);
|
const newHistories = new Map(state.histories);
|
||||||
const history = newHistories.get(documentId);
|
const history = newHistories.get(stateId);
|
||||||
|
|
||||||
if (!history) {
|
if (!history) {
|
||||||
console.warn(`History not initialized for document ${documentId}`);
|
console.warn(`History not initialized for state ${stateId}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📝 pushAction:", {
|
console.log("📝 pushAction:", {
|
||||||
description: action.description,
|
description: action.description,
|
||||||
actionStateNodes: action.documentState.graph.nodes.length,
|
actionStateNodes: action.graphState.nodes.length,
|
||||||
actionStateEdges: action.documentState.graph.edges.length,
|
actionStateEdges: action.graphState.edges.length,
|
||||||
currentUndoStackSize: history.undoStack.length,
|
currentUndoStackSize: history.undoStack.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The action.documentState contains the state BEFORE the action was performed
|
// The action.graphState contains the state BEFORE the action was performed
|
||||||
// We push this to the undo stack so we can restore it if the user clicks undo
|
// We push this to the undo stack so we can restore it if the user clicks undo
|
||||||
const newUndoStack = [...history.undoStack];
|
const newUndoStack = [...history.undoStack];
|
||||||
newUndoStack.push({
|
newUndoStack.push({
|
||||||
description: action.description,
|
description: action.description,
|
||||||
timestamp: action.timestamp,
|
timestamp: action.timestamp,
|
||||||
documentState: JSON.parse(JSON.stringify(action.documentState)), // Deep copy
|
graphState: JSON.parse(JSON.stringify(action.graphState)), // Deep copy
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trim undo stack if it exceeds max size
|
// Trim undo stack if it exceeds max size
|
||||||
|
|
@ -128,7 +132,7 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
// Clear redo stack when a new action is performed (can't redo after new action)
|
// Clear redo stack when a new action is performed (can't redo after new action)
|
||||||
const newRedoStack: HistoryAction[] = [];
|
const newRedoStack: HistoryAction[] = [];
|
||||||
|
|
||||||
newHistories.set(documentId, {
|
newHistories.set(stateId, {
|
||||||
undoStack: newUndoStack,
|
undoStack: newUndoStack,
|
||||||
redoStack: newRedoStack,
|
redoStack: newRedoStack,
|
||||||
});
|
});
|
||||||
|
|
@ -137,20 +141,18 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
description: action.description,
|
description: action.description,
|
||||||
newUndoStackSize: newUndoStack.length,
|
newUndoStackSize: newUndoStack.length,
|
||||||
topOfStackNodes:
|
topOfStackNodes:
|
||||||
newUndoStack[newUndoStack.length - 1]?.documentState.graph.nodes
|
newUndoStack[newUndoStack.length - 1]?.graphState.nodes.length,
|
||||||
.length,
|
|
||||||
topOfStackEdges:
|
topOfStackEdges:
|
||||||
newUndoStack[newUndoStack.length - 1]?.documentState.graph.edges
|
newUndoStack[newUndoStack.length - 1]?.graphState.edges.length,
|
||||||
.length,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { histories: newHistories };
|
return { histories: newHistories };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
undo: (documentId: string) => {
|
undo: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
|
|
||||||
if (!history || history.undoStack.length === 0) {
|
if (!history || history.undoStack.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -170,40 +172,33 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
|
|
||||||
// Get current state from graphStore and push it to redo stack
|
// Get current state from graphStore and push it to redo stack
|
||||||
const currentGraphState = useGraphStore.getState();
|
const currentGraphState = useGraphStore.getState();
|
||||||
const currentStateSnapshot = {
|
const currentStateSnapshot: GraphSnapshot = {
|
||||||
graph: {
|
nodes: currentGraphState.nodes,
|
||||||
nodes: currentGraphState.nodes,
|
edges: currentGraphState.edges,
|
||||||
edges: currentGraphState.edges,
|
nodeTypes: currentGraphState.nodeTypes,
|
||||||
nodeTypes: currentGraphState.nodeTypes,
|
edgeTypes: currentGraphState.edgeTypes,
|
||||||
edgeTypes: currentGraphState.edgeTypes,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
version: "1.0" as const,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newRedoStack = [...history.redoStack];
|
const newRedoStack = [...history.redoStack];
|
||||||
newRedoStack.push({
|
newRedoStack.push({
|
||||||
description: lastAction.description,
|
description: lastAction.description,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
documentState: JSON.parse(JSON.stringify(currentStateSnapshot)), // Deep copy
|
graphState: JSON.parse(JSON.stringify(currentStateSnapshot)), // Deep copy
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore the previous state (deep copy)
|
// Restore the previous state (deep copy)
|
||||||
const restoredState = JSON.parse(
|
const restoredState: GraphSnapshot = JSON.parse(
|
||||||
JSON.stringify(lastAction.documentState),
|
JSON.stringify(lastAction.graphState),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("⏪ after undo:", {
|
console.log("⏪ after undo:", {
|
||||||
restoredStateNodes: restoredState.graph.nodes.length,
|
restoredStateNodes: restoredState.nodes.length,
|
||||||
restoredStateEdges: restoredState.graph.edges.length,
|
restoredStateEdges: restoredState.edges.length,
|
||||||
undoStackSize: newUndoStack.length,
|
undoStackSize: newUndoStack.length,
|
||||||
redoStackSize: newRedoStack.length,
|
redoStackSize: newRedoStack.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
newHistories.set(documentId, {
|
newHistories.set(stateId, {
|
||||||
undoStack: newUndoStack,
|
undoStack: newUndoStack,
|
||||||
redoStack: newRedoStack,
|
redoStack: newRedoStack,
|
||||||
});
|
});
|
||||||
|
|
@ -213,9 +208,9 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
return restoredState;
|
return restoredState;
|
||||||
},
|
},
|
||||||
|
|
||||||
redo: (documentId: string) => {
|
redo: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
|
|
||||||
if (!history || history.redoStack.length === 0) {
|
if (!history || history.redoStack.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -229,25 +224,18 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
|
|
||||||
// Get current state from graphStore and push it to undo stack
|
// Get current state from graphStore and push it to undo stack
|
||||||
const currentGraphState = useGraphStore.getState();
|
const currentGraphState = useGraphStore.getState();
|
||||||
const currentStateSnapshot = {
|
const currentStateSnapshot: GraphSnapshot = {
|
||||||
graph: {
|
nodes: currentGraphState.nodes,
|
||||||
nodes: currentGraphState.nodes,
|
edges: currentGraphState.edges,
|
||||||
edges: currentGraphState.edges,
|
nodeTypes: currentGraphState.nodeTypes,
|
||||||
nodeTypes: currentGraphState.nodeTypes,
|
edgeTypes: currentGraphState.edgeTypes,
|
||||||
edgeTypes: currentGraphState.edgeTypes,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
version: "1.0" as const,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newUndoStack = [...history.undoStack];
|
const newUndoStack = [...history.undoStack];
|
||||||
newUndoStack.push({
|
newUndoStack.push({
|
||||||
description: lastAction.description,
|
description: lastAction.description,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
documentState: JSON.parse(JSON.stringify(currentStateSnapshot)), // Deep copy
|
graphState: JSON.parse(JSON.stringify(currentStateSnapshot)), // Deep copy
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trim if exceeds max size
|
// Trim if exceeds max size
|
||||||
|
|
@ -256,11 +244,11 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the future state (deep copy)
|
// Restore the future state (deep copy)
|
||||||
const restoredState = JSON.parse(
|
const restoredState: GraphSnapshot = JSON.parse(
|
||||||
JSON.stringify(lastAction.documentState),
|
JSON.stringify(lastAction.graphState),
|
||||||
);
|
);
|
||||||
|
|
||||||
newHistories.set(documentId, {
|
newHistories.set(stateId, {
|
||||||
undoStack: newUndoStack,
|
undoStack: newUndoStack,
|
||||||
redoStack: newRedoStack,
|
redoStack: newRedoStack,
|
||||||
});
|
});
|
||||||
|
|
@ -270,21 +258,21 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
return restoredState;
|
return restoredState;
|
||||||
},
|
},
|
||||||
|
|
||||||
canUndo: (documentId: string) => {
|
canUndo: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
return history ? history.undoStack.length > 0 : false;
|
return history ? history.undoStack.length > 0 : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
canRedo: (documentId: string) => {
|
canRedo: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
return history ? history.redoStack.length > 0 : false;
|
return history ? history.redoStack.length > 0 : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
getUndoDescription: (documentId: string) => {
|
getUndoDescription: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
|
|
||||||
if (!history || history.undoStack.length === 0) {
|
if (!history || history.undoStack.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -294,9 +282,9 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
return lastAction.description;
|
return lastAction.description;
|
||||||
},
|
},
|
||||||
|
|
||||||
getRedoDescription: (documentId: string) => {
|
getRedoDescription: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
|
|
||||||
if (!history || history.redoStack.length === 0) {
|
if (!history || history.redoStack.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -306,13 +294,13 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
return lastAction.description;
|
return lastAction.description;
|
||||||
},
|
},
|
||||||
|
|
||||||
clearHistory: (documentId: string) => {
|
clearHistory: (stateId: string) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newHistories = new Map(state.histories);
|
const newHistories = new Map(state.histories);
|
||||||
const history = newHistories.get(documentId);
|
const history = newHistories.get(stateId);
|
||||||
|
|
||||||
if (history) {
|
if (history) {
|
||||||
newHistories.set(documentId, {
|
newHistories.set(stateId, {
|
||||||
undoStack: [],
|
undoStack: [],
|
||||||
redoStack: [],
|
redoStack: [],
|
||||||
});
|
});
|
||||||
|
|
@ -322,17 +310,17 @@ export const useHistoryStore = create<HistoryStore & HistoryActions>(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeHistory: (documentId: string) => {
|
removeHistory: (stateId: string) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newHistories = new Map(state.histories);
|
const newHistories = new Map(state.histories);
|
||||||
newHistories.delete(documentId);
|
newHistories.delete(stateId);
|
||||||
return { histories: newHistories };
|
return { histories: newHistories };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getHistoryStats: (documentId: string) => {
|
getHistoryStats: (stateId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const history = state.histories.get(documentId);
|
const history = state.histories.get(stateId);
|
||||||
|
|
||||||
if (!history) {
|
if (!history) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -30,25 +30,37 @@ interface PanelState {
|
||||||
rightPanelWidth: number;
|
rightPanelWidth: number;
|
||||||
rightPanelCollapsed: boolean;
|
rightPanelCollapsed: boolean;
|
||||||
|
|
||||||
|
// Bottom Panel (Timeline)
|
||||||
|
bottomPanelVisible: boolean;
|
||||||
|
bottomPanelHeight: number;
|
||||||
|
bottomPanelCollapsed: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleLeftPanel: () => void;
|
toggleLeftPanel: () => void;
|
||||||
toggleRightPanel: () => void;
|
toggleRightPanel: () => void;
|
||||||
setLeftPanelWidth: (width: number) => void;
|
setLeftPanelWidth: (width: number) => void;
|
||||||
setRightPanelWidth: (width: number) => void;
|
setRightPanelWidth: (width: number) => void;
|
||||||
|
setBottomPanelHeight: (height: number) => void;
|
||||||
toggleLeftPanelSection: (section: keyof PanelState['leftPanelSections']) => void;
|
toggleLeftPanelSection: (section: keyof PanelState['leftPanelSections']) => void;
|
||||||
collapseLeftPanel: () => void;
|
collapseLeftPanel: () => void;
|
||||||
expandLeftPanel: () => void;
|
expandLeftPanel: () => void;
|
||||||
collapseRightPanel: () => void;
|
collapseRightPanel: () => void;
|
||||||
expandRightPanel: () => void;
|
expandRightPanel: () => void;
|
||||||
|
collapseBottomPanel: () => void;
|
||||||
|
expandBottomPanel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_LEFT_WIDTH = 280;
|
const DEFAULT_LEFT_WIDTH = 280;
|
||||||
const DEFAULT_RIGHT_WIDTH = 320;
|
const DEFAULT_RIGHT_WIDTH = 320;
|
||||||
|
const DEFAULT_BOTTOM_HEIGHT = 200;
|
||||||
const MIN_LEFT_WIDTH = 240;
|
const MIN_LEFT_WIDTH = 240;
|
||||||
const MAX_LEFT_WIDTH = 400;
|
const MAX_LEFT_WIDTH = 400;
|
||||||
const MIN_RIGHT_WIDTH = 280;
|
const MIN_RIGHT_WIDTH = 280;
|
||||||
const MAX_RIGHT_WIDTH = 500;
|
const MAX_RIGHT_WIDTH = 500;
|
||||||
|
const MIN_BOTTOM_HEIGHT = 150;
|
||||||
|
const MAX_BOTTOM_HEIGHT = 500;
|
||||||
const COLLAPSED_LEFT_WIDTH = 40;
|
const COLLAPSED_LEFT_WIDTH = 40;
|
||||||
|
const COLLAPSED_BOTTOM_HEIGHT = 40;
|
||||||
|
|
||||||
export const usePanelStore = create<PanelState>()(
|
export const usePanelStore = create<PanelState>()(
|
||||||
persist(
|
persist(
|
||||||
|
|
@ -70,6 +82,10 @@ export const usePanelStore = create<PanelState>()(
|
||||||
rightPanelWidth: DEFAULT_RIGHT_WIDTH,
|
rightPanelWidth: DEFAULT_RIGHT_WIDTH,
|
||||||
rightPanelCollapsed: false,
|
rightPanelCollapsed: false,
|
||||||
|
|
||||||
|
bottomPanelVisible: true, // Timeline panel is always visible (can be collapsed but not hidden)
|
||||||
|
bottomPanelHeight: DEFAULT_BOTTOM_HEIGHT,
|
||||||
|
bottomPanelCollapsed: false,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleLeftPanel: () =>
|
toggleLeftPanel: () =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|
@ -91,6 +107,11 @@ export const usePanelStore = create<PanelState>()(
|
||||||
rightPanelWidth: Math.max(MIN_RIGHT_WIDTH, Math.min(MAX_RIGHT_WIDTH, width)),
|
rightPanelWidth: Math.max(MIN_RIGHT_WIDTH, Math.min(MAX_RIGHT_WIDTH, width)),
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
setBottomPanelHeight: (height: number) =>
|
||||||
|
set(() => ({
|
||||||
|
bottomPanelHeight: Math.max(MIN_BOTTOM_HEIGHT, Math.min(MAX_BOTTOM_HEIGHT, height)),
|
||||||
|
})),
|
||||||
|
|
||||||
toggleLeftPanelSection: (section) =>
|
toggleLeftPanelSection: (section) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
leftPanelSections: {
|
leftPanelSections: {
|
||||||
|
|
@ -118,6 +139,16 @@ export const usePanelStore = create<PanelState>()(
|
||||||
set(() => ({
|
set(() => ({
|
||||||
rightPanelCollapsed: false,
|
rightPanelCollapsed: false,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
collapseBottomPanel: () =>
|
||||||
|
set(() => ({
|
||||||
|
bottomPanelCollapsed: true,
|
||||||
|
})),
|
||||||
|
|
||||||
|
expandBottomPanel: () =>
|
||||||
|
set(() => ({
|
||||||
|
bottomPanelCollapsed: false,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'constellation-panel-state',
|
name: 'constellation-panel-state',
|
||||||
|
|
@ -129,9 +160,13 @@ export const usePanelStore = create<PanelState>()(
|
||||||
export const PANEL_CONSTANTS = {
|
export const PANEL_CONSTANTS = {
|
||||||
DEFAULT_LEFT_WIDTH,
|
DEFAULT_LEFT_WIDTH,
|
||||||
DEFAULT_RIGHT_WIDTH,
|
DEFAULT_RIGHT_WIDTH,
|
||||||
|
DEFAULT_BOTTOM_HEIGHT,
|
||||||
MIN_LEFT_WIDTH,
|
MIN_LEFT_WIDTH,
|
||||||
MAX_LEFT_WIDTH,
|
MAX_LEFT_WIDTH,
|
||||||
MIN_RIGHT_WIDTH,
|
MIN_RIGHT_WIDTH,
|
||||||
MAX_RIGHT_WIDTH,
|
MAX_RIGHT_WIDTH,
|
||||||
|
MIN_BOTTOM_HEIGHT,
|
||||||
|
MAX_BOTTOM_HEIGHT,
|
||||||
COLLAPSED_LEFT_WIDTH,
|
COLLAPSED_LEFT_WIDTH,
|
||||||
|
COLLAPSED_BOTTOM_HEIGHT,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
||||||
import type { ConstellationDocument } from './types';
|
import type { ConstellationDocument } from './types';
|
||||||
import { createDocument } from './saver';
|
import { createDocument, serializeActors, serializeRelations } from './saver';
|
||||||
import { validateDocument, deserializeGraphState } from './loader';
|
import { validateDocument, deserializeGraphState } from './loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -8,19 +8,12 @@ import { validateDocument, deserializeGraphState } from './loader';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export current graph state to a JSON file
|
* Export a complete ConstellationDocument to a JSON file
|
||||||
|
* Includes all timeline states and metadata
|
||||||
*/
|
*/
|
||||||
export function exportGraphToFile(
|
export function exportDocumentToFile(document: ConstellationDocument): void {
|
||||||
nodes: Actor[],
|
|
||||||
edges: Relation[],
|
|
||||||
nodeTypes: NodeTypeConfig[],
|
|
||||||
edgeTypes: EdgeTypeConfig[]
|
|
||||||
): void {
|
|
||||||
// Create the document using the existing saver
|
|
||||||
const doc = createDocument(nodes, edges, nodeTypes, edgeTypes);
|
|
||||||
|
|
||||||
// Convert to JSON with pretty formatting
|
// Convert to JSON with pretty formatting
|
||||||
const jsonString = JSON.stringify(doc, null, 2);
|
const jsonString = JSON.stringify(document, null, 2);
|
||||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
|
@ -28,7 +21,9 @@ export function exportGraphToFile(
|
||||||
const link = window.document.createElement('a');
|
const link = window.document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
const dateStr = new Date().toISOString().slice(0, 10);
|
const dateStr = new Date().toISOString().slice(0, 10);
|
||||||
link.download = `constellation-analysis-${dateStr}.json`;
|
const title = document.metadata.title || 'constellation-analysis';
|
||||||
|
const sanitizedTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||||
|
link.download = `${sanitizedTitle}-${dateStr}.json`;
|
||||||
|
|
||||||
// Trigger download
|
// Trigger download
|
||||||
window.document.body.appendChild(link);
|
window.document.body.appendChild(link);
|
||||||
|
|
@ -39,6 +34,27 @@ export function exportGraphToFile(
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export current graph state to a JSON file
|
||||||
|
* Creates a new document with a single "Initial State"
|
||||||
|
*/
|
||||||
|
export function exportGraphToFile(
|
||||||
|
nodes: Actor[],
|
||||||
|
edges: Relation[],
|
||||||
|
nodeTypes: NodeTypeConfig[],
|
||||||
|
edgeTypes: EdgeTypeConfig[]
|
||||||
|
): void {
|
||||||
|
// Serialize actors and relations
|
||||||
|
const serializedNodes = serializeActors(nodes);
|
||||||
|
const serializedEdges = serializeRelations(edges);
|
||||||
|
|
||||||
|
// Create the document using the existing saver
|
||||||
|
const doc = createDocument(serializedNodes, serializedEdges, nodeTypes, edgeTypes);
|
||||||
|
|
||||||
|
// Use the main export function
|
||||||
|
exportDocumentToFile(doc);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import graph state from a JSON file
|
* Import graph state from a JSON file
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -37,70 +37,29 @@ export function validateDocument(doc: unknown): doc is ConstellationDocument {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check graph structure
|
// Check for global node and edge types
|
||||||
if (!document.graph ||
|
if (!Array.isArray(document.nodeTypes) || !Array.isArray(document.edgeTypes)) {
|
||||||
typeof document.graph !== 'object' ||
|
|
||||||
document.graph === null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = document.graph as Record<string, unknown>;
|
// Check timeline structure
|
||||||
|
if (!document.timeline ||
|
||||||
if (!Array.isArray(graph.nodes) ||
|
typeof document.timeline !== 'object' ||
|
||||||
!Array.isArray(graph.edges) ||
|
document.timeline === null) {
|
||||||
!Array.isArray(graph.nodeTypes) ||
|
|
||||||
!Array.isArray(graph.edgeTypes)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate nodes
|
const timeline = document.timeline as Record<string, unknown>;
|
||||||
for (const node of graph.nodes) {
|
|
||||||
if (!node || typeof node !== 'object') {
|
if (!timeline.states ||
|
||||||
return false;
|
typeof timeline.states !== 'object' ||
|
||||||
}
|
typeof timeline.currentStateId !== 'string' ||
|
||||||
const n = node as Record<string, unknown>;
|
typeof timeline.rootStateId !== 'string') {
|
||||||
if (!n.id || !n.type || !n.position || !n.data) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pos = n.position as Record<string, unknown>;
|
|
||||||
if (typeof pos.x !== 'number' || typeof pos.y !== 'number') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate edges
|
|
||||||
for (const edge of graph.edges) {
|
|
||||||
if (!edge || typeof edge !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const e = edge as Record<string, unknown>;
|
|
||||||
if (!e.id || !e.source || !e.target) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate node types
|
|
||||||
for (const nodeType of graph.nodeTypes) {
|
|
||||||
if (!nodeType || typeof nodeType !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const nt = nodeType as Record<string, unknown>;
|
|
||||||
if (!nt.id || !nt.label || !nt.color) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate edge types
|
|
||||||
for (const edgeType of graph.edgeTypes) {
|
|
||||||
if (!edgeType || typeof edgeType !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const et = edgeType as Record<string, unknown>;
|
|
||||||
if (!et.id || !et.label || !et.color) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeline validation is sufficient - we'll validate the current state's graph
|
||||||
|
// when we actually load it
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +111,35 @@ export function loadDocument(): ConstellationDocument | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current graph from a document's timeline
|
||||||
|
export function getCurrentGraphFromDocument(document: ConstellationDocument): {
|
||||||
|
nodes: SerializedActor[];
|
||||||
|
edges: SerializedRelation[];
|
||||||
|
nodeTypes: NodeTypeConfig[];
|
||||||
|
edgeTypes: EdgeTypeConfig[];
|
||||||
|
} | null {
|
||||||
|
try {
|
||||||
|
const { timeline, nodeTypes, edgeTypes } = document;
|
||||||
|
const currentState = timeline.states[timeline.currentStateId];
|
||||||
|
|
||||||
|
if (!currentState || !currentState.graph) {
|
||||||
|
console.error('Current state or graph not found in timeline');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine state graph with document types
|
||||||
|
return {
|
||||||
|
nodes: currentState.graph.nodes,
|
||||||
|
edges: currentState.graph.edges,
|
||||||
|
nodeTypes,
|
||||||
|
edgeTypes,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get current graph from document:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deserialize graph state from a document
|
// Deserialize graph state from a document
|
||||||
export function deserializeGraphState(document: ConstellationDocument): {
|
export function deserializeGraphState(document: ConstellationDocument): {
|
||||||
nodes: Actor[];
|
nodes: Actor[];
|
||||||
|
|
@ -160,14 +148,19 @@ export function deserializeGraphState(document: ConstellationDocument): {
|
||||||
edgeTypes: EdgeTypeConfig[];
|
edgeTypes: EdgeTypeConfig[];
|
||||||
} | null {
|
} | null {
|
||||||
try {
|
try {
|
||||||
const nodes = deserializeActors(document.graph.nodes);
|
const currentGraph = getCurrentGraphFromDocument(document);
|
||||||
const edges = deserializeRelations(document.graph.edges);
|
if (!currentGraph) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = deserializeActors(currentGraph.nodes);
|
||||||
|
const edges = deserializeRelations(currentGraph.edges);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
nodeTypes: document.graph.nodeTypes,
|
nodeTypes: currentGraph.nodeTypes,
|
||||||
edgeTypes: document.graph.edgeTypes,
|
edgeTypes: currentGraph.edgeTypes,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to deserialize graph state:', error);
|
console.error('Failed to deserialize graph state:', error);
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import type { StateCreator } from 'zustand';
|
|
||||||
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
|
||||||
import { debouncedSave } from './saver';
|
|
||||||
import { DEBOUNCE_CONFIG } from './constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persistence Middleware - Auto-saves graph state to localStorage
|
|
||||||
*
|
|
||||||
* This middleware intercepts state changes in the Zustand store and
|
|
||||||
* triggers debounced saves to localStorage.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const persistenceMiddleware = <
|
|
||||||
T extends {
|
|
||||||
nodes: Actor[];
|
|
||||||
edges: Relation[];
|
|
||||||
nodeTypes: NodeTypeConfig[];
|
|
||||||
edgeTypes: EdgeTypeConfig[];
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
config: StateCreator<T>
|
|
||||||
): StateCreator<T> => (set, get, api) => {
|
|
||||||
const stateCreator = config(set, get, api);
|
|
||||||
|
|
||||||
api.subscribe((state) => {
|
|
||||||
if (state.nodes && state.edges && state.nodeTypes && state.edgeTypes) {
|
|
||||||
debouncedSave(
|
|
||||||
state.nodes,
|
|
||||||
state.edges,
|
|
||||||
state.nodeTypes,
|
|
||||||
state.edgeTypes,
|
|
||||||
DEBOUNCE_CONFIG.DELAY,
|
|
||||||
DEBOUNCE_CONFIG.MAX_WAIT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stateCreator;
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
|
||||||
import type { ConstellationDocument, SerializedActor, SerializedRelation } from './types';
|
import type { ConstellationDocument, SerializedActor, SerializedRelation } from './types';
|
||||||
|
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
||||||
import { STORAGE_KEYS, SCHEMA_VERSION, APP_NAME } from './constants';
|
import { STORAGE_KEYS, SCHEMA_VERSION, APP_NAME } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saver - Handles serialization and saving to localStorage
|
* Saver - Handles serialization and saving to localStorage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Serialize a single actor (node) for storage
|
// Serialize actors for storage (strip React Flow internals)
|
||||||
// Excludes transient UI state like selected and dragging
|
export function serializeActors(actors: Actor[]): SerializedActor[] {
|
||||||
function serializeActor(actor: Actor): SerializedActor {
|
return actors.map(actor => ({
|
||||||
return {
|
|
||||||
id: actor.id,
|
id: actor.id,
|
||||||
type: actor.type ?? 'default',
|
type: actor.type || 'custom', // Default to 'custom' if undefined
|
||||||
position: actor.position,
|
position: actor.position,
|
||||||
data: actor.data,
|
data: actor.data,
|
||||||
};
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize a single relation (edge) for storage
|
// Serialize relations for storage (strip React Flow internals)
|
||||||
function serializeRelation(relation: Relation): SerializedRelation {
|
export function serializeRelations(relations: Relation[]): SerializedRelation[] {
|
||||||
return {
|
return relations.map(relation => ({
|
||||||
id: relation.id,
|
id: relation.id,
|
||||||
source: relation.source,
|
source: relation.source,
|
||||||
target: relation.target,
|
target: relation.target,
|
||||||
|
|
@ -27,37 +26,63 @@ function serializeRelation(relation: Relation): SerializedRelation {
|
||||||
data: relation.data,
|
data: relation.data,
|
||||||
sourceHandle: relation.sourceHandle,
|
sourceHandle: relation.sourceHandle,
|
||||||
targetHandle: relation.targetHandle,
|
targetHandle: relation.targetHandle,
|
||||||
};
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a complete document from current state
|
// Generate unique state ID
|
||||||
|
function generateStateId(): string {
|
||||||
|
return `state_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a complete document from graph data
|
||||||
|
// Creates a document with a single initial timeline state containing the provided graph
|
||||||
export function createDocument(
|
export function createDocument(
|
||||||
nodes: Actor[],
|
nodes: SerializedActor[],
|
||||||
edges: Relation[],
|
edges: SerializedRelation[],
|
||||||
nodeTypes: NodeTypeConfig[],
|
nodeTypes: NodeTypeConfig[],
|
||||||
edgeTypes: EdgeTypeConfig[],
|
edgeTypes: EdgeTypeConfig[],
|
||||||
existingDocument?: ConstellationDocument
|
existingDocument?: ConstellationDocument
|
||||||
): ConstellationDocument {
|
): ConstellationDocument {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const rootStateId = generateStateId();
|
||||||
|
|
||||||
|
// Create the initial timeline state with the provided graph (nodes and edges only)
|
||||||
|
const initialState = {
|
||||||
|
id: rootStateId,
|
||||||
|
label: 'Initial State',
|
||||||
|
parentStateId: undefined,
|
||||||
|
graph: {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create document with global types and timeline containing the initial state
|
||||||
return {
|
return {
|
||||||
metadata: {
|
metadata: {
|
||||||
version: SCHEMA_VERSION,
|
version: SCHEMA_VERSION,
|
||||||
appName: APP_NAME,
|
appName: APP_NAME,
|
||||||
createdAt: existingDocument?.metadata.createdAt || now,
|
createdAt: existingDocument?.metadata?.createdAt || now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
lastSavedBy: 'browser',
|
lastSavedBy: 'browser',
|
||||||
},
|
},
|
||||||
graph: {
|
nodeTypes,
|
||||||
nodes: nodes.map(serializeActor),
|
edgeTypes,
|
||||||
edges: edges.map(serializeRelation),
|
timeline: {
|
||||||
nodeTypes,
|
states: {
|
||||||
edgeTypes,
|
[rootStateId]: initialState,
|
||||||
|
},
|
||||||
|
currentStateId: rootStateId,
|
||||||
|
rootStateId: rootStateId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save document to localStorage
|
// Save document to localStorage (legacy function for old single-document system)
|
||||||
|
// NOTE: This is only used for migration purposes. Workspace documents are saved
|
||||||
|
// via workspace/persistence.ts
|
||||||
export function saveDocument(document: ConstellationDocument): boolean {
|
export function saveDocument(document: ConstellationDocument): boolean {
|
||||||
try {
|
try {
|
||||||
const json = JSON.stringify(document);
|
const json = JSON.stringify(document);
|
||||||
|
|
@ -67,7 +92,6 @@ export function saveDocument(document: ConstellationDocument): boolean {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||||
console.error('Storage quota exceeded');
|
console.error('Storage quota exceeded');
|
||||||
// TODO: Handle quota exceeded error in Phase 2
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to save document:', error);
|
console.error('Failed to save document:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -75,69 +99,7 @@ export function saveDocument(document: ConstellationDocument): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current graph state
|
// Clear saved state (legacy function)
|
||||||
export function saveGraphState(
|
|
||||||
nodes: Actor[],
|
|
||||||
edges: Relation[],
|
|
||||||
nodeTypes: NodeTypeConfig[],
|
|
||||||
edgeTypes: EdgeTypeConfig[]
|
|
||||||
): boolean {
|
|
||||||
try {
|
|
||||||
// Try to load existing document to preserve createdAt timestamp
|
|
||||||
const existingJson = localStorage.getItem(STORAGE_KEYS.GRAPH_STATE);
|
|
||||||
let existingDocument: ConstellationDocument | undefined;
|
|
||||||
|
|
||||||
if (existingJson) {
|
|
||||||
try {
|
|
||||||
existingDocument = JSON.parse(existingJson);
|
|
||||||
} catch {
|
|
||||||
// Ignore parse errors, we'll create a new document
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = createDocument(nodes, edges, nodeTypes, edgeTypes, existingDocument);
|
|
||||||
return saveDocument(document);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save graph state:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a debounced save function
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let lastSaveTime = 0;
|
|
||||||
|
|
||||||
export function debouncedSave(
|
|
||||||
nodes: Actor[],
|
|
||||||
edges: Relation[],
|
|
||||||
nodeTypes: NodeTypeConfig[],
|
|
||||||
edgeTypes: EdgeTypeConfig[],
|
|
||||||
delay: number = 1000,
|
|
||||||
maxWait: number = 5000
|
|
||||||
): void {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Clear existing timeout
|
|
||||||
if (saveTimeout) {
|
|
||||||
clearTimeout(saveTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force save if max wait time exceeded
|
|
||||||
if (now - lastSaveTime >= maxWait) {
|
|
||||||
saveGraphState(nodes, edges, nodeTypes, edgeTypes);
|
|
||||||
lastSaveTime = now;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule debounced save
|
|
||||||
saveTimeout = setTimeout(() => {
|
|
||||||
saveGraphState(nodes, edges, nodeTypes, edgeTypes);
|
|
||||||
lastSaveTime = now;
|
|
||||||
saveTimeout = null;
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear saved state
|
|
||||||
export function clearSavedState(): void {
|
export function clearSavedState(): void {
|
||||||
localStorage.removeItem(STORAGE_KEYS.GRAPH_STATE);
|
localStorage.removeItem(STORAGE_KEYS.GRAPH_STATE);
|
||||||
localStorage.removeItem(STORAGE_KEYS.LAST_SAVED);
|
localStorage.removeItem(STORAGE_KEYS.LAST_SAVED);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ActorData, RelationData, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
import type { ActorData, RelationData, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
||||||
|
import type { ConstellationState } from '../../types/timeline';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistence Types
|
* Persistence Types
|
||||||
|
|
@ -26,6 +27,8 @@ export interface SerializedRelation {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete document structure for storage
|
// Complete document structure for storage
|
||||||
|
// Every document has a timeline with states. The current graph is always
|
||||||
|
// derived from the current state in the timeline.
|
||||||
export interface ConstellationDocument {
|
export interface ConstellationDocument {
|
||||||
metadata: {
|
metadata: {
|
||||||
version: string; // Schema version (e.g., "1.0.0")
|
version: string; // Schema version (e.g., "1.0.0")
|
||||||
|
|
@ -33,14 +36,18 @@ export interface ConstellationDocument {
|
||||||
createdAt: string; // ISO timestamp
|
createdAt: string; // ISO timestamp
|
||||||
updatedAt: string; // ISO timestamp
|
updatedAt: string; // ISO timestamp
|
||||||
lastSavedBy: string; // Browser fingerprint or "unknown"
|
lastSavedBy: string; // Browser fingerprint or "unknown"
|
||||||
documentId?: string; // NEW: Unique document ID (for workspace)
|
documentId?: string; // Unique document ID (for workspace)
|
||||||
title?: string; // NEW: Document title (for workspace)
|
title?: string; // Document title (for workspace)
|
||||||
};
|
};
|
||||||
graph: {
|
// Global node and edge types for the entire document
|
||||||
nodes: SerializedActor[];
|
nodeTypes: NodeTypeConfig[];
|
||||||
edges: SerializedRelation[];
|
edgeTypes: EdgeTypeConfig[];
|
||||||
nodeTypes: NodeTypeConfig[];
|
// Timeline with multiple states - every document has this
|
||||||
edgeTypes: EdgeTypeConfig[];
|
// The graph is stored within each state (nodes and edges only, not types)
|
||||||
|
timeline: {
|
||||||
|
states: Record<string, ConstellationState>; // Map serialized as object
|
||||||
|
currentStateId: string;
|
||||||
|
rootStateId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
568
src/stores/timelineStore.ts
Normal file
568
src/stores/timelineStore.ts
Normal file
|
|
@ -0,0 +1,568 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import type {
|
||||||
|
Timeline,
|
||||||
|
ConstellationState,
|
||||||
|
StateId,
|
||||||
|
TimelineActions,
|
||||||
|
} from "../types/timeline";
|
||||||
|
import type { Actor, Relation } from "../types";
|
||||||
|
import type { SerializedActor, SerializedRelation } from "./persistence/types";
|
||||||
|
import { useGraphStore } from "./graphStore";
|
||||||
|
import { useWorkspaceStore } from "./workspaceStore";
|
||||||
|
import { useToastStore } from "./toastStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline Store
|
||||||
|
*
|
||||||
|
* Manages multiple constellation states within a document.
|
||||||
|
* Each document can have its own timeline with branching states.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface TimelineStore {
|
||||||
|
// Map of documentId -> Timeline
|
||||||
|
timelines: Map<string, Timeline>;
|
||||||
|
|
||||||
|
// Currently active document's timeline
|
||||||
|
activeDocumentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique state ID
|
||||||
|
function generateStateId(): StateId {
|
||||||
|
return `state_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTimelineStore = create<TimelineStore & TimelineActions>(
|
||||||
|
(set, get) => ({
|
||||||
|
timelines: new Map(),
|
||||||
|
activeDocumentId: null,
|
||||||
|
|
||||||
|
initializeTimeline: (
|
||||||
|
documentId: string,
|
||||||
|
initialGraph: ConstellationState["graph"],
|
||||||
|
) => {
|
||||||
|
const state = get();
|
||||||
|
|
||||||
|
// Don't re-initialize if already exists
|
||||||
|
if (state.timelines.has(documentId)) {
|
||||||
|
console.warn(`Timeline already initialized for document ${documentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootStateId = generateStateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const rootState: ConstellationState = {
|
||||||
|
id: rootStateId,
|
||||||
|
label: "Initial State",
|
||||||
|
parentStateId: undefined,
|
||||||
|
graph: JSON.parse(JSON.stringify(initialGraph)), // Deep copy
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeline: Timeline = {
|
||||||
|
states: new Map([[rootStateId, rootState]]),
|
||||||
|
currentStateId: rootStateId,
|
||||||
|
rootStateId: rootStateId,
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
newTimelines.set(documentId, timeline);
|
||||||
|
return {
|
||||||
|
timelines: newTimelines,
|
||||||
|
activeDocumentId: documentId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Timeline initialized for document ${documentId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTimeline: (documentId: string, timeline: Timeline) => {
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
|
||||||
|
// Convert plain objects back to Maps if needed
|
||||||
|
const statesMap =
|
||||||
|
timeline.states instanceof Map
|
||||||
|
? timeline.states
|
||||||
|
: new Map(
|
||||||
|
Object.entries(timeline.states) as [
|
||||||
|
string,
|
||||||
|
ConstellationState,
|
||||||
|
][],
|
||||||
|
);
|
||||||
|
|
||||||
|
newTimelines.set(documentId, {
|
||||||
|
...timeline,
|
||||||
|
states: statesMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
timelines: newTimelines,
|
||||||
|
activeDocumentId: documentId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createState: (
|
||||||
|
label: string,
|
||||||
|
description?: string,
|
||||||
|
cloneFromCurrent: boolean = true,
|
||||||
|
) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) {
|
||||||
|
console.error("No active document");
|
||||||
|
useToastStore.getState().showToast("No active document", "error");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) {
|
||||||
|
console.error("No timeline for active document");
|
||||||
|
useToastStore.getState().showToast("Timeline not initialized", "error");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStateId = generateStateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Get graph to clone (nodes and edges only, types are global)
|
||||||
|
let graphToClone: ConstellationState["graph"];
|
||||||
|
if (cloneFromCurrent) {
|
||||||
|
// Clone from current graph state (nodes and edges only)
|
||||||
|
const graphStore = useGraphStore.getState();
|
||||||
|
graphToClone = {
|
||||||
|
nodes: graphStore.nodes as unknown as SerializedActor[],
|
||||||
|
edges: graphStore.edges as unknown as SerializedRelation[],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Empty graph
|
||||||
|
graphToClone = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState: ConstellationState = {
|
||||||
|
id: newStateId,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
parentStateId: timeline.currentStateId, // Branch from current
|
||||||
|
graph: JSON.parse(JSON.stringify(graphToClone)), // Deep copy
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
newStates.set(newStateId, newState);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
currentStateId: newStateId, // Switch to new state
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load new state's graph into graph store
|
||||||
|
// Types come from the document and are already in the graph store
|
||||||
|
useGraphStore.setState({
|
||||||
|
nodes: newState.graph.nodes,
|
||||||
|
edges: newState.graph.edges,
|
||||||
|
// nodeTypes and edgeTypes remain unchanged (they're global per document)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark document as dirty
|
||||||
|
useWorkspaceStore.getState().markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
|
useToastStore.getState().showToast(`State "${label}" created`, "success");
|
||||||
|
|
||||||
|
return newStateId;
|
||||||
|
},
|
||||||
|
|
||||||
|
switchToState: (stateId: StateId) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) {
|
||||||
|
console.error("No active document");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) {
|
||||||
|
console.error("No timeline for active document");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetState = timeline.states.get(stateId);
|
||||||
|
if (!targetState) {
|
||||||
|
console.error(`State ${stateId} not found`);
|
||||||
|
useToastStore.getState().showToast("State not found", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current graph state to current state before switching (nodes and edges only)
|
||||||
|
const currentState = timeline.states.get(timeline.currentStateId);
|
||||||
|
if (currentState) {
|
||||||
|
const graphStore = useGraphStore.getState();
|
||||||
|
currentState.graph = {
|
||||||
|
nodes: graphStore.nodes as unknown as SerializedActor[],
|
||||||
|
edges: graphStore.edges as unknown as SerializedRelation[],
|
||||||
|
};
|
||||||
|
currentState.updatedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to target state
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
currentStateId: stateId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load target state's graph (nodes and edges only, types are global)
|
||||||
|
useGraphStore.setState({
|
||||||
|
nodes: targetState.graph.nodes as unknown as Actor[],
|
||||||
|
edges: targetState.graph.edges as unknown as Relation[],
|
||||||
|
// nodeTypes and edgeTypes remain unchanged (they're global per document)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateState: (
|
||||||
|
stateId: StateId,
|
||||||
|
updates: Partial<
|
||||||
|
Pick<ConstellationState, "label" | "description" | "metadata">
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return;
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return;
|
||||||
|
|
||||||
|
const stateToUpdate = timeline.states.get(stateId);
|
||||||
|
if (!stateToUpdate) {
|
||||||
|
console.error(`State ${stateId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...stateToUpdate,
|
||||||
|
...updates,
|
||||||
|
metadata: updates.metadata
|
||||||
|
? { ...stateToUpdate.metadata, ...updates.metadata }
|
||||||
|
: stateToUpdate.metadata,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
newStates.set(stateId, updatedState);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark document as dirty
|
||||||
|
useWorkspaceStore.getState().markDocumentDirty(activeDocumentId);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteState: (stateId: StateId) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return false;
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return false;
|
||||||
|
|
||||||
|
// Can't delete root state
|
||||||
|
if (stateId === timeline.rootStateId) {
|
||||||
|
useToastStore.getState().showToast("Cannot delete root state", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't delete current state
|
||||||
|
if (stateId === timeline.currentStateId) {
|
||||||
|
useToastStore
|
||||||
|
.getState()
|
||||||
|
.showToast(
|
||||||
|
"Cannot delete current state. Switch to another state first.",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if state has children
|
||||||
|
const children = get().getChildStates(stateId);
|
||||||
|
if (children.length > 0) {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`This state has ${children.length} child state(s). Delete anyway? Children will be orphaned.`,
|
||||||
|
);
|
||||||
|
if (!confirmed) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateToDelete = timeline.states.get(stateId);
|
||||||
|
const stateName = stateToDelete?.label || "Unknown";
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
|
||||||
|
newStates.delete(stateId);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark document as dirty
|
||||||
|
useWorkspaceStore.getState().markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
|
useToastStore
|
||||||
|
.getState()
|
||||||
|
.showToast(`State "${stateName}" deleted`, "info");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
duplicateState: (stateId: StateId, newLabel?: string) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) {
|
||||||
|
console.error("No active document");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) {
|
||||||
|
console.error("No timeline for active document");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateToDuplicate = timeline.states.get(stateId);
|
||||||
|
if (!stateToDuplicate) {
|
||||||
|
console.error(`State ${stateId} not found`);
|
||||||
|
useToastStore.getState().showToast("State not found", "error");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStateId = generateStateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const label = newLabel || `${stateToDuplicate.label} (Copy)`;
|
||||||
|
|
||||||
|
const duplicatedState: ConstellationState = {
|
||||||
|
...stateToDuplicate,
|
||||||
|
id: newStateId,
|
||||||
|
label,
|
||||||
|
parentStateId: stateToDuplicate.parentStateId, // Same parent as original (parallel)
|
||||||
|
graph: JSON.parse(JSON.stringify(stateToDuplicate.graph)), // Deep copy
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
newStates.set(newStateId, duplicatedState);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark document as dirty
|
||||||
|
useWorkspaceStore.getState().markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
|
useToastStore
|
||||||
|
.getState()
|
||||||
|
.showToast(`State "${label}" created`, "success");
|
||||||
|
|
||||||
|
return newStateId;
|
||||||
|
},
|
||||||
|
|
||||||
|
duplicateStateAsChild: (stateId: StateId, newLabel?: string) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) {
|
||||||
|
console.error("No active document");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) {
|
||||||
|
console.error("No timeline for active document");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateToDuplicate = timeline.states.get(stateId);
|
||||||
|
if (!stateToDuplicate) {
|
||||||
|
console.error(`State ${stateId} not found`);
|
||||||
|
useToastStore.getState().showToast("State not found", "error");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStateId = generateStateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const label = newLabel || `${stateToDuplicate.label} (Copy)`;
|
||||||
|
|
||||||
|
const duplicatedState: ConstellationState = {
|
||||||
|
...stateToDuplicate,
|
||||||
|
id: newStateId,
|
||||||
|
label,
|
||||||
|
parentStateId: stateId, // Original state becomes parent (series)
|
||||||
|
graph: JSON.parse(JSON.stringify(stateToDuplicate.graph)), // Deep copy
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
newStates.set(newStateId, duplicatedState);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark document as dirty
|
||||||
|
useWorkspaceStore.getState().markDocumentDirty(activeDocumentId);
|
||||||
|
|
||||||
|
useToastStore
|
||||||
|
.getState()
|
||||||
|
.showToast(`State "${label}" created`, "success");
|
||||||
|
|
||||||
|
return newStateId;
|
||||||
|
},
|
||||||
|
|
||||||
|
getState: (stateId: StateId) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return null;
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return null;
|
||||||
|
|
||||||
|
return timeline.states.get(stateId) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildStates: (stateId: StateId) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return [];
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return [];
|
||||||
|
|
||||||
|
const children: ConstellationState[] = [];
|
||||||
|
timeline.states.forEach((state) => {
|
||||||
|
if (state.parentStateId === stateId) {
|
||||||
|
children.push(state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return children;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllStates: () => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return [];
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return [];
|
||||||
|
|
||||||
|
return Array.from(timeline.states.values());
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCurrentGraph: (graph: ConstellationState["graph"]) => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return;
|
||||||
|
|
||||||
|
const timeline = state.timelines.get(activeDocumentId);
|
||||||
|
if (!timeline) return;
|
||||||
|
|
||||||
|
const currentState = timeline.states.get(timeline.currentStateId);
|
||||||
|
if (!currentState) return;
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
const timeline = newTimelines.get(activeDocumentId)!;
|
||||||
|
const newStates = new Map(timeline.states);
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...currentState,
|
||||||
|
graph: JSON.parse(JSON.stringify(graph)), // Deep copy
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
newStates.set(timeline.currentStateId, updatedState);
|
||||||
|
|
||||||
|
newTimelines.set(activeDocumentId, {
|
||||||
|
...timeline,
|
||||||
|
states: newStates,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTimeline: () => {
|
||||||
|
const state = get();
|
||||||
|
const { activeDocumentId } = state;
|
||||||
|
|
||||||
|
if (!activeDocumentId) return;
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const newTimelines = new Map(state.timelines);
|
||||||
|
newTimelines.delete(activeDocumentId);
|
||||||
|
return { timelines: newTimelines };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -52,11 +52,12 @@ export function migrateToWorkspace(): WorkspaceState | null {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create workspace settings from legacy document
|
// Create workspace settings from legacy document
|
||||||
|
// Node and edge types are now global per document
|
||||||
const settings: WorkspaceSettings = {
|
const settings: WorkspaceSettings = {
|
||||||
maxOpenDocuments: 10,
|
maxOpenDocuments: 10,
|
||||||
autoSaveEnabled: true,
|
autoSaveEnabled: true,
|
||||||
defaultNodeTypes: legacyDoc.graph.nodeTypes,
|
defaultNodeTypes: legacyDoc.nodeTypes || [],
|
||||||
defaultEdgeTypes: legacyDoc.graph.edgeTypes,
|
defaultEdgeTypes: legacyDoc.edgeTypes || [],
|
||||||
recentFiles: [],
|
recentFiles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useWorkspaceStore } from '../workspaceStore';
|
import { useWorkspaceStore } from '../workspaceStore';
|
||||||
import { useGraphStore } from '../graphStore';
|
import { useGraphStore } from '../graphStore';
|
||||||
|
import { useTimelineStore } from '../timelineStore';
|
||||||
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
|
||||||
|
import { getCurrentGraphFromDocument } from '../persistence/loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useActiveDocument Hook
|
* useActiveDocument Hook
|
||||||
|
|
@ -59,22 +61,29 @@ export function useActiveDocument() {
|
||||||
if (activeDocument && activeDocumentId) {
|
if (activeDocument && activeDocumentId) {
|
||||||
console.log(`Loading document into graph editor: ${activeDocumentId}`, activeDocument.metadata.title);
|
console.log(`Loading document into graph editor: ${activeDocumentId}`, activeDocument.metadata.title);
|
||||||
|
|
||||||
|
// Get the current graph from the document's timeline
|
||||||
|
const currentGraph = getCurrentGraphFromDocument(activeDocument);
|
||||||
|
if (!currentGraph) {
|
||||||
|
console.error('Failed to get current graph from document');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set loading flag before updating graph state
|
// Set loading flag before updating graph state
|
||||||
isLoadingRef.current = true;
|
isLoadingRef.current = true;
|
||||||
lastLoadedDocIdRef.current = activeDocumentId;
|
lastLoadedDocIdRef.current = activeDocumentId;
|
||||||
|
|
||||||
setNodes(activeDocument.graph.nodes as never[]);
|
setNodes(currentGraph.nodes as never[]);
|
||||||
setEdges(activeDocument.graph.edges as never[]);
|
setEdges(currentGraph.edges as never[]);
|
||||||
setNodeTypes(activeDocument.graph.nodeTypes as never[]);
|
setNodeTypes(currentGraph.nodeTypes as never[]);
|
||||||
setEdgeTypes(activeDocument.graph.edgeTypes as never[]);
|
setEdgeTypes(currentGraph.edgeTypes as never[]);
|
||||||
|
|
||||||
// Update the last synced state to match what we just loaded
|
// Update the last synced state to match what we just loaded
|
||||||
lastSyncedStateRef.current = {
|
lastSyncedStateRef.current = {
|
||||||
documentId: activeDocumentId,
|
documentId: activeDocumentId,
|
||||||
nodes: activeDocument.graph.nodes as Actor[],
|
nodes: currentGraph.nodes as Actor[],
|
||||||
edges: activeDocument.graph.edges as Relation[],
|
edges: currentGraph.edges as Relation[],
|
||||||
nodeTypes: activeDocument.graph.nodeTypes as NodeTypeConfig[],
|
nodeTypes: currentGraph.nodeTypes as NodeTypeConfig[],
|
||||||
edgeTypes: activeDocument.graph.edgeTypes as EdgeTypeConfig[],
|
edgeTypes: currentGraph.edgeTypes as EdgeTypeConfig[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear loading flag after a brief delay to allow state to settle
|
// Clear loading flag after a brief delay to allow state to settle
|
||||||
|
|
@ -153,14 +162,11 @@ export function useActiveDocument() {
|
||||||
edgeTypes: graphEdgeTypes as EdgeTypeConfig[],
|
edgeTypes: graphEdgeTypes as EdgeTypeConfig[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the document in the workspace store
|
// Update the timeline's current state with the new graph data (nodes and edges only)
|
||||||
const updatedDoc = documents.get(activeDocumentId);
|
useTimelineStore.getState().saveCurrentGraph({
|
||||||
if (updatedDoc) {
|
nodes: graphNodes as never[],
|
||||||
updatedDoc.graph.nodes = graphNodes as never[];
|
edges: graphEdges as never[],
|
||||||
updatedDoc.graph.edges = graphEdges as never[];
|
});
|
||||||
updatedDoc.graph.nodeTypes = graphNodeTypes as never[];
|
|
||||||
updatedDoc.graph.edgeTypes = graphEdgeTypes as never[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounced save
|
// Debounced save
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { ConstellationDocument } from './persistence/types';
|
import type { ConstellationDocument } from './persistence/types';
|
||||||
import type { Workspace, WorkspaceActions, DocumentMetadata, WorkspaceSettings } from './workspace/types';
|
import type { Workspace, WorkspaceActions, DocumentMetadata, WorkspaceSettings } from './workspace/types';
|
||||||
import { createDocument as createDocumentHelper } from './persistence/saver';
|
import { createDocument as createDocumentHelper, serializeActors, serializeRelations } from './persistence/saver';
|
||||||
import { selectFileForImport, exportGraphToFile } from './persistence/fileIO';
|
import { selectFileForImport, exportDocumentToFile } from './persistence/fileIO';
|
||||||
import {
|
import {
|
||||||
generateWorkspaceId,
|
generateWorkspaceId,
|
||||||
generateDocumentId,
|
generateDocumentId,
|
||||||
|
|
@ -23,6 +23,10 @@ import {
|
||||||
selectWorkspaceZipForImport,
|
selectWorkspaceZipForImport,
|
||||||
} from './workspace/workspaceIO';
|
} from './workspace/workspaceIO';
|
||||||
import { useToastStore } from './toastStore';
|
import { useToastStore } from './toastStore';
|
||||||
|
import { useTimelineStore } from './timelineStore';
|
||||||
|
import { useGraphStore } from './graphStore';
|
||||||
|
import type { ConstellationState, Timeline } from '../types/timeline';
|
||||||
|
import { getCurrentGraphFromDocument } from './persistence/loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workspace Store
|
* Workspace Store
|
||||||
|
|
@ -81,6 +85,11 @@ function initializeWorkspace(): Workspace {
|
||||||
const doc = loadDocumentFromStorage(savedState.activeDocumentId);
|
const doc = loadDocumentFromStorage(savedState.activeDocumentId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
documents.set(savedState.activeDocumentId, doc);
|
documents.set(savedState.activeDocumentId, doc);
|
||||||
|
|
||||||
|
// Load timeline if it exists
|
||||||
|
if (doc.timeline) {
|
||||||
|
useTimelineStore.getState().loadTimeline(savedState.activeDocumentId, doc.timeline as unknown as Timeline);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,6 +154,9 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
saveDocumentToStorage(documentId, newDoc);
|
saveDocumentToStorage(documentId, newDoc);
|
||||||
saveDocumentMetadata(documentId, metadata);
|
saveDocumentMetadata(documentId, metadata);
|
||||||
|
|
||||||
|
// Load the timeline from the newly created document into timelineStore
|
||||||
|
useTimelineStore.getState().loadTimeline(documentId, newDoc.timeline as unknown as Timeline);
|
||||||
|
|
||||||
// Update workspace
|
// Update workspace
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newDocuments = new Map(state.documents);
|
const newDocuments = new Map(state.documents);
|
||||||
|
|
@ -192,12 +204,19 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
const documentId = generateDocumentId();
|
const documentId = generateDocumentId();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Get node and edge types from source document's current graph
|
||||||
|
const sourceGraph = getCurrentGraphFromDocument(sourceDoc);
|
||||||
|
if (!sourceGraph) {
|
||||||
|
console.error('Failed to get graph from source document');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// Create new document with the same node and edge types, but no actors/relations
|
// Create new document with the same node and edge types, but no actors/relations
|
||||||
const newDoc = createDocumentHelper(
|
const newDoc = createDocumentHelper(
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
sourceDoc.graph.nodeTypes,
|
sourceGraph.nodeTypes,
|
||||||
sourceDoc.graph.edgeTypes
|
sourceGraph.edgeTypes
|
||||||
);
|
);
|
||||||
newDoc.metadata.documentId = documentId;
|
newDoc.metadata.documentId = documentId;
|
||||||
newDoc.metadata.title = title;
|
newDoc.metadata.title = title;
|
||||||
|
|
@ -213,6 +232,9 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
saveDocumentToStorage(documentId, newDoc);
|
saveDocumentToStorage(documentId, newDoc);
|
||||||
saveDocumentMetadata(documentId, metadata);
|
saveDocumentMetadata(documentId, metadata);
|
||||||
|
|
||||||
|
// Load the timeline from the newly created document into timelineStore
|
||||||
|
useTimelineStore.getState().loadTimeline(documentId, newDoc.timeline as unknown as Timeline);
|
||||||
|
|
||||||
// Update workspace
|
// Update workspace
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newDocuments = new Map(state.documents);
|
const newDocuments = new Map(state.documents);
|
||||||
|
|
@ -261,6 +283,11 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load timeline if it exists
|
||||||
|
if (doc.timeline) {
|
||||||
|
useTimelineStore.getState().loadTimeline(documentId, doc.timeline as unknown as Timeline);
|
||||||
|
}
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newDocuments = new Map(state.documents);
|
const newDocuments = new Map(state.documents);
|
||||||
newDocuments.set(documentId, doc);
|
newDocuments.set(documentId, doc);
|
||||||
|
|
@ -472,6 +499,10 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize timeline for duplicated document - always copy the timeline
|
||||||
|
// since all documents now have timelines
|
||||||
|
useTimelineStore.getState().loadTimeline(newDocumentId, duplicatedDoc.timeline as unknown as Timeline);
|
||||||
|
|
||||||
useToastStore.getState().showToast(`Document duplicated as "${newTitle}"`, 'success');
|
useToastStore.getState().showToast(`Document duplicated as "${newTitle}"`, 'success');
|
||||||
|
|
||||||
return newDocumentId;
|
return newDocumentId;
|
||||||
|
|
@ -525,9 +556,13 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
const documentId = generateDocumentId();
|
const documentId = generateDocumentId();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Serialize actors and relations for storage
|
||||||
|
const serializedNodes = serializeActors(data.nodes);
|
||||||
|
const serializedEdges = serializeRelations(data.edges);
|
||||||
|
|
||||||
const importedDoc = createDocumentHelper(
|
const importedDoc = createDocumentHelper(
|
||||||
data.nodes,
|
serializedNodes,
|
||||||
data.edges,
|
serializedEdges,
|
||||||
data.nodeTypes,
|
data.nodeTypes,
|
||||||
data.edgeTypes
|
data.edgeTypes
|
||||||
);
|
);
|
||||||
|
|
@ -544,6 +579,9 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
saveDocumentToStorage(documentId, importedDoc);
|
saveDocumentToStorage(documentId, importedDoc);
|
||||||
saveDocumentMetadata(documentId, metadata);
|
saveDocumentMetadata(documentId, metadata);
|
||||||
|
|
||||||
|
// Load the timeline from the imported document into timelineStore
|
||||||
|
useTimelineStore.getState().loadTimeline(documentId, importedDoc.timeline as unknown as Timeline);
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newDocuments = new Map(state.documents);
|
const newDocuments = new Map(state.documents);
|
||||||
newDocuments.set(documentId, importedDoc);
|
newDocuments.set(documentId, importedDoc);
|
||||||
|
|
@ -593,12 +631,26 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
exportGraphToFile(
|
// Ensure timeline is up-to-date before exporting (similar to saveDocument)
|
||||||
doc.graph.nodes,
|
const timelineState = useTimelineStore.getState();
|
||||||
doc.graph.edges,
|
const timeline = timelineState.timelines.get(documentId);
|
||||||
doc.graph.nodeTypes,
|
|
||||||
doc.graph.edgeTypes
|
if (timeline) {
|
||||||
);
|
// Serialize timeline (convert Map to object)
|
||||||
|
const serializedStates: Record<string, ConstellationState> = {};
|
||||||
|
timeline.states.forEach((state: ConstellationState, id: string) => {
|
||||||
|
serializedStates[id] = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.timeline = {
|
||||||
|
states: serializedStates,
|
||||||
|
currentStateId: timeline.currentStateId,
|
||||||
|
rootStateId: timeline.rootStateId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the complete document with all timeline states
|
||||||
|
exportDocumentToFile(doc);
|
||||||
useToastStore.getState().showToast('Document exported successfully', 'success');
|
useToastStore.getState().showToast('Document exported successfully', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|
@ -673,6 +725,30 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
||||||
const doc = state.documents.get(documentId);
|
const doc = state.documents.get(documentId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
doc.metadata.updatedAt = new Date().toISOString();
|
doc.metadata.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// Save global node and edge types from graph store
|
||||||
|
const graphStore = useGraphStore.getState();
|
||||||
|
doc.nodeTypes = graphStore.nodeTypes;
|
||||||
|
doc.edgeTypes = graphStore.edgeTypes;
|
||||||
|
|
||||||
|
// Save timeline data if exists
|
||||||
|
const timelineState = useTimelineStore.getState();
|
||||||
|
const timeline = timelineState.timelines.get(documentId);
|
||||||
|
|
||||||
|
if (timeline) {
|
||||||
|
// Serialize timeline (convert Map to object)
|
||||||
|
const serializedStates: Record<string, ConstellationState> = {};
|
||||||
|
timeline.states.forEach((state: ConstellationState, id: string) => {
|
||||||
|
serializedStates[id] = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.timeline = {
|
||||||
|
states: serializedStates,
|
||||||
|
currentStateId: timeline.currentStateId,
|
||||||
|
rootStateId: timeline.rootStateId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
saveDocumentToStorage(documentId, doc);
|
saveDocumentToStorage(documentId, doc);
|
||||||
|
|
||||||
const metadata = state.documentMetadata.get(documentId);
|
const metadata = state.documentMetadata.get(documentId);
|
||||||
|
|
|
||||||
100
src/types/timeline.ts
Normal file
100
src/types/timeline.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import type { SerializedActor, SerializedRelation } from '../stores/persistence/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline Types
|
||||||
|
*
|
||||||
|
* Support for multiple constellation states within a single document.
|
||||||
|
* States can represent time-based evolution or alternative scenarios.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type StateId = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single constellation state - a snapshot of the graph at a point in time or scenario
|
||||||
|
*/
|
||||||
|
export interface ConstellationState {
|
||||||
|
id: StateId;
|
||||||
|
label: string; // User-defined label (e.g., "Jan 2024", "Strategy A")
|
||||||
|
description?: string; // Optional detailed description
|
||||||
|
parentStateId?: string; // Parent state (null/undefined = root state)
|
||||||
|
|
||||||
|
// Graph snapshot (nodes and edges only, types are global per document)
|
||||||
|
graph: {
|
||||||
|
nodes: SerializedActor[];
|
||||||
|
edges: SerializedRelation[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional metadata - users can use these or ignore them
|
||||||
|
metadata?: {
|
||||||
|
date?: string; // Optional ISO date string
|
||||||
|
tags?: string[]; // Optional categorization tags
|
||||||
|
color?: string; // Optional color for visualization
|
||||||
|
notes?: string; // Optional presenter notes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
createdAt: string; // ISO timestamp
|
||||||
|
updatedAt: string; // ISO timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline - collection of constellation states with branching structure
|
||||||
|
*/
|
||||||
|
export interface Timeline {
|
||||||
|
states: Map<StateId, ConstellationState>; // All states by ID
|
||||||
|
currentStateId: StateId; // Currently active state
|
||||||
|
rootStateId: StateId; // Initial/root state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge in the state graph (for visualization)
|
||||||
|
*/
|
||||||
|
export interface StateEdge {
|
||||||
|
id: string;
|
||||||
|
source: StateId; // Parent state
|
||||||
|
target: StateId; // Child state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline actions
|
||||||
|
*/
|
||||||
|
export interface TimelineActions {
|
||||||
|
// Initialize timeline for a document
|
||||||
|
initializeTimeline: (documentId: string, initialGraph: ConstellationState['graph']) => void;
|
||||||
|
|
||||||
|
// Load timeline from document
|
||||||
|
loadTimeline: (documentId: string, timeline: Timeline) => void;
|
||||||
|
|
||||||
|
// Create new state
|
||||||
|
createState: (label: string, description?: string, cloneFromCurrent?: boolean) => StateId;
|
||||||
|
|
||||||
|
// Switch to different state
|
||||||
|
switchToState: (stateId: StateId) => void;
|
||||||
|
|
||||||
|
// Update state metadata
|
||||||
|
updateState: (stateId: StateId, updates: Partial<Pick<ConstellationState, 'label' | 'description' | 'metadata'>>) => void;
|
||||||
|
|
||||||
|
// Delete state
|
||||||
|
deleteState: (stateId: StateId) => boolean;
|
||||||
|
|
||||||
|
// Duplicate state (parallel - same parent as original)
|
||||||
|
duplicateState: (stateId: StateId, newLabel?: string) => StateId;
|
||||||
|
|
||||||
|
// Duplicate state as child (series - original becomes parent)
|
||||||
|
duplicateStateAsChild: (stateId: StateId, newLabel?: string) => StateId;
|
||||||
|
|
||||||
|
// Get state by ID
|
||||||
|
getState: (stateId: StateId) => ConstellationState | null;
|
||||||
|
|
||||||
|
// Get child states
|
||||||
|
getChildStates: (stateId: StateId) => ConstellationState[];
|
||||||
|
|
||||||
|
// Get all states
|
||||||
|
getAllStates: () => ConstellationState[];
|
||||||
|
|
||||||
|
// Save current graph to current state
|
||||||
|
saveCurrentGraph: (graph: ConstellationState['graph']) => void;
|
||||||
|
|
||||||
|
// Clear timeline
|
||||||
|
clearTimeline: () => void;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue