UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

257 lines (250 loc) 8.47 kB
/** * 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 }; } } }