From 1ceeabed67ace694910766fe69d9fe61024eb784 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 18 Nov 2025 15:25:34 +0000 Subject: [PATCH] Enhance Site Builder Functionality and UI Components - Added a new method to fetch blueprints from the API, improving data retrieval for site structures. - Updated the WizardPage component to include a progress modal for better user feedback during site structure generation. - Refactored state management in builderStore to track structure generation tasks, enhancing the overall user experience. - Replaced SyncIcon with RefreshCw in integration components for a more consistent iconography. - Improved the site structure generation prompt in utils.py to provide clearer instructions for AI-driven site architecture. --- backend/celerybeat-schedule | Bin 0 -> 16384 bytes backend/igny8_core/modules/system/utils.py | 64 +++++++++++ .../integration/IntegrationStatus.tsx | 4 +- .../integration/SiteIntegrationsSection.tsx | 4 +- site-builder/src/api/builder.api.ts | 4 + site-builder/src/api/system.api.ts | 27 +++++ site-builder/src/hooks/useTaskProgress.ts | 102 ++++++++++++++++++ site-builder/src/pages/wizard/WizardPage.tsx | 70 +++++++++++- .../wizard/__tests__/WizardPage.test.tsx | 27 +++-- site-builder/src/state/builderStore.ts | 14 ++- 10 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 backend/celerybeat-schedule create mode 100644 site-builder/src/api/system.api.ts create mode 100644 site-builder/src/hooks/useTaskProgress.ts diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule new file mode 100644 index 0000000000000000000000000000000000000000..9276c3eae92f2c2f3cc29e28d33dc94f9a5e9f75 GIT binary patch literal 16384 zcmeI3PiqrF7{(_}q^W7Jv?^*qiXM#BY())-mkLEh0v>Ed@Zhq$8M8w-yJdG)Odybx zSlC;q7e9a(5yUUx*^?i@gLw2Jco%dgGx>vfXr+`Y&xV=z-PxHpGrxKFVK12lF4*={I&1V!$=z{T!2e+QgEKg50^k4+lLWM1 zdHn8d$~}8!LZ9(GIpzfY)GWZ^?>IPH-xg@dA_Oqe;Ke6P9}ZjT07yw%sr64E_ZmXPh0WTXpN2uy|Lts=QD~mVdCm zDl8?*G<4j6Hv5Ga)+pM}XY>0(PgJwhG2IMk$P&NSjzaDSothWZCgX|7N*)bp+_7Bh zaFLbQJfAkxfZmqV>WFPo^>f)x)(Sf}Y%hwb)sEEF#Bx~>u&`xuC)r4>QGDCPw@2}d zO@~v?+Eip?cU!C;o&H*yFE1|VUtgA2+>`~Jg>pXAEy^9vWBJ`#u^WYyg-;^sK9cTT z<;wOMI}JCPw-HeBT-+BLMI)ztRT?ClGpkC>a#Ub1mTPxhwWtP>eR4~pk@G8Dj_3*Z zVo%n}&(6_j)Jr+74F@-CPRgUU!{yG@VzqqX7pngG?9K!jH%(Yblf)YLHE>Kgdlb$d zcd<>l;slQ^;h)gU@Xu7U{L*q=_!a&6eXi-mhEJ&smj^a{kfHm(;k!zV{m+SG>Mgi8 g%!!k`$caH3paKC9009sH0T2KI5C8!XID7; case 'syncing': - return ; + return ; default: return ; } diff --git a/frontend/src/components/integration/SiteIntegrationsSection.tsx b/frontend/src/components/integration/SiteIntegrationsSection.tsx index 9c89189c..7da5a944 100644 --- a/frontend/src/components/integration/SiteIntegrationsSection.tsx +++ b/frontend/src/components/integration/SiteIntegrationsSection.tsx @@ -3,7 +3,7 @@ * Phase 6: Site Integration & Multi-Destination Publishing */ import React, { useState, useEffect } from 'react'; -import { PlusIcon, TrashIcon, TestTubeIcon, SyncIcon } from 'lucide-react'; +import { PlusIcon, TrashIcon, TestTubeIcon, RefreshCw } from 'lucide-react'; import Button from '../ui/button/Button'; import { Modal } from '../ui/modal'; import FormModal, { FormField } from '../common/FormModal'; @@ -303,7 +303,7 @@ export default function SiteIntegrationsSection({ siteId }: SiteIntegrationsSect onClick={() => handleSync(integration)} className="flex-1" > - + Sync )} diff --git a/site-builder/src/api/builder.api.ts b/site-builder/src/api/builder.api.ts index 59caa5aa..ec4f55f8 100644 --- a/site-builder/src/api/builder.api.ts +++ b/site-builder/src/api/builder.api.ts @@ -31,6 +31,10 @@ export interface GenerateStructurePayload { } export const builderApi = { + async getBlueprint(blueprintId: number): Promise { + const res = await client.get(`/blueprints/${blueprintId}/`); + return res.data; + }, async listBlueprints(): Promise { const res = await client.get('/blueprints/'); if (Array.isArray(res.data?.results)) { diff --git a/site-builder/src/api/system.api.ts b/site-builder/src/api/system.api.ts new file mode 100644 index 00000000..715efb6b --- /dev/null +++ b/site-builder/src/api/system.api.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; + +const API_ROOT = import.meta.env.VITE_API_URL ?? 'http://localhost:8010/api'; + +const client = axios.create({ + baseURL: API_ROOT, + withCredentials: true, +}); + +export interface TaskProgressResponse { + state: 'PENDING' | 'PROGRESS' | 'SUCCESS' | 'FAILURE'; + meta?: { + phase?: string; + percentage?: number; + message?: string; + error?: string; + }; +} + +export const systemApi = { + async getTaskProgress(taskId: string): Promise { + const res = await client.get(`/v1/system/settings/task_progress/${taskId}/`); + return res.data; + }, +}; + + diff --git a/site-builder/src/hooks/useTaskProgress.ts b/site-builder/src/hooks/useTaskProgress.ts new file mode 100644 index 00000000..1f2c6b35 --- /dev/null +++ b/site-builder/src/hooks/useTaskProgress.ts @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; +import { systemApi, type TaskProgressResponse } from '../api/system.api'; + +type TaskStatus = 'idle' | 'pending' | 'processing' | 'completed' | 'error'; + +interface UseTaskProgressOptions { + onComplete?: () => void; + onError?: (message: string) => void; + onUpdate?: (meta: TaskProgressResponse['meta']) => void; +} + +interface TaskProgressState { + status: TaskStatus; + percentage: number; + message?: string; + phase?: string; +} + +export function useTaskProgress(taskId: string | null, options: UseTaskProgressOptions = {}) { + const { onComplete, onError, onUpdate } = options; + const [state, setState] = useState({ + status: 'idle', + percentage: 0, + }); + + useEffect(() => { + if (!taskId) { + setState({ status: 'idle', percentage: 0 }); + return; + } + + let isActive = true; + let interval: ReturnType | null = null; + + const mapResponseToState = (response: TaskProgressResponse): TaskProgressState => { + const { state: taskState, meta } = response; + const percentage = typeof meta?.percentage === 'number' ? meta.percentage : 0; + const message = meta?.message; + const phase = meta?.phase; + + if (taskState === 'SUCCESS') { + return { status: 'completed', percentage: 100, message: message ?? 'Site structure ready.', phase }; + } + if (taskState === 'FAILURE') { + return { status: 'error', percentage, message: meta?.error ?? message ?? 'Task failed.', phase }; + } + if (taskState === 'PROGRESS') { + return { status: 'processing', percentage: Math.min(percentage || 10, 95), message, phase }; + } + return { status: 'pending', percentage, message, phase }; + }; + + const poll = async () => { + try { + const response = await systemApi.getTaskProgress(taskId); + + if (!isActive) return; + + const nextState = mapResponseToState(response); + setState(nextState); + onUpdate?.(response.meta); + + if (nextState.status === 'completed') { + onComplete?.(); + if (interval) { + clearInterval(interval); + interval = null; + } + } else if (nextState.status === 'error') { + onError?.(nextState.message ?? 'Task failed'); + if (interval) { + clearInterval(interval); + interval = null; + } + } + } catch (error) { + if (!isActive) return; + const message = error instanceof Error ? error.message : 'Unable to load task progress'; + setState({ status: 'error', percentage: 0, message }); + onError?.(message); + if (interval) { + clearInterval(interval); + interval = null; + } + } + }; + + poll(); + interval = setInterval(poll, 2000); + + return () => { + isActive = false; + if (interval) { + clearInterval(interval); + } + }; + }, [taskId, onComplete, onError, onUpdate]); + + return state; +} + + diff --git a/site-builder/src/pages/wizard/WizardPage.tsx b/site-builder/src/pages/wizard/WizardPage.tsx index 56787162..7c444076 100644 --- a/site-builder/src/pages/wizard/WizardPage.tsx +++ b/site-builder/src/pages/wizard/WizardPage.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Loader2, PlayCircle, RefreshCw } from 'lucide-react'; import { useBuilderStore } from '../../state/builderStore'; import { useSiteDefinitionStore } from '../../state/siteDefinitionStore'; @@ -7,6 +7,9 @@ import { BriefStep } from './steps/BriefStep'; import { ObjectivesStep } from './steps/ObjectivesStep'; import { StyleStep } from './steps/StyleStep'; import { Card } from '../../components/common/Card'; +import { ProgressModal } from '../../components/common/ProgressModal'; +import { useTaskProgress } from '../../hooks/useTaskProgress'; +import { builderApi } from '../../api/builder.api'; const stepTitles = ['Business', 'Brief', 'Objectives', 'Style']; @@ -26,8 +29,54 @@ export function WizardPage() { error, activeBlueprint, refreshPages, + structureTaskId, + setStructureTaskId, } = useBuilderStore(); - const { structure } = useSiteDefinitionStore(); + const structure = useSiteDefinitionStore((state) => state.structure); + const setStructure = useSiteDefinitionStore((state) => state.setStructure); + const [showProgressModal, setShowProgressModal] = useState(false); + const [progressMessage, setProgressMessage] = useState('Initializing Site Builder AI…'); + + const syncLatestStructure = useCallback(async () => { + if (!activeBlueprint) return; + try { + const latestBlueprint = await builderApi.getBlueprint(activeBlueprint.id); + if (latestBlueprint.structure_json) { + setStructure(latestBlueprint.structure_json); + } + await refreshPages(activeBlueprint.id); + } catch (syncError) { + const message = syncError instanceof Error ? syncError.message : 'Unable to sync blueprint'; + useBuilderStore.setState({ error: message }); + } finally { + setStructureTaskId(null); + } + }, [activeBlueprint, refreshPages, setStructure, setStructureTaskId]); + + const taskProgress = useTaskProgress(structureTaskId, { + onUpdate: (meta) => { + if (meta?.message) { + setProgressMessage(meta.message); + } + }, + onComplete: async () => { + setProgressMessage('Site structure ready!'); + await syncLatestStructure(); + setTimeout(() => setShowProgressModal(false), 500); + }, + onError: (message) => { + useBuilderStore.setState({ error: message }); + setShowProgressModal(false); + setStructureTaskId(null); + }, + }); + + useEffect(() => { + if (structureTaskId) { + setShowProgressModal(true); + setProgressMessage('Sending request to Site Builder AI…'); + } + }, [structureTaskId]); const stepComponents = useMemo( () => [ @@ -112,6 +161,23 @@ export function WizardPage() { )} + { + if (taskProgress.status === 'processing') { + return; + } + setShowProgressModal(false); + setStructureTaskId(null); + }} + title="Generating site structure" + message={progressMessage} + progress={{ + current: Math.round(taskProgress.percentage), + total: 100, + }} + taskId={structureTaskId || undefined} + /> ); } diff --git a/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx b/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx index a15b8f50..d5d7b664 100644 --- a/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx +++ b/site-builder/src/pages/wizard/__tests__/WizardPage.test.tsx @@ -17,6 +17,9 @@ describe('WizardPage', () => { const mockAddObjective = vi.fn(); const mockRemoveObjective = vi.fn(); const mockSubmitWizard = vi.fn(); + const mockSetStructureTaskId = vi.fn(); + const mockRefreshPages = vi.fn(); + const mockSetStructure = vi.fn(); beforeEach(() => { vi.clearAllMocks(); @@ -37,6 +40,7 @@ describe('WizardPage', () => { isSubmitting: false, error: undefined, activeBlueprint: undefined, + structureTaskId: null, setField: mockSetField, nextStep: mockNextStep, previousStep: mockPreviousStep, @@ -45,10 +49,15 @@ describe('WizardPage', () => { addObjective: mockAddObjective, removeObjective: mockRemoveObjective, submitWizard: mockSubmitWizard, - refreshPages: vi.fn(), + refreshPages: mockRefreshPages, + setStructureTaskId: mockSetStructureTaskId, }); - (useSiteDefinitionStore as any).mockReturnValue({ - structure: undefined, + (useSiteDefinitionStore as any).mockImplementation((selector?: (state: any) => any) => { + const state = { + structure: undefined, + setStructure: mockSetStructure, + }; + return selector ? selector(state) : state; }); }); @@ -85,6 +94,7 @@ describe('WizardPage', () => { isSubmitting: false, error: undefined, activeBlueprint: undefined, + structureTaskId: null, setField: mockSetField, nextStep: mockNextStep, previousStep: mockPreviousStep, @@ -93,7 +103,8 @@ describe('WizardPage', () => { addObjective: mockAddObjective, removeObjective: mockRemoveObjective, submitWizard: mockSubmitWizard, - refreshPages: vi.fn(), + refreshPages: mockRefreshPages, + setStructureTaskId: mockSetStructureTaskId, }); render(); @@ -112,6 +123,7 @@ describe('WizardPage', () => { isSubmitting: false, error: 'Test error message', activeBlueprint: undefined, + structureTaskId: null, setField: mockSetField, nextStep: mockNextStep, previousStep: mockPreviousStep, @@ -120,7 +132,8 @@ describe('WizardPage', () => { addObjective: mockAddObjective, removeObjective: mockRemoveObjective, submitWizard: mockSubmitWizard, - refreshPages: vi.fn(), + refreshPages: mockRefreshPages, + setStructureTaskId: mockSetStructureTaskId, }); render(); @@ -151,6 +164,7 @@ describe('WizardPage', () => { isSubmitting: true, error: undefined, activeBlueprint: undefined, + structureTaskId: null, setField: mockSetField, nextStep: mockNextStep, previousStep: mockPreviousStep, @@ -159,7 +173,8 @@ describe('WizardPage', () => { addObjective: mockAddObjective, removeObjective: mockRemoveObjective, submitWizard: mockSubmitWizard, - refreshPages: vi.fn(), + refreshPages: mockRefreshPages, + setStructureTaskId: mockSetStructureTaskId, }); render(); diff --git a/site-builder/src/state/builderStore.ts b/site-builder/src/state/builderStore.ts index 7b448200..3b5be2e2 100644 --- a/site-builder/src/state/builderStore.ts +++ b/site-builder/src/state/builderStore.ts @@ -34,6 +34,7 @@ interface BuilderState { isSubmitting: boolean; error?: string; activeBlueprint?: SiteBlueprint; + structureTaskId: string | null; pages: PageBlueprint[]; selectedPageIds: number[]; isGenerating: boolean; @@ -51,6 +52,7 @@ interface BuilderState { previousStep: () => void; reset: () => void; submitWizard: () => Promise; + setStructureTaskId: (taskId: string | null) => void; refreshPages: (blueprintId: number) => Promise; togglePageSelection: (pageId: number) => void; selectAllPages: () => void; @@ -62,7 +64,9 @@ export const useBuilderStore = create((set, get) => ({ form: defaultForm, currentStep: 0, isSubmitting: false, + structureTaskId: null, pages: [], + setStructureTaskId: (taskId) => set({ structureTaskId: taskId }), selectedPageIds: [], isGenerating: false, @@ -118,7 +122,7 @@ export const useBuilderStore = create((set, get) => ({ return; } - set({ isSubmitting: true, error: undefined }); + set({ isSubmitting: true, error: undefined, structureTaskId: null }); try { const payload = { name: form.siteName || `Site Blueprint (${form.industry || 'New'})`, @@ -143,11 +147,17 @@ export const useBuilderStore = create((set, get) => ({ metadata: { targetAudience: form.targetAudience }, }); + if (generation?.task_id) { + set({ structureTaskId: generation.task_id }); + } + if (generation?.structure) { useSiteDefinitionStore.getState().setStructure(generation.structure); } - await get().refreshPages(blueprint.id); + if (!generation?.task_id) { + await get().refreshPages(blueprint.id); + } } catch (error) { set({ error: error instanceof Error ? error.message : 'Unexpected error' }); } finally {