diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 8ee23a85..4162dc55 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/backend/igny8_core/business/site_building/models.py b/backend/igny8_core/business/site_building/models.py index 217cfbd5..4a43546d 100644 --- a/backend/igny8_core/business/site_building/models.py +++ b/backend/igny8_core/business/site_building/models.py @@ -330,9 +330,13 @@ class WorkflowState(SiteSectorBaseModel): def save(self, *args, **kwargs): if self.site_blueprint: - self.account = self.site_blueprint.account - self.site = self.site_blueprint.site - self.sector = self.site_blueprint.sector + # Only set fields if blueprint has them (avoid errors if blueprint is missing fields) + if self.site_blueprint.account_id: + self.account_id = self.site_blueprint.account_id + if self.site_blueprint.site_id: + self.site_id = self.site_blueprint.site_id + if self.site_blueprint.sector_id: + self.sector_id = self.site_blueprint.sector_id super().save(*args, **kwargs) def __str__(self): diff --git a/backend/igny8_core/business/site_building/services/workflow_state_service.py b/backend/igny8_core/business/site_building/services/workflow_state_service.py index c2027103..c5441e9c 100644 --- a/backend/igny8_core/business/site_building/services/workflow_state_service.py +++ b/backend/igny8_core/business/site_building/services/workflow_state_service.py @@ -100,7 +100,29 @@ class WorkflowStateService: state.step_status = step_status state.blocking_reason = blocking_reason state.completed = all(value.get('status') == 'ready' for value in step_status.values()) - state.save(update_fields=['step_status', 'blocking_reason', 'completed', 'updated_at']) + + # Ensure account/site/sector are set from blueprint before saving + update_fields = ['step_status', 'blocking_reason', 'completed', 'updated_at'] + if state.site_blueprint: + if state.site_blueprint.account_id: + state.account_id = state.site_blueprint.account_id + update_fields.append('account') + if state.site_blueprint.site_id: + state.site_id = state.site_blueprint.site_id + update_fields.append('site') + if state.site_blueprint.sector_id: + state.sector_id = state.site_blueprint.sector_id + update_fields.append('sector') + + try: + state.save(update_fields=update_fields) + except Exception as e: + logger.error( + f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}. " + f"Blueprint fields: account_id={site_blueprint.account_id}, site_id={site_blueprint.site_id}, sector_id={site_blueprint.sector_id}", + exc_info=True + ) + raise return state def update_step( @@ -149,11 +171,28 @@ class WorkflowStateService: ) else: state.completed = False + + # Ensure account/site/sector are set from blueprint before saving + update_fields = ['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at'] + if state.site_blueprint: + if state.site_blueprint.account_id: + state.account_id = state.site_blueprint.account_id + update_fields.append('account') + if state.site_blueprint.site_id: + state.site_id = state.site_blueprint.site_id + update_fields.append('site') + if state.site_blueprint.sector_id: + state.sector_id = state.site_blueprint.sector_id + update_fields.append('sector') try: - state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at']) + state.save(update_fields=update_fields) except Exception as e: - logger.error(f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}") + logger.error( + f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}. " + f"Blueprint fields: account_id={site_blueprint.account_id}, site_id={site_blueprint.site_id}, sector_id={site_blueprint.sector_id}", + exc_info=True + ) raise self._emit_event(site_blueprint, 'wizard_step_updated', { diff --git a/backend/igny8_core/modules/site_builder/serializers.py b/backend/igny8_core/modules/site_builder/serializers.py index ceda54cc..3f8422be 100644 --- a/backend/igny8_core/modules/site_builder/serializers.py +++ b/backend/igny8_core/modules/site_builder/serializers.py @@ -45,8 +45,9 @@ class PageBlueprintSerializer(serializers.ModelSerializer): class SiteBlueprintSerializer(serializers.ModelSerializer): pages = PageBlueprintSerializer(many=True, read_only=True) - site_id = serializers.IntegerField(write_only=True, required=False) - sector_id = serializers.IntegerField(write_only=True, required=False) + site_id = serializers.IntegerField(required=False, read_only=True) + sector_id = serializers.IntegerField(required=False, read_only=True) + account_id = serializers.IntegerField(read_only=True) workflow_state = serializers.SerializerMethodField() gating_messages = serializers.SerializerMethodField() @@ -62,6 +63,7 @@ class SiteBlueprintSerializer(serializers.ModelSerializer): 'hosting_type', 'version', 'deployed_version', + 'account_id', 'site_id', 'sector_id', 'created_at', diff --git a/backend/igny8_core/modules/site_builder/views.py b/backend/igny8_core/modules/site_builder/views.py index b20571d3..9e17c463 100644 --- a/backend/igny8_core/modules/site_builder/views.py +++ b/backend/igny8_core/modules/site_builder/views.py @@ -27,7 +27,6 @@ from igny8_core.business.site_building.services import ( PageGenerationService, SiteBuilderFileService, StructureGenerationService, - WorkflowStateService, TaxonomyService, WizardContextService, ) @@ -51,7 +50,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.workflow_service = WorkflowStateService() self.taxonomy_service = TaxonomyService() self.wizard_context_service = WizardContextService() @@ -115,18 +113,10 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): raise ValidationError({'sector_id': 'Sector does not belong to the selected site.'}) blueprint = serializer.save(account=site.account, site=site, sector=sector) - if self.workflow_service.enabled: - self.workflow_service.initialize(blueprint) @action(detail=True, methods=['post']) def generate_structure(self, request, pk=None): blueprint = self.get_object() - if self.workflow_service.enabled: - try: - self.workflow_service.validate_step(blueprint, 'clusters') - self.workflow_service.validate_step(blueprint, 'taxonomies') - except ValidationError as exc: - return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) business_brief = request.data.get('business_brief') or \ blueprint.config_json.get('business_brief', '') objectives = request.data.get('objectives') or \ @@ -143,8 +133,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): metadata=request.data.get('metadata', {}), ) response = Response(result, status=status.HTTP_202_ACCEPTED if 'task_id' in result else status.HTTP_200_OK) - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) return response @action(detail=True, methods=['post']) @@ -162,11 +150,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): page_ids = request.data.get('page_ids') force = request.data.get('force', False) - if self.workflow_service.enabled: - try: - self.workflow_service.validate_step(blueprint, 'sitemap') - except ValidationError as exc: - return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) service = PageGenerationService() try: result = service.bulk_generate_pages( @@ -176,8 +159,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): ) response_status = status.HTTP_202_ACCEPTED if result.get('success') else status.HTTP_400_BAD_REQUEST response = success_response(result, request=request, status_code=response_status) - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) return response except Exception as e: return error_response(str(e), status.HTTP_400_BAD_REQUEST, request) @@ -200,12 +181,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): blueprint = self.get_object() page_ids = request.data.get('page_ids') - if self.workflow_service.enabled: - try: - self.workflow_service.validate_step(blueprint, 'coverage') - except ValidationError as exc: - return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) - service = PageGenerationService() try: tasks = service.create_tasks_for_pages(blueprint, page_ids=page_ids) @@ -215,8 +190,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): serializer = TasksSerializer(tasks, many=True) response = success_response({'tasks': serializer.data, 'count': len(tasks)}, request=request) - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) return response except Exception as e: return error_response(str(e), status.HTTP_400_BAD_REQUEST, request) @@ -306,103 +279,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): request=request ) - @action(detail=True, methods=['get'], url_path='workflow/context') - def workflow_context(self, request, pk=None): - """Return aggregated wizard context (steps, clusters, taxonomies, coverage).""" - blueprint = self.get_object() - if not self.workflow_service.enabled: - # Return empty context structure matching frontend expectations - return success_response( - data={ - 'workflow': None, - 'cluster_summary': {'attached_count': 0, 'coverage_counts': {}, 'clusters': []}, - 'taxonomy_summary': {'total_taxonomies': 0, 'counts_by_type': {}, 'taxonomies': []}, - 'sitemap_summary': {'pages_total': 0, 'pages_by_status': {}, 'pages_by_type': {}}, - 'coverage': {'pages_total': 0, 'pages_by_status': {}, 'pages_by_type': {}}, - 'next_actions': None, - }, - request=request, - ) - - payload = self.wizard_context_service.build_context(blueprint) - return success_response(payload, request=request) - - @action(detail=True, methods=['post'], url_path='workflow/step') - def update_workflow_step(self, request, pk=None): - """ - Update workflow step status. - - Request body: - { - "step": "business_details", # Step name - "status": "ready", # Status: ready, blocked, in_progress - "metadata": {} # Optional metadata - } - - Returns: - { - "current_step": "business_details", - "step_status": {...}, - "completed": false - } - """ - blueprint = self.get_object() - if not self.workflow_service.enabled: - return error_response( - 'Workflow service not enabled', - status.HTTP_400_BAD_REQUEST, - request - ) - - step = request.data.get('step') - status_value = request.data.get('status') - metadata = request.data.get('metadata', {}) - - if not step or not status_value: - return error_response( - 'step and status are required', - status.HTTP_400_BAD_REQUEST, - request - ) - - valid_statuses = ['ready', 'blocked', 'in_progress', 'complete'] - if status_value not in valid_statuses: - return error_response( - f'Invalid status. Must be one of: {", ".join(valid_statuses)}', - status.HTTP_400_BAD_REQUEST, - request - ) - - try: - updated_state = self.workflow_service.update_step( - blueprint, - step, - status_value, - metadata - ) - - if not updated_state: - return error_response( - 'Failed to update workflow step', - status.HTTP_500_INTERNAL_SERVER_ERROR, - request - ) - - # Serialize state - serialized = self.workflow_service.serialize_state(updated_state) - - return success_response( - data=serialized, - request=request - ) - except Exception as e: - logger.exception(f"Error updating workflow step for blueprint {blueprint.id}: {str(e)}") - return error_response( - f'Internal server error: {str(e)}', - status.HTTP_500_INTERNAL_SERVER_ERROR, - request - ) - @action(detail=True, methods=['post'], url_path='clusters/attach') def attach_clusters(self, request, pk=None): """ @@ -492,10 +368,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): 'link_id': existing.id }) - # Refresh workflow state if enabled - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) - return success_response( data={ 'attached_count': len(attached), @@ -543,10 +415,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): detached_count = query.count() query.delete() - # Refresh workflow state if enabled - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) - return success_response( data={'detached_count': detached_count}, request=request @@ -636,10 +504,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): external_reference=external_reference, ) - # Refresh workflow state - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) - return success_response( data={ 'id': taxonomy.id, @@ -689,10 +553,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): default_type=default_type ) - # Refresh workflow state - if self.workflow_service.enabled: - self.workflow_service.refresh_state(blueprint) - return success_response( data={ 'imported_count': len(imported), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d622120f..697c3b56 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -99,7 +99,6 @@ const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel")); // 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")); @@ -524,11 +523,6 @@ export default function App() { } /> - - - - } /> diff --git a/frontend/src/components/sites/SiteProgressWidget.tsx b/frontend/src/components/sites/SiteProgressWidget.tsx index 0f123149..3d962423 100644 --- a/frontend/src/components/sites/SiteProgressWidget.tsx +++ b/frontend/src/components/sites/SiteProgressWidget.tsx @@ -293,16 +293,6 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress - {/* Deep Link to Blueprint */} -
- -
{/* Error banner if data loaded but has errors */} {error && progress && ( diff --git a/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx b/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx deleted file mode 100644 index 9dedff4a..00000000 --- a/frontend/src/pages/Sites/Builder/WorkflowWizard.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Site Builder Workflow Wizard (Stage 2) - * Self-guided wizard with state-aware gating and progress tracking - */ -import { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { useBuilderWorkflowStore, WizardStep } from '../../../store/builderWorkflowStore'; -import WizardProgress from './components/WizardProgress'; -import HelperDrawer from './components/HelperDrawer'; -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 Button from '../../../components/ui/button/Button'; -import { InfoIcon } from '../../../icons'; - -interface StepComponentProps { - blueprintId: number; -} - -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, - goToStep, - } = useBuilderWorkflowStore(); - - const [helperDrawerOpen, setHelperDrawerOpen] = useState(false); - 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]); - - // Keyboard navigation - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't interfere with input fields - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement - ) { - return; - } - - // Escape key: Close helper drawer - if (e.key === 'Escape' && helperDrawerOpen) { - setHelperDrawerOpen(false); - return; - } - - // F1 or ? key: Toggle helper drawer - if (e.key === 'F1' || (e.key === '?' && !e.shiftKey && !e.ctrlKey && !e.metaKey)) { - e.preventDefault(); - setHelperDrawerOpen(!helperDrawerOpen); - return; - } - - // Arrow keys for navigation (when not in input) - if (e.key === 'ArrowLeft' && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - // Navigate to previous step (if allowed) - const steps: WizardStep[] = ['business_details', 'clusters', 'taxonomies', 'sitemap', 'coverage', 'ideas']; - const currentIndex = steps.indexOf(currentStep); - if (currentIndex > 0) { - goToStep(steps[currentIndex - 1]); - } - } - - if (e.key === 'ArrowRight' && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - // Navigate to next step (if allowed) - const steps: WizardStep[] = ['business_details', 'clusters', 'taxonomies', 'sitemap', 'coverage', 'ideas']; - const currentIndex = steps.indexOf(currentStep); - if (currentIndex < steps.length - 1) { - goToStep(steps[currentIndex + 1]); - } - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [currentStep, helperDrawerOpen, goToStep]); - - if (!id) { - return ( -
- -
- ); - } - - if (loading && !context) { - return ( -
-
-
- ); - } - - if (error) { - return ( -
- -
- ); - } - - const StepComponent = STEP_COMPONENTS[currentStep]; - - return ( -
- - -
- {/* Header with Help Button */} -
-
-

- Site Builder Workflow -

-

- Step: {STEP_LABELS[currentStep]} -

-
- -
- - {/* Progress Indicator */} - - - {/* Main Content */} -
- {StepComponent && } -
-
- - {/* Helper Drawer */} - setHelperDrawerOpen(false)} - /> -
- ); -} - diff --git a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx index bc820f7e..ca83e22b 100644 --- a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx +++ b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx @@ -6,14 +6,16 @@ * - Stage 1 Wizard: data, onChange, metadata, selectedSectors * - Stage 2 Workflow: blueprintId */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useMemo } 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/form/input/InputField'; import Alert from '../../../../components/ui/alert/Alert'; -import { BoltIcon, GridIcon } from '../../../../icons'; +import { Dropdown } from '../../../../components/ui/dropdown/Dropdown'; +import SelectDropdown from '../../../../components/form/SelectDropdown'; +import { BoltIcon, GridIcon, CheckLineIcon } from '../../../../icons'; import type { BuilderFormData, SiteBuilderMetadata } from '../../../../types/siteBuilder'; // Stage 1 Wizard props @@ -70,7 +72,31 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { useEffect(() => { // Load blueprint data fetchSiteBlueprintById(blueprintId) - .then(setBlueprint) + .then((bp) => { + setBlueprint(bp); + // Check if blueprint is missing required fields (only show error if fields are actually missing) + // Note: account_id might not be in response, but site_id and sector_id should be + // Check explicitly for null/undefined (not just falsy, since 0 could be valid) + if (bp && (bp.site_id == null || bp.sector_id == null)) { + const missing = []; + if (bp.site_id == null) missing.push('site'); + if (bp.sector_id == null) missing.push('sector'); + console.error('Blueprint missing required fields:', { + blueprintId: bp.id, + site_id: bp.site_id, + sector_id: bp.sector_id, + account_id: bp.account_id, + fullBlueprint: bp + }); + setError( + `This blueprint is missing required fields: ${missing.join(', ')}. ` + + `Please contact support to fix this issue.` + ); + } else { + // Clear any previous errors if fields are present + setError(undefined); + } + }) .catch(err => setError(err.message)); }, [blueprintId]); @@ -118,8 +144,21 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { // Check if it's a server error (500) - might be workflow service not enabled const isServerError = workflowErr?.status === 500; + const isClientError = workflowErr?.status >= 400 && workflowErr?.status < 500; const errorDetail = workflowErr?.response?.error || workflowErr?.response?.message || ''; + // Check if error is about missing blueprint fields + if (isClientError && (errorDetail.includes('missing required fields') || + errorDetail.includes('account') || + errorDetail.includes('site') || + errorDetail.includes('sector'))) { + setError( + `Cannot proceed: ${errorDetail}. ` + + `This blueprint needs to be configured with account, site, and sector. Please contact support.` + ); + return; // Don't advance - user needs to fix this first + } + if (isServerError && errorDetail.includes('Workflow service not enabled')) { // Workflow service is disabled - just advance without marking as complete console.warn('Workflow service not enabled, advancing to next step'); @@ -131,7 +170,7 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { console.warn('Workflow step update failed:', workflowErrorMsg, workflowErr); - // Mark step as completed locally so user can proceed + // For other errors, allow user to proceed but show warning const { completedSteps, goToStep } = useBuilderWorkflowStore.getState(); const updatedCompletedSteps = new Set(completedSteps); updatedCompletedSteps.add('business_details'); @@ -151,7 +190,10 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { } }; - const canProceed = formData.name.trim().length > 0; + const canProceed = formData.name.trim().length > 0 && + blueprint && + (blueprint.site_id !== undefined && blueprint.site_id !== null) && + (blueprint.sector_id !== undefined && blueprint.sector_id !== null); return ( @@ -163,7 +205,13 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
{error && ( - + )}
@@ -223,13 +271,182 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { {!canProceed && ( - Please provide a site name to continue. + {!formData.name.trim() + ? 'Please provide a site name to continue.' + : blueprint && (!blueprint.site_id || !blueprint.sector_id) + ? 'This blueprint is missing required configuration (site or sector). Please contact support to fix this issue.' + : 'Please complete all required fields to continue.'} )} ); } +// Target Audience Selector Component (multi-select dropdown) +function TargetAudienceSelector({ + data, + onChange, + metadata, +}: { + data: BuilderFormData; + onChange: (key: K, value: BuilderFormData[K]) => void; + metadata: SiteBuilderMetadata; +}) { + const [audienceDropdownOpen, setAudienceDropdownOpen] = useState(false); + const audienceButtonRef = useRef(null); + const [showCustomInput, setShowCustomInput] = useState(false); + + const audienceOptions = metadata.audience_profiles ?? []; + const selectedAudienceIds = data.targetAudienceIds ?? []; + + const selectedAudienceOptions = useMemo( + () => audienceOptions.filter((option) => selectedAudienceIds.includes(option.id)), + [audienceOptions, selectedAudienceIds], + ); + + const toggleAudience = (id: number) => { + const isSelected = selectedAudienceIds.includes(id); + const next = isSelected + ? selectedAudienceIds.filter((value) => value !== id) + : [...selectedAudienceIds, id]; + onChange('targetAudienceIds', next); + + // Update targetAudience text field with selected names + const selectedNames = audienceOptions + .filter((opt) => next.includes(opt.id)) + .map((opt) => opt.name); + onChange('targetAudience', selectedNames.join(', ')); + }; + + const handleCustomAudienceChange = (value: string) => { + onChange('customTargetAudience', value); + // Also update targetAudience if no selections from dropdown + if (selectedAudienceIds.length === 0) { + onChange('targetAudience', value); + } else { + // Combine selected names with custom + const selectedNames = selectedAudienceOptions.map((opt) => opt.name); + if (value.trim()) { + onChange('targetAudience', [...selectedNames, value].join(', ')); + } else { + onChange('targetAudience', selectedNames.join(', ')); + } + } + }; + + return ( +
+
+ + 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. Use the custom field below. +
+ ) : ( + audienceOptions.map((option) => { + const isSelected = selectedAudienceIds.includes(option.id); + return ( + + ); + }) + )} +
+
+ + {selectedAudienceOptions.length > 0 && ( +
+ {selectedAudienceOptions.map((option) => ( + + {option.name} + + + ))} +
+ )} + +
+ + {showCustomInput && ( + handleCustomAudienceChange(e.target.value)} + placeholder="Operations leaders at fast-scaling eCommerce brands" + /> + )} +
+ +

