@actuate-media/cms-admin 0.4.0 → 0.7.0

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.
Files changed (212) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +35 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  6. package/dist/components/Breadcrumbs.js +1 -0
  7. package/dist/components/Breadcrumbs.js.map +1 -1
  8. package/dist/components/ErrorBoundary.js +1 -1
  9. package/dist/components/ErrorBoundary.js.map +1 -1
  10. package/dist/hooks/useBuilderState.d.ts +49 -0
  11. package/dist/hooks/useBuilderState.d.ts.map +1 -0
  12. package/dist/hooks/useBuilderState.js +238 -0
  13. package/dist/hooks/useBuilderState.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/layout/Sidebar.d.ts.map +1 -1
  19. package/dist/layout/Sidebar.js +2 -2
  20. package/dist/layout/Sidebar.js.map +1 -1
  21. package/dist/views/ForgotPassword.d.ts +5 -0
  22. package/dist/views/ForgotPassword.d.ts.map +1 -0
  23. package/dist/views/ForgotPassword.js +41 -0
  24. package/dist/views/ForgotPassword.js.map +1 -0
  25. package/dist/views/ResetPassword.d.ts +6 -0
  26. package/dist/views/ResetPassword.d.ts.map +1 -0
  27. package/dist/views/ResetPassword.js +46 -0
  28. package/dist/views/ResetPassword.js.map +1 -0
  29. package/dist/views/ScriptTagEditor.d.ts +6 -0
  30. package/dist/views/ScriptTagEditor.d.ts.map +1 -0
  31. package/dist/views/ScriptTagEditor.js +109 -0
  32. package/dist/views/ScriptTagEditor.js.map +1 -0
  33. package/dist/views/ScriptTags.d.ts +5 -0
  34. package/dist/views/ScriptTags.d.ts.map +1 -0
  35. package/dist/views/ScriptTags.js +54 -0
  36. package/dist/views/ScriptTags.js.map +1 -0
  37. package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
  38. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  39. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  40. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  41. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  42. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  43. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  44. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  45. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  46. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  47. package/dist/views/page-builder/BlockEditor.js +67 -0
  48. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  49. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  50. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  51. package/dist/views/page-builder/BlockPicker.js +102 -0
  52. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  53. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  54. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  55. package/dist/views/page-builder/BottomBar.js +13 -0
  56. package/dist/views/page-builder/BottomBar.js.map +1 -0
  57. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  58. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  59. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  60. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  61. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  62. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  63. package/dist/views/page-builder/ContextPanel.js +40 -0
  64. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  65. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  66. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  67. package/dist/views/page-builder/DesignScore.js +93 -0
  68. package/dist/views/page-builder/DesignScore.js.map +1 -0
  69. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  70. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  71. package/dist/views/page-builder/NodeSettings.js +80 -0
  72. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  73. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  74. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  75. package/dist/views/page-builder/PageBuilder.js +126 -0
  76. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  77. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  78. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  79. package/dist/views/page-builder/PageSettings.js +27 -0
  80. package/dist/views/page-builder/PageSettings.js.map +1 -0
  81. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  82. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  83. package/dist/views/page-builder/SEOPanel.js +105 -0
  84. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  85. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  86. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  87. package/dist/views/page-builder/SavedSections.js +145 -0
  88. package/dist/views/page-builder/SavedSections.js.map +1 -0
  89. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  90. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  91. package/dist/views/page-builder/TemplatePicker.js +68 -0
  92. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  93. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  94. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  95. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  96. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  97. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  98. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  99. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  100. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  101. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  102. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  103. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  104. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  105. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  106. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  107. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  108. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  109. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  110. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  111. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  112. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  113. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  114. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  115. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  116. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  117. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  118. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  119. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  120. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  121. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  122. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  123. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  124. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  125. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  126. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  127. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  128. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  129. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  130. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  131. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  132. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  133. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  134. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  135. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  136. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  137. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  138. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  139. package/dist/views/page-builder/block-renderers/index.js +25 -0
  140. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  141. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  142. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  143. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  144. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  145. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  146. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  147. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  148. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  149. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  150. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  151. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  152. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  153. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  154. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  155. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  156. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  157. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  158. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  159. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  160. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  161. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  162. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  163. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  164. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  165. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  166. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  167. package/dist/views/page-builder/canvas/index.js +2 -0
  168. package/dist/views/page-builder/canvas/index.js.map +1 -0
  169. package/package.json +7 -4
  170. package/src/AdminRoot.tsx +41 -0
  171. package/src/components/Breadcrumbs.tsx +1 -0
  172. package/src/components/ErrorBoundary.tsx +3 -3
  173. package/src/hooks/useBuilderState.ts +328 -0
  174. package/src/index.ts +8 -0
  175. package/src/layout/Sidebar.tsx +7 -0
  176. package/src/views/ForgotPassword.tsx +136 -0
  177. package/src/views/ResetPassword.tsx +192 -0
  178. package/src/views/ScriptTagEditor.tsx +361 -0
  179. package/src/views/ScriptTags.tsx +174 -0
  180. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  181. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  182. package/src/views/page-builder/BlockEditor.tsx +352 -0
  183. package/src/views/page-builder/BlockPicker.tsx +338 -0
  184. package/src/views/page-builder/BottomBar.tsx +64 -0
  185. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  186. package/src/views/page-builder/ContextPanel.tsx +145 -0
  187. package/src/views/page-builder/DesignScore.tsx +258 -0
  188. package/src/views/page-builder/NodeSettings.tsx +515 -0
  189. package/src/views/page-builder/PageBuilder.tsx +288 -0
  190. package/src/views/page-builder/PageSettings.tsx +161 -0
  191. package/src/views/page-builder/SEOPanel.tsx +485 -0
  192. package/src/views/page-builder/SavedSections.tsx +486 -0
  193. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  194. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  195. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  196. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  197. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  198. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  199. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  200. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  201. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  202. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  203. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  204. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  205. package/src/views/page-builder/block-renderers/index.ts +34 -0
  206. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  207. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  208. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  209. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  210. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  211. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  212. 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 &amp; Apply
567
+ </button>
568
+ </>
569
+ )}
570
+ </div>
571
+ </div>
572
+ </div>
573
+ );
574
+ }