contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
257 lines (250 loc) • 8.47 kB
JavaScript
/**
* StateManager - Manages story project workflow state
*
* Handles:
* - Detecting story projects (story_outline.md existence)
* - Parsing YAML frontmatter from outline files
* - Reading and writing workflow state
* - Managing user preferences (auto_generate_sketches, sketch_style_preference)
*/
import fs from 'fs/promises';
import path from 'path';
import yaml from 'js-yaml';
export class StateManager {
constructor(baseDir) {
this.outlineFileName = 'story_outline.md';
this.baseDir = baseDir || process.cwd();
}
/**
* Check if a story project exists in the current directory
*/
async hasStoryProject() {
try {
const outlinePath = path.join(this.baseDir, this.outlineFileName);
await fs.access(outlinePath);
return true;
}
catch {
return false;
}
}
/**
* Get the full path to the story outline file
*/
getOutlinePath() {
return path.join(this.baseDir, this.outlineFileName);
}
/**
* Parse YAML frontmatter from markdown content
* Returns null if no frontmatter found
*/
parseFrontmatter(content) {
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return null;
}
try {
const frontmatter = yaml.load(match[1]);
const contentWithoutFrontmatter = match[2];
return { frontmatter, content: contentWithoutFrontmatter };
}
catch (error) {
console.error('Error parsing YAML frontmatter:', error);
return null;
}
}
/**
* Read and parse the story outline file
* Returns null if file doesn't exist or has no valid state
*/
async readState() {
try {
const outlinePath = this.getOutlinePath();
const rawContent = await fs.readFile(outlinePath, 'utf-8');
const parsed = this.parseFrontmatter(rawContent);
if (!parsed || !parsed.frontmatter) {
return null;
}
const { frontmatter, content } = parsed;
// Validate required fields
if (!frontmatter.workflow_state) {
return null;
}
const state = {
workflow_state: frontmatter.workflow_state,
auto_generate_sketches: frontmatter.auto_generate_sketches ?? false,
sketch_style_preference: frontmatter.sketch_style_preference,
created_at: frontmatter.created_at,
last_updated: frontmatter.last_updated
};
return {
state,
content,
rawContent
};
}
catch (error) {
// File doesn't exist or can't be read
return null;
}
}
/**
* Write state to the story outline file
* Preserves existing content, only updates frontmatter
*/
async writeState(state, content) {
try {
const outlinePath = this.getOutlinePath();
// If content not provided, try to preserve existing content
let outlineContent = content;
if (!outlineContent) {
const existing = await this.readState();
outlineContent = existing?.content || '';
}
// Update last_updated timestamp
const stateWithTimestamp = {
...state,
last_updated: new Date().toISOString()
};
// If created_at doesn't exist, add it
if (!stateWithTimestamp.created_at) {
stateWithTimestamp.created_at = new Date().toISOString();
}
// Generate YAML frontmatter
const frontmatter = yaml.dump(stateWithTimestamp, {
indent: 2,
lineWidth: -1 // Disable line wrapping
});
// Combine frontmatter and content
const fullContent = `---\n${frontmatter}---\n\n${outlineContent}`;
// Write to file
await fs.writeFile(outlinePath, fullContent, 'utf-8');
}
catch (error) {
console.error('Error writing state:', error);
throw new Error('Failed to write story project state');
}
}
/**
* Update only the workflow state, preserving other fields
*/
async updateWorkflowState(newState) {
const current = await this.readState();
if (!current) {
throw new Error('No story project found to update');
}
const updatedState = {
...current.state,
workflow_state: newState
};
await this.writeState(updatedState, current.content);
}
/**
* Update user preferences (auto_generate_sketches, sketch_style_preference)
*/
async updatePreferences(preferences) {
const current = await this.readState();
if (!current) {
throw new Error('No story project found to update');
}
const updatedState = {
...current.state,
...preferences
};
await this.writeState(updatedState, current.content);
}
/**
* Create a new story project with initial state
*/
async createStoryProject(title, concept, initialState) {
const state = {
workflow_state: 'OUTLINE_DRAFT',
auto_generate_sketches: false,
sketch_style_preference: 'storyboard',
created_at: new Date().toISOString(),
last_updated: new Date().toISOString(),
...initialState
};
const content = `# ${title}
## Core Concept
${concept}
## Characters
[Add character descriptions here]
## Plot Structure
[Add plot outline here]
`;
await this.writeState(state, content);
}
/**
* Get current workflow state
* Returns null if no story project exists
*/
async getCurrentState() {
const parsed = await this.readState();
return parsed?.state.workflow_state || null;
}
/**
* Check if auto-sketch generation is enabled
*/
async isAutoSketchEnabled() {
const parsed = await this.readState();
return parsed?.state.auto_generate_sketches ?? false;
}
/**
* Get sketch style preference
*/
async getSketchStyle() {
const parsed = await this.readState();
return parsed?.state.sketch_style_preference || 'storyboard';
}
/**
* Validate state transition
* Returns true if transition is allowed, false otherwise
*/
validateStateTransition(currentState, newState) {
const validTransitions = {
'OUTLINE_DRAFT': ['OUTLINE_CONFIRMED'],
'OUTLINE_CONFIRMED': ['SCREENPLAY_WRITING', 'OUTLINE_DRAFT'], // Can go back to draft
'SCREENPLAY_WRITING': ['ITERATION'],
'ITERATION': ['OUTLINE_DRAFT'] // Can restart from outline
};
return validTransitions[currentState]?.includes(newState) ?? false;
}
/**
* Get state constraints for current state
* Returns what operations are allowed in the current state
*/
getStateConstraints(state) {
switch (state) {
case 'OUTLINE_DRAFT':
return {
canGenerateScreenplay: false,
canGenerateSketches: false,
canEditOutline: true,
canIterate: false
};
case 'OUTLINE_CONFIRMED':
return {
canGenerateScreenplay: false,
canGenerateSketches: false,
canEditOutline: false,
canIterate: false
};
case 'SCREENPLAY_WRITING':
return {
canGenerateScreenplay: true,
canGenerateSketches: false,
canEditOutline: false,
canIterate: false
};
case 'ITERATION':
return {
canGenerateScreenplay: true,
canGenerateSketches: true, // Respects auto_generate_sketches preference
canEditOutline: false,
canIterate: true
};
}
}
}