+ Helps the AI craft messaging, examples, and tone. +

+
+ ); +} + // Stage 1 Wizard Component function BusinessDetailsStepStage1({ data, @@ -313,14 +530,24 @@ function BusinessDetailsStepStage1({ - onChange('targetAudience', e.target.value)} - placeholder="Operations leaders at fast-scaling eCommerce brands" - /> -

- Helps the AI craft messaging, examples, and tone. -

+ {metadata?.audience_profiles && metadata.audience_profiles.length > 0 ? ( + + ) : ( + <> + onChange('targetAudience', e.target.value)} + placeholder="Operations leaders at fast-scaling eCommerce brands" + /> +

+ Helps the AI craft messaging, examples, and tone. +

+ + )}
@@ -329,11 +556,55 @@ function BusinessDetailsStepStage1({ - onChange('businessType', e.target.value)} - placeholder="B2B SaaS platform" - /> + {metadata?.business_types && metadata.business_types.length > 0 ? ( + <> + { + if (value === 'custom') { + onChange('businessTypeId', null); + onChange('customBusinessType', ''); + } else { + const id = value ? parseInt(value) : null; + onChange('businessTypeId', id); + if (id) { + const option = metadata.business_types?.find(bt => bt.id === id); + if (option) { + onChange('businessType', option.name); + onChange('customBusinessType', ''); + } + } + } + }} + options={[ + { value: '', label: 'Select business type...' }, + ...(metadata.business_types.map(bt => ({ + value: bt.id.toString(), + label: bt.name, + }))), + { value: 'custom', label: '+ Add custom business type' }, + ]} + placeholder="Select business type..." + /> + {(data.businessTypeId === null || data.businessTypeId === undefined || data.businessTypeId === 0) && ( + { + onChange('customBusinessType', e.target.value); + onChange('businessType', e.target.value); + }} + placeholder="B2B SaaS platform" + className="mt-2" + /> + )} + + ) : ( + onChange('businessType', e.target.value)} + placeholder="B2B SaaS platform" + /> + )}