Refactor site building workflow and context handling; update API response structure for improved clarity and consistency. Adjust frontend components to align with new data structure, including error handling and loading states.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-20 21:50:16 +00:00
parent 781052c719
commit 1b4cd59e5b
24 changed files with 386 additions and 164 deletions

Binary file not shown.

View File

@@ -35,11 +35,13 @@ class WizardContextService:
workflow_payload = self.workflow_service.serialize_state(workflow_state) if workflow_state else None workflow_payload = self.workflow_service.serialize_state(workflow_state) if workflow_state else None
coverage_data = self._coverage_summary(site_blueprint)
context = { context = {
'workflow': workflow_payload, 'workflow': workflow_payload,
'clusters': self._cluster_summary(site_blueprint), 'cluster_summary': self._cluster_summary(site_blueprint),
'taxonomies': self._taxonomy_summary(site_blueprint), 'taxonomy_summary': self._taxonomy_summary(site_blueprint),
'coverage': self._coverage_summary(site_blueprint), 'sitemap_summary': coverage_data, # Frontend expects 'sitemap_summary' not 'coverage'
'coverage': coverage_data, # Keep for backward compatibility
} }
context['next_actions'] = self._next_actions(workflow_payload) context['next_actions'] = self._next_actions(workflow_payload)
return context return context

View File

