/** * Ideation Store - State management for brainstorming and idea management */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { Idea, IdeaCategory, IdeaStatus, IdeationPrompt, AnalysisSuggestion, ProjectAnalysisResult, IdeationContextSources, } from '@automaker/types'; import { DEFAULT_IDEATION_CONTEXT_SOURCES } from '@automaker/types'; // ============================================================================ // Generation Job Types // ============================================================================ export type GenerationJobStatus = 'generating' | 'ready' | 'error'; export interface GenerationJob { id: string; projectPath: string; prompt: IdeationPrompt; status: GenerationJobStatus; suggestions: AnalysisSuggestion[]; error: string | null; startedAt: string; completedAt: string | null; } // ============================================================================ // State Interface // ============================================================================ export type IdeationMode = 'dashboard' | 'prompts'; interface IdeationState { // Ideas (saved for later) ideas: Idea[]; selectedIdeaId: string | null; // Generation jobs (multiple concurrent generations) generationJobs: GenerationJob[]; selectedJobId: string | null; // Legacy - keep for backwards compat during transition suggestions: AnalysisSuggestion[]; selectedPrompt: IdeationPrompt | null; isGenerating: boolean; generatingError: string | null; // Analysis analysisResult: ProjectAnalysisResult | null; isAnalyzing: boolean; analysisProgress: number; analysisMessage: string; // UI state currentMode: IdeationMode; selectedCategory: IdeaCategory | null; filterStatus: IdeaStatus | 'all'; // Context sources per project contextSourcesByProject: Record>; } // ============================================================================ // Actions Interface // ============================================================================ interface IdeationActions { // Ideas setIdeas: (ideas: Idea[]) => void; addIdea: (idea: Idea) => void; updateIdea: (id: string, updates: Partial) => void; removeIdea: (id: string) => void; setSelectedIdea: (id: string | null) => void; getSelectedIdea: () => Idea | null; // Generation Jobs addGenerationJob: (projectPath: string, prompt: IdeationPrompt) => string; updateJobStatus: ( jobId: string, status: GenerationJobStatus, suggestions?: AnalysisSuggestion[], error?: string ) => void; removeJob: (jobId: string) => void; clearCompletedJobs: () => void; setSelectedJob: (jobId: string | null) => void; getJob: (jobId: string) => GenerationJob | null; removeSuggestionFromJob: (jobId: string, suggestionId: string) => void; appendSuggestionsToJob: (jobId: string, suggestions: AnalysisSuggestion[]) => void; setJobGenerating: (jobId: string, generating: boolean) => void; // Legacy Suggestions (kept for backwards compat) setSuggestions: (suggestions: AnalysisSuggestion[]) => void; clearSuggestions: () => void; removeSuggestion: (id: string) => void; setSelectedPrompt: (prompt: IdeationPrompt | null) => void; setIsGenerating: (isGenerating: boolean) => void; setGeneratingError: (error: string | null) => void; // Analysis setAnalysisResult: (result: ProjectAnalysisResult | null) => void; setIsAnalyzing: (isAnalyzing: boolean) => void; setAnalysisProgress: (progress: number, message?: string) => void; // UI setMode: (mode: IdeationMode) => void; setCategory: (category: IdeaCategory | null) => void; setFilterStatus: (status: IdeaStatus | 'all') => void; // Context sources /** * Returns the effective context-source settings for a project, * merging defaults with any stored overrides. */ getContextSources: (projectPath: string) => IdeationContextSources; /** * Updates a single context-source flag for a project. */ setContextSource: ( projectPath: string, key: keyof IdeationContextSources, value: boolean ) => void; // Reset reset: () => void; resetSuggestions: () => void; } // ============================================================================ // Initial State // ============================================================================ const initialState: IdeationState = { ideas: [], selectedIdeaId: null, generationJobs: [], selectedJobId: null, suggestions: [], selectedPrompt: null, isGenerating: false, generatingError: null, analysisResult: null, isAnalyzing: false, analysisProgress: 0, analysisMessage: '', currentMode: 'dashboard', selectedCategory: null, filterStatus: 'all', contextSourcesByProject: {}, }; // ============================================================================ // Store // ============================================================================ export const useIdeationStore = create()( persist( (set, get) => ({ ...initialState, // Ideas setIdeas: (ideas) => set({ ideas }), addIdea: (idea) => set((state) => ({ ideas: [idea, ...state.ideas], })), updateIdea: (id, updates) => set((state) => ({ ideas: state.ideas.map((idea) => (idea.id === id ? { ...idea, ...updates } : idea)), })), removeIdea: (id) => set((state) => ({ ideas: state.ideas.filter((idea) => idea.id !== id), selectedIdeaId: state.selectedIdeaId === id ? null : state.selectedIdeaId, })), setSelectedIdea: (id) => set({ selectedIdeaId: id }), getSelectedIdea: () => { const state = get(); return state.ideas.find((idea) => idea.id === state.selectedIdeaId) || null; }, // Generation Jobs addGenerationJob: (projectPath, prompt) => { const jobId = `job-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; const job: GenerationJob = { id: jobId, projectPath, prompt, status: 'generating', suggestions: [], error: null, startedAt: new Date().toISOString(), completedAt: null, }; set((state) => ({ generationJobs: [job, ...state.generationJobs], })); return jobId; }, updateJobStatus: (jobId, status, suggestions, error) => set((state) => ({ generationJobs: state.generationJobs.map((job) => job.id === jobId ? { ...job, status, suggestions: suggestions || job.suggestions, error: error || null, completedAt: status !== 'generating' ? new Date().toISOString() : null, } : job ), })), removeJob: (jobId) => set((state) => ({ generationJobs: state.generationJobs.filter((job) => job.id !== jobId), selectedJobId: state.selectedJobId === jobId ? null : state.selectedJobId, })), clearCompletedJobs: () => set((state) => ({ generationJobs: state.generationJobs.filter((job) => job.status === 'generating'), })), setSelectedJob: (jobId) => set({ selectedJobId: jobId }), getJob: (jobId) => { const state = get(); return state.generationJobs.find((job) => job.id === jobId) || null; }, removeSuggestionFromJob: (jobId, suggestionId) => set((state) => ({ generationJobs: state.generationJobs.map((job) => job.id === jobId ? { ...job, suggestions: job.suggestions.filter((s) => s.id !== suggestionId), } : job ), })), appendSuggestionsToJob: (jobId, suggestions) => set((state) => ({ generationJobs: state.generationJobs.map((job) => job.id === jobId ? { ...job, suggestions: [...job.suggestions, ...suggestions], status: 'ready' as const, } : job ), })), setJobGenerating: (jobId, generating) => set((state) => ({ generationJobs: state.generationJobs.map((job) => job.id === jobId ? { ...job, status: generating ? ('generating' as const) : ('ready' as const), } : job ), })), // Suggestions (legacy) setSuggestions: (suggestions) => set({ suggestions }), clearSuggestions: () => set({ suggestions: [], generatingError: null }), removeSuggestion: (id) => set((state) => ({ suggestions: state.suggestions.filter((s) => s.id !== id), })), setSelectedPrompt: (prompt) => set({ selectedPrompt: prompt }), setIsGenerating: (isGenerating) => set({ isGenerating }), setGeneratingError: (error) => set({ generatingError: error }), // Analysis setAnalysisResult: (result) => set({ analysisResult: result }), setIsAnalyzing: (isAnalyzing) => set({ isAnalyzing, analysisProgress: isAnalyzing ? 0 : get().analysisProgress, analysisMessage: isAnalyzing ? 'Starting analysis...' : '', }), setAnalysisProgress: (progress, message) => set({ analysisProgress: progress, analysisMessage: message || get().analysisMessage, }), // UI setMode: (mode) => set({ currentMode: mode }), setCategory: (category) => set({ selectedCategory: category }), setFilterStatus: (status) => set({ filterStatus: status }), // Context sources getContextSources: (projectPath) => { const state = get(); const projectOverrides = state.contextSourcesByProject[projectPath] ?? {}; return { ...DEFAULT_IDEATION_CONTEXT_SOURCES, ...projectOverrides }; }, setContextSource: (projectPath, key, value) => set((state) => ({ contextSourcesByProject: { ...state.contextSourcesByProject, [projectPath]: { ...state.contextSourcesByProject[projectPath], [key]: value, }, }, })), // Reset reset: () => set(initialState), resetSuggestions: () => set({ suggestions: [], selectedPrompt: null, isGenerating: false, generatingError: null, }), }), { name: 'automaker-ideation-store', version: 5, partialize: (state) => ({ // Only persist these fields ideas: state.ideas, generationJobs: state.generationJobs, analysisResult: state.analysisResult, filterStatus: state.filterStatus, contextSourcesByProject: state.contextSourcesByProject, }), migrate: (persistedState: unknown, version: number) => { const state = persistedState as Record; if (version < 4) { // Remove legacy jobs that don't have projectPath (from before project-scoping was added) const jobs = (state.generationJobs as GenerationJob[]) || []; return { ...state, generationJobs: jobs.filter((job) => job.projectPath !== undefined), }; } if (version < 5) { // Initialize contextSourcesByProject if not present return { ...state, contextSourcesByProject: state.contextSourcesByProject ?? {}, }; } return state; }, } ) );