reaminig 5-t-9

This commit is contained in:
alorig
2025-11-18 06:36:56 +05:00
parent 9facd12082
commit 68a98208b1
31 changed files with 5210 additions and 153 deletions

View File

@@ -56,6 +56,27 @@ export const builderApi = {
const res = await client.get(`/pages/?site_blueprint=${blueprintId}`);
return Array.isArray(res.data?.results) ? res.data.results : res.data;
},
async generateAllPages(
blueprintId: number,
options?: { pageIds?: number[]; force?: boolean },
): Promise<{ success: boolean; pages_queued: number; task_ids: number[]; celery_task_id?: string }> {
const res = await client.post(`/blueprints/${blueprintId}/generate_all_pages/`, {
page_ids: options?.pageIds,
force: options?.force || false,
});
return res.data?.data || res.data;
},
async createTasksForPages(
blueprintId: number,
pageIds?: number[],
): Promise<{ tasks: unknown[]; count: number }> {
const res = await client.post(`/blueprints/${blueprintId}/create_tasks/`, {
page_ids: pageIds,
});
return res.data?.data || res.data;
},
};

View File

@@ -0,0 +1,118 @@
.progress-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.progress-modal {
background: white;
border-radius: 8px;
padding: 24px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.progress-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.progress-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.progress-modal-close {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: color 0.2s;
}
.progress-modal-close:hover {
color: #000;
}
.progress-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.progress-modal-message {
margin: 0;
color: #666;
font-size: 14px;
}
.progress-modal-bar {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-modal-bar-track {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-modal-bar-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.progress-modal-bar-text {
font-size: 12px;
color: #666;
text-align: center;
}
.progress-modal-spinner {
display: flex;
justify-content: center;
padding: 16px;
}
.progress-modal-spinner .spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.progress-modal-task-id {
margin: 0;
font-size: 12px;
color: #999;
text-align: center;
}
.progress-modal-task-id code {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}

View File

@@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { X, Loader2 } from 'lucide-react';
import './ProgressModal.css';
interface ProgressModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
message?: string;
progress?: {
current: number;
total: number;
};
taskId?: string;
}
export function ProgressModal({ isOpen, onClose, title, message, progress, taskId }: ProgressModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
const progressPercent = progress ? Math.round((progress.current / progress.total) * 100) : 0;
return (
<div className="progress-modal-overlay" onClick={onClose}>
<div className="progress-modal" onClick={(e) => e.stopPropagation()}>
<div className="progress-modal-header">
<h3>{title}</h3>
<button type="button" className="progress-modal-close" onClick={onClose} aria-label="Close">
<X size={20} />
</button>
</div>
<div className="progress-modal-content">
{message && <p className="progress-modal-message">{message}</p>}
{progress && (
<div className="progress-modal-bar">
<div className="progress-modal-bar-track">
<div
className="progress-modal-bar-fill"
style={{ width: `${progressPercent}%` }}
/>
</div>
<span className="progress-modal-bar-text">
{progress.current} of {progress.total} pages
</span>
</div>
)}
{!progress && (
<div className="progress-modal-spinner">
<Loader2 className="spin" size={24} />
</div>
)}
{taskId && (
<p className="progress-modal-task-id">
Task ID: <code>{taskId}</code>
</p>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,13 +1,17 @@
import { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { Loader2, Play } from 'lucide-react';
import { builderApi } from '../../api/builder.api';
import type { SiteBlueprint } from '../../types/siteBuilder';
import { Card } from '../../components/common/Card';
import { useBuilderStore } from '../../state/builderStore';
import { ProgressModal } from '../../components/common/ProgressModal';
export function SiteDashboard() {
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>();
const { generateAllPages, isGenerating, generationProgress } = useBuilderStore();
const [showProgress, setShowProgress] = useState(false);
useEffect(() => {
const fetchData = async () => {
@@ -24,32 +28,73 @@ export function SiteDashboard() {
fetchData();
}, []);
const handleGenerateAll = async (blueprintId: number) => {
setShowProgress(true);
try {
await generateAllPages(blueprintId);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate pages');
}
};
return (
<Card title="Blueprint history" description="Every generated structure is stored and can be reopened.">
{loading && (
<div className="sb-loading">
<Loader2 className="spin" size={18} /> Loading blueprints
</div>
)}
<>
<Card title="Blueprint history" description="Every generated structure is stored and can be reopened.">
{loading && (
<div className="sb-loading">
<Loader2 className="spin" size={18} /> Loading blueprints
</div>
)}
{error && <p className="sb-error">{error}</p>}
{error && <p className="sb-error">{error}</p>}
{!loading && !blueprints.length && (
<p className="sb-muted">You havent generated any sites yet. Launch the wizard to create your first one.</p>
)}
{!loading && !blueprints.length && (
<p className="sb-muted">You haven't generated any sites yet. Launch the wizard to create your first one.</p>
)}
<ul className="sb-blueprint-list">
{blueprints.map((bp) => (
<li key={bp.id}>
<div>
<strong>{bp.name}</strong>
<span>{bp.description}</span>
</div>
<span className={`status-dot status-${bp.status}`}>{bp.status}</span>
</li>
))}
</ul>
</Card>
<ul className="sb-blueprint-list">
{blueprints.map((bp) => (
<li key={bp.id}>
<div>
<strong>{bp.name}</strong>
<span>{bp.description}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{bp.status === 'ready' && (
<button
type="button"
onClick={() => handleGenerateAll(bp.id)}
disabled={isGenerating}
className="sb-button sb-button--primary"
style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 12px' }}
>
<Play size={14} />
Generate All Pages
</button>
)}
<span className={`status-dot status-${bp.status}`}>{bp.status}</span>
</div>
</li>
))}
</ul>
</Card>
<ProgressModal
isOpen={showProgress}
onClose={() => setShowProgress(false)}
title="Generating Pages"
message={isGenerating ? 'Generating content for all pages...' : 'Generation completed!'}
progress={
generationProgress
? {
current: generationProgress.pagesQueued,
total: generationProgress.pagesQueued,
}
: undefined
}
taskId={generationProgress?.celeryTaskId}
/>
</>
);
}

View File

@@ -8,6 +8,7 @@ import {
type StatItem,
} from '@shared';
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
import { useBuilderStore } from '../../state/builderStore';
import type { PageBlock, PageBlueprint, SiteStructure } from '../../types/siteBuilder';
type StructuredContent = Record<string, unknown> & {
@@ -20,6 +21,8 @@ type StructuredContent = Record<string, unknown> & {
export function PreviewCanvas() {
const { structure, pages, selectedSlug, selectPage } = useSiteDefinitionStore();
const { selectedPageIds, togglePageSelection, selectAllPages, clearPageSelection, activeBlueprint } =
useBuilderStore();
const page = useMemo(() => {
if (structure?.pages?.length) {
@@ -66,9 +69,68 @@ export function PreviewCanvas() {
</div>
);
// Only show page selection if we have actual PageBlueprint objects with IDs
const hasPageBlueprints = pages.length > 0 && pages.every((p) => p.id > 0);
const allSelected = hasPageBlueprints && pages.length > 0 && selectedPageIds.length === pages.length;
const someSelected = hasPageBlueprints && selectedPageIds.length > 0 && selectedPageIds.length < pages.length;
return (
<div className="preview-canvas">
<div className="preview-nav">
{hasPageBlueprints && activeBlueprint && (
<div className="preview-page-selection" style={{ marginBottom: '12px', padding: '8px', background: '#f5f5f5', borderRadius: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<input
type="checkbox"
checked={allSelected}
ref={(input) => {
if (input) input.indeterminate = someSelected;
}}
onChange={(e) => {
if (e.target.checked) {
selectAllPages();
} else {
clearPageSelection();
}
}}
style={{ cursor: 'pointer' }}
/>
<label style={{ cursor: 'pointer', fontSize: '14px', fontWeight: '500' }}>
Select pages for bulk generation ({selectedPageIds.length} selected)
</label>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{pages.map((p) => {
const isSelected = selectedPageIds.includes(p.id);
return (
<label
key={p.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
cursor: 'pointer',
fontSize: '12px',
padding: '4px 8px',
background: isSelected ? '#e3f2fd' : 'white',
border: `1px solid ${isSelected ? '#2196f3' : '#ddd'}`,
borderRadius: '4px',
}}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => togglePageSelection(p.id)}
style={{ cursor: 'pointer' }}
/>
<span>{p.title || p.slug.replace('-', ' ')}</span>
</label>
);
})}
</div>
</div>
)}
{navItems?.map((slug) => (
<button
key={slug}

View File

@@ -35,6 +35,13 @@ interface BuilderState {
error?: string;
activeBlueprint?: SiteBlueprint;
pages: PageBlueprint[];
selectedPageIds: number[];
isGenerating: boolean;
generationProgress?: {
pagesQueued: number;
taskIds: number[];
celeryTaskId?: string;
};
setField: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
updateStyle: (partial: Partial<StylePreferences>) => void;
addObjective: (value: string) => void;
@@ -45,6 +52,10 @@ interface BuilderState {
reset: () => void;
submitWizard: () => Promise<void>;
refreshPages: (blueprintId: number) => Promise<void>;
togglePageSelection: (pageId: number) => void;
selectAllPages: () => void;
clearPageSelection: () => void;
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
}
export const useBuilderStore = create<BuilderState>((set, get) => ({
@@ -52,6 +63,8 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
currentStep: 0,
isSubmitting: false,
pages: [],
selectedPageIds: [],
isGenerating: false,
setField: (key, value) =>
set((state) => ({
@@ -151,6 +164,54 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
set({ error: error instanceof Error ? error.message : 'Unable to load pages' });
}
},
togglePageSelection: (pageId: number) => {
set((state) => {
const isSelected = state.selectedPageIds.includes(pageId);
return {
selectedPageIds: isSelected
? state.selectedPageIds.filter((id) => id !== pageId)
: [...state.selectedPageIds, pageId],
};
});
},
selectAllPages: () => {
set((state) => ({
selectedPageIds: state.pages.map((p) => p.id),
}));
},
clearPageSelection: () => {
set({ selectedPageIds: [] });
},
generateAllPages: async (blueprintId: number, force = false) => {
const { selectedPageIds } = get();
set({ isGenerating: true, error: undefined, generationProgress: undefined });
try {
const result = await builderApi.generateAllPages(blueprintId, {
pageIds: selectedPageIds.length > 0 ? selectedPageIds : undefined,
force,
});
set({
generationProgress: {
pagesQueued: result.pages_queued,
taskIds: result.task_ids,
celeryTaskId: result.celery_task_id,
},
});
// Refresh pages to update their status
await get().refreshPages(blueprintId);
} catch (error) {
set({ error: error instanceof Error ? error.message : 'Failed to generate pages' });
} finally {
set({ isGenerating: false });
}
},
}));