@actuate-media/cms-admin 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +17 -0
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts +49 -0
- package/dist/hooks/useBuilderState.d.ts.map +1 -0
- package/dist/hooks/useBuilderState.js +238 -0
- package/dist/hooks/useBuilderState.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +2 -2
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/FormSubmissions.js +11 -11
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -8
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts.js +1 -1
- package/dist/views/Posts.js.map +1 -1
- package/dist/views/Redirects.js +2 -2
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/SEO.js +3 -3
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/Users.js +3 -3
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.js +40 -0
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.js +170 -0
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
- package/dist/views/page-builder/BlockEditor.d.ts +11 -0
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
- package/dist/views/page-builder/BlockEditor.js +67 -0
- package/dist/views/page-builder/BlockEditor.js.map +1 -0
- package/dist/views/page-builder/BlockPicker.d.ts +7 -0
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
- package/dist/views/page-builder/BlockPicker.js +102 -0
- package/dist/views/page-builder/BlockPicker.js.map +1 -0
- package/dist/views/page-builder/BottomBar.d.ts +9 -0
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
- package/dist/views/page-builder/BottomBar.js +13 -0
- package/dist/views/page-builder/BottomBar.js.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.js +18 -0
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
- package/dist/views/page-builder/ContextPanel.d.ts +20 -0
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
- package/dist/views/page-builder/ContextPanel.js +40 -0
- package/dist/views/page-builder/ContextPanel.js.map +1 -0
- package/dist/views/page-builder/DesignScore.d.ts +6 -0
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
- package/dist/views/page-builder/DesignScore.js +93 -0
- package/dist/views/page-builder/DesignScore.js.map +1 -0
- package/dist/views/page-builder/NodeSettings.d.ts +12 -0
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
- package/dist/views/page-builder/NodeSettings.js +80 -0
- package/dist/views/page-builder/NodeSettings.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts +8 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
- package/dist/views/page-builder/PageBuilder.js +126 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -0
- package/dist/views/page-builder/PageSettings.d.ts +7 -0
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
- package/dist/views/page-builder/PageSettings.js +27 -0
- package/dist/views/page-builder/PageSettings.js.map +1 -0
- package/dist/views/page-builder/PageTemplates.d.ts +5 -0
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -0
- package/dist/views/page-builder/PageTemplates.js +13 -0
- package/dist/views/page-builder/PageTemplates.js.map +1 -0
- package/dist/views/page-builder/SEOPanel.d.ts +10 -0
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
- package/dist/views/page-builder/SEOPanel.js +105 -0
- package/dist/views/page-builder/SEOPanel.js.map +1 -0
- package/dist/views/page-builder/SavedSections.d.ts +6 -0
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
- package/dist/views/page-builder/SavedSections.js +145 -0
- package/dist/views/page-builder/SavedSections.js.map +1 -0
- package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
- package/dist/views/page-builder/TemplatePicker.js +68 -0
- package/dist/views/page-builder/TemplatePicker.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/index.js +25 -0
- package/dist/views/page-builder/block-renderers/index.js.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/index.d.ts +3 -0
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/index.js +2 -0
- package/dist/views/page-builder/canvas/index.js.map +1 -0
- package/package.json +3 -2
- package/src/AdminRoot.tsx +21 -0
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/hooks/useBuilderState.ts +328 -0
- package/src/index.ts +4 -0
- package/src/layout/Sidebar.tsx +5 -0
- package/src/views/FormSubmissions.tsx +12 -12
- package/src/views/Forms.tsx +1 -1
- package/src/views/MediaBrowser.tsx +46 -15
- package/src/views/Posts.tsx +1 -1
- package/src/views/Redirects.tsx +2 -2
- package/src/views/SEO.tsx +3 -3
- package/src/views/Users.tsx +3 -3
- package/src/views/page-builder/AIBlockAssist.tsx +68 -0
- package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
- package/src/views/page-builder/BlockEditor.tsx +352 -0
- package/src/views/page-builder/BlockPicker.tsx +338 -0
- package/src/views/page-builder/BottomBar.tsx +64 -0
- package/src/views/page-builder/BuilderToolbar.tsx +218 -0
- package/src/views/page-builder/ContextPanel.tsx +145 -0
- package/src/views/page-builder/DesignScore.tsx +258 -0
- package/src/views/page-builder/NodeSettings.tsx +515 -0
- package/src/views/page-builder/PageBuilder.tsx +288 -0
- package/src/views/page-builder/PageSettings.tsx +161 -0
- package/src/views/page-builder/PageTemplates.tsx +105 -0
- package/src/views/page-builder/SEOPanel.tsx +485 -0
- package/src/views/page-builder/SavedSections.tsx +486 -0
- package/src/views/page-builder/TemplatePicker.tsx +201 -0
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
- package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
- package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
- package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
- package/src/views/page-builder/block-renderers/index.ts +34 -0
- package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
- package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
- package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
- package/src/views/page-builder/canvas/index.ts +2 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Sparkles,
|
|
6
|
+
X,
|
|
7
|
+
Loader2,
|
|
8
|
+
Check,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
ChevronUp,
|
|
11
|
+
AlertTriangle,
|
|
12
|
+
Wand2,
|
|
13
|
+
FileText,
|
|
14
|
+
Search,
|
|
15
|
+
Accessibility,
|
|
16
|
+
ArrowRight,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { toast } from 'sonner';
|
|
19
|
+
import { cmsApi } from '../../lib/api.js';
|
|
20
|
+
import type { PageNode, GenerationStep } from '@actuate-media/cms-core';
|
|
21
|
+
|
|
22
|
+
export interface AIGenerateDialogProps {
|
|
23
|
+
open: boolean;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
onAccept: (tree: PageNode) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface StepInfo {
|
|
29
|
+
key: GenerationStep;
|
|
30
|
+
label: string;
|
|
31
|
+
icon: React.ReactNode;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ALL_STEPS: StepInfo[] = [
|
|
36
|
+
{
|
|
37
|
+
key: 'structure',
|
|
38
|
+
label: 'Structure',
|
|
39
|
+
icon: <Wand2 size={14} />,
|
|
40
|
+
description: 'Generate page layout and block placement',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'content',
|
|
44
|
+
label: 'Content',
|
|
45
|
+
icon: <FileText size={14} />,
|
|
46
|
+
description: 'Write compelling copy for every block',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'seo',
|
|
50
|
+
label: 'SEO',
|
|
51
|
+
icon: <Search size={14} />,
|
|
52
|
+
description: 'Optimize headings, meta tags, and keywords',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'accessibility',
|
|
56
|
+
label: 'Accessibility',
|
|
57
|
+
icon: <Accessibility size={14} />,
|
|
58
|
+
description: 'Fix heading hierarchy, alt text, and ARIA',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
type Tone = 'professional' | 'casual' | 'friendly' | 'technical' | 'luxury';
|
|
63
|
+
|
|
64
|
+
const TONES: { value: Tone; label: string }[] = [
|
|
65
|
+
{ value: 'professional', label: 'Professional' },
|
|
66
|
+
{ value: 'casual', label: 'Casual' },
|
|
67
|
+
{ value: 'friendly', label: 'Friendly' },
|
|
68
|
+
{ value: 'technical', label: 'Technical' },
|
|
69
|
+
{ value: 'luxury', label: 'Luxury' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
type Phase = 'prompt' | 'generating' | 'review';
|
|
73
|
+
|
|
74
|
+
interface GenerationStepResult {
|
|
75
|
+
step: string;
|
|
76
|
+
metadata: {
|
|
77
|
+
tokensUsed: number;
|
|
78
|
+
durationMs: number;
|
|
79
|
+
changes: string[];
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface GenerationResultData {
|
|
84
|
+
tree: PageNode;
|
|
85
|
+
steps: GenerationStepResult[];
|
|
86
|
+
totalTokensUsed: number;
|
|
87
|
+
totalDurationMs: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function AIGenerateDialog({ open, onClose, onAccept }: AIGenerateDialogProps) {
|
|
91
|
+
const [phase, setPhase] = useState<Phase>('prompt');
|
|
92
|
+
const [prompt, setPrompt] = useState('');
|
|
93
|
+
const [tone, setTone] = useState<Tone>('professional');
|
|
94
|
+
const [enabledSteps, setEnabledSteps] = useState<Set<GenerationStep>>(
|
|
95
|
+
new Set(['structure', 'content', 'seo', 'accessibility']),
|
|
96
|
+
);
|
|
97
|
+
const [showContext, setShowContext] = useState(false);
|
|
98
|
+
const [businessName, setBusiness] = useState('');
|
|
99
|
+
const [industry, setIndustry] = useState('');
|
|
100
|
+
const [phone, setPhone] = useState('');
|
|
101
|
+
const [address, setAddress] = useState('');
|
|
102
|
+
const [services, setServices] = useState('');
|
|
103
|
+
|
|
104
|
+
const [currentStep, setCurrentStep] = useState<string>('');
|
|
105
|
+
const [completedSteps, setCompletedSteps] = useState<string[]>([]);
|
|
106
|
+
const [stepMessage, setStepMessage] = useState('');
|
|
107
|
+
|
|
108
|
+
const [result, setResult] = useState<GenerationResultData | null>(null);
|
|
109
|
+
const [error, setError] = useState<string | null>(null);
|
|
110
|
+
const [expandedStep, setExpandedStep] = useState<string | null>(null);
|
|
111
|
+
|
|
112
|
+
const toggleStep = (step: GenerationStep) => {
|
|
113
|
+
const next = new Set(enabledSteps);
|
|
114
|
+
if (step === 'structure') return;
|
|
115
|
+
if (next.has(step)) {
|
|
116
|
+
next.delete(step);
|
|
117
|
+
} else {
|
|
118
|
+
next.add(step);
|
|
119
|
+
}
|
|
120
|
+
setEnabledSteps(next);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resetState = useCallback(() => {
|
|
124
|
+
setPhase('prompt');
|
|
125
|
+
setPrompt('');
|
|
126
|
+
setTone('professional');
|
|
127
|
+
setEnabledSteps(new Set(['structure', 'content', 'seo', 'accessibility']));
|
|
128
|
+
setShowContext(false);
|
|
129
|
+
setBusiness('');
|
|
130
|
+
setIndustry('');
|
|
131
|
+
setPhone('');
|
|
132
|
+
setAddress('');
|
|
133
|
+
setServices('');
|
|
134
|
+
setCurrentStep('');
|
|
135
|
+
setCompletedSteps([]);
|
|
136
|
+
setStepMessage('');
|
|
137
|
+
setResult(null);
|
|
138
|
+
setError(null);
|
|
139
|
+
setExpandedStep(null);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const handleClose = useCallback(() => {
|
|
143
|
+
resetState();
|
|
144
|
+
onClose();
|
|
145
|
+
}, [resetState, onClose]);
|
|
146
|
+
|
|
147
|
+
const handleGenerate = useCallback(async () => {
|
|
148
|
+
if (!prompt.trim()) {
|
|
149
|
+
toast.error('Please describe the page you want to create');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setPhase('generating');
|
|
154
|
+
setError(null);
|
|
155
|
+
|
|
156
|
+
const steps = ALL_STEPS.filter((s) => enabledSteps.has(s.key)).map((s) => s.key);
|
|
157
|
+
|
|
158
|
+
setCurrentStep(steps[0]!);
|
|
159
|
+
setStepMessage('Starting generation...');
|
|
160
|
+
|
|
161
|
+
const context: Record<string, unknown> = {};
|
|
162
|
+
if (businessName) context.businessName = businessName;
|
|
163
|
+
if (industry) context.industry = industry;
|
|
164
|
+
if (phone) context.phone = phone;
|
|
165
|
+
if (address) context.address = address;
|
|
166
|
+
if (services) context.services = services.split(',').map((s) => s.trim()).filter(Boolean);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const res = await cmsApi<GenerationResultData>('/page-builder/generate', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
prompt: prompt.trim(),
|
|
173
|
+
steps,
|
|
174
|
+
tone,
|
|
175
|
+
context: Object.keys(context).length > 0 ? context : undefined,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (res.error) {
|
|
180
|
+
setError(res.error);
|
|
181
|
+
setPhase('prompt');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (res.data) {
|
|
186
|
+
setResult(res.data);
|
|
187
|
+
setCompletedSteps(res.data.steps.map((s) => s.step));
|
|
188
|
+
setPhase('review');
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
setError(err instanceof Error ? err.message : 'Generation failed');
|
|
192
|
+
setPhase('prompt');
|
|
193
|
+
}
|
|
194
|
+
}, [prompt, tone, enabledSteps, businessName, industry, phone, address, services]);
|
|
195
|
+
|
|
196
|
+
const handleAccept = useCallback(() => {
|
|
197
|
+
if (result?.tree) {
|
|
198
|
+
onAccept(result.tree);
|
|
199
|
+
toast.success('AI-generated page applied');
|
|
200
|
+
handleClose();
|
|
201
|
+
}
|
|
202
|
+
}, [result, onAccept, handleClose]);
|
|
203
|
+
|
|
204
|
+
if (!open) return null;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
208
|
+
<div className="absolute inset-0 bg-black/50" onClick={handleClose} />
|
|
209
|
+
<div className="relative bg-card rounded-xl shadow-2xl border border-border w-full max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
|
210
|
+
{/* Header */}
|
|
211
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
212
|
+
<div className="flex items-center gap-2.5">
|
|
213
|
+
<div className="p-1.5 rounded-lg bg-primary/10">
|
|
214
|
+
<Sparkles size={18} className="text-primary" />
|
|
215
|
+
</div>
|
|
216
|
+
<div>
|
|
217
|
+
<h2 className="text-base font-medium text-foreground">
|
|
218
|
+
AI Page Generator
|
|
219
|
+
</h2>
|
|
220
|
+
<p className="text-xs text-muted-foreground">
|
|
221
|
+
Describe your page and let AI build it
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<button
|
|
226
|
+
onClick={handleClose}
|
|
227
|
+
className="p-1.5 rounded-md hover:bg-muted transition-colors"
|
|
228
|
+
aria-label="Close"
|
|
229
|
+
>
|
|
230
|
+
<X size={16} className="text-muted-foreground" />
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Content */}
|
|
235
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
236
|
+
{phase === 'prompt' && (
|
|
237
|
+
<div className="space-y-5">
|
|
238
|
+
{error && (
|
|
239
|
+
<div className="flex items-start gap-2.5 p-3 rounded-lg bg-destructive/5 border border-destructive/20">
|
|
240
|
+
<AlertTriangle size={16} className="text-destructive shrink-0 mt-0.5" />
|
|
241
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Prompt */}
|
|
246
|
+
<div>
|
|
247
|
+
<label className="block text-sm font-medium text-foreground mb-1.5">
|
|
248
|
+
Describe your page
|
|
249
|
+
</label>
|
|
250
|
+
<textarea
|
|
251
|
+
value={prompt}
|
|
252
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
253
|
+
placeholder="e.g. A landing page for a plumbing company with a hero section, service cards, testimonials, and a contact form"
|
|
254
|
+
rows={4}
|
|
255
|
+
className="w-full px-3 py-2.5 text-sm bg-background border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground placeholder:text-muted-foreground"
|
|
256
|
+
autoFocus
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Tone */}
|
|
261
|
+
<div>
|
|
262
|
+
<label className="block text-sm font-medium text-foreground mb-1.5">
|
|
263
|
+
Tone
|
|
264
|
+
</label>
|
|
265
|
+
<div className="flex flex-wrap gap-2">
|
|
266
|
+
{TONES.map((t) => (
|
|
267
|
+
<button
|
|
268
|
+
key={t.value}
|
|
269
|
+
onClick={() => setTone(t.value)}
|
|
270
|
+
className={`px-3 py-1.5 text-xs rounded-full border transition-colors ${
|
|
271
|
+
tone === t.value
|
|
272
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
273
|
+
: 'border-border bg-background text-muted-foreground hover:border-primary/40'
|
|
274
|
+
}`}
|
|
275
|
+
>
|
|
276
|
+
{t.label}
|
|
277
|
+
</button>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Steps */}
|
|
283
|
+
<div>
|
|
284
|
+
<label className="block text-sm font-medium text-foreground mb-1.5">
|
|
285
|
+
Generation steps
|
|
286
|
+
</label>
|
|
287
|
+
<div className="space-y-1.5">
|
|
288
|
+
{ALL_STEPS.map((step) => (
|
|
289
|
+
<label
|
|
290
|
+
key={step.key}
|
|
291
|
+
className={`flex items-center gap-3 p-2.5 rounded-lg border transition-colors cursor-pointer ${
|
|
292
|
+
enabledSteps.has(step.key)
|
|
293
|
+
? 'border-primary/30 bg-primary/5'
|
|
294
|
+
: 'border-border bg-background'
|
|
295
|
+
} ${step.key === 'structure' ? 'opacity-70 cursor-not-allowed' : 'hover:border-primary/20'}`}
|
|
296
|
+
>
|
|
297
|
+
<input
|
|
298
|
+
type="checkbox"
|
|
299
|
+
checked={enabledSteps.has(step.key)}
|
|
300
|
+
onChange={() => toggleStep(step.key)}
|
|
301
|
+
disabled={step.key === 'structure'}
|
|
302
|
+
className="sr-only"
|
|
303
|
+
/>
|
|
304
|
+
<div
|
|
305
|
+
className={`w-5 h-5 rounded flex items-center justify-center shrink-0 ${
|
|
306
|
+
enabledSteps.has(step.key)
|
|
307
|
+
? 'bg-primary text-white'
|
|
308
|
+
: 'bg-muted border border-border'
|
|
309
|
+
}`}
|
|
310
|
+
>
|
|
311
|
+
{enabledSteps.has(step.key) && <Check size={12} />}
|
|
312
|
+
</div>
|
|
313
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
314
|
+
<span className="text-muted-foreground">{step.icon}</span>
|
|
315
|
+
<div>
|
|
316
|
+
<span className="text-sm text-foreground">{step.label}</span>
|
|
317
|
+
<span className="text-xs text-muted-foreground ml-2">
|
|
318
|
+
{step.description}
|
|
319
|
+
</span>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</label>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* Business context (collapsible) */}
|
|
328
|
+
<div>
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => setShowContext(!showContext)}
|
|
331
|
+
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
332
|
+
>
|
|
333
|
+
{showContext ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
334
|
+
Business context (optional)
|
|
335
|
+
</button>
|
|
336
|
+
{showContext && (
|
|
337
|
+
<div className="mt-3 space-y-3 p-4 bg-muted/50 rounded-lg border border-border">
|
|
338
|
+
<div className="grid grid-cols-2 gap-3">
|
|
339
|
+
<div>
|
|
340
|
+
<label className="block text-xs text-muted-foreground mb-1">Business name</label>
|
|
341
|
+
<input
|
|
342
|
+
type="text"
|
|
343
|
+
value={businessName}
|
|
344
|
+
onChange={(e) => setBusiness(e.target.value)}
|
|
345
|
+
className="w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground"
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
<div>
|
|
349
|
+
<label className="block text-xs text-muted-foreground mb-1">Industry</label>
|
|
350
|
+
<input
|
|
351
|
+
type="text"
|
|
352
|
+
value={industry}
|
|
353
|
+
onChange={(e) => setIndustry(e.target.value)}
|
|
354
|
+
className="w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground"
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
<div>
|
|
358
|
+
<label className="block text-xs text-muted-foreground mb-1">Phone</label>
|
|
359
|
+
<input
|
|
360
|
+
type="text"
|
|
361
|
+
value={phone}
|
|
362
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
363
|
+
className="w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground"
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
<div>
|
|
367
|
+
<label className="block text-xs text-muted-foreground mb-1">Address</label>
|
|
368
|
+
<input
|
|
369
|
+
type="text"
|
|
370
|
+
value={address}
|
|
371
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
372
|
+
className="w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground"
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div>
|
|
377
|
+
<label className="block text-xs text-muted-foreground mb-1">
|
|
378
|
+
Services (comma-separated)
|
|
379
|
+
</label>
|
|
380
|
+
<input
|
|
381
|
+
type="text"
|
|
382
|
+
value={services}
|
|
383
|
+
onChange={(e) => setServices(e.target.value)}
|
|
384
|
+
placeholder="e.g. Web design, SEO, Content marketing"
|
|
385
|
+
className="w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 text-foreground placeholder:text-muted-foreground"
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
{phase === 'generating' && (
|
|
395
|
+
<div className="py-8 space-y-6">
|
|
396
|
+
<div className="text-center">
|
|
397
|
+
<Loader2 size={32} className="animate-spin text-primary mx-auto mb-3" />
|
|
398
|
+
<p className="text-sm font-medium text-foreground">{stepMessage || 'Generating...'}</p>
|
|
399
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
400
|
+
This may take 15-30 seconds
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div className="space-y-2">
|
|
405
|
+
{ALL_STEPS.filter((s) => enabledSteps.has(s.key)).map((step) => {
|
|
406
|
+
const isComplete = completedSteps.includes(step.key);
|
|
407
|
+
const isCurrent = currentStep === step.key;
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div
|
|
411
|
+
key={step.key}
|
|
412
|
+
className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${
|
|
413
|
+
isComplete
|
|
414
|
+
? 'border-primary/30 bg-primary/5'
|
|
415
|
+
: isCurrent
|
|
416
|
+
? 'border-primary/50 bg-primary/10'
|
|
417
|
+
: 'border-border bg-muted/30'
|
|
418
|
+
}`}
|
|
419
|
+
>
|
|
420
|
+
<div className="w-6 h-6 rounded-full flex items-center justify-center shrink-0">
|
|
421
|
+
{isComplete ? (
|
|
422
|
+
<div className="w-6 h-6 rounded-full bg-primary flex items-center justify-center">
|
|
423
|
+
<Check size={12} className="text-white" />
|
|
424
|
+
</div>
|
|
425
|
+
) : isCurrent ? (
|
|
426
|
+
<Loader2 size={16} className="animate-spin text-primary" />
|
|
427
|
+
) : (
|
|
428
|
+
<div className="w-6 h-6 rounded-full bg-muted border border-border" />
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
<div className="flex items-center gap-2">
|
|
432
|
+
<span className="text-muted-foreground">{step.icon}</span>
|
|
433
|
+
<span className={`text-sm ${isCurrent || isComplete ? 'text-foreground' : 'text-muted-foreground'}`}>
|
|
434
|
+
{step.label}
|
|
435
|
+
</span>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{phase === 'review' && result && (
|
|
445
|
+
<div className="space-y-5">
|
|
446
|
+
{/* Summary */}
|
|
447
|
+
<div className="flex items-center gap-3 p-4 rounded-lg bg-primary/5 border border-primary/20">
|
|
448
|
+
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
|
449
|
+
<Sparkles size={18} className="text-primary" />
|
|
450
|
+
</div>
|
|
451
|
+
<div>
|
|
452
|
+
<p className="text-sm font-medium text-foreground">
|
|
453
|
+
Page generated successfully
|
|
454
|
+
</p>
|
|
455
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
456
|
+
{result.totalTokensUsed.toLocaleString()} tokens
|
|
457
|
+
{' \u00B7 '}
|
|
458
|
+
{(result.totalDurationMs / 1000).toFixed(1)}s
|
|
459
|
+
{' \u00B7 '}
|
|
460
|
+
{result.steps.length} step{result.steps.length !== 1 ? 's' : ''}
|
|
461
|
+
</p>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
{/* Step details */}
|
|
466
|
+
<div className="space-y-2">
|
|
467
|
+
<h3 className="text-sm font-medium text-foreground">Generation steps</h3>
|
|
468
|
+
{result.steps.map((step) => {
|
|
469
|
+
const info = ALL_STEPS.find((s) => s.key === step.step);
|
|
470
|
+
const isExpanded = expandedStep === step.step;
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<div key={step.step} className="rounded-lg border border-border overflow-hidden">
|
|
474
|
+
<button
|
|
475
|
+
onClick={() => setExpandedStep(isExpanded ? null : step.step)}
|
|
476
|
+
className="w-full flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors text-left"
|
|
477
|
+
>
|
|
478
|
+
<div className="w-6 h-6 rounded-full bg-primary flex items-center justify-center shrink-0">
|
|
479
|
+
<Check size={12} className="text-white" />
|
|
480
|
+
</div>
|
|
481
|
+
<span className="text-muted-foreground">{info?.icon}</span>
|
|
482
|
+
<span className="text-sm text-foreground flex-1">{info?.label}</span>
|
|
483
|
+
<span className="text-xs text-muted-foreground">
|
|
484
|
+
{(step.metadata.durationMs / 1000).toFixed(1)}s
|
|
485
|
+
</span>
|
|
486
|
+
{isExpanded ? (
|
|
487
|
+
<ChevronUp size={14} className="text-muted-foreground" />
|
|
488
|
+
) : (
|
|
489
|
+
<ChevronDown size={14} className="text-muted-foreground" />
|
|
490
|
+
)}
|
|
491
|
+
</button>
|
|
492
|
+
{isExpanded && (
|
|
493
|
+
<div className="px-3 pb-3 border-t border-border pt-2">
|
|
494
|
+
<ul className="space-y-1">
|
|
495
|
+
{step.metadata.changes.map((change, i) => (
|
|
496
|
+
<li key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
|
|
497
|
+
<ArrowRight size={10} className="shrink-0 mt-0.5" />
|
|
498
|
+
{change}
|
|
499
|
+
</li>
|
|
500
|
+
))}
|
|
501
|
+
</ul>
|
|
502
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
503
|
+
Tokens: {step.metadata.tokensUsed.toLocaleString()}
|
|
504
|
+
</p>
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
})}
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<p className="text-xs text-muted-foreground">
|
|
513
|
+
Review the generated page in the canvas after accepting.
|
|
514
|
+
You can always undo to revert.
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
{/* Footer */}
|
|
521
|
+
<div className="px-6 py-4 border-t border-border flex items-center justify-end gap-3">
|
|
522
|
+
{phase === 'prompt' && (
|
|
523
|
+
<>
|
|
524
|
+
<button
|
|
525
|
+
onClick={handleClose}
|
|
526
|
+
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
527
|
+
>
|
|
528
|
+
Cancel
|
|
529
|
+
</button>
|
|
530
|
+
<button
|
|
531
|
+
onClick={handleGenerate}
|
|
532
|
+
disabled={!prompt.trim()}
|
|
533
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
534
|
+
>
|
|
535
|
+
<Sparkles size={14} />
|
|
536
|
+
Generate Page
|
|
537
|
+
</button>
|
|
538
|
+
</>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{phase === 'generating' && (
|
|
542
|
+
<button
|
|
543
|
+
onClick={handleClose}
|
|
544
|
+
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
545
|
+
>
|
|
546
|
+
Cancel
|
|
547
|
+
</button>
|
|
548
|
+
)}
|
|
549
|
+
|
|
550
|
+
{phase === 'review' && (
|
|
551
|
+
<>
|
|
552
|
+
<button
|
|
553
|
+
onClick={() => {
|
|
554
|
+
setPhase('prompt');
|
|
555
|
+
setResult(null);
|
|
556
|
+
}}
|
|
557
|
+
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
558
|
+
>
|
|
559
|
+
Regenerate
|
|
560
|
+
</button>
|
|
561
|
+
<button
|
|
562
|
+
onClick={handleAccept}
|
|
563
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
|
|
564
|
+
>
|
|
565
|
+
<Check size={14} />
|
|
566
|
+
Accept & Apply
|
|
567
|
+
</button>
|
|
568
|
+
</>
|
|
569
|
+
)}
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|