stage2-2 and docs

This commit is contained in:
alorig
2025-11-19 21:19:53 +05:00
parent 72e1f25bc7
commit 52c9c9f3d5
14 changed files with 1259 additions and 347 deletions

View File

@@ -0,0 +1,220 @@
/**
* Builder Workflow Store (Zustand)
* Manages wizard progress + gating state for site blueprints
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {
fetchWizardContext,
updateWorkflowStep,
WizardContext,
WorkflowState,
} from '../services/api';
export type WizardStep =
| 'business_details'
| 'clusters'
| 'taxonomies'
| 'sitemap'
| 'coverage'
| 'ideas';
interface BuilderWorkflowState {
// Current blueprint being worked on
blueprintId: number | null;
// Workflow state
currentStep: WizardStep;
completedSteps: Set<WizardStep>;
blockingIssues: Array<{ step: WizardStep; message: string }>;
workflowState: WorkflowState | null;
// Wizard context (cluster/taxonomy summaries)
context: WizardContext | null;
// Loading/error states
loading: boolean;
error: string | null;
// Telemetry queue (for future event tracking)
telemetryQueue: Array<{ event: string; data: Record<string, any>; timestamp: string }>;
// Actions
initialize: (blueprintId: number) => Promise<void>;
refreshState: () => Promise<void>;
goToStep: (step: WizardStep) => void;
completeStep: (step: WizardStep, metadata?: Record<string, any>) => Promise<void>;
setBlockingIssue: (step: WizardStep, message: string) => void;
clearBlockingIssue: (step: WizardStep) => void;
flushTelemetry: () => void;
reset: () => void;
}
const DEFAULT_STEP: WizardStep = 'business_details';
export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
persist<BuilderWorkflowState>(
(set, get) => ({
blueprintId: null,
currentStep: DEFAULT_STEP,
completedSteps: new Set(),
blockingIssues: [],
workflowState: null,
context: null,
loading: false,
error: null,
telemetryQueue: [],
initialize: async (blueprintId: number) => {
set({ blueprintId, loading: true, error: null });
try {
const context = await fetchWizardContext(blueprintId);
const workflow = context.workflow;
// Determine completed steps from workflow state
const completedSteps = new Set<WizardStep>();
Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
if (status.status === 'ready' || status.status === 'complete') {
completedSteps.add(step as WizardStep);
}
});
// Extract blocking issues
const blockingIssues: Array<{ step: WizardStep; message: string }> = [];
Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
if (status.status === 'blocked' && status.message) {
blockingIssues.push({ step: step as WizardStep, message: status.message });
}
});
set({
blueprintId,
currentStep: (workflow.current_step as WizardStep) || DEFAULT_STEP,
completedSteps,
blockingIssues,
workflowState: workflow,
context,
loading: false,
error: null,
});
// Emit telemetry event
get().flushTelemetry();
} catch (error: any) {
set({
error: error.message || 'Failed to initialize workflow',
loading: false,
});
}
},
refreshState: async () => {
const { blueprintId } = get();
if (!blueprintId) {
return;
}
await get().initialize(blueprintId);
},
goToStep: (step: WizardStep) => {
set({ currentStep: step });
// Emit telemetry
const { blueprintId } = get();
if (blueprintId) {
get().flushTelemetry();
}
},
completeStep: async (step: WizardStep, metadata?: Record<string, any>) => {
const { blueprintId } = get();
if (!blueprintId) {
throw new Error('No blueprint initialized');
}
set({ loading: true, error: null });
try {
const updatedState = await updateWorkflowStep(blueprintId, step, 'ready', metadata);
// Update local state
const completedSteps = new Set(get().completedSteps);
completedSteps.add(step);
const blockingIssues = get().blockingIssues.filter(issue => issue.step !== step);
set({
workflowState: updatedState,
completedSteps,
blockingIssues,
loading: false,
});
// Refresh full context to get updated summaries
await get().refreshState();
// Emit telemetry
get().flushTelemetry();
} catch (error: any) {
set({
error: error.message || `Failed to complete step: ${step}`,
loading: false,
});
throw error;
}
},
setBlockingIssue: (step: WizardStep, message: string) => {
const blockingIssues = [...get().blockingIssues];
const existingIndex = blockingIssues.findIndex(issue => issue.step === step);
if (existingIndex >= 0) {
blockingIssues[existingIndex] = { step, message };
} else {
blockingIssues.push({ step, message });
}
set({ blockingIssues });
},
clearBlockingIssue: (step: WizardStep) => {
const blockingIssues = get().blockingIssues.filter(issue => issue.step !== step);
set({ blockingIssues });
},
flushTelemetry: () => {
// TODO: In Stage 2, implement actual telemetry dispatch
// For now, just clear the queue
const queue = get().telemetryQueue;
if (queue.length > 0) {
// Future: dispatch to analytics service
console.debug('Telemetry events (to be dispatched):', queue);
set({ telemetryQueue: [] });
}
},
reset: () => {
set({
blueprintId: null,
currentStep: DEFAULT_STEP,
completedSteps: new Set(),
blockingIssues: [],
workflowState: null,
context: null,
loading: false,
error: null,
telemetryQueue: [],
});
},
}),
{
name: 'builder-workflow-storage',
partialize: (state) => ({
blueprintId: state.blueprintId,
currentStep: state.currentStep,
// Note: completedSteps, blockingIssues, workflowState, context are not persisted
// They should be refreshed from API on mount
}),
}
)
);