@@ -111,13 +111,21 @@ class WorkflowStateService:
metadata: Optional[Dict[str, str]] = None, metadata: Optional[Dict[str, str]] = None,
) -> Optional[WorkflowState]: ) -> Optional[WorkflowState]:
"""Persist explicit step updates coming from the wizard.""" """Persist explicit step updates coming from the wizard."""
if not self.enabled:
return None
state = self.initialize(site_blueprint) state = self.initialize(site_blueprint)
if not state: if not state:
return None return None
metadata = metadata or {} metadata = metadata or {}
timestamp = timezone.now().isoformat() timestamp = timezone.now().isoformat()
step_status = state.step_status or {}
# Ensure step_status is a dict (handle None case)
if state.step_status is None:
state.step_status = {}
step_status = dict(state.step_status) # Create a copy to avoid mutation issues
entry = self._build_step_entry( entry = self._build_step_entry(
step=step, step=step,
status=status, status=status,
@@ -132,8 +140,22 @@ class WorkflowStateService:
state.step_status = step_status state.step_status = step_status
state.blocking_reason = metadata.get('message') state.blocking_reason = metadata.get('message')
state.completed = all(value.get('status') == 'ready' for value in step_status.values())
state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at']) # Calculate completed status - only true if all steps are ready and we have at least one step
if step_status:
state.completed = all(
value.get('status') == 'ready' or value.get('status') == 'complete'
for value in step_status.values()
)
else:
state.completed = False
try:
state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at'])
except Exception as e:
logger.error(f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}")
raise
self._emit_event(site_blueprint, 'wizard_step_updated', { self._emit_event(site_blueprint, 'wizard_step_updated', {
'step': step, 'step': step,
'status': status, 'status': status,
@@ -173,7 +195,7 @@ class WorkflowStateService:
'completed': state.completed, 'completed': state.completed,
'blocking_reason': state.blocking_reason, 'blocking_reason': state.blocking_reason,
'steps': steps_payload, 'steps': steps_payload,
'updated_at': state.updated_at, 'updated_at': state.updated_at.isoformat() if hasattr(state.updated_at, 'isoformat') else str(state.updated_at),
} }
def _build_step_entry( def _build_step_entry(

View File

@@ -1,3 +1,4 @@
import logging
from django.conf import settings from django.conf import settings
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
@@ -6,6 +7,8 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
logger = logging.getLogger(__name__)
from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.response import success_response, error_response from igny8_core.api.response import success_response, error_response
@@ -308,8 +311,16 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
"""Return aggregated wizard context (steps, clusters, taxonomies, coverage).""" """Return aggregated wizard context (steps, clusters, taxonomies, coverage)."""
blueprint = self.get_object() blueprint = self.get_object()
if not self.workflow_service.enabled: if not self.workflow_service.enabled:
# Return empty context structure matching frontend expectations
return success_response( return success_response(
data={'workflow': None, 'clusters': {}, 'taxonomies': {}, 'coverage': {}}, 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, request=request,
) )
@@ -362,27 +373,35 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
request request
) )
updated_state = self.workflow_service.update_step( try:
blueprint, updated_state = self.workflow_service.update_step(
step, blueprint,
status_value, step,
metadata status_value,
) metadata
)
if not updated_state:
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( return error_response(
'Failed to update workflow step', f'Internal server error: {str(e)}',
status.HTTP_500_INTERNAL_SERVER_ERROR, status.HTTP_500_INTERNAL_SERVER_ERROR,
request request
) )
# Serialize state
serialized = self.workflow_service.serialize_state(updated_state)
return success_response(
data=serialized,
request=request
)
@action(detail=True, methods=['post'], url_path='clusters/attach') @action(detail=True, methods=['post'], url_path='clusters/attach')
def attach_clusters(self, request, pk=None): def attach_clusters(self, request, pk=None):

View File

@@ -138,11 +138,9 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips"));
const Videos = lazy(() => import("./pages/Settings/UiElements/Videos")); const Videos = lazy(() => import("./pages/Settings/UiElements/Videos"));
export default function App() { export default function App() {
const { isAuthenticated, refreshUser, logout } = useAuthStore((state) => ({ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
isAuthenticated: state.isAuthenticated, const refreshUser = useAuthStore((state) => state.refreshUser);
refreshUser: state.refreshUser, const logout = useAuthStore((state) => state.logout);
logout: state.logout,
}));
useEffect(() => { useEffect(() => {
if (!isAuthenticated) { if (!isAuthenticated) {

View File

@@ -119,6 +119,7 @@ export {
// Aliases for commonly used icon names // Aliases for commonly used icon names
export { AngleLeftIcon as ArrowLeftIcon }; export { AngleLeftIcon as ArrowLeftIcon };
export { FileIcon as FileTextIcon }; export { FileIcon as FileTextIcon };
export { FileIcon as ImageIcon }; // Use FileIcon as ImageIcon alias
export { TimeIcon as ClockIcon }; export { TimeIcon as ClockIcon };
export { ErrorIcon as XCircleIcon }; export { ErrorIcon as XCircleIcon };
export { BoxIcon as TagIcon }; export { BoxIcon as TagIcon };

View File

@@ -111,15 +111,14 @@ export default function IndustriesSectorsKeywords() {
} catch (error: any) { } catch (error: any) {
if (error instanceof AccountSettingsError) { if (error instanceof AccountSettingsError) {
if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') {
console.debug('No saved user preferences yet.'); // Preferences don't exist yet - this is expected for new users
return; return;
} }
console.warn('Failed to load user preferences:', error); // For other errors (500, etc.), silently handle - user can still use the page
toast.error(getAccountSettingsPreferenceMessage(error)); // Don't show error toast for server errors - graceful degradation
return; return;
} }
console.warn('Failed to load user preferences:', error); // For non-AccountSettingsError errors, silently handle - graceful degradation
toast.error('Unable to load your saved preferences right now.');
} }
}; };

View File

@@ -4,7 +4,17 @@ import PageMeta from "../../../components/common/PageMeta";
import { Card } from "../../../components/ui/card"; import { Card } from "../../../components/ui/card";
import Button from "../../../components/ui/button/Button"; import Button from "../../../components/ui/button/Button";
import { useToast } from "../../../components/ui/toast/ToastContainer"; import { useToast } from "../../../components/ui/toast/ToastContainer";
import { FileText, Loader2, Plus, Trash2, CheckSquare, Square, Rocket } from "lucide-react"; import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
import {
TableIcon,
PlusIcon,
FileIcon,
TrashBinIcon,
CheckLineIcon,
BoxIcon,
PaperPlaneIcon,
BoltIcon
} from "../../../icons";
import { useSiteStore } from "../../../store/siteStore"; import { useSiteStore } from "../../../store/siteStore";
import { useBuilderStore } from "../../../store/builderStore"; import { useBuilderStore } from "../../../store/builderStore";
import { siteBuilderApi } from "../../../services/siteBuilder.api"; import { siteBuilderApi } from "../../../services/siteBuilder.api";
@@ -135,9 +145,20 @@ export default function SiteBuilderBlueprints() {
} }
}; };
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
<PageMeta title="Blueprints - IGNY8" /> <PageMeta title="Blueprints - IGNY8" description="View and manage site blueprints" />
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div> <div>
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50"> <p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
@@ -156,7 +177,7 @@ export default function SiteBuilderBlueprints() {
onClick={handleBulkDeleteClick} onClick={handleBulkDeleteClick}
variant="solid" variant="solid"
tone="danger" tone="danger"
startIcon={<Trash2 className="h-4 w-4" />} startIcon={<TrashBinIcon className="h-4 w-4" />}
> >
Delete {selectedIds.size} selected Delete {selectedIds.size} selected
</Button> </Button>
@@ -165,7 +186,7 @@ export default function SiteBuilderBlueprints() {
onClick={() => navigate("/sites/builder")} onClick={() => navigate("/sites/builder")}
variant="solid" variant="solid"
tone="brand" tone="brand"
startIcon={<Plus className="h-4 w-4" />} startIcon={<PlusIcon className="h-4 w-4" />}
> >
Create blueprint Create blueprint
</Button> </Button>
@@ -180,12 +201,12 @@ export default function SiteBuilderBlueprints() {
</Card> </Card>
) : loading ? ( ) : loading ? (
<div className="flex h-64 items-center justify-center text-gray-500 dark:text-gray-400"> <div className="flex h-64 items-center justify-center text-gray-500 dark:text-gray-400">
<Loader2 className="mr-2 h-5 w-5 animate-spin" /> <div className="mr-2 h-5 w-5 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading blueprints Loading blueprints
</div> </div>
) : blueprints.length === 0 ? ( ) : blueprints.length === 0 ? (
<Card className="p-12 text-center"> <Card className="p-12 text-center">
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" /> <FileIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<p className="mb-4 text-gray-600 dark:text-gray-400"> <p className="mb-4 text-gray-600 dark:text-gray-400">
No blueprints created yet for {activeSite.name}. No blueprints created yet for {activeSite.name}.
</p> </p>
@@ -202,9 +223,9 @@ export default function SiteBuilderBlueprints() {
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
> >
{selectedIds.size === blueprints.length ? ( {selectedIds.size === blueprints.length ? (
<CheckSquare className="h-4 w-4" /> <CheckLineIcon className="h-4 w-4" />
) : ( ) : (
<Square className="h-4 w-4" /> <BoxIcon className="h-4 w-4" />
)} )}
<span> <span>
{selectedIds.size === blueprints.length {selectedIds.size === blueprints.length
@@ -244,9 +265,9 @@ export default function SiteBuilderBlueprints() {
className="ml-2 flex-shrink-0" className="ml-2 flex-shrink-0"
> >
{selectedIds.has(blueprint.id) ? ( {selectedIds.has(blueprint.id) ? (
<CheckSquare className="h-5 w-5 text-brand-600 dark:text-brand-400" /> <CheckLineIcon className="h-5 w-5 text-brand-600 dark:text-brand-400" />
) : ( ) : (
<Square className="h-5 w-5 text-gray-400" /> <BoxIcon className="h-5 w-5 text-gray-400" />
)} )}
</button> </button>
</div> </div>
@@ -260,13 +281,21 @@ export default function SiteBuilderBlueprints() {
<span className="capitalize">{blueprint.status}</span> <span className="capitalize">{blueprint.status}</span>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Button
variant="solid"
tone="brand"
onClick={() => navigate(`/sites/builder/workflow/${blueprint.id}`)}
startIcon={<BoltIcon className="h-4 w-4" />}
>
Continue Workflow
</Button>
{(blueprint.status === 'ready' || blueprint.status === 'deployed') && ( {(blueprint.status === 'ready' || blueprint.status === 'deployed') && (
<Button <Button
variant="solid" variant="solid"
tone="brand" tone="brand"
disabled={deployingId === blueprint.id} disabled={deployingId === blueprint.id}
onClick={() => handleDeploy(blueprint)} onClick={() => handleDeploy(blueprint)}
startIcon={deployingId === blueprint.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />} startIcon={deployingId === blueprint.id ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
> >
{deployingId === blueprint.id ? "Deploying..." : blueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"} {deployingId === blueprint.id ? "Deploying..." : blueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
</Button> </Button>
@@ -279,7 +308,7 @@ export default function SiteBuilderBlueprints() {
> >
{isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? ( {isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading Loading
</> </>
) : ( ) : (
@@ -299,7 +328,7 @@ export default function SiteBuilderBlueprints() {
variant="ghost" variant="ghost"
tone="danger" tone="danger"
onClick={() => handleDeleteClick(blueprint)} onClick={() => handleDeleteClick(blueprint)}
startIcon={<Trash2 className="h-4 w-4" />} startIcon={<TrashBinIcon className="h-4 w-4" />}
> >
Delete Delete
</Button> </Button>

View File

@@ -11,7 +11,7 @@ import Alert from "../../../components/ui/alert/Alert";
import { useBuilderStore } from "../../../store/builderStore"; import { useBuilderStore } from "../../../store/builderStore";
import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore"; import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore";
import ProgressModal from "../../../components/common/ProgressModal"; import ProgressModal from "../../../components/common/ProgressModal";
import { Eye, Play, Loader2, Rocket } from "lucide-react"; import { EyeIcon, PaperPlaneIcon } from "../../../icons";
import { useToast } from "../../../components/ui/toast/ToastContainer"; import { useToast } from "../../../components/ui/toast/ToastContainer";
import { siteBuilderApi } from "../../../services/siteBuilder.api"; import { siteBuilderApi } from "../../../services/siteBuilder.api";
@@ -76,9 +76,9 @@ export default function SiteBuilderPreview() {
if (!activeBlueprint) { if (!activeBlueprint) {
return ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
<PageMeta title="Site Preview - IGNY8" /> <PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
<Card className="p-12 text-center"> <Card className="p-12 text-center">
<Eye className="mx-auto mb-4 h-16 w-16 text-gray-400" /> <EyeIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Run the Site Builder wizard or open a blueprint to preview it. Run the Site Builder wizard or open a blueprint to preview it.
</p> </p>
@@ -97,7 +97,7 @@ export default function SiteBuilderPreview() {
return ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
<PageMeta title="Site Preview - IGNY8" /> <PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div> <div>
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50"> <p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
@@ -117,7 +117,7 @@ export default function SiteBuilderPreview() {
tone="brand" tone="brand"
onClick={handleGenerateAll} onClick={handleGenerateAll}
disabled={isGenerating} disabled={isGenerating}
startIcon={isGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />} startIcon={isGenerating ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
> >
{isGenerating ? "Generating..." : "Generate All Pages"} {isGenerating ? "Generating..." : "Generate All Pages"}
</Button> </Button>
@@ -128,7 +128,7 @@ export default function SiteBuilderPreview() {
tone="brand" tone="brand"
onClick={handleDeploy} onClick={handleDeploy}
disabled={isDeploying} disabled={isDeploying}
startIcon={isDeploying ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />} startIcon={isDeploying ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
> >
{isDeploying ? "Deploying..." : activeBlueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"} {isDeploying ? "Deploying..." : activeBlueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
</Button> </Button>

View File

@@ -10,11 +10,15 @@ import PageMeta from "../../../components/common/PageMeta";
import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector"; import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector";
import PageHeader from "../../../components/common/PageHeader"; import PageHeader from "../../../components/common/PageHeader";
import Alert from "../../../components/ui/alert/Alert"; import Alert from "../../../components/ui/alert/Alert";
import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
import { import {
GridIcon, GridIcon,
ArrowLeftIcon, ArrowLeftIcon,
ArrowRightIcon, ArrowRightIcon,
BoltIcon, BoltIcon,
TableIcon,
PlusIcon,
FileIcon,
} from "../../../icons"; } from "../../../icons";
import { useSiteStore } from "../../../store/siteStore"; import { useSiteStore } from "../../../store/siteStore";
import { useSectorStore } from "../../../store/sectorStore"; import { useSectorStore } from "../../../store/sectorStore";
@@ -54,6 +58,7 @@ export default function SiteBuilderWizard() {
metadataError, metadataError,
isMetadataLoading, isMetadataLoading,
loadMetadata, loadMetadata,
loadBlueprint,
} = useBuilderStore(); } = useBuilderStore();
// Progress modal for AI functions // Progress modal for AI functions
@@ -304,9 +309,20 @@ export default function SiteBuilderWizard() {
return undefined; return undefined;
}; };
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
<PageMeta title="Create Site - IGNY8" /> <PageMeta title="Create Site - IGNY8" />
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<PageHeader <PageHeader
title="Site Builder" title="Site Builder"

View File

@@ -16,9 +16,13 @@ import IdeasHandoffStep from './steps/IdeasHandoffStep';
import Alert from '../../../components/ui/alert/Alert'; import Alert from '../../../components/ui/alert/Alert';
import PageMeta from '../../../components/common/PageMeta'; import PageMeta from '../../../components/common/PageMeta';
import Button from '../../../components/ui/button/Button'; import Button from '../../../components/ui/button/Button';
import { Loader2, HelpCircle } from 'lucide-react'; import { InfoIcon } from '../../../icons';
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType> = { interface StepComponentProps {
blueprintId: number;
}
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType<StepComponentProps>> = {
business_details: BusinessDetailsStep, business_details: BusinessDetailsStep,
clusters: ClusterAssignmentStep, clusters: ClusterAssignmentStep,
taxonomies: TaxonomyBuilderStep, taxonomies: TaxonomyBuilderStep,
@@ -124,7 +128,7 @@ export default function WorkflowWizard() {
if (!id) { if (!id) {
return ( return (
<div className="p-6"> <div className="p-6">
<Alert variant="error">Invalid blueprint ID</Alert> <Alert variant="error" title="Error" message="Invalid blueprint ID" />
</div> </div>
); );
} }
@@ -132,7 +136,7 @@ export default function WorkflowWizard() {
if (loading && !context) { if (loading && !context) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-8 w-8 animate-spin text-primary" /> <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div> </div>
); );
} }
@@ -140,7 +144,7 @@ export default function WorkflowWizard() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6">
<Alert variant="error">{error}</Alert> <Alert variant="error" title="Error" message={error} />
</div> </div>
); );
} }
@@ -149,7 +153,10 @@ export default function WorkflowWizard() {
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<PageMeta title={`Site Builder - ${STEP_LABELS[currentStep]}`} /> <PageMeta
title={`Site Builder - ${STEP_LABELS[currentStep]}`}
description={`Site Builder Workflow: ${STEP_LABELS[currentStep]}`}
/>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header with Help Button */} {/* Header with Help Button */}
@@ -166,9 +173,8 @@ export default function WorkflowWizard() {
onClick={() => setHelperDrawerOpen(!helperDrawerOpen)} onClick={() => setHelperDrawerOpen(!helperDrawerOpen)}
variant="outline" variant="outline"
size="sm" size="sm"
title="Press F1 or ? for help" startIcon={<InfoIcon className="h-4 w-4" />}
> >
<HelpCircle className="h-4 w-4 mr-2" />
Help Help
</Button> </Button>
</div> </div>

View File

@@ -3,9 +3,9 @@
* Contextual help for each wizard step * Contextual help for each wizard step
*/ */
import { useState } from 'react'; import { useState } from 'react';
import { X, HelpCircle, ChevronRight } from 'lucide-react';
import { WizardStep } from '../../../../store/builderWorkflowStore'; import { WizardStep } from '../../../../store/builderWorkflowStore';
import Button from '../../../../components/ui/button/Button'; import Button from '../../../../components/ui/button/Button';
import { CloseIcon, InfoIcon, ArrowRightIcon } from '../../../../icons';
interface HelperDrawerProps { interface HelperDrawerProps {
currentStep: WizardStep; currentStep: WizardStep;
@@ -95,7 +95,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <InfoIcon className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 id="helper-drawer-title" className="text-lg font-semibold"> <h2 id="helper-drawer-title" className="text-lg font-semibold">
Help & Tips Help & Tips
</h2> </h2>
@@ -107,7 +107,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
className="p-1" className="p-1"
aria-label="Close help drawer" aria-label="Close help drawer"
> >
<X className="h-5 w-5" /> <CloseIcon className="h-5 w-5" />
</Button> </Button>
</div> </div>
@@ -119,7 +119,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
<ul className="space-y-3"> <ul className="space-y-3">
{help.content.map((item, index) => ( {help.content.map((item, index) => (
<li key={index} className="flex items-start gap-3"> <li key={index} className="flex items-start gap-3">
<ChevronRight className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" /> <ArrowRightIcon className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-700 dark:text-gray-300">{item}</span> <span className="text-sm text-gray-700 dark:text-gray-300">{item}</span>
</li> </li>
))} ))}

View File

@@ -3,7 +3,7 @@
* Shows breadcrumb with step completion status * Shows breadcrumb with step completion status
*/ */
import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore'; import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore';
import { CheckCircle2, Circle } from 'lucide-react'; import { CheckCircleIcon } from '../../../../icons';
const STEPS: Array<{ key: WizardStep; label: string }> = [ const STEPS: Array<{ key: WizardStep; label: string }> = [
{ key: 'business_details', label: 'Business Details' }, { key: 'business_details', label: 'Business Details' },
@@ -19,9 +19,23 @@ interface WizardProgressProps {
} }
export default function WizardProgress({ currentStep }: WizardProgressProps) { export default function WizardProgress({ currentStep }: WizardProgressProps) {
const { completedSteps, blockingIssues } = useBuilderWorkflowStore(); const { completedSteps, blockingIssues, goToStep } = useBuilderWorkflowStore();
const currentIndex = STEPS.findIndex(s => s.key === currentStep); const currentIndex = STEPS.findIndex(s => s.key === currentStep);
const handleStepClick = (step: WizardStep, index: number) => {
// Allow navigation to:
// 1. Current step
// 2. Completed steps
// 3. Next step if current step is completed
const isCompleted = completedSteps.has(step.key);
const isCurrent = step.key === currentStep;
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
if (canNavigate && !blockingIssues.some(issue => issue.step === step.key)) {
goToStep(step);
}
};
return ( return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<nav aria-label="Progress"> <nav aria-label="Progress">
@@ -31,34 +45,39 @@ export default function WizardProgress({ currentStep }: WizardProgressProps) {
const isCurrent = step.key === currentStep; const isCurrent = step.key === currentStep;
const isBlocked = blockingIssues.some(issue => issue.step === step.key); const isBlocked = blockingIssues.some(issue => issue.step === step.key);
const isAccessible = index <= currentIndex || isCompleted; const isAccessible = index <= currentIndex || isCompleted;
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
return ( return (
<li key={step.key} className="flex-1 flex items-center"> <li key={step.key} className="flex-1 flex items-center">
<div className="flex items-center w-full"> <div className="flex items-center w-full">
{/* Step Circle */} {/* Step Circle */}
<div className="flex flex-col items-center flex-1"> <div className="flex flex-col items-center flex-1">
<div <button
type="button"
onClick={() => handleStepClick(step.key, index)}
disabled={!canNavigate || isBlocked}
className={` className={`
flex items-center justify-center w-10 h-10 rounded-full border-2 flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all
${ ${
isCompleted isCompleted
? 'bg-green-500 border-green-500 text-white' ? 'bg-green-500 border-green-500 text-white cursor-pointer hover:bg-green-600'
: isCurrent : isCurrent
? 'bg-primary border-primary text-white' ? 'bg-primary border-primary text-white cursor-default'
: isBlocked : isBlocked
? 'bg-red-100 border-red-500 text-red-500' ? 'bg-red-100 border-red-500 text-red-500 cursor-not-allowed'
: isAccessible : canNavigate
? 'bg-gray-100 border-gray-300 text-gray-600' ? 'bg-gray-100 border-gray-300 text-gray-600 cursor-pointer hover:bg-gray-200 hover:border-gray-400'
: 'bg-gray-50 border-gray-200 text-gray-400' : 'bg-gray-50 border-gray-200 text-gray-400 cursor-not-allowed'
} }
`} `}
title={isBlocked ? 'This step is blocked' : canNavigate ? `Go to ${step.label}` : 'Complete previous steps first'}
> >
{isCompleted ? ( {isCompleted ? (
<CheckCircle2 className="w-6 h-6" /> <CheckCircleIcon className="w-6 h-6" />
) : ( ) : (
<span className="text-sm font-semibold">{index + 1}</span> <span className="text-sm font-semibold">{index + 1}</span>
)} )}
</div> </button>
<span <span
className={` className={`
mt-2 text-xs font-medium text-center mt-2 text-xs font-medium text-center

View File

@@ -56,7 +56,7 @@ export function BusinessDetailsStep(props: BusinessDetailsStepProps) {
// Stage 2 Workflow Component // Stage 2 Workflow Component
function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
const { context, completeStep, loading } = useBuilderWorkflowStore(); const { context, completeStep, loading, goToStep } = useBuilderWorkflowStore();
const [blueprint, setBlueprint] = useState<SiteBlueprint | null>(null); const [blueprint, setBlueprint] = useState<SiteBlueprint | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -100,13 +100,52 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
}); });
setBlueprint(updated); setBlueprint(updated);
// Mark step as complete // Mark step as complete - catch and handle workflow errors separately
await completeStep('business_details', { try {
blueprint_name: formData.name, await completeStep('business_details', {
hosting_type: formData.hosting_type, blueprint_name: formData.name,
}); hosting_type: formData.hosting_type,
});
// If successful, automatically advance to next step
const { goToStep } = useBuilderWorkflowStore.getState();
goToStep('clusters');
} catch (workflowErr: any) {
// If workflow update fails but blueprint was saved, mark step as complete locally and advance
const workflowErrorMsg = workflowErr?.response?.error ||
workflowErr?.response?.message ||
workflowErr?.message ||
'Workflow step update failed';
// Check if it's a server error (500) - might be workflow service not enabled
const isServerError = workflowErr?.status === 500;
const errorDetail = workflowErr?.response?.error || workflowErr?.response?.message || '';
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');
const { goToStep } = useBuilderWorkflowStore.getState();
goToStep('clusters');
// Don't show error - workflow is optional
return;
}
console.warn('Workflow step update failed:', workflowErrorMsg, workflowErr);
// Mark step as completed locally so user can proceed
const { completedSteps, goToStep } = useBuilderWorkflowStore.getState();
const updatedCompletedSteps = new Set(completedSteps);
updatedCompletedSteps.add('business_details');
useBuilderWorkflowStore.setState({
completedSteps: updatedCompletedSteps,
currentStep: 'clusters' // Advance to step 2
});
// Show a non-blocking warning to the user
setError(`Blueprint saved successfully, but workflow step update failed: ${workflowErrorMsg}. You can continue to the next step.`);
}
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to save business details'); const errorMessage = err?.response?.error || err?.message || 'Failed to save business details';
setError(errorMessage);
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -124,9 +163,7 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
</div> </div>
{error && ( {error && (
<Alert variant="error" className="mt-4"> <Alert variant="error" title="Error" message={error} />
{error}
</Alert>
)} )}
<div className="mt-6 space-y-4"> <div className="mt-6 space-y-4">
@@ -307,6 +344,7 @@ function BusinessDetailsStepStage1({
onChange={(e) => onChange('industry', e.target.value)} onChange={(e) => onChange('industry', e.target.value)}
placeholder={userPreferences?.selectedIndustry || "Supply chain automation"} placeholder={userPreferences?.selectedIndustry || "Supply chain automation"}
/> />
</div>
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-4 shadow-theme-sm dark:border-white/5 dark:bg-white/[0.02]"> <div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-4 shadow-theme-sm dark:border-white/5 dark:bg-white/[0.02]">
<label className="text-sm font-semibold text-gray-900 dark:text-white"> <label className="text-sm font-semibold text-gray-900 dark:text-white">
Hosting preference Hosting preference

View File

@@ -27,7 +27,7 @@ import {
} from '../../../../components/ui/table'; } from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer'; import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore'; import { useSectorStore } from '../../../../store/sectorStore';
import { Loader2, CheckCircle2Icon, XCircleIcon } from 'lucide-react'; import { CheckCircleIcon, XCircleIcon } from '../../../../icons';
interface ClusterAssignmentStepProps { interface ClusterAssignmentStepProps {
blueprintId: number; blueprintId: number;
@@ -218,16 +218,16 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
<div className="grid grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Total Clusters</div> <div className="text-sm text-gray-600 dark:text-gray-400">Total Clusters</div>
<div className="text-2xl font-bold">{context.cluster_summary.total}</div> <div className="text-2xl font-bold">{context.cluster_summary.clusters?.length || 0}</div>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Attached</div> <div className="text-sm text-gray-600 dark:text-gray-400">Attached</div>
<div className="text-2xl font-bold">{context.cluster_summary.attached}</div> <div className="text-2xl font-bold">{context.cluster_summary.attached_count || 0}</div>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Coverage</div> <div className="text-sm text-gray-600 dark:text-gray-400">Coverage</div>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{context.cluster_summary.coverage_stats.complete} / {context.cluster_summary.total} {context.cluster_summary.coverage_counts?.complete || 0} / {context.cluster_summary.clusters?.length || 0}
</div> </div>
</div> </div>
</div> </div>
@@ -291,7 +291,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
> >
{attaching ? ( {attaching ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Attaching... Attaching...
</> </>
) : ( ) : (
@@ -305,7 +305,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
> >
{detaching ? ( {detaching ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Detaching... Detaching...
</> </>
) : ( ) : (
@@ -319,7 +319,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
{/* Cluster Table */} {/* Cluster Table */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" /> <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div> </div>
) : filteredClusters.length === 0 ? ( ) : filteredClusters.length === 0 ? (
<Alert variant="info" className="mt-4"> <Alert variant="info" className="mt-4">
@@ -371,7 +371,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
</div> </div>
)} )}
</TableCell> </TableCell>
<TableCell>{cluster.keywords_count || 0}</TableCell> <TableCell>{cluster.keyword_count || 0}</TableCell>
<TableCell>{cluster.volume?.toLocaleString() || 0}</TableCell> <TableCell>{cluster.volume?.toLocaleString() || 0}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs ${ <span className={`px-2 py-1 rounded text-xs ${
@@ -385,7 +385,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
<TableCell> <TableCell>
{isAttached ? ( {isAttached ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400"> <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2Icon className="h-4 w-4" /> <CheckCircleIcon className="h-4 w-4" />
<span className="text-sm">Attached</span> <span className="text-sm">Attached</span>
</div> </div>
) : ( ) : (
@@ -433,7 +433,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
> >
{workflowLoading ? ( {workflowLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Loading... Loading...
</> </>
) : ( ) : (

View File

@@ -15,20 +15,22 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
const { context, completeStep, blockingIssues } = useBuilderWorkflowStore(); const { context, completeStep, blockingIssues } = useBuilderWorkflowStore();
const coverageBlocking = blockingIssues.find(issue => issue.step === 'coverage'); const coverageBlocking = blockingIssues.find(issue => issue.step === 'coverage');
const clusterStats = context?.cluster_summary?.coverage_stats || { const clusterStats = {
complete: 0, complete: context?.cluster_summary?.coverage_counts?.complete || 0,
in_progress: 0, in_progress: context?.cluster_summary?.coverage_counts?.in_progress || 0,
pending: 0, pending: context?.cluster_summary?.coverage_counts?.pending || 0,
}; };
const totalClusters = context?.cluster_summary?.total || 0; const totalClusters = context?.cluster_summary?.clusters?.length || 0;
const attachedClusters = context?.cluster_summary?.attached || 0; const attachedClusters = context?.cluster_summary?.attached_count || 0;
const clusterCoverage = totalClusters > 0 ? (attachedClusters / totalClusters) * 100 : 0; const clusterCoverage = totalClusters > 0 ? (attachedClusters / totalClusters) * 100 : 0;
const totalTaxonomies = context?.taxonomy_summary?.total || 0; const totalTaxonomies = context?.taxonomy_summary?.total_taxonomies || 0;
const taxonomyByType = context?.taxonomy_summary?.by_type || {}; const taxonomyByType = context?.taxonomy_summary?.counts_by_type || {};
const sitemapCoverage = context?.sitemap_summary?.coverage_percentage || 0; // Calculate sitemap coverage from pages_by_status if available
const totalPages = context?.sitemap_summary?.total_pages || 0; const sitemapPages = context?.sitemap_summary?.pages_total || 0;
const sitemapCoverage = sitemapPages > 0 ? 100 : 0; // Simplified - backend doesn't provide coverage_percentage
const totalPages = sitemapPages;
const getCoverageStatus = (percentage: number) => { const getCoverageStatus = (percentage: number) => {
if (percentage >= 90) return { status: 'excellent', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' }; if (percentage >= 90) return { status: 'excellent', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' };
@@ -152,9 +154,9 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span> <span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
<span className="font-medium">{totalPages}</span> <span className="font-medium">{totalPages}</span>
</div> </div>
{context?.sitemap_summary?.by_type && ( {context?.sitemap_summary?.pages_by_type && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => ( {Object.entries(context.sitemap_summary.pages_by_type).map(([type, count]) => (
<div key={type} className="flex justify-between text-xs"> <div key={type} className="flex justify-between text-xs">
<span className="text-gray-600 dark:text-gray-400 capitalize">{type}:</span> <span className="text-gray-600 dark:text-gray-400 capitalize">{type}:</span>
<span className="font-medium">{count as number}</span> <span className="font-medium">{count as number}</span>

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import type { BuilderFormData } from "../../../../types/siteBuilder"; import type { BuilderFormData } from "../../../../types/siteBuilder";
import { Card } from "../../../../components/ui/card/Card"; import { Card } from "../../../../components/ui/card";
import Button from "../../../../components/ui/button/Button"; import Button from "../../../../components/ui/button/Button";
const inputClass = const inputClass =

View File

@@ -135,16 +135,22 @@ export default function SitemapReviewStep({ blueprintId }: SitemapReviewStepProp
<div className="grid grid-cols-3 gap-4 text-sm"> <div className="grid grid-cols-3 gap-4 text-sm">
<div> <div>
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span> <span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
<span className="ml-2 font-semibold">{context.sitemap_summary.total_pages}</span> <span className="ml-2 font-semibold">{context.sitemap_summary.pages_total || 0}</span>
</div> </div>
<div> <div>
<span className="text-gray-600 dark:text-gray-400">Coverage:</span> <span className="text-gray-600 dark:text-gray-400">By Status:</span>
<span className="ml-2 font-semibold">{context.sitemap_summary.coverage_percentage}%</span> <div className="mt-1 flex flex-wrap gap-2">
{Object.entries(context.sitemap_summary.pages_by_status || {}).map(([status, count]) => (
<span key={status} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
{status}: {count}
</span>
))}
</div>
</div> </div>
<div> <div>
<span className="text-gray-600 dark:text-gray-400">By Type:</span> <span className="text-gray-600 dark:text-gray-400">By Type:</span>
<div className="mt-1 flex flex-wrap gap-2"> <div className="mt-1 flex flex-wrap gap-2">
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => ( {Object.entries(context.sitemap_summary.pages_by_type || {}).map(([type, count]) => (
<span key={type} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded"> <span key={type} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
{type}: {count} {type}: {count}
</span> </span>

View File

@@ -6,7 +6,7 @@ import type {
} from "../../../../types/siteBuilder"; } from "../../../../types/siteBuilder";
import { Card } from "../../../../components/ui/card"; import { Card } from "../../../../components/ui/card";
import { Dropdown } from "../../../../components/ui/dropdown/Dropdown"; import { Dropdown } from "../../../../components/ui/dropdown/Dropdown";
import { Check } from "lucide-react"; import { CheckLineIcon } from "../../../../icons";
const labelClass = const labelClass =
"text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block"; "text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
@@ -197,7 +197,7 @@ export function StyleStep({
</span> </span>
)} )}
</span> </span>
{isSelected && <Check className="h-4 w-4" />} {isSelected && <CheckLineIcon className="h-4 w-4" />}
</button> </button>
); );
}) })
@@ -312,7 +312,7 @@ export function StyleStep({
</span> </span>
)} )}
</span> </span>
{isSelected && <Check className="h-4 w-4" />} {isSelected && <CheckLineIcon className="h-4 w-4" />}
</button> </button>
); );
}) })

View File

@@ -29,7 +29,7 @@ import {
} from '../../../../components/ui/table'; } from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer'; import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore'; import { useSectorStore } from '../../../../store/sectorStore';
import { Loader2, PlusIcon, UploadIcon, EditIcon, TrashIcon } from 'lucide-react'; import { PlusIcon, DownloadIcon, PencilIcon, TrashBinIcon } from '../../../../icons';
import FormModal from '../../../../components/common/FormModal'; import FormModal from '../../../../components/common/FormModal';
interface TaxonomyBuilderStepProps { interface TaxonomyBuilderStepProps {
@@ -223,9 +223,9 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
<div className="grid grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Total Taxonomies</div> <div className="text-sm text-gray-600 dark:text-gray-400">Total Taxonomies</div>
<div className="text-2xl font-bold">{context.taxonomy_summary.total}</div> <div className="text-2xl font-bold">{context.taxonomy_summary.total_taxonomies}</div>
</div> </div>
{Object.entries(context.taxonomy_summary.by_type || {}).slice(0, 3).map(([type, count]) => ( {Object.entries(context.taxonomy_summary.counts_by_type || {}).slice(0, 3).map(([type, count]) => (
<div key={type} className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"> <div key={type} className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400 capitalize"> <div className="text-sm text-gray-600 dark:text-gray-400 capitalize">
{type.replace('_', ' ')} {type.replace('_', ' ')}
@@ -269,7 +269,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
onClick={handleImport} onClick={handleImport}
variant="outline" variant="outline"
> >
<UploadIcon className="mr-2 h-4 w-4" /> <DownloadIcon className="mr-2 h-4 w-4" />
Import Import
</Button> </Button>
</div> </div>
@@ -279,7 +279,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
{/* Taxonomy Table */} {/* Taxonomy Table */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" /> <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div> </div>
) : filteredTaxonomies.length === 0 ? ( ) : filteredTaxonomies.length === 0 ? (
<Alert variant="info" className="mt-4"> <Alert variant="info" className="mt-4">
@@ -333,7 +333,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
size="sm" size="sm"
onClick={() => handleOpenModal(taxonomy)} onClick={() => handleOpenModal(taxonomy)}
> >
<EditIcon className="h-4 w-4" /> <PencilIcon className="h-4 w-4" />
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
@@ -414,7 +414,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
> >
{workflowLoading ? ( {workflowLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading... Loading...
</> </>
) : ( ) : (

View File

@@ -15,6 +15,7 @@ import FormModal, { FormField } from '../../components/common/FormModal';
import Alert from '../../components/ui/alert/Alert'; import Alert from '../../components/ui/alert/Alert';
import Switch from '../../components/form/switch/Switch'; import Switch from '../../components/form/switch/Switch';
import ViewToggle from '../../components/common/ViewToggle'; import ViewToggle from '../../components/common/ViewToggle';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import { import {
PlusIcon, PlusIcon,
PencilIcon, PencilIcon,
@@ -108,9 +109,13 @@ export default function SiteList() {
} }
} catch (error: any) { } catch (error: any) {
// 404 means preferences don't exist yet - that's fine // 404 means preferences don't exist yet - that's fine
if (error.status !== 404) { // 500 and other errors should be handled silently - user can still use the page
console.warn('Failed to load user preferences:', error); if (error?.status === 404) {
// Preferences don't exist yet - this is expected for new users
return;
} }
// Silently handle other errors (500, network errors, etc.) - don't spam console
// User can still use the page without preferences
} }
}; };
@@ -166,10 +171,9 @@ export default function SiteList() {
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry); allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
} }
} catch (error: any) { } catch (error: any) {
// 404 means preferences don't exist yet - show all industries // 404 means preferences don't exist yet - show all industries (expected for new users)
if (error.status !== 404) { // 500 and other errors - show all industries (graceful degradation)
console.warn('Failed to load user preferences for filtering:', error); // Silently handle errors - user can still use the page
}
} }
setIndustries(allIndustries); setIndustries(allIndustries);
@@ -678,6 +682,13 @@ export default function SiteList() {
); );
} }
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return ( return (
<div className="p-6"> <div className="p-6">
<PageMeta title="Sites Management - IGNY8" /> <PageMeta title="Sites Management - IGNY8" />
@@ -687,6 +698,9 @@ export default function SiteList() {
hideSiteSector={true} hideSiteSector={true}
/> />
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
{/* Info Alert */} {/* Info Alert */}
<div className="mb-6"> <div className="mb-6">
<Alert <Alert

View File

@@ -2159,54 +2159,62 @@ export interface PageBlueprint {
export interface WorkflowState { export interface WorkflowState {
current_step: string; current_step: string;
step_status: Record<string, { status: string; code?: string; message?: string; updated_at?: string }>;
blocking_reason?: string;
completed: boolean; completed: boolean;
blocking_reason?: string;
steps: Array<{
step: string;
status: string;
code?: string;
message?: string;
updated_at?: string;
}>;
updated_at: string; updated_at: string;
} }
export interface WizardContext { export interface WizardContext {
workflow: WorkflowState; workflow: WorkflowState;
cluster_summary: { cluster_summary: {
total: number; attached_count: number;
attached: number; coverage_counts: Record<string, number>;
coverage_stats: {
complete: number;
in_progress: number;
pending: number;
};
clusters: Array<{ clusters: Array<{
id: number; id: number;
name: string; name: string;
keywords_count: number; keyword_count: number;
volume: number; volume: number;
context_type?: string; context_type?: string;
dimension_meta?: Record<string, any>; dimension_meta?: Record<string, any>;
coverage_status: string; coverage_status: string;
role: string; role: string;
metadata?: Record<string, any>;
suggested_taxonomies?: string[];
attribute_hints?: string[];
}>; }>;
}; };
taxonomy_summary: { taxonomy_summary: {
total: number; total_taxonomies: number;
by_type: Record<string, number>; counts_by_type: Record<string, number>;
taxonomies: Array<{ taxonomies: Array<{
id: number; id: number;
name: string; name: string;
slug: string; slug: string;
taxonomy_type: string; taxonomy_type: string;
cluster_count: number; description?: string;
cluster_ids: number[];
metadata?: Record<string, any>;
external_reference?: string;
}>; }>;
}; };
sitemap_summary: { sitemap_summary?: {
total_pages: number; pages_total: number;
by_type: Record<string, number>; pages_by_status: Record<string, number>;
coverage_percentage: number; pages_by_type: Record<string, number>;
};
next_actions?: {
step: string | null;
status: string;
message: string | null;
code: string | null;
}; };
next_actions: Array<{
step: string;
action: string;
message: string;
}>;
} }
export async function fetchSiteBlueprints(filters?: { export async function fetchSiteBlueprints(filters?: {

View File

@@ -235,8 +235,23 @@ export const useAuthStore = create<AuthState>()(
set({ user: refreshedUser, isAuthenticated: true }); set({ user: refreshedUser, isAuthenticated: true });
} catch (error: any) { } catch (error: any) {
console.warn('Failed to refresh user data:', error); // Only logout on authentication/authorization errors, not on network errors
set({ user: null, token: null, refreshToken: null, isAuthenticated: false }); // Network errors (500, timeout, etc.) should not log the user out
const isAuthError = error?.code === 'ACCOUNT_REQUIRED' ||
error?.code === 'PLAN_REQUIRED' ||
error?.status === 401 ||
error?.status === 403 ||
(error?.message && error.message.includes('Not authenticated'));
if (isAuthError) {
// Real authentication error - logout user
console.warn('Authentication error during refresh, logging out:', error);
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
} else {
// Network/server error - don't logout, just throw the error
// The caller (AppLayout) will handle it gracefully
console.debug('Non-auth error during refresh (will retry):', error);
}
throw error; throw error;
} }
}, },

View File

@@ -70,21 +70,38 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
set({ blueprintId, loading: true, error: null }); set({ blueprintId, loading: true, error: null });
try { try {
const context = await fetchWizardContext(blueprintId); const context = await fetchWizardContext(blueprintId);
const workflow = context.workflow; 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 // Determine completed steps from workflow state
// Backend returns 'steps' as an array, not 'step_status' as an object
const completedSteps = new Set<WizardStep>(); const completedSteps = new Set<WizardStep>();
Object.entries(workflow.step_status || {}).forEach(([step, status]) => { const steps = workflow.steps || [];
if (status.status === 'ready' || status.status === 'complete') { steps.forEach((stepData: any) => {
completedSteps.add(step as WizardStep); if (stepData?.status === 'ready' || stepData?.status === 'complete') {
completedSteps.add(stepData.step as WizardStep);
} }
}); });
// Extract blocking issues // Extract blocking issues
const blockingIssues: Array<{ step: WizardStep; message: string }> = []; const blockingIssues: Array<{ step: WizardStep; message: string }> = [];
Object.entries(workflow.step_status || {}).forEach(([step, status]) => { steps.forEach((stepData: any) => {
if (status.status === 'blocked' && status.message) { if (stepData?.status === 'blocked' && stepData?.message) {
blockingIssues.push({ step: step as WizardStep, message: status.message }); blockingIssues.push({ step: stepData.step as WizardStep, message: stepData.message });
} }
}); });
@@ -133,11 +150,17 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
}, },
completeStep: async (step: WizardStep, metadata?: Record<string, any>) => { completeStep: async (step: WizardStep, metadata?: Record<string, any>) => {
const { blueprintId } = get(); const { blueprintId, workflowState } = get();
if (!blueprintId) { if (!blueprintId) {
throw new Error('No blueprint initialized'); 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 }); set({ loading: true, error: null });
try { try {
const updatedState = await updateWorkflowStep(blueprintId, step, 'ready', metadata); const updatedState = await updateWorkflowStep(blueprintId, step, 'ready', metadata);
@@ -161,8 +184,13 @@ export const useBuilderWorkflowStore = create<BuilderWorkflowState>()(
// Emit telemetry // Emit telemetry
get().flushTelemetry(); get().flushTelemetry();
} catch (error: any) { } 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({ set({
error: error.message || `Failed to complete step: ${step}`, error: errorMessage,
loading: false, loading: false,
}); });
throw error; throw error;