@actuate-media/cms-admin 0.49.0 → 0.49.3
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/CHANGELOG.md +34 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +10 -11
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/modal.render.test.d.ts +2 -0
- package/dist/__tests__/components/modal.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/modal.render.test.js +37 -0
- package/dist/__tests__/components/modal.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/FolderTree.js +19 -19
- package/dist/components/FolderTree.js.map +1 -1
- package/dist/components/MediaPickerModal.d.ts +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +12 -11
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/SchedulePublishDialog.d.ts +1 -1
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -1
- package/dist/components/SchedulePublishDialog.js +6 -4
- package/dist/components/SchedulePublishDialog.js.map +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -1
- package/dist/components/SharePreviewLinkDialog.js +10 -9
- package/dist/components/SharePreviewLinkDialog.js.map +1 -1
- package/dist/components/VersionHistory.d.ts +1 -1
- package/dist/components/VersionHistory.d.ts.map +1 -1
- package/dist/components/VersionHistory.js +14 -14
- package/dist/components/VersionHistory.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +17 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +16 -15
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/fields/BlockBuilderField.d.ts.map +1 -1
- package/dist/fields/BlockBuilderField.js +6 -4
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +3 -3
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +3 -3
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/Redirects.js +7 -7
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +5 -5
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +23 -23
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +35 -30
- package/src/__tests__/components/modal.render.test.tsx +57 -0
- package/src/components/FolderTree.tsx +32 -32
- package/src/components/MediaPickerModal.tsx +143 -119
- package/src/components/SchedulePublishDialog.tsx +118 -93
- package/src/components/SharePreviewLinkDialog.tsx +115 -93
- package/src/components/VersionHistory.tsx +98 -82
- package/src/components/ui/Modal.tsx +48 -39
- package/src/fields/BlockBuilderField.tsx +26 -19
- package/src/views/MediaBrowser.tsx +128 -120
- package/src/views/Pages/PagesListView.tsx +123 -115
- package/src/views/Posts/PostsListView.tsx +114 -110
- package/src/views/Redirects.tsx +48 -44
- package/src/views/ScriptTagEditor.tsx +40 -40
- package/src/views/ScriptTags.tsx +34 -30
- package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback } from 'react'
|
|
4
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
5
|
import {
|
|
5
6
|
Sparkles,
|
|
6
7
|
X,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from 'lucide-react'
|
|
18
19
|
import { toast } from 'sonner'
|
|
19
20
|
import { cmsApi } from '../../lib/api.js'
|
|
21
|
+
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
20
22
|
import type { PageNode, GenerationStep } from '@actuate-media/cms-core'
|
|
21
23
|
|
|
22
24
|
export interface AIGenerateDialogProps {
|
|
@@ -205,377 +207,387 @@ export function AIGenerateDialog({ open, onClose, onAccept }: AIGenerateDialogPr
|
|
|
205
207
|
}
|
|
206
208
|
}, [result, onAccept, handleClose])
|
|
207
209
|
|
|
208
|
-
if (!open) return null
|
|
209
|
-
|
|
210
210
|
return (
|
|
211
|
-
<
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<div className="flex items-center
|
|
217
|
-
<div className="
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
211
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && handleClose()}>
|
|
212
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
213
|
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/50" />
|
|
214
|
+
<Dialog.Content className="bg-card border-border text-card-foreground fixed top-1/2 left-1/2 z-50 flex max-h-[85vh] w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border shadow-2xl focus:outline-none">
|
|
215
|
+
{/* Header */}
|
|
216
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
217
|
+
<div className="flex items-center gap-2.5">
|
|
218
|
+
<div className="bg-primary/10 rounded-lg p-1.5">
|
|
219
|
+
<Sparkles size={18} className="text-primary" aria-hidden />
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<Dialog.Title className="text-foreground text-base font-medium">
|
|
223
|
+
AI Page Generator
|
|
224
|
+
</Dialog.Title>
|
|
225
|
+
<Dialog.Description className="text-muted-foreground text-xs">
|
|
226
|
+
Describe your page and let AI build it
|
|
227
|
+
</Dialog.Description>
|
|
228
|
+
</div>
|
|
225
229
|
</div>
|
|
230
|
+
<Dialog.Close asChild>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
className="hover:bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
234
|
+
aria-label="Close AI page generator"
|
|
235
|
+
>
|
|
236
|
+
<X size={16} aria-hidden />
|
|
237
|
+
</button>
|
|
238
|
+
</Dialog.Close>
|
|
226
239
|
</div>
|
|
227
|
-
<button
|
|
228
|
-
onClick={handleClose}
|
|
229
|
-
className="hover:bg-muted rounded-md p-1.5 transition-colors"
|
|
230
|
-
aria-label="Close"
|
|
231
|
-
>
|
|
232
|
-
<X size={16} className="text-muted-foreground" />
|
|
233
|
-
</button>
|
|
234
|
-
</div>
|
|
235
|
-
|
|
236
|
-
{/* Content */}
|
|
237
|
-
<div className="flex-1 overflow-y-auto p-6">
|
|
238
|
-
{phase === 'prompt' && (
|
|
239
|
-
<div className="space-y-5">
|
|
240
|
-
{error && (
|
|
241
|
-
<div className="bg-destructive/5 border-destructive/20 flex items-start gap-2.5 rounded-lg border p-3">
|
|
242
|
-
<AlertTriangle size={16} className="text-destructive mt-0.5 shrink-0" />
|
|
243
|
-
<p className="text-destructive text-sm">{error}</p>
|
|
244
|
-
</div>
|
|
245
|
-
)}
|
|
246
240
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full resize-none rounded-lg border px-3 py-2.5 text-sm focus:ring-2 focus:outline-none"
|
|
258
|
-
autoFocus
|
|
259
|
-
/>
|
|
260
|
-
</div>
|
|
241
|
+
{/* Content */}
|
|
242
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
243
|
+
{phase === 'prompt' && (
|
|
244
|
+
<div className="space-y-5">
|
|
245
|
+
{error && (
|
|
246
|
+
<div className="bg-destructive/5 border-destructive/20 flex items-start gap-2.5 rounded-lg border p-3">
|
|
247
|
+
<AlertTriangle size={16} className="text-destructive mt-0.5 shrink-0" />
|
|
248
|
+
<p className="text-destructive text-sm">{error}</p>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
261
251
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
>
|
|
276
|
-
{t.label}
|
|
277
|
-
</button>
|
|
278
|
-
))}
|
|
252
|
+
{/* Prompt */}
|
|
253
|
+
<div>
|
|
254
|
+
<label className="text-foreground mb-1.5 block text-sm font-medium">
|
|
255
|
+
Describe your page
|
|
256
|
+
</label>
|
|
257
|
+
<textarea
|
|
258
|
+
value={prompt}
|
|
259
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
260
|
+
placeholder="e.g. A landing page for a plumbing company with a hero section, service cards, testimonials, and a contact form"
|
|
261
|
+
rows={4}
|
|
262
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full resize-none rounded-lg border px-3 py-2.5 text-sm focus:ring-2 focus:outline-none"
|
|
263
|
+
autoFocus
|
|
264
|
+
/>
|
|
279
265
|
</div>
|
|
280
|
-
</div>
|
|
281
266
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
: 'border-border bg-background'
|
|
295
|
-
} ${step.key === 'structure' ? 'cursor-not-allowed opacity-70' : '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={`flex h-5 w-5 shrink-0 items-center justify-center rounded ${
|
|
306
|
-
enabledSteps.has(step.key)
|
|
307
|
-
? 'bg-primary text-white'
|
|
308
|
-
: 'bg-muted border-border border'
|
|
267
|
+
{/* Tone */}
|
|
268
|
+
<div>
|
|
269
|
+
<label className="text-foreground mb-1.5 block text-sm font-medium">Tone</label>
|
|
270
|
+
<div className="flex flex-wrap gap-2">
|
|
271
|
+
{TONES.map((t) => (
|
|
272
|
+
<button
|
|
273
|
+
key={t.value}
|
|
274
|
+
onClick={() => setTone(t.value)}
|
|
275
|
+
className={`rounded-full border px-3 py-1.5 text-xs transition-colors ${
|
|
276
|
+
tone === t.value
|
|
277
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
278
|
+
: 'border-border bg-background text-muted-foreground hover:border-primary/40'
|
|
309
279
|
}`}
|
|
310
280
|
>
|
|
311
|
-
{
|
|
312
|
-
</
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
<div>
|
|
316
|
-
<span className="text-foreground text-sm">{step.label}</span>
|
|
317
|
-
<span className="text-muted-foreground ml-2 text-xs">
|
|
318
|
-
{step.description}
|
|
319
|
-
</span>
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
</label>
|
|
323
|
-
))}
|
|
281
|
+
{t.label}
|
|
282
|
+
</button>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
324
285
|
</div>
|
|
325
|
-
</div>
|
|
326
286
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
</label>
|
|
343
|
-
<input
|
|
344
|
-
type="text"
|
|
345
|
-
value={businessName}
|
|
346
|
-
onChange={(e) => setBusiness(e.target.value)}
|
|
347
|
-
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
348
|
-
/>
|
|
349
|
-
</div>
|
|
350
|
-
<div>
|
|
351
|
-
<label className="text-muted-foreground mb-1 block text-xs">Industry</label>
|
|
352
|
-
<input
|
|
353
|
-
type="text"
|
|
354
|
-
value={industry}
|
|
355
|
-
onChange={(e) => setIndustry(e.target.value)}
|
|
356
|
-
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
357
|
-
/>
|
|
358
|
-
</div>
|
|
359
|
-
<div>
|
|
360
|
-
<label className="text-muted-foreground mb-1 block text-xs">Phone</label>
|
|
287
|
+
{/* Steps */}
|
|
288
|
+
<div>
|
|
289
|
+
<label className="text-foreground mb-1.5 block text-sm font-medium">
|
|
290
|
+
Generation steps
|
|
291
|
+
</label>
|
|
292
|
+
<div className="space-y-1.5">
|
|
293
|
+
{ALL_STEPS.map((step) => (
|
|
294
|
+
<label
|
|
295
|
+
key={step.key}
|
|
296
|
+
className={`flex cursor-pointer items-center gap-3 rounded-lg border p-2.5 transition-colors ${
|
|
297
|
+
enabledSteps.has(step.key)
|
|
298
|
+
? 'border-primary/30 bg-primary/5'
|
|
299
|
+
: 'border-border bg-background'
|
|
300
|
+
} ${step.key === 'structure' ? 'cursor-not-allowed opacity-70' : 'hover:border-primary/20'}`}
|
|
301
|
+
>
|
|
361
302
|
<input
|
|
362
|
-
type="
|
|
363
|
-
|
|
364
|
-
onChange={(
|
|
365
|
-
|
|
303
|
+
type="checkbox"
|
|
304
|
+
checked={enabledSteps.has(step.key)}
|
|
305
|
+
onChange={() => toggleStep(step.key)}
|
|
306
|
+
disabled={step.key === 'structure'}
|
|
307
|
+
className="sr-only"
|
|
366
308
|
/>
|
|
309
|
+
<div
|
|
310
|
+
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded ${
|
|
311
|
+
enabledSteps.has(step.key)
|
|
312
|
+
? 'bg-primary text-primary-foreground'
|
|
313
|
+
: 'bg-muted border-border border'
|
|
314
|
+
}`}
|
|
315
|
+
>
|
|
316
|
+
{enabledSteps.has(step.key) && <Check size={12} />}
|
|
317
|
+
</div>
|
|
318
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
319
|
+
<span className="text-muted-foreground">{step.icon}</span>
|
|
320
|
+
<div>
|
|
321
|
+
<span className="text-foreground text-sm">{step.label}</span>
|
|
322
|
+
<span className="text-muted-foreground ml-2 text-xs">
|
|
323
|
+
{step.description}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</label>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* Business context (collapsible) */}
|
|
333
|
+
<div>
|
|
334
|
+
<button
|
|
335
|
+
onClick={() => setShowContext(!showContext)}
|
|
336
|
+
className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors"
|
|
337
|
+
>
|
|
338
|
+
{showContext ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
339
|
+
Business context (optional)
|
|
340
|
+
</button>
|
|
341
|
+
{showContext && (
|
|
342
|
+
<div className="bg-muted/50 border-border mt-3 space-y-3 rounded-lg border p-4">
|
|
343
|
+
<div className="grid grid-cols-2 gap-3">
|
|
344
|
+
<div>
|
|
345
|
+
<label className="text-muted-foreground mb-1 block text-xs">
|
|
346
|
+
Business name
|
|
347
|
+
</label>
|
|
348
|
+
<input
|
|
349
|
+
type="text"
|
|
350
|
+
value={businessName}
|
|
351
|
+
onChange={(e) => setBusiness(e.target.value)}
|
|
352
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
<div>
|
|
356
|
+
<label className="text-muted-foreground mb-1 block text-xs">
|
|
357
|
+
Industry
|
|
358
|
+
</label>
|
|
359
|
+
<input
|
|
360
|
+
type="text"
|
|
361
|
+
value={industry}
|
|
362
|
+
onChange={(e) => setIndustry(e.target.value)}
|
|
363
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
<div>
|
|
367
|
+
<label className="text-muted-foreground mb-1 block text-xs">Phone</label>
|
|
368
|
+
<input
|
|
369
|
+
type="text"
|
|
370
|
+
value={phone}
|
|
371
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
372
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<label className="text-muted-foreground mb-1 block text-xs">
|
|
377
|
+
Address
|
|
378
|
+
</label>
|
|
379
|
+
<input
|
|
380
|
+
type="text"
|
|
381
|
+
value={address}
|
|
382
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
383
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
367
386
|
</div>
|
|
368
387
|
<div>
|
|
369
|
-
<label className="text-muted-foreground mb-1 block text-xs">
|
|
388
|
+
<label className="text-muted-foreground mb-1 block text-xs">
|
|
389
|
+
Services (comma-separated)
|
|
390
|
+
</label>
|
|
370
391
|
<input
|
|
371
392
|
type="text"
|
|
372
|
-
value={
|
|
373
|
-
onChange={(e) =>
|
|
374
|
-
|
|
393
|
+
value={services}
|
|
394
|
+
onChange={(e) => setServices(e.target.value)}
|
|
395
|
+
placeholder="e.g. Web design, SEO, Content marketing"
|
|
396
|
+
className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
375
397
|
/>
|
|
376
398
|
</div>
|
|
377
399
|
</div>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
Services (comma-separated)
|
|
381
|
-
</label>
|
|
382
|
-
<input
|
|
383
|
-
type="text"
|
|
384
|
-
value={services}
|
|
385
|
-
onChange={(e) => setServices(e.target.value)}
|
|
386
|
-
placeholder="e.g. Web design, SEO, Content marketing"
|
|
387
|
-
className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
388
|
-
/>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
)}
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
|
|
396
|
-
{phase === 'generating' && (
|
|
397
|
-
<div className="space-y-6 py-8">
|
|
398
|
-
<div className="text-center">
|
|
399
|
-
<Loader2 size={32} className="text-primary mx-auto mb-3 animate-spin" />
|
|
400
|
-
<p className="text-foreground text-sm font-medium">
|
|
401
|
-
{stepMessage || 'Generating...'}
|
|
402
|
-
</p>
|
|
403
|
-
<p className="text-muted-foreground mt-1 text-xs">This may take 15-30 seconds</p>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
404
402
|
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{phase === 'generating' && (
|
|
406
|
+
<div className="space-y-6 py-8">
|
|
407
|
+
<div className="text-center">
|
|
408
|
+
<Loader2 size={32} className="text-primary mx-auto mb-3 animate-spin" />
|
|
409
|
+
<p className="text-foreground text-sm font-medium">
|
|
410
|
+
{stepMessage || 'Generating...'}
|
|
411
|
+
</p>
|
|
412
|
+
<p className="text-muted-foreground mt-1 text-xs">This may take 15-30 seconds</p>
|
|
413
|
+
</div>
|
|
405
414
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
415
|
+
<div className="space-y-2">
|
|
416
|
+
{ALL_STEPS.filter((s) => enabledSteps.has(s.key)).map((step) => {
|
|
417
|
+
const isComplete = completedSteps.includes(step.key)
|
|
418
|
+
const isCurrent = currentStep === step.key
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<div
|
|
422
|
+
key={step.key}
|
|
423
|
+
className={`flex items-center gap-3 rounded-lg border p-3 transition-colors ${
|
|
424
|
+
isComplete
|
|
425
|
+
? 'border-primary/30 bg-primary/5'
|
|
426
|
+
: isCurrent
|
|
427
|
+
? 'border-primary/50 bg-primary/10'
|
|
428
|
+
: 'border-border bg-muted/30'
|
|
429
|
+
}`}
|
|
430
|
+
>
|
|
431
|
+
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
|
|
432
|
+
{isComplete ? (
|
|
433
|
+
<div className="bg-primary flex h-6 w-6 items-center justify-center rounded-full">
|
|
434
|
+
<Check size={12} className="text-primary-foreground" />
|
|
435
|
+
</div>
|
|
436
|
+
) : isCurrent ? (
|
|
437
|
+
<Loader2 size={16} className="text-primary animate-spin" />
|
|
438
|
+
) : (
|
|
439
|
+
<div className="bg-muted border-border h-6 w-6 rounded-full border" />
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
<div className="flex items-center gap-2">
|
|
443
|
+
<span className="text-muted-foreground">{step.icon}</span>
|
|
444
|
+
<span
|
|
445
|
+
className={`text-sm ${isCurrent || isComplete ? 'text-foreground' : 'text-muted-foreground'}`}
|
|
446
|
+
>
|
|
447
|
+
{step.label}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
440
450
|
</div>
|
|
441
|
-
|
|
442
|
-
)
|
|
443
|
-
})}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
)}
|
|
447
|
-
|
|
448
|
-
{phase === 'review' && result && (
|
|
449
|
-
<div className="space-y-5">
|
|
450
|
-
{/* Summary */}
|
|
451
|
-
<div className="bg-primary/5 border-primary/20 flex items-center gap-3 rounded-lg border p-4">
|
|
452
|
-
<div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
|
|
453
|
-
<Sparkles size={18} className="text-primary" />
|
|
454
|
-
</div>
|
|
455
|
-
<div>
|
|
456
|
-
<p className="text-foreground text-sm font-medium">Page generated successfully</p>
|
|
457
|
-
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
458
|
-
{result.totalTokensUsed.toLocaleString()} tokens
|
|
459
|
-
{' \u00B7 '}
|
|
460
|
-
{(result.totalDurationMs / 1000).toFixed(1)}s{' \u00B7 '}
|
|
461
|
-
{result.steps.length} step{result.steps.length !== 1 ? 's' : ''}
|
|
462
|
-
</p>
|
|
451
|
+
)
|
|
452
|
+
})}
|
|
463
453
|
</div>
|
|
464
454
|
</div>
|
|
455
|
+
)}
|
|
456
|
+
|
|
457
|
+
{phase === 'review' && result && (
|
|
458
|
+
<div className="space-y-5">
|
|
459
|
+
{/* Summary */}
|
|
460
|
+
<div className="bg-primary/5 border-primary/20 flex items-center gap-3 rounded-lg border p-4">
|
|
461
|
+
<div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
|
|
462
|
+
<Sparkles size={18} className="text-primary" />
|
|
463
|
+
</div>
|
|
464
|
+
<div>
|
|
465
|
+
<p className="text-foreground text-sm font-medium">
|
|
466
|
+
Page generated successfully
|
|
467
|
+
</p>
|
|
468
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
469
|
+
{result.totalTokensUsed.toLocaleString()} tokens
|
|
470
|
+
{' \u00B7 '}
|
|
471
|
+
{(result.totalDurationMs / 1000).toFixed(1)}s{' \u00B7 '}
|
|
472
|
+
{result.steps.length} step{result.steps.length !== 1 ? 's' : ''}
|
|
473
|
+
</p>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
465
476
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
>
|
|
478
|
-
<button
|
|
479
|
-
onClick={() => setExpandedStep(isExpanded ? null : step.step)}
|
|
480
|
-
className="hover:bg-muted/50 flex w-full items-center gap-3 p-3 text-left transition-colors"
|
|
477
|
+
{/* Step details */}
|
|
478
|
+
<div className="space-y-2">
|
|
479
|
+
<h3 className="text-foreground text-sm font-medium">Generation steps</h3>
|
|
480
|
+
{result.steps.map((step) => {
|
|
481
|
+
const info = ALL_STEPS.find((s) => s.key === step.step)
|
|
482
|
+
const isExpanded = expandedStep === step.step
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div
|
|
486
|
+
key={step.step}
|
|
487
|
+
className="border-border overflow-hidden rounded-lg border"
|
|
481
488
|
>
|
|
482
|
-
<
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
<
|
|
492
|
-
|
|
493
|
-
|
|
489
|
+
<button
|
|
490
|
+
onClick={() => setExpandedStep(isExpanded ? null : step.step)}
|
|
491
|
+
className="hover:bg-muted/50 flex w-full items-center gap-3 p-3 text-left transition-colors"
|
|
492
|
+
>
|
|
493
|
+
<div className="bg-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
|
|
494
|
+
<Check size={12} className="text-primary-foreground" />
|
|
495
|
+
</div>
|
|
496
|
+
<span className="text-muted-foreground">{info?.icon}</span>
|
|
497
|
+
<span className="text-foreground flex-1 text-sm">{info?.label}</span>
|
|
498
|
+
<span className="text-muted-foreground text-xs">
|
|
499
|
+
{(step.metadata.durationMs / 1000).toFixed(1)}s
|
|
500
|
+
</span>
|
|
501
|
+
{isExpanded ? (
|
|
502
|
+
<ChevronUp size={14} className="text-muted-foreground" />
|
|
503
|
+
) : (
|
|
504
|
+
<ChevronDown size={14} className="text-muted-foreground" />
|
|
505
|
+
)}
|
|
506
|
+
</button>
|
|
507
|
+
{isExpanded && (
|
|
508
|
+
<div className="border-border border-t px-3 pt-2 pb-3">
|
|
509
|
+
<ul className="space-y-1">
|
|
510
|
+
{step.metadata.changes.map((change, i) => (
|
|
511
|
+
<li
|
|
512
|
+
key={i}
|
|
513
|
+
className="text-muted-foreground flex items-start gap-2 text-xs"
|
|
514
|
+
>
|
|
515
|
+
<ArrowRight size={10} className="mt-0.5 shrink-0" />
|
|
516
|
+
{change}
|
|
517
|
+
</li>
|
|
518
|
+
))}
|
|
519
|
+
</ul>
|
|
520
|
+
<p className="text-muted-foreground mt-2 text-xs">
|
|
521
|
+
Tokens: {step.metadata.tokensUsed.toLocaleString()}
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|
|
494
524
|
)}
|
|
495
|
-
</
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
<ArrowRight size={10} className="mt-0.5 shrink-0" />
|
|
505
|
-
{change}
|
|
506
|
-
</li>
|
|
507
|
-
))}
|
|
508
|
-
</ul>
|
|
509
|
-
<p className="text-muted-foreground mt-2 text-xs">
|
|
510
|
-
Tokens: {step.metadata.tokensUsed.toLocaleString()}
|
|
511
|
-
</p>
|
|
512
|
-
</div>
|
|
513
|
-
)}
|
|
514
|
-
</div>
|
|
515
|
-
)
|
|
516
|
-
})}
|
|
525
|
+
</div>
|
|
526
|
+
)
|
|
527
|
+
})}
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<p className="text-muted-foreground text-xs">
|
|
531
|
+
Review the generated page in the canvas after accepting. You can always undo to
|
|
532
|
+
revert.
|
|
533
|
+
</p>
|
|
517
534
|
</div>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
518
537
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
538
|
+
{/* Footer */}
|
|
539
|
+
<div className="border-border flex items-center justify-end gap-3 border-t px-6 py-4">
|
|
540
|
+
{phase === 'prompt' && (
|
|
541
|
+
<>
|
|
542
|
+
<button
|
|
543
|
+
onClick={handleClose}
|
|
544
|
+
className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
|
|
545
|
+
>
|
|
546
|
+
Cancel
|
|
547
|
+
</button>
|
|
548
|
+
<button
|
|
549
|
+
onClick={handleGenerate}
|
|
550
|
+
disabled={!prompt.trim()}
|
|
551
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
552
|
+
>
|
|
553
|
+
<Sparkles size={14} />
|
|
554
|
+
Generate Page
|
|
555
|
+
</button>
|
|
556
|
+
</>
|
|
557
|
+
)}
|
|
526
558
|
|
|
527
|
-
|
|
528
|
-
<div className="border-border flex items-center justify-end gap-3 border-t px-6 py-4">
|
|
529
|
-
{phase === 'prompt' && (
|
|
530
|
-
<>
|
|
559
|
+
{phase === 'generating' && (
|
|
531
560
|
<button
|
|
532
561
|
onClick={handleClose}
|
|
533
562
|
className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
|
|
534
563
|
>
|
|
535
564
|
Cancel
|
|
536
565
|
</button>
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}}
|
|
564
|
-
className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
|
|
565
|
-
>
|
|
566
|
-
Regenerate
|
|
567
|
-
</button>
|
|
568
|
-
<button
|
|
569
|
-
onClick={handleAccept}
|
|
570
|
-
className="bg-primary hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors"
|
|
571
|
-
>
|
|
572
|
-
<Check size={14} />
|
|
573
|
-
Accept & Apply
|
|
574
|
-
</button>
|
|
575
|
-
</>
|
|
576
|
-
)}
|
|
577
|
-
</div>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
566
|
+
)}
|
|
567
|
+
|
|
568
|
+
{phase === 'review' && (
|
|
569
|
+
<>
|
|
570
|
+
<button
|
|
571
|
+
onClick={() => {
|
|
572
|
+
setPhase('prompt')
|
|
573
|
+
setResult(null)
|
|
574
|
+
}}
|
|
575
|
+
className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
|
|
576
|
+
>
|
|
577
|
+
Regenerate
|
|
578
|
+
</button>
|
|
579
|
+
<button
|
|
580
|
+
onClick={handleAccept}
|
|
581
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
582
|
+
>
|
|
583
|
+
<Check size={14} />
|
|
584
|
+
Accept & Apply
|
|
585
|
+
</button>
|
|
586
|
+
</>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
</Dialog.Content>
|
|
590
|
+
</Dialog.Portal>
|
|
591
|
+
</Dialog.Root>
|
|
580
592
|
)
|
|
581
593
|
}
|