Refactor workflow state management in site building; enhance error handling and field validation in models and serializers. Remove obsolete workflow components from frontend and adjust API response structure for clarity.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-20 23:08:07 +00:00
parent 1b4cd59e5b
commit c31567ec9f
13 changed files with 437 additions and 704 deletions

View File

@@ -237,71 +237,74 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
error: undefined,
});
try {
let lastBlueprint: SiteBlueprint | undefined;
let lastStructure: SiteStructure | undefined;
for (const sectorId of preparedForm.sectorIds) {
const payload = {
name:
preparedForm.siteName ||
`Site Blueprint (${preparedForm.industry || "New"})`,
description: targetAudienceSummary
? `${businessTypeName}${targetAudienceSummary}`
: businessTypeName,
site_id: preparedForm.siteId!,
// Use only the first sector to create ONE blueprint (not one per sector)
const sectorId = preparedForm.sectorIds[0];
if (!sectorId) {
set({
error: "No sector selected. Please select at least one sector.",
});
return;
}
const payload = {
name:
preparedForm.siteName ||
`Site Blueprint (${preparedForm.industry || "New"})`,
description: targetAudienceSummary
? `${businessTypeName}${targetAudienceSummary}`
: businessTypeName,
site_id: preparedForm.siteId!,
sector_id: sectorId,
hosting_type: preparedForm.hostingType,
config_json: {
business_type_id: preparedForm.businessTypeId,
business_type: businessTypeName,
custom_business_type: preparedForm.customBusinessType,
industry: preparedForm.industry,
target_audience_ids: preparedForm.targetAudienceIds,
target_audience: audienceNames,
custom_target_audience: preparedForm.customTargetAudience,
brand_personality_ids: preparedForm.brandPersonalityIds,
brand_personality: brandPersonalityNames,
custom_brand_personality: preparedForm.customBrandPersonality,
hero_imagery_direction_id: preparedForm.heroImageryDirectionId,
hero_imagery_direction: heroImageryName,
custom_hero_imagery_direction:
preparedForm.customHeroImageryDirection,
sector_id: sectorId,
hosting_type: preparedForm.hostingType,
config_json: {
business_type_id: preparedForm.businessTypeId,
business_type: businessTypeName,
custom_business_type: preparedForm.customBusinessType,
industry: preparedForm.industry,
target_audience_ids: preparedForm.targetAudienceIds,
target_audience: audienceNames,
custom_target_audience: preparedForm.customTargetAudience,
brand_personality_ids: preparedForm.brandPersonalityIds,
brand_personality: brandPersonalityNames,
custom_brand_personality: preparedForm.customBrandPersonality,
hero_imagery_direction_id: preparedForm.heroImageryDirectionId,
hero_imagery_direction: heroImageryName,
custom_hero_imagery_direction:
preparedForm.customHeroImageryDirection,
sector_id: sectorId,
},
};
const blueprint = await siteBuilderApi.createBlueprint(payload);
const generation = await siteBuilderApi.generateStructure(
blueprint.id,
{
business_brief: preparedForm.businessBrief,
objectives: preparedForm.objectives,
style: stylePreferences,
metadata: {
targetAudience: audienceNames,
brandPersonality: brandPersonalityNames,
sectorId,
},
};
},
);
const blueprint = await siteBuilderApi.createBlueprint(payload);
lastBlueprint = blueprint;
const generation = await siteBuilderApi.generateStructure(
blueprint.id,
{
business_brief: preparedForm.businessBrief,
objectives: preparedForm.objectives,
style: stylePreferences,
metadata: {
targetAudience: audienceNames,
brandPersonality: brandPersonalityNames,
sectorId,
},
},
);
if (generation?.task_id) {
set({ structureTaskId: generation.task_id });
}
if (generation?.structure) {
lastStructure = generation.structure;
}
if (generation?.task_id) {
set({ structureTaskId: generation.task_id });
}
if (lastBlueprint) {
set({ activeBlueprint: lastBlueprint });
if (lastStructure) {
useSiteDefinitionStore.getState().setStructure(lastStructure);
}
await get().refreshPages(lastBlueprint.id);
let lastStructure: SiteStructure | undefined;
if (generation?.structure) {
lastStructure = generation.structure;
}
set({ activeBlueprint: blueprint });
if (lastStructure) {
useSiteDefinitionStore.getState().setStructure(lastStructure);
}
await get().refreshPages(blueprint.id);
} catch (error: any) {
set({
error: error?.message || "Unexpected error while running wizard",

View File

@@ -1,254 +0,0 @@
/**
* 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>;
refreshContext: () => Promise<void>; // Alias for refreshState
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;
// If workflow is null, initialize with defaults
if (!workflow) {
set({
blueprintId,
currentStep: DEFAULT_STEP,
completedSteps: new Set<WizardStep>(),
blockingIssues: [],
workflowState: null,
context,
loading: false,
error: null,
});
return;
}
// Determine completed steps from workflow state
// Backend returns 'steps' as an array, not 'step_status' as an object
const completedSteps = new Set<WizardStep>();
const steps = workflow.steps || [];
steps.forEach((stepData: any) => {
if (stepData?.status === 'ready' || stepData?.status === 'complete') {
completedSteps.add(stepData.step as WizardStep);
}
});
// Extract blocking issues
const blockingIssues: Array<{ step: WizardStep; message: string }> = [];
steps.forEach((stepData: any) => {
if (stepData?.status === 'blocked' && stepData?.message) {
blockingIssues.push({ step: stepData.step as WizardStep, message: stepData.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);
},
refreshContext: async () => {
// Alias for refreshState
await get().refreshState();
},
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, workflowState } = get();
if (!blueprintId) {
throw new Error('No blueprint initialized');
}
// Ensure workflow is initialized before updating
if (!workflowState) {
// Try to initialize first
await get().initialize(blueprintId);
}
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) {
// Extract more detailed error message if available
const errorMessage = error?.response?.error ||
error?.response?.message ||
error?.message ||
`Failed to complete step: ${step}`;
set({
error: errorMessage,
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
}),
}
)
);