From 52c9c9f3d506f0472ca207bbf9fb28c49669105a Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:19:53 +0500 Subject: [PATCH] stage2-2 and docs --- frontend/src/App.tsx | 6 + .../pages/Sites/Builder/WorkflowWizard.tsx | 111 ++++ .../Builder/components/WizardProgress.tsx | 102 ++++ .../Builder/steps/BusinessDetailsStep.tsx | 482 +++++------------- .../Builder/steps/ClusterAssignmentStep.tsx | 70 +++ .../Builder/steps/CoverageValidationStep.tsx | 52 ++ .../Sites/Builder/steps/IdeasHandoffStep.tsx | 52 ++ .../Sites/Builder/steps/SitemapReviewStep.tsx | 55 ++ .../Builder/steps/TaxonomyBuilderStep.tsx | 55 ++ frontend/src/services/api.ts | 138 +++++ frontend/src/store/builderWorkflowStore.ts | 220 ++++++++ refactor-plan/refactor-stage-2.md | 89 ++++ refactor-plan/refactor-stage-3.md | 85 +++ refactor-plan/refactor-stage-4.md | 89 ++++ 14 files changed, 1259 insertions(+), 347 deletions(-) create mode 100644 frontend/src/pages/Sites/Builder/WorkflowWizard.tsx create mode 100644 frontend/src/pages/Sites/Builder/components/WizardProgress.tsx create mode 100644 frontend/src/pages/Sites/Builder/steps/ClusterAssignmentStep.tsx create mode 100644 frontend/src/pages/Sites/Builder/steps/CoverageValidationStep.tsx create mode 100644 frontend/src/pages/Sites/Builder/steps/IdeasHandoffStep.tsx create mode 100644 frontend/src/pages/Sites/Builder/steps/SitemapReviewStep.tsx create mode 100644 frontend/src/pages/Sites/Builder/steps/TaxonomyBuilderStep.tsx create mode 100644 frontend/src/store/builderWorkflowStore.ts create mode 100644 refactor-plan/refactor-stage-2.md create mode 100644 refactor-plan/refactor-stage-3.md create mode 100644 refactor-plan/refactor-stage-4.md diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 134642d1..61d53aae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -93,6 +93,7 @@ const SiteSettings = lazy(() => import("./pages/Sites/Settings")); // Site Builder - Lazy loaded (will be moved from separate container) const SiteBuilderWizard = lazy(() => import("./pages/Sites/Builder/Wizard")); +const WorkflowWizard = lazy(() => import("./pages/Sites/Builder/WorkflowWizard")); const SiteBuilderPreview = lazy(() => import("./pages/Sites/Builder/Preview")); const SiteBuilderBlueprints = lazy(() => import("./pages/Sites/Builder/Blueprints")); @@ -520,6 +521,11 @@ export default function App() { } /> + + + + } /> diff --git a/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx b/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx new file mode 100644 index 00000000..c280db87 --- /dev/null +++ b/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx @@ -0,0 +1,111 @@ +/** + * Site Builder Workflow Wizard (Stage 2) + * Self-guided wizard with state-aware gating and progress tracking + */ +import { useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useBuilderWorkflowStore, WizardStep } from '../../../store/builderWorkflowStore'; +import WizardProgress from './components/WizardProgress'; +import BusinessDetailsStep from './steps/BusinessDetailsStep'; +import ClusterAssignmentStep from './steps/ClusterAssignmentStep'; +import TaxonomyBuilderStep from './steps/TaxonomyBuilderStep'; +import SitemapReviewStep from './steps/SitemapReviewStep'; +import CoverageValidationStep from './steps/CoverageValidationStep'; +import IdeasHandoffStep from './steps/IdeasHandoffStep'; +import Alert from '../../../components/ui/alert/Alert'; +import PageMeta from '../../../components/common/PageMeta'; +import { Loader2 } from 'lucide-react'; + +const STEP_COMPONENTS: Record = { + business_details: BusinessDetailsStep, + clusters: ClusterAssignmentStep, + taxonomies: TaxonomyBuilderStep, + sitemap: SitemapReviewStep, + coverage: CoverageValidationStep, + ideas: IdeasHandoffStep, +}; + +const STEP_LABELS: Record = { + business_details: 'Business Details', + clusters: 'Cluster Assignment', + taxonomies: 'Taxonomy Builder', + sitemap: 'AI Sitemap Review', + coverage: 'Coverage Validation', + ideas: 'Ideas Hand-off', +}; + +export default function WorkflowWizard() { + const { blueprintId } = useParams<{ blueprintId: string }>(); + const navigate = useNavigate(); + const { + blueprintId: storeBlueprintId, + currentStep, + loading, + error, + context, + initialize, + refreshState, + } = useBuilderWorkflowStore(); + + const id = blueprintId ? parseInt(blueprintId, 10) : null; + + useEffect(() => { + if (id && id !== storeBlueprintId) { + initialize(id); + } + }, [id, storeBlueprintId, initialize]); + + useEffect(() => { + // Refresh state periodically to keep it in sync + if (id && storeBlueprintId === id) { + const interval = setInterval(() => { + refreshState(); + }, 10000); // Refresh every 10 seconds + + return () => clearInterval(interval); + } + }, [id, storeBlueprintId, refreshState]); + + if (!id) { + return ( +
+ Invalid blueprint ID +
+ ); + } + + if (loading && !context) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + const StepComponent = STEP_COMPONENTS[currentStep]; + + return ( +
+ + +
+ {/* Progress Indicator */} + + + {/* Main Content */} +
+ {StepComponent && } +
+
+
+ ); +} + diff --git a/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx b/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx new file mode 100644 index 00000000..7da53d4c --- /dev/null +++ b/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx @@ -0,0 +1,102 @@ +/** + * Wizard Progress Indicator + * Shows breadcrumb with step completion status + */ +import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore'; +import { CheckCircle2, Circle } from 'lucide-react'; + +const STEPS: Array<{ key: WizardStep; label: string }> = [ + { key: 'business_details', label: 'Business Details' }, + { key: 'clusters', label: 'Clusters' }, + { key: 'taxonomies', label: 'Taxonomies' }, + { key: 'sitemap', label: 'Sitemap' }, + { key: 'coverage', label: 'Coverage' }, + { key: 'ideas', label: 'Ideas' }, +]; + +interface WizardProgressProps { + currentStep: WizardStep; +} + +export default function WizardProgress({ currentStep }: WizardProgressProps) { + const { completedSteps, blockingIssues } = useBuilderWorkflowStore(); + const currentIndex = STEPS.findIndex(s => s.key === currentStep); + + return ( +
+ +
+ ); +} + diff --git a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx index 9b74c6cf..2c39e183 100644 --- a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx +++ b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx @@ -1,363 +1,151 @@ -import { useMemo, useRef, useState } from "react"; -import type { - BuilderFormData, - SiteBuilderMetadata, -} from "../../../../types/siteBuilder"; -import { Card } from "../../../../components/ui/card"; -import { useSiteStore } from "../../../../store/siteStore"; -import { Dropdown } from "../../../../components/ui/dropdown/Dropdown"; -import { Check } from "lucide-react"; +/** + * Step 1: Business Details + * Site type selection, hosting detection, brand inputs + */ +import { useState, useEffect } from 'react'; +import { useBuilderWorkflowStore } from '../../../../store/builderWorkflowStore'; +import { fetchSiteBlueprintById, updateSiteBlueprint, SiteBlueprint } from '../../../../services/api'; +import { Card, CardDescription, CardTitle } from '../../../../components/ui/card'; +import Button from '../../../../components/ui/button/Button'; +import Input from '../../../../components/ui/input/Input'; +import Alert from '../../../../components/ui/alert/Alert'; +import { Loader2 } from 'lucide-react'; -const inputClass = - "h-11 w-full rounded-xl border border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"; - -const labelClass = - "text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block"; - -interface Props { - data: BuilderFormData; - metadata?: SiteBuilderMetadata; - selectedSectors: Array<{ id: number; name: string }>; - onChange: ( - key: K, - value: BuilderFormData[K], - ) => void; +interface BusinessDetailsStepProps { + blueprintId: number; } -export function BusinessDetailsStep({ - data, - metadata, - selectedSectors, - onChange, -}: Props) { - const { activeSite } = useSiteStore(); - const [businessDropdownOpen, setBusinessDropdownOpen] = useState(false); - const businessButtonRef = useRef(null); - const [audienceDropdownOpen, setAudienceDropdownOpen] = useState(false); - const audienceButtonRef = useRef(null); +export default function BusinessDetailsStep({ blueprintId }: BusinessDetailsStepProps) { + const { context, completeStep, loading } = useBuilderWorkflowStore(); + const [blueprint, setBlueprint] = useState(null); + const [formData, setFormData] = useState({ + name: '', + description: '', + hosting_type: 'igny8_sites' as const, + business_type: '', + }); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); - const businessOptions = metadata?.business_types ?? []; - const audienceOptions = metadata?.audience_profiles ?? []; + useEffect(() => { + // Load blueprint data + fetchSiteBlueprintById(blueprintId) + .then(setBlueprint) + .catch(err => setError(err.message)); + }, [blueprintId]); - const selectedBusinessType = businessOptions.find( - (option) => option.id === data.businessTypeId, - ); - - const selectedAudienceOptions = useMemo( - () => - audienceOptions.filter((option) => - data.targetAudienceIds.includes(option.id), - ), - [audienceOptions, data.targetAudienceIds], - ); - - const computeAudienceSummary = ( - ids: number[], - custom?: string, - ): string => { - const names = audienceOptions - .filter((option) => ids.includes(option.id)) - .map((option) => option.name); - if (custom?.trim()) { - names.push(custom.trim()); + useEffect(() => { + if (blueprint) { + setFormData({ + name: blueprint.name || '', + description: blueprint.description || '', + hosting_type: (blueprint.hosting_type as any) || 'igny8_sites', + business_type: blueprint.config_json?.business_type || '', + }); + } + }, [blueprint]); + + const handleSave = async () => { + setSaving(true); + setError(null); + try { + const updated = await updateSiteBlueprint(blueprintId, { + name: formData.name, + description: formData.description, + hosting_type: formData.hosting_type, + config_json: { + ...blueprint?.config_json, + business_type: formData.business_type, + }, + }); + setBlueprint(updated); + + // Mark step as complete + await completeStep('business_details', { + blueprint_name: formData.name, + hosting_type: formData.hosting_type, + }); + } catch (err: any) { + setError(err.message || 'Failed to save business details'); + } finally { + setSaving(false); } - return names.join(", "); }; - const toggleAudience = (audienceId: number) => { - const isSelected = data.targetAudienceIds.includes(audienceId); - const next = isSelected - ? data.targetAudienceIds.filter((id) => id !== audienceId) - : [...data.targetAudienceIds, audienceId]; - onChange("targetAudienceIds", next); - onChange("targetAudience", computeAudienceSummary(next, data.customTargetAudience)); - }; - - const handleCustomAudienceChange = (value: string) => { - onChange("customTargetAudience", value); - onChange("targetAudience", computeAudienceSummary(data.targetAudienceIds, value)); - }; - - const handleCustomBusinessTypeChange = (value: string) => { - onChange("customBusinessType", value); - onChange("businessTypeId", null); - onChange("businessType", value); - }; + const canProceed = formData.name.trim().length > 0; return ( -
- -
-

- Context -

-

- Site & sectors -

-

- IGNY8 will generate a blueprint for every sector configured on this site. -

-
-
-
-

- Active site -

-

- {activeSite?.name ?? "No site selected"} -

-
-
-

- Included sectors -

- {selectedSectors.length > 0 ? ( -
- {selectedSectors.map((sector) => ( - - {sector.name} - - ))} -
- ) : ( -

- No sectors configured for this site -

- )} -
-
-
+ + Business Details + + Tell us about your business and site type to get started. + - -
-
- - onChange("siteName", event.target.value)} - /> -
-
- - -
-
+ {error && ( + + {error} + + )} -
-
- - - setBusinessDropdownOpen(false)} - anchorRef={businessButtonRef} - placement="bottom-left" - className="w-72 max-h-72 overflow-y-auto p-2" - > - {businessOptions.length === 0 ? ( -
- No business types defined yet. -
- ) : ( - businessOptions.map((option) => { - const isSelected = option.id === data.businessTypeId; - return ( - - ); - }) - )} -
- - handleCustomBusinessTypeChange(event.target.value) - } - /> -
-
- -
- {data.industry || "No industry selected"} -
-
-
-
- - -
-
- -

- Choose one or more audience profiles from the IGNY8 library. Add your own if needed. -

-
-
- - setAudienceDropdownOpen(false)} - anchorRef={audienceButtonRef} - placement="bottom-left" - className="w-80 max-h-80 overflow-y-auto p-2" - > - {audienceOptions.length === 0 ? ( -
- No audience profiles defined yet. -
- ) : ( - audienceOptions.map((option) => { - const isSelected = data.targetAudienceIds.includes(option.id); - return ( - - ); - }) - )} -
-
-
- {selectedAudienceOptions.map((option) => ( - - {option.name} - - - ))} - {data.customTargetAudience?.trim() && ( - - {data.customTargetAudience.trim()} - - )} -
- handleCustomAudienceChange(event.target.value)} +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="My Awesome Site" + required />
- -
+ +
+ +