PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / src / templates / components / CreateWithAI.tsx
everest-forms / src / templates / components Last commit date
CreateFormCTA.tsx 2 months ago CreateWithAI.tsx 5 days ago Main.tsx 2 months ago PluginStatus.tsx 2 months ago Sidebar.tsx 2 months ago TemplateList.tsx 5 days ago TemplatesSkeleton.tsx 5 months ago
CreateWithAI.tsx
1997 lines
1 import {
2 Box,
3 Flex,
4 HStack,
5 Heading,
6 Icon,
7 Modal,
8 ModalBody,
9 ModalCloseButton,
10 ModalContent,
11 ModalFooter,
12 ModalHeader,
13 ModalOverlay,
14 Popover,
15 PopoverArrow,
16 PopoverBody,
17 PopoverContent,
18 PopoverTrigger,
19 SimpleGrid,
20 Spinner,
21 Text,
22 Textarea,
23 Tooltip,
24 VStack,
25 keyframes,
26 useDisclosure,
27 useToast,
28 } from '@chakra-ui/react';
29 import { __, _n, sprintf } from '@wordpress/i18n';
30 import React, { useEffect, useState } from 'react';
31 import { BsStars } from 'react-icons/bs';
32 import {
33 FiArrowLeft,
34 FiArrowUp,
35 FiCheck,
36 FiEdit3,
37 FiRefreshCw,
38 } from 'react-icons/fi';
39 import { templatesScriptData } from '../utils/global';
40
41 const pulseGlow = keyframes`
42 0% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); }
43 50% { box-shadow: 0 0 28px 10px rgba(117,69,187,0.12); transform: scale(1.06); }
44 100% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); }
45 `;
46
47 const dotBounce = keyframes`
48 0%, 80%, 100% { transform: scale(0.35); opacity: 0.3; }
49 40% { transform: scale(1); opacity: 1; }
50 `;
51
52 const fadeUp = keyframes`
53 from { opacity: 0; transform: translateY(12px); }
54 to { opacity: 1; transform: translateY(0); }
55 `;
56
57 const shimmer = keyframes`
58 0% { background-position: -400px 0; }
59 100% { background-position: 400px 0; }
60 `;
61
62 const INSPIRATION_CARDS = [
63 {
64 title: __('Online store checkout', 'everest-forms'),
65 description: __(
66 'Product selection with prices, quantity, coupon code, order total, and payment gateway.',
67 'everest-forms',
68 ),
69 prompt:
70 'An online store checkout form with customer name and email, product selection as payment radio buttons with prices, quantity field, coupon code field, order subtotal display, order total display, shipping address, and a payment gateway section.',
71 },
72 {
73 title: __('Subscription sign-up', 'everest-forms'),
74 description: __(
75 'Subscription plan selection, billing details, and payment gateway for a recurring membership.',
76 'everest-forms',
77 ),
78 prompt:
79 'A subscription membership sign-up form with first name, last name, email, phone number, subscription plan selection with monthly and annual pricing options, billing address, and payment gateway fields.',
80 },
81 {
82 title: __('Event ticket purchase', 'everest-forms'),
83 description: __(
84 'Ticket type with pricing, quantity, coupon code, total, and payment gateway.',
85 'everest-forms',
86 ),
87 prompt:
88 'An event ticket purchase form with attendee name and email, ticket type as payment radio buttons with prices (general admission, VIP, student), ticket quantity field, coupon code field, order subtotal and total display, and payment gateway fields.',
89 },
90 {
91 title: __('Freelance contract & NDA', 'everest-forms'),
92 description: __(
93 'Client details, project scope, agreed rate, contract terms, and digital signature.',
94 'everest-forms',
95 ),
96 prompt:
97 'A freelance contract and NDA form with client name, company, email, project title text field, project scope textarea, deliverables textarea, agreed rate number field, project start date, contract terms and conditions textarea, and a digital signature field for client approval.',
98 },
99 {
100 title: __('Health & wellness check-in', 'everest-forms'),
101 description: __(
102 'Pain, energy, and stress levels via range sliders, symptoms checklist, and notes.',
103 'everest-forms',
104 ),
105 prompt:
106 'A daily health and wellness check-in form with patient name, date, pain level range slider (0 to 10), energy level range slider (0 to 10), stress level range slider (0 to 10), sleep hours number field, symptoms checkboxes (headache, fatigue, nausea, dizziness), and a notes textarea for additional observations.',
107 },
108 {
109 title: __('User account registration', 'everest-forms'),
110 description: __(
111 'Username, email, password with confirmation, profile photo, and preferences.',
112 'everest-forms',
113 ),
114 prompt:
115 'A user account registration form with first name, last name, username text field, email address, password field, confirm password field, profile photo file upload, date of birth, country dropdown, and a newsletter subscription checkbox.',
116 },
117 ];
118
119 const GEN_STEPS = [
120 __('Understanding your prompt', 'everest-forms'),
121 __('Designing field structure', 'everest-forms'),
122 __('Configuring validation & logic', 'everest-forms'),
123 __('Finalizing your form', 'everest-forms'),
124 ];
125
126 const MAX_CHARS = 500;
127
128 // Matches Style Customizer v2's own `.pv-iframe { min-height: 480px }` (style.scss) — the two
129 // AI-preview surfaces should feel like the same feature, not a taller one and a cramped one.
130 const MIN_PREVIEW_HEIGHT = 480;
131
132 // Style Customizer v2's PreviewPane forces its preview visible after a short deadline no matter
133 // what ("Force the reveal after a short deadline so the preview is ALWAYS shown" — PreviewPane.tsx)
134 // so a slow/blocked load never leaves the user staring at a skeleton forever. Mirrored here.
135 const PREVIEW_REVEAL_DEADLINE = 2500;
136
137 const UPGRADE_URL =
138 'https://everestforms.net/upgrade/?utm_source=evf-free&utm_medium=ai-form-builder&utm_campaign=ai-rate-limit&utm_content=Upgrade+to+Pro';
139
140 /**
141 * The "you've hit the daily cap" popover body — shared by the three limit-reached buttons
142 * below. Pro has its own (much higher) daily limit too, so a Pro user who hits it sees a
143 * plain "try again tomorrow" message instead of a nonsensical "upgrade to Pro" CTA.
144 */
145 const LimitReachedBody: React.FC<{ tier: string }> = ({ tier }) => (
146 <>
147 <Text fontSize="12px" color="#555" mb={2} lineHeight="1.5">
148 {tier === 'pro'
149 ? __(
150 "You've reached today's request limit. It resets tomorrow.",
151 'everest-forms',
152 )
153 : __("You've reached your daily free limit.", 'everest-forms')}
154 </Text>
155 {tier !== 'pro' && (
156 <Box
157 as="a"
158 href={UPGRADE_URL}
159 target="_blank"
160 rel="noopener noreferrer"
161 color="#7545BB"
162 fontSize="12px"
163 fontWeight="600"
164 _hover={{ textDecoration: 'underline' }}
165 >
166 {__('Upgrade to Pro →', 'everest-forms')}
167 </Box>
168 )}
169 </>
170 );
171
172 /** Daily-request usage snapshot the gateway now returns on every AI response. */
173 interface UsageInfo {
174 remaining: number;
175 limit: number;
176 used: number;
177 }
178
179 // At/below this many remaining requests the note switches to a gentle amber warning.
180 const USAGE_LOW_THRESHOLD = 3;
181
182 /** Read the { remaining, limit, used } usage object off a raw AI response, or null. */
183 const readUsage = (raw: any): UsageInfo | null => {
184 const u = raw && raw.usage;
185 if (u && typeof u.remaining === 'number') {
186 return { remaining: u.remaining, limit: u.limit, used: u.used };
187 }
188 return null;
189 };
190
191 // Full tooltip label, e.g. "18 of 20 AI requests left today · resets daily".
192 const usageTooltip = (usage: UsageInfo): string => {
193 if (typeof usage.limit === 'number' && usage.limit > 0) {
194 return sprintf(
195 /* translators: 1: remaining requests, 2: daily limit. */
196 __('%1$d of %2$d AI requests left today · resets daily', 'everest-forms'),
197 usage.remaining,
198 usage.limit,
199 );
200 }
201 return sprintf(
202 _n(
203 '%d request left today',
204 '%d requests left today',
205 usage.remaining,
206 'everest-forms',
207 ),
208 usage.remaining,
209 );
210 };
211
212 /**
213 * The daily-request "credits" pill — a sparkle, an `18/20` count, and a slim meter that
214 * drains as requests are used. Muted purple normally, amber when low, red when exhausted.
215 * Shows a shimmer skeleton while the first count is still loading so it reserves its space.
216 */
217 const CreditsPill: React.FC<{ usage: UsageInfo | null; loading?: boolean }> = ({
218 usage,
219 loading,
220 }) => {
221 if (loading && !usage) {
222 return (
223 <Box
224 h="26px"
225 w="76px"
226 borderRadius="full"
227 sx={{
228 background:
229 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)',
230 backgroundSize: '400px 100%',
231 animation: `${shimmer} 1.6s ease-in-out infinite`,
232 }}
233 />
234 );
235 }
236 if (!usage) return null;
237
238 const hasLimit = typeof usage.limit === 'number' && usage.limit > 0;
239 const { remaining } = usage;
240 const exhausted = remaining <= 0;
241 const low = remaining <= USAGE_LOW_THRESHOLD;
242 const frac = hasLimit ? Math.max(0, Math.min(1, remaining / usage.limit)) : 1;
243
244 const c = exhausted
245 ? { bg: '#fef2f2', border: '#fecaca', num: '#dc2626', den: '#f5a3a3', track: '#fcdcdc', fill: '#dc2626', icon: '#dc2626' }
246 : low
247 ? { bg: '#fff7ed', border: '#fed7aa', num: '#b45309', den: '#d9a066', track: '#fde4c4', fill: '#f59e0b', icon: '#d97706' }
248 : { bg: '#f7f4fc', border: '#e9e2f3', num: '#7545BB', den: '#b3a3d6', track: '#e9e2f3', fill: '#7545BB', icon: '#7545BB' };
249
250 return (
251 <Tooltip
252 label={usageTooltip(usage)}
253 placement="top"
254 hasArrow
255 bg="#1a1a1a"
256 color="white"
257 fontSize="12px"
258 borderRadius="6px"
259 px="10px"
260 py="6px"
261 openDelay={200}
262 >
263 <HStack
264 spacing="7px"
265 h="26px"
266 pl="9px"
267 pr="11px"
268 borderRadius="full"
269 bg={c.bg}
270 border="1px solid"
271 borderColor={c.border}
272 cursor="default"
273 userSelect="none"
274 transition="background 0.25s, border-color 0.25s"
275 >
276 <Icon as={BsStars} boxSize="11px" color={c.icon} />
277 <HStack spacing="1px" align="baseline">
278 <Text fontSize="12.5px" fontWeight="700" color={c.num} lineHeight="1" m="0">
279 {remaining}
280 </Text>
281 {hasLimit && (
282 <Text fontSize="12.5px" fontWeight="500" color={c.den} lineHeight="1" m="0">
283 /{usage.limit}
284 </Text>
285 )}
286 </HStack>
287 {hasLimit && (
288 <Box w="26px" h="4px" borderRadius="full" bg={c.track} overflow="hidden">
289 <Box
290 h="100%"
291 borderRadius="full"
292 bg={c.fill}
293 w={`${Math.round(frac * 100)}%`}
294 transition="width 0.45s ease"
295 />
296 </Box>
297 )}
298 </HStack>
299 </Tooltip>
300 );
301 };
302
303 const SkeletonField: React.FC<{ delay?: string }> = ({ delay = '0s' }) => (
304 <Box sx={{ animation: `${fadeUp} 0.4s ease ${delay} both` }}>
305 <Box
306 h="12px"
307 w="30%"
308 mb="8px"
309 borderRadius="4px"
310 sx={{
311 background:
312 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)',
313 backgroundSize: '400px 100%',
314 animation: `${shimmer} 1.6s ease-in-out infinite`,
315 }}
316 />
317 <Box
318 h="36px"
319 borderRadius="6px"
320 sx={{
321 background:
322 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)',
323 backgroundSize: '400px 100%',
324 animation: `${shimmer} 1.6s ease-in-out infinite`,
325 }}
326 />
327 </Box>
328 );
329
330 const PageShell: React.FC<{
331 onBack: () => void;
332 backLabel?: string;
333 headerRight?: React.ReactNode;
334 children: React.ReactNode;
335 }> = ({ onBack, backLabel, headerRight, children }) => {
336 useEffect(() => {
337 const prev = document.body.style.overflow;
338 document.body.style.overflow = 'hidden';
339 return () => {
340 document.body.style.overflow = prev;
341 };
342 }, []);
343 return (
344 <Flex
345 position="fixed"
346 top="0"
347 left="0"
348 right="0"
349 bottom="0"
350 zIndex={100000}
351 direction="column"
352 overflow="hidden"
353 bg="#f6f6f8"
354 >
355 <Box h="4px" bg="#7545BB" flexShrink={0} />
356 <Flex
357 as="header"
358 align="center"
359 h="56px"
360 bg="white"
361 borderBottom="1px solid #e2e8f0"
362 px="24px"
363 flexShrink={0}
364 >
365 <HStack spacing="12px" flex="1">
366 <Box
367 as="button"
368 w="32px"
369 h="32px"
370 display="inline-flex"
371 alignItems="center"
372 justifyContent="center"
373 borderRadius="8px"
374 color="#383838"
375 bg="transparent"
376 border="none"
377 cursor="pointer"
378 onClick={onBack}
379 _hover={{ bg: '#f8fafc' }}
380 transition="background 0.2s"
381 >
382 <Icon as={FiArrowLeft} boxSize="4" />
383 </Box>
384 <Heading
385 as="h1"
386 fontSize="16px"
387 fontWeight="500"
388 color="#0e0e0e"
389 m="0"
390 >
391 {backLabel || __('Create with AI', 'everest-forms')}
392 </Heading>
393 </HStack>
394 {headerRight}
395 </Flex>
396 <Box flex={1} overflowY="auto" display="flex" flexDirection="column">
397 {children}
398 </Box>
399 </Flex>
400 );
401 };
402
403 interface CreateWithAIProps {
404 onBack: () => void;
405 initialFormId?: number;
406 initialTitle?: string;
407 }
408
409 interface ChatMessage {
410 role: 'user' | 'assistant';
411 text: string;
412 loading?: boolean;
413 error?: boolean;
414 notice?: boolean;
415 noticeUrl?: string;
416 }
417
418 const { homeUrl, ajaxUrl, aiNonce } = templatesScriptData;
419
420 /** Normalize a same-site URL to the CURRENT window's exact origin (protocol + host + port),
421 * keeping only the path/query — mirrors src/style-customizer-v2/PreviewPane.tsx's
422 * `previewSrc`. Without this, a proxy/alias/scheme mismatch between the admin page's
423 * actual origin and what home_url() computes server-side can make the iframe genuinely
424 * cross-origin (or trigger a mixed-content block), rendering it blank. */
425 const sameOriginUrl = (raw: string): string => {
426 try {
427 const url = new URL(raw, window.location.href);
428 url.protocol = window.location.protocol;
429 url.host = window.location.host;
430 return url.href;
431 } catch (e) {
432 return raw;
433 }
434 };
435
436 const callAi = async (
437 action: string,
438 data: Record<string, string | number> = {},
439 ): Promise<any> => {
440 const body = new URLSearchParams();
441 body.append('action', action);
442 body.append('nonce', aiNonce);
443 Object.entries(data).forEach(([key, value]) =>
444 body.append(key, String(value)),
445 );
446
447 const response = await fetch(ajaxUrl, {
448 method: 'POST',
449 credentials: 'same-origin',
450 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
451 body: body.toString(),
452 });
453
454 return response.json();
455 };
456
457 const CreateWithAI: React.FC<CreateWithAIProps> = ({
458 onBack,
459 initialFormId,
460 initialTitle,
461 }) => {
462 const toast = useToast();
463 const [prompt, setPrompt] = useState('');
464 const [isRateLimited, setIsRateLimited] = useState(false);
465 // Which tier hit the daily cap — a Pro user has their own (much higher) limit and
466 // shouldn't be told to "upgrade to Pro" when they're already on it.
467 const [limitTier, setLimitTier] = useState('free');
468 // Daily-request usage snapshot — drives the "credits" pill. Seeded on mount from
469 // evf_ai_get_usage so it's visible before the first generate, then refreshed from every
470 // generate/update response.
471 const [usage, setUsage] = useState<UsageInfo | null>(null);
472 const [usageLoading, setUsageLoading] = useState(true);
473 const [genState, setGenState] = useState<'idle' | 'generating' | 'generated'>(
474 'idle',
475 );
476 const [genStep, setGenStep] = useState(-1);
477 const [hint, setHint] = useState({ show: false, x: 0, y: 0 });
478 const [isRegenerating, setIsRegenerating] = useState(false);
479 const [isCreatingForm, setIsCreatingForm] = useState(false);
480 // Real front-end preview (iframe of ?form_id=&evf_preview=true) — bumping the key forces a
481 // fresh remount/reload after every successful generate/refine, since the src string itself
482 // doesn't change. See render of the "generated" screen below for the iframe + overlay.
483 const [previewReloadKey, setPreviewReloadKey] = useState(0);
484 const [isPreviewLoading, setIsPreviewLoading] = useState(false);
485 // Auto-sized to the iframe's actual content height (see onLoad below) so the WHOLE
486 // form is visible and the surrounding page scrolls normally — no inner iframe
487 // scrollbar, which is easy to miss/awkward to use nested inside the admin page.
488 const [previewHeight, setPreviewHeight] = useState(MIN_PREVIEW_HEIGHT);
489 const previewResizeObserverRef = React.useRef<ResizeObserver | null>(null);
490 const [formId, setFormId] = useState(0);
491 const [formTitle, setFormTitle] = useState(initialTitle || '');
492 const [editUrl, setEditUrl] = useState('');
493 const [refinePrompt, setRefinePrompt] = useState('');
494 const [messages, setMessages] = useState<ChatMessage[]>([]);
495 const {
496 isOpen: isDiscardOpen,
497 onOpen: openDiscardModal,
498 onClose: closeDiscardModal,
499 } = useDisclosure();
500 const [isDiscarding, setIsDiscarding] = useState(false);
501 const [isFreshDraft, setIsFreshDraft] = useState(false);
502 const previewHintTimer = React.useRef<ReturnType<typeof setTimeout> | null>(
503 null,
504 );
505 const aiResponseRef = React.useRef<any>(null);
506 const promptInputRef = React.useRef<HTMLTextAreaElement | null>(null);
507
508 useEffect(() => {
509 return () => previewResizeObserverRef.current?.disconnect();
510 }, []);
511
512 // Seed the credits pill on mount so it's visible before the first generate. Best-effort —
513 // on an unregistered/local site this just fails quietly and the pill stays hidden.
514 useEffect(() => {
515 let cancelled = false;
516 callAi('evf_ai_get_usage')
517 .then((res) => {
518 if (cancelled) return;
519 const u = readUsage(res?.data);
520 if (u) setUsage(u);
521 })
522 .catch(() => {})
523 .finally(() => {
524 if (!cancelled) setUsageLoading(false);
525 });
526 return () => {
527 cancelled = true;
528 };
529 }, []);
530
531 // Never leave the user staring at the skeleton forever if the iframe's `load` event is slow
532 // or never fires (a security/caching plugin blocking the preview route, a very slow theme) —
533 // mirrors PreviewPane.tsx's identical "force the reveal after a short deadline" safety net.
534 useEffect(() => {
535 if (!isPreviewLoading) return;
536 const t = setTimeout(
537 () => setIsPreviewLoading(false),
538 PREVIEW_REVEAL_DEADLINE,
539 );
540 return () => clearTimeout(t);
541 }, [isPreviewLoading, previewReloadKey]);
542
543 useEffect(() => {
544 if (genState !== 'idle') return;
545 const t = setTimeout(() => promptInputRef.current?.focus(), 50);
546 return () => clearTimeout(t);
547 }, [genState]);
548
549 useEffect(() => {
550 if (typeof initialFormId !== 'number' || initialFormId <= 0) return;
551 setFormId(initialFormId);
552 setPrompt(initialTitle || __('this form', 'everest-forms'));
553 setMessages([
554 {
555 role: 'assistant',
556 text: initialTitle
557 ? /* translators: %s: template name. */
558 sprintf(
559 __(
560 'Loaded the “%s” template. Tell me what to change — add fields, reword labels, or anything else.',
561 'everest-forms',
562 ),
563 initialTitle,
564 )
565 : __('Loaded your form. Tell me what to change.', 'everest-forms'),
566 },
567 ]);
568 setGenState('generated');
569 // eslint-disable-next-line react-hooks/exhaustive-deps
570 }, [initialFormId]);
571
572 const handleUseThisForm = async () => {
573 if (isCreatingForm || !formId) return;
574 setIsCreatingForm(true);
575 try {
576 const res = await callAi('evf_ai_activate_form', { form_id: formId });
577
578 if (res?.success && res.data?.edit_url) {
579 window.location.href = res.data.edit_url;
580 } else if (editUrl) {
581 window.location.href = editUrl;
582 } else {
583 throw new Error(res?.data?.message || 'Unexpected response');
584 }
585 } catch (e: any) {
586 setIsCreatingForm(false);
587 toast({
588 title: __('Error', 'everest-forms'),
589 description:
590 e?.message ||
591 __('Could not open the form. Please try again.', 'everest-forms'),
592 status: 'error',
593 position: 'bottom',
594 duration: 5000,
595 isClosable: true,
596 variant: 'subtle',
597 });
598 }
599 };
600
601 const resetToNewPrompt = () => {
602 setGenState('idle');
603 setFormId(0);
604 setIsFreshDraft(false);
605 setFormTitle('');
606 setEditUrl('');
607 setMessages([]);
608 setPrompt('');
609 setRefinePrompt('');
610 };
611
612 const handleBackToNewPrompt = () => {
613 if (isFreshDraft && formId) {
614 openDiscardModal();
615 } else {
616 resetToNewPrompt();
617 }
618 };
619
620 const handleKeepAsDraft = () => {
621 closeDiscardModal();
622 resetToNewPrompt();
623 };
624
625 const handleDiscardConfirm = async () => {
626 if (!formId || isDiscarding) return;
627 setIsDiscarding(true);
628 try {
629 await callAi('evf_ai_discard_form', { form_id: formId });
630 } catch (e) {
631 } finally {
632 setIsDiscarding(false);
633 closeDiscardModal();
634 resetToNewPrompt();
635 }
636 };
637
638 const resolveLoading = (
639 text: string,
640 isError = false,
641 isNotice = false,
642 noticeUrl = '',
643 ) =>
644 setMessages((m) => {
645 const next = [...m];
646 for (let i = next.length - 1; i >= 0; i--) {
647 if (next[i].role === 'assistant' && next[i].loading) {
648 next[i] = {
649 role: 'assistant',
650 text,
651 error: isError,
652 notice: isNotice,
653 noticeUrl,
654 };
655 break;
656 }
657 }
658 return next;
659 });
660
661 const handleUpdate = async (refinePromptText: string = '') => {
662 if (isRegenerating || !formId || !prompt.trim()) return;
663 const refine = (refinePromptText || '').trim();
664 const userText = refine || __('Regenerate the form', 'everest-forms');
665
666 setMessages((m) => [
667 ...m,
668 { role: 'user', text: userText },
669 { role: 'assistant', text: '', loading: true },
670 ]);
671 setRefinePrompt('');
672 setIsRegenerating(true);
673 try {
674 const res = await callAi('evf_ai_update_form', {
675 form_id: formId,
676 prompt,
677 refine_prompt: refine,
678 });
679 const usageSnapshot = readUsage(res?.data);
680 if (usageSnapshot) setUsage(usageSnapshot);
681 if (res?.success) {
682 if (res.data?.form_title) setFormTitle(res.data.form_title);
683 const notice = res.data?.notice || '';
684 const noticeUrl = res.data?.notice_url || '';
685 const doneText = refine
686 ? __(
687 "Done — I've updated your form. Check the preview on the right.",
688 'everest-forms',
689 )
690 : __("Here's a fresh version of your form.", 'everest-forms');
691
692 if (notice && !messages.some((m) => m.notice && m.text === notice)) {
693 setMessages((m) => {
694 const next = [...m];
695 for (let i = next.length - 1; i >= 0; i--) {
696 if (next[i].role === 'assistant' && next[i].loading) {
697 next[i] = { role: 'assistant', text: doneText };
698 break;
699 }
700 }
701 return [
702 ...next,
703 { role: 'assistant', text: notice, notice: true, noticeUrl },
704 ];
705 });
706 } else {
707 resolveLoading(doneText, false, false, '');
708 }
709 setIsPreviewLoading(true);
710 setPreviewReloadKey((k) => k + 1);
711 } else {
712 if (res?.data?.code === 'daily_limit_reached') {
713 setIsRateLimited(true);
714 setLimitTier(res?.data?.tier || 'free');
715 }
716 throw new Error(
717 res?.data?.message ||
718 __('Could not update the form. Please try again.', 'everest-forms'),
719 );
720 }
721 } catch (e: any) {
722 const message =
723 e?.message ||
724 __('Could not update the form. Please try again.', 'everest-forms');
725 resolveLoading(message, true);
726 } finally {
727 setIsRegenerating(false);
728 }
729 };
730
731 const handleRegenerate = () => handleUpdate();
732
733 const handleFieldClick = (e: React.MouseEvent) => {
734 const { clientX, clientY } = e;
735 setHint({ show: true, x: clientX, y: clientY });
736 if (previewHintTimer.current) clearTimeout(previewHintTimer.current);
737 previewHintTimer.current = setTimeout(
738 () => setHint((h) => ({ ...h, show: false })),
739 2600,
740 );
741 };
742
743 const hasPrompt = prompt.trim().length > 0;
744
745 useEffect(() => {
746 if (genState !== 'generating') return;
747 setGenStep(-1);
748 aiResponseRef.current = null;
749
750 let cancelled = false;
751 let intervalId: ReturnType<typeof setInterval> | null = null;
752
753 const showError = (message: string, isRateLimit = false, tier = 'free') => {
754 if (cancelled) return;
755 if (intervalId) clearInterval(intervalId);
756 if (isRateLimit) {
757 setIsRateLimited(true);
758 setLimitTier(tier);
759 }
760 setGenState('idle');
761 toast({
762 title: isRateLimit
763 ? __('Daily limit reached', 'everest-forms')
764 : __('AI generation failed', 'everest-forms'),
765 description: isRateLimit ? (
766 <Box>
767 <Text mb={1}>{message}</Text>
768 {tier !== 'pro' && (
769 <Box
770 as="a"
771 href={UPGRADE_URL}
772 target="_blank"
773 rel="noopener noreferrer"
774 fontWeight="600"
775 textDecoration="underline"
776 _hover={{ opacity: 0.8 }}
777 >
778 {__('Upgrade to Pro →', 'everest-forms')}
779 </Box>
780 )}
781 </Box>
782 ) : (
783 message
784 ),
785 status: 'error',
786 position: 'bottom',
787 duration: 5000,
788 isClosable: true,
789 variant: 'subtle',
790 });
791 };
792
793 callAi('evf_ai_generate_form', { prompt })
794 .then((res: any) => {
795 if (!cancelled) {
796 const usageSnapshot = readUsage(res?.data);
797 if (usageSnapshot) setUsage(usageSnapshot);
798 }
799 if (res?.success && res.data?.form_id) {
800 aiResponseRef.current = {
801 ok: true,
802 formId: res.data.form_id,
803 formTitle: res.data.form_title || '',
804 editUrl: res.data.edit_url || '',
805 notice: res.data.notice || '',
806 noticeUrl: res.data.notice_url || '',
807 };
808 } else {
809 showError(
810 res?.data?.message ||
811 __('Something went wrong. Please try again.', 'everest-forms'),
812 res?.data?.code === 'daily_limit_reached',
813 res?.data?.tier || 'free',
814 );
815 }
816 })
817 .catch(() => {
818 showError(
819 __(
820 'Could not reach the AI service. Please try again.',
821 'everest-forms',
822 ),
823 );
824 });
825
826 let step = -1;
827 intervalId = setInterval(() => {
828 step += 1;
829 setGenStep(step);
830 if (step >= GEN_STEPS.length - 1) {
831 if (intervalId) clearInterval(intervalId);
832 const finish = () => {
833 if (cancelled) return;
834 const result = aiResponseRef.current;
835 if (!result) {
836 setTimeout(finish, 200);
837 return;
838 }
839 if (result.ok) {
840 setFormId(result.formId);
841 setIsFreshDraft(true);
842 setFormTitle(result.formTitle || '');
843 setEditUrl(result.editUrl);
844 setIsPreviewLoading(true);
845 setPreviewReloadKey((k) => k + 1);
846 setMessages([
847 { role: 'user', text: prompt },
848 {
849 role: 'assistant',
850 text:
851 result.notice ||
852 __(
853 'Here\'s your form! Review the preview on the right and click "Use This Form" when you\'re happy with it.',
854 'everest-forms',
855 ),
856 notice: !!result.notice,
857 noticeUrl: result.noticeUrl || '',
858 },
859 ]);
860 setGenState('generated');
861 }
862 };
863 setTimeout(finish, 700);
864 }
865 }, 950);
866
867 return () => {
868 cancelled = true;
869 if (intervalId) clearInterval(intervalId);
870 };
871 }, [genState]);
872
873 // Real front-end preview URL — assembled once formId is known; normalized to the
874 // current origin (see sameOriginUrl) and cache-busted by remounting on previewReloadKey
875 // change (the <iframe key={previewReloadKey}> below), not by varying this string.
876 const previewSrc = React.useMemo(() => {
877 if (!formId) return '';
878 const url = new URL(homeUrl, window.location.href);
879 url.searchParams.set('form_id', String(formId));
880 url.searchParams.set('evf_preview', 'true');
881 // Embed mode — just the form, no toolbar/device-switcher/side panel (this screen
882 // already renders its own chrome around the iframe). See evf-form-preview-embed.php.
883 url.searchParams.set('evf_preview_mode', 'embed');
884 return sameOriginUrl(url.href);
885 // eslint-disable-next-line react-hooks/exhaustive-deps
886 }, [formId]);
887
888 const handleGenerate = () => {
889 if (!hasPrompt) return;
890 setGenState('generating');
891 };
892
893 if (genState === 'generating') {
894 return (
895 <PageShell onBack={onBack}>
896 <Flex
897 flex="1"
898 overflow="hidden"
899 sx={{ animation: `${fadeUp} 0.3s ease` }}
900 >
901 <Flex
902 w="480px"
903 flexShrink={0}
904 bg="white"
905 borderRight="1px solid #e2e8f0"
906 direction="column"
907 align="center"
908 justify="center"
909 px="36px"
910 py="40px"
911 gap="0"
912 >
913 <Box
914 w="60px"
915 h="60px"
916 borderRadius="full"
917 background="linear-gradient(135deg, #9c6de8 0%, #7545BB 100%)"
918 display="flex"
919 alignItems="center"
920 justifyContent="center"
921 mb="20px"
922 sx={{ animation: `${pulseGlow} 2s ease-in-out infinite` }}
923 >
924 <Icon as={BsStars} boxSize={6} color="white" />
925 </Box>
926
927 <VStack spacing="4px" textAlign="center" mb="24px">
928 <Heading
929 as="h2"
930 fontSize="20px"
931 fontWeight="700"
932 color="#0e0e0e"
933 margin="0"
934 letterSpacing="-0.3px"
935 >
936 {__('Building your form…', 'everest-forms')}
937 </Heading>
938 <Text fontSize="13px" color="#9a9a9a" margin="0">
939 {__('This usually takes a few seconds', 'everest-forms')}
940 </Text>
941 </VStack>
942
943 <Box
944 bg="#faf9ff"
945 borderRadius="12px"
946 border="1px solid #ede8f8"
947 p="20px 24px"
948 width="100%"
949 mb="20px"
950 >
951 <VStack align="stretch" spacing="14px">
952 {GEN_STEPS.map((step, i) => {
953 const isDone = i < genStep;
954 const isActive = i === genStep;
955 const isPending = i > genStep;
956 return (
957 <HStack
958 key={i}
959 spacing="12px"
960 opacity={isPending ? 0.3 : 1}
961 transition="opacity 0.4s"
962 >
963 <Flex
964 w="20px"
965 h="20px"
966 borderRadius="full"
967 flexShrink={0}
968 bg={isDone ? '#7545BB' : 'transparent'}
969 border={isDone ? 'none' : '2px solid'}
970 borderColor={isActive ? '#7545BB' : '#ddd'}
971 align="center"
972 justify="center"
973 transition="all 0.35s"
974 >
975 {isDone && (
976 <Icon
977 as={FiCheck}
978 color="white"
979 sx={{ width: '9px', height: '9px', strokeWidth: 3 }}
980 />
981 )}
982 {isActive && (
983 <Box
984 w="6px"
985 h="6px"
986 borderRadius="full"
987 bg="#7545BB"
988 sx={{
989 animation: `${dotBounce} 1.1s ease-in-out infinite`,
990 }}
991 />
992 )}
993 </Flex>
994 <Text
995 flex={1}
996 fontSize="13px"
997 margin="0"
998 fontWeight={isActive ? '600' : isDone ? '500' : '400'}
999 color={
1000 isActive ? '#7545BB' : isDone ? '#1a1a1a' : '#c0c0cc'
1001 }
1002 transition="all 0.35s"
1003 >
1004 {step}
1005 </Text>
1006 {isActive && (
1007 <HStack spacing="3px">
1008 {[0, 1, 2].map((d) => (
1009 <Box
1010 key={d}
1011 w="4px"
1012 h="4px"
1013 borderRadius="full"
1014 bg="#7545BB"
1015 sx={{
1016 animation: `${dotBounce} 1.1s ease-in-out ${d * 0.18}s infinite`,
1017 }}
1018 />
1019 ))}
1020 </HStack>
1021 )}
1022 </HStack>
1023 );
1024 })}
1025 </VStack>
1026 </Box>
1027 </Flex>
1028
1029 <Box flex={1} bg="#f6f6f8" overflowY="auto" p="24px">
1030 <Box
1031 bg="white"
1032 border="1px solid #e2e8f0"
1033 borderRadius="16px"
1034 overflow="hidden"
1035 sx={{ animation: `${fadeUp} 0.4s ease 0.15s both` }}
1036 >
1037 <Flex
1038 align="center"
1039 px="24px"
1040 py="16px"
1041 borderBottom="1px solid #e2e8f0"
1042 gap="10px"
1043 >
1044 <Box
1045 w="32px"
1046 h="32px"
1047 borderRadius="8px"
1048 sx={{
1049 background:
1050 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)',
1051 backgroundSize: '400px 100%',
1052 animation: `${shimmer} 1.6s ease-in-out infinite`,
1053 }}
1054 />
1055 <Box
1056 h="14px"
1057 w="40%"
1058 borderRadius="4px"
1059 sx={{
1060 background:
1061 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)',
1062 backgroundSize: '400px 100%',
1063 animation: `${shimmer} 1.6s ease-in-out infinite`,
1064 }}
1065 />
1066 </Flex>
1067 <Box p="24px">
1068 <VStack spacing="16px" align="stretch">
1069 <SkeletonField delay="0.1s" />
1070 <SkeletonField delay="0.22s" />
1071 <SkeletonField delay="0.34s" />
1072 <SkeletonField delay="0.46s" />
1073 <SkeletonField delay="0.58s" />
1074 </VStack>
1075 </Box>
1076 </Box>
1077 </Box>
1078 </Flex>
1079 </PageShell>
1080 );
1081 }
1082
1083 if (genState === 'generated') {
1084 let lastAssistantIdx = -1;
1085 let useThisFormIdx = -1;
1086 messages.forEach((m, i) => {
1087 if (m.role === 'assistant' && !m.loading) {
1088 lastAssistantIdx = i;
1089 if (!m.error) useThisFormIdx = i;
1090 }
1091 });
1092 return (
1093 <PageShell
1094 onBack={handleBackToNewPrompt}
1095 backLabel={__('New Prompt', 'everest-forms')}
1096 headerRight={
1097 <Box
1098 as="button"
1099 display="inline-flex"
1100 alignItems="center"
1101 gap="6px"
1102 h="32px"
1103 px="14px"
1104 borderRadius="8px"
1105 border="none"
1106 bg={isCreatingForm ? '#9660db' : '#7545BB'}
1107 color="white"
1108 fontSize="13px"
1109 fontWeight="600"
1110 cursor={isCreatingForm ? 'not-allowed' : 'pointer'}
1111 opacity={isCreatingForm ? 0.85 : 1}
1112 onClick={handleUseThisForm}
1113 _hover={{ bg: isCreatingForm ? '#9660db' : '#6a3daa' }}
1114 transition="background 0.2s, opacity 0.2s"
1115 >
1116 {isCreatingForm && (
1117 <Spinner size="xs" color="white" thickness="2px" speed="0.65s" />
1118 )}
1119 {isCreatingForm
1120 ? __('Creating…', 'everest-forms')
1121 : __('Use This Form', 'everest-forms')}
1122 </Box>
1123 }
1124 >
1125 <Modal
1126 isCentered
1127 isOpen={isDiscardOpen}
1128 onClose={isDiscarding ? () => {} : closeDiscardModal}
1129 size="sm"
1130 >
1131 <ModalOverlay bg="rgba(0,0,0,0.4)" backdropFilter="blur(2px)" zIndex={200000} />
1132 <ModalContent
1133 borderRadius="12px"
1134 mx="16px"
1135 containerProps={{ zIndex: 200000 }}
1136 >
1137 <ModalHeader fontSize="16px" fontWeight="600" pb="4px">
1138 {__('Discard this draft?', 'everest-forms')}
1139 </ModalHeader>
1140 {!isDiscarding && <ModalCloseButton />}
1141 <ModalBody pb="20px">
1142 <Text fontSize="14px" color="#555" lineHeight="1.6" margin="0">
1143 {__(
1144 "This form hasn't been used yet — it's saved as a draft. You can keep it and find it later in All Forms, or discard it now.",
1145 'everest-forms',
1146 )}
1147 </Text>
1148 </ModalBody>
1149 <ModalFooter gap="8px" pt="0">
1150 <Box
1151 as="button"
1152 onClick={isDiscarding ? undefined : handleKeepAsDraft}
1153 px="14px"
1154 h="36px"
1155 borderRadius="8px"
1156 border="1px solid #e2e8f0"
1157 bg="white"
1158 fontSize="13px"
1159 fontWeight="600"
1160 color="#383838"
1161 cursor={isDiscarding ? 'not-allowed' : 'pointer'}
1162 opacity={isDiscarding ? 0.6 : 1}
1163 _hover={isDiscarding ? {} : { bg: '#f8fafc' }}
1164 transition="background 0.2s"
1165 >
1166 {__('Keep as draft', 'everest-forms')}
1167 </Box>
1168 <Box
1169 as="button"
1170 onClick={handleDiscardConfirm}
1171 display="inline-flex"
1172 alignItems="center"
1173 gap="6px"
1174 px="14px"
1175 h="36px"
1176 borderRadius="8px"
1177 border="none"
1178 bg="#e53e3e"
1179 color="white"
1180 fontSize="13px"
1181 fontWeight="600"
1182 cursor={isDiscarding ? 'not-allowed' : 'pointer'}
1183 opacity={isDiscarding ? 0.75 : 1}
1184 _hover={isDiscarding ? {} : { bg: '#c53030' }}
1185 transition="background 0.2s, opacity 0.2s"
1186 >
1187 {isDiscarding && (
1188 <Spinner size="xs" color="white" thickness="2px" speed="0.65s" />
1189 )}
1190 {isDiscarding
1191 ? __('Discarding…', 'everest-forms')
1192 : __('Discard', 'everest-forms')}
1193 </Box>
1194 </ModalFooter>
1195 </ModalContent>
1196 </Modal>
1197
1198 {hint.show && (
1199 <Box
1200 position="fixed"
1201 top={`${hint.y + 14}px`}
1202 left={`${hint.x}px`}
1203 transform="translateX(-50%)"
1204 bg="rgba(18,18,18,0.93)"
1205 color="white"
1206 borderRadius="8px"
1207 px="14px"
1208 py="10px"
1209 textAlign="center"
1210 pointerEvents="none"
1211 zIndex={9999}
1212 boxShadow="0 4px 20px rgba(0,0,0,0.2)"
1213 maxW="260px"
1214 sx={{ animation: `${fadeUp} 0.15s ease` }}
1215 >
1216 <Box
1217 position="absolute"
1218 top="-5px"
1219 left="50%"
1220 transform="translateX(-50%)"
1221 width="0"
1222 height="0"
1223 borderLeft="5px solid transparent"
1224 borderRight="5px solid transparent"
1225 borderBottom="5px solid rgba(18,18,18,0.93)"
1226 />
1227 <Text
1228 fontSize="12px"
1229 fontWeight="600"
1230 margin="0 0 3px"
1231 lineHeight="1.45"
1232 >
1233 {__('This is just a preview of your form.', 'everest-forms')}
1234 </Text>
1235 <Text
1236 fontSize="12px"
1237 color="rgba(255,255,255,0.65)"
1238 margin="0"
1239 lineHeight="1.45"
1240 >
1241 {__('Click "Use This Form" to start editing.', 'everest-forms')}
1242 </Text>
1243 </Box>
1244 )}
1245
1246 <Flex
1247 flex="1"
1248 overflow="hidden"
1249 sx={{ animation: `${fadeUp} 0.35s ease` }}
1250 >
1251 <Flex
1252 w="480px"
1253 flexShrink={0}
1254 bg="white"
1255 borderRight="1px solid #e2e8f0"
1256 direction="column"
1257 overflow="hidden"
1258 >
1259 <Box flex={1} p="20px" overflowY="auto">
1260 {messages.map((msg, idx) => {
1261 if (msg.role === 'user') {
1262 return (
1263 <Flex key={idx} justify="flex-end" mb="16px">
1264 <Box
1265 bg="#7545BB"
1266 color="white"
1267 borderRadius="16px 16px 4px 16px"
1268 px="14px"
1269 py="10px"
1270 fontSize="14px"
1271 lineHeight="1.6"
1272 maxW="340px"
1273 boxShadow="0 2px 8px rgba(117,69,187,0.2)"
1274 >
1275 {msg.text}
1276 </Box>
1277 </Flex>
1278 );
1279 }
1280
1281 const isLastAssistant = idx === lastAssistantIdx;
1282 const isUseThisFormMsg = idx === useThisFormIdx;
1283 return (
1284 <HStack key={idx} align="flex-start" spacing="10px" mb="16px">
1285 <Flex
1286 w="28px"
1287 h="28px"
1288 borderRadius="full"
1289 bg="rgba(117,69,187,0.1)"
1290 align="center"
1291 justify="center"
1292 flexShrink={0}
1293 mt="2px"
1294 >
1295 <Icon as={BsStars} boxSize="13px" color="#7545BB" />
1296 </Flex>
1297
1298 <Box
1299 flex={1}
1300 bg={msg.error || msg.notice ? '#fff8f8' : '#faf9ff'}
1301 border={
1302 msg.error || msg.notice
1303 ? '1px solid #fcd5d5'
1304 : '1px solid #ede8f8'
1305 }
1306 borderRadius="4px 16px 16px 16px"
1307 p="16px"
1308 boxShadow={
1309 msg.error || msg.notice
1310 ? 'none'
1311 : '0 2px 10px rgba(117,69,187,0.06)'
1312 }
1313 >
1314 {msg.loading ? (
1315 <HStack spacing="8px">
1316 <Spinner
1317 size="xs"
1318 color="#7545BB"
1319 thickness="2px"
1320 speed="0.65s"
1321 />
1322 <Text
1323 fontSize="14px"
1324 color="#7545BB"
1325 margin="0"
1326 fontWeight="500"
1327 >
1328 {__('Updating your form…', 'everest-forms')}
1329 </Text>
1330 </HStack>
1331 ) : (
1332 <>
1333 <Text
1334 fontSize="14px"
1335 color={msg.error || msg.notice ? '#c0392b' : '#444'}
1336 lineHeight="1.65"
1337 margin={
1338 isUseThisFormMsg
1339 ? '0 0 14px'
1340 : msg.noticeUrl
1341 ? '0 0 10px'
1342 : '0'
1343 }
1344 >
1345 {msg.text}
1346 </Text>
1347
1348 {msg.noticeUrl && (
1349 <Box
1350 as="a"
1351 href={msg.noticeUrl}
1352 target="_blank"
1353 rel="noopener noreferrer"
1354 display="inline-block"
1355 mb={isUseThisFormMsg ? '10px' : '0'}
1356 color="#7545BB"
1357 fontSize="12px"
1358 fontWeight="600"
1359 _hover={{ textDecoration: 'underline' }}
1360 >
1361 {__('Upgrade to Pro →', 'everest-forms')}
1362 </Box>
1363 )}
1364
1365 {isUseThisFormMsg && (
1366 <Box
1367 as="button"
1368 w="100%"
1369 display="flex"
1370 alignItems="center"
1371 justifyContent="center"
1372 gap="8px"
1373 h="36px"
1374 borderRadius="8px"
1375 bg={isCreatingForm ? '#9660db' : '#7545BB'}
1376 color="white"
1377 fontSize="13px"
1378 fontWeight="600"
1379 border="none"
1380 cursor={
1381 isCreatingForm ? 'not-allowed' : 'pointer'
1382 }
1383 mb="12px"
1384 opacity={isCreatingForm ? 0.85 : 1}
1385 onClick={handleUseThisForm}
1386 _hover={{
1387 bg: isCreatingForm ? '#9660db' : '#6a3daa',
1388 }}
1389 transition="background 0.2s, opacity 0.2s"
1390 >
1391 {isCreatingForm && (
1392 <Spinner
1393 size="xs"
1394 color="white"
1395 thickness="2px"
1396 speed="0.65s"
1397 />
1398 )}
1399 {isCreatingForm
1400 ? __('Creating…', 'everest-forms')
1401 : __('Use This Form', 'everest-forms')}
1402 </Box>
1403 )}
1404
1405 <HStack justify="flex-end" pt="2px">
1406 {isRateLimited ? (
1407 <Popover trigger="hover" placement="top" isLazy>
1408 <PopoverTrigger>
1409 <HStack
1410 spacing="5px"
1411 cursor="not-allowed"
1412 opacity={0.5}
1413 >
1414 <Icon
1415 as={FiRefreshCw}
1416 boxSize="12px"
1417 color="#9ca3af"
1418 />
1419 <Text
1420 fontSize="12px"
1421 color="#9ca3af"
1422 margin="0"
1423 fontWeight="500"
1424 >
1425 {__('Redo', 'everest-forms')}
1426 </Text>
1427 </HStack>
1428 </PopoverTrigger>
1429 <PopoverContent
1430 w="auto"
1431 maxW="220px"
1432 zIndex={200000}
1433 _focus={{ outline: 'none' }}
1434 >
1435 <PopoverArrow />
1436 <PopoverBody p={3}>
1437 <LimitReachedBody tier={limitTier} />
1438 </PopoverBody>
1439 </PopoverContent>
1440 </Popover>
1441 ) : (
1442 <HStack
1443 spacing="5px"
1444 cursor={
1445 isRegenerating ? 'not-allowed' : 'pointer'
1446 }
1447 opacity={isRegenerating ? 0.5 : 1}
1448 _hover={{
1449 opacity: isRegenerating ? 0.5 : 0.65,
1450 }}
1451 onClick={
1452 isRegenerating ? undefined : handleRegenerate
1453 }
1454 >
1455 {isRegenerating && isLastAssistant ? (
1456 <Spinner
1457 size="xs"
1458 color="#9ca3af"
1459 thickness="2px"
1460 speed="0.65s"
1461 />
1462 ) : (
1463 <Icon
1464 as={FiRefreshCw}
1465 boxSize="12px"
1466 color="#9ca3af"
1467 />
1468 )}
1469 <Text
1470 fontSize="12px"
1471 color="#9ca3af"
1472 margin="0"
1473 fontWeight="500"
1474 >
1475 {__('Redo', 'everest-forms')}
1476 </Text>
1477 </HStack>
1478 )}
1479 </HStack>
1480 </>
1481 )}
1482 </Box>
1483 </HStack>
1484 );
1485 })}
1486 </Box>
1487
1488 <Box borderTop="1px solid #e2e8f0" p="16px">
1489 <Box
1490 border="1px solid #e2e8f0"
1491 borderRadius="12px"
1492 bg="white"
1493 overflow="hidden"
1494 transition="border-color 0.2s"
1495 _focusWithin={{ borderColor: '#7545BB' }}
1496 >
1497 <Textarea
1498 placeholder={__('Refine or follow up…', 'everest-forms')}
1499 border="none"
1500 fontSize="14px"
1501 color="#333"
1502 _focus={{ boxShadow: 'none' }}
1503 p="12px 16px"
1504 minHeight="56px"
1505 resize="none"
1506 _placeholder={{ color: '#c0c0cc' }}
1507 value={refinePrompt}
1508 isDisabled={isRegenerating}
1509 onChange={(e) =>
1510 setRefinePrompt(e.target.value.slice(0, MAX_CHARS))
1511 }
1512 onKeyDown={(e) => {
1513 if (e.key === 'Enter' && !e.shiftKey && !isRateLimited) {
1514 e.preventDefault();
1515 handleUpdate(refinePrompt);
1516 }
1517 }}
1518 />
1519 <Flex justify="flex-end" px="12px" pb="10px">
1520 {isRateLimited ? (
1521 <Popover trigger="hover" placement="top" isLazy>
1522 <PopoverTrigger>
1523 <Box
1524 as="button"
1525 w="28px"
1526 h="28px"
1527 bg="#c9bce4"
1528 borderRadius="6px"
1529 display="inline-flex"
1530 alignItems="center"
1531 justifyContent="center"
1532 border="none"
1533 cursor="not-allowed"
1534 transition="background 0.2s"
1535 >
1536 <Icon as={FiArrowUp} boxSize="3.5" color="white" />
1537 </Box>
1538 </PopoverTrigger>
1539 <PopoverContent
1540 w="auto"
1541 maxW="220px"
1542 zIndex={200000}
1543 _focus={{ outline: 'none' }}
1544 >
1545 <PopoverArrow />
1546 <PopoverBody p={3}>
1547 <LimitReachedBody tier={limitTier} />
1548 </PopoverBody>
1549 </PopoverContent>
1550 </Popover>
1551 ) : (
1552 <Box
1553 as="button"
1554 w="28px"
1555 h="28px"
1556 bg={
1557 refinePrompt.trim() && !isRegenerating
1558 ? '#7545BB'
1559 : '#c9bce4'
1560 }
1561 borderRadius="6px"
1562 display="inline-flex"
1563 alignItems="center"
1564 justifyContent="center"
1565 border="none"
1566 cursor={
1567 refinePrompt.trim() && !isRegenerating
1568 ? 'pointer'
1569 : 'not-allowed'
1570 }
1571 _hover={{
1572 bg:
1573 refinePrompt.trim() && !isRegenerating
1574 ? '#6a3daa'
1575 : '#c9bce4',
1576 }}
1577 transition="background 0.2s"
1578 onClick={() => handleUpdate(refinePrompt)}
1579 >
1580 {isRegenerating ? (
1581 <Spinner
1582 size="xs"
1583 color="white"
1584 thickness="2px"
1585 speed="0.65s"
1586 />
1587 ) : (
1588 <Icon as={FiArrowUp} boxSize="3.5" color="white" />
1589 )}
1590 </Box>
1591 )}
1592 </Flex>
1593 </Box>
1594 {(usage || usageLoading) && (
1595 <Flex justify="flex-end" mt="10px" pr="2px">
1596 <CreditsPill usage={usage} loading={usageLoading} />
1597 </Flex>
1598 )}
1599 </Box>
1600 </Flex>
1601
1602 <Box
1603 flex={1}
1604 bg="#f6f6f8"
1605 display="flex"
1606 flexDirection="column"
1607 overflow="hidden"
1608 opacity={isRegenerating ? 0.5 : 1}
1609 transition="opacity 0.3s"
1610 >
1611 <Box flex={1} overflowY="auto" p="24px">
1612 <Box
1613 bg="white"
1614 border="1px solid #e2e8f0"
1615 borderRadius="16px"
1616 overflow="hidden"
1617 mb="0"
1618 position="relative"
1619 transition="border-color 0.3s"
1620 >
1621 <Flex
1622 align="center"
1623 px="24px"
1624 py="16px"
1625 borderBottom="1px solid #e2e8f0"
1626 justify="space-between"
1627 >
1628 <HStack spacing="10px">
1629 <Box
1630 w="32px"
1631 h="32px"
1632 bg="rgba(117,69,187,0.1)"
1633 borderRadius="8px"
1634 display="flex"
1635 alignItems="center"
1636 justifyContent="center"
1637 >
1638 <Icon as={FiEdit3} boxSize="15px" color="#7545BB" />
1639 </Box>
1640 <Text
1641 fontSize="16px"
1642 fontWeight="600"
1643 color="#0e0e0e"
1644 margin="0"
1645 >
1646 {formTitle || __('AI Generated Form', 'everest-forms')}
1647 </Text>
1648 </HStack>
1649 {isRegenerating && (
1650 <HStack
1651 spacing="5px"
1652 sx={{ animation: `${fadeUp} 0.2s ease` }}
1653 >
1654 {[0, 1, 2].map((d) => (
1655 <Box
1656 key={d}
1657 w="5px"
1658 h="5px"
1659 borderRadius="full"
1660 bg="#7545BB"
1661 sx={{
1662 animation: `${dotBounce} 1.1s ease-in-out ${d * 0.2}s infinite`,
1663 }}
1664 />
1665 ))}
1666 <Text
1667 fontSize="11px"
1668 color="#7545BB"
1669 margin="0"
1670 fontWeight="500"
1671 >
1672 {__('Regenerating', 'everest-forms')}
1673 </Text>
1674 </HStack>
1675 )}
1676 </Flex>
1677
1678 <Box
1679 position="relative"
1680 minH={`${MIN_PREVIEW_HEIGHT}px`}
1681 pointerEvents={
1682 isRegenerating || isCreatingForm ? 'none' : 'auto'
1683 }
1684 >
1685 {isPreviewLoading && (
1686 <Box
1687 position="absolute"
1688 inset="0"
1689 bg="white"
1690 zIndex={1}
1691 p="24px"
1692 >
1693 <VStack spacing="20px" align="stretch">
1694 {Array.from({ length: 5 }).map((_, idx) => (
1695 <SkeletonField key={idx} delay={`${idx * 0.1}s`} />
1696 ))}
1697 </VStack>
1698 </Box>
1699 )}
1700 {previewSrc && (
1701 <iframe
1702 key={previewReloadKey}
1703 src={previewSrc}
1704 title={__('Form preview', 'everest-forms')}
1705 onLoad={(e) => {
1706 setIsPreviewLoading(false);
1707 previewResizeObserverRef.current?.disconnect();
1708 try {
1709 const doc = (e.target as HTMLIFrameElement)
1710 .contentDocument;
1711 if (!doc) return;
1712 const measure = () =>
1713 setPreviewHeight(
1714 Math.max(
1715 MIN_PREVIEW_HEIGHT,
1716 doc.documentElement.scrollHeight,
1717 ),
1718 );
1719 measure();
1720 // Catch late layout shifts (web fonts, images,
1721 // conditional-logic field toggles) — same-origin,
1722 // so this is safe DOM access, not a postMessage bridge.
1723 const ro = new ResizeObserver(measure);
1724 ro.observe(doc.documentElement);
1725 previewResizeObserverRef.current = ro;
1726 } catch (err) {
1727 // Cross-origin or blocked — keep the default height.
1728 }
1729 }}
1730 style={{
1731 display: 'block',
1732 width: '100%',
1733 height: `${previewHeight}px`,
1734 border: 'none',
1735 }}
1736 />
1737 )}
1738 {/* Real front-end render — deliberately "look, don't touch": this
1739 overlay blocks interaction with the iframe's actual form inputs
1740 and shows the same "just a preview" tooltip clicking always did. */}
1741 <Box
1742 position="absolute"
1743 inset="0"
1744 onClick={handleFieldClick}
1745 cursor="default"
1746 />
1747 </Box>
1748 </Box>
1749 </Box>
1750 </Box>
1751 </Flex>
1752 </PageShell>
1753 );
1754 }
1755
1756 return (
1757 <PageShell onBack={onBack}>
1758 <Flex
1759 flex="1"
1760 direction="column"
1761 align="center"
1762 justify="center"
1763 px="24px"
1764 py="16px"
1765 >
1766 <Box width="100%" maxW="896px">
1767 <VStack spacing="8px" mb="14px" textAlign="center">
1768 <Heading
1769 as="h1"
1770 fontSize={{ base: '24px', md: '32px' }}
1771 fontWeight="600"
1772 color="#0e0e0e"
1773 lineHeight="1.15"
1774 m="0"
1775 letterSpacing="-0.02em"
1776 >
1777 {__('What should we build today?', 'everest-forms')}
1778 </Heading>
1779 <Text fontSize="15px" color="#6b6b6b" m="0" lineHeight="1.55">
1780 {__(
1781 "Describe your form in your own words. We'll handle the fields, logic and layout — in seconds.",
1782 'everest-forms',
1783 )}
1784 </Text>
1785 </VStack>
1786
1787 <Box
1788 borderRadius="16px"
1789 border="1px solid #e2e8f0"
1790 bg="white"
1791 boxShadow="0 1px 2px rgba(15,15,15,0.04)"
1792 mb="8px"
1793 >
1794 <Flex align="flex-start" gap="12px" px="20px" pt="20px">
1795 <Icon
1796 as={BsStars}
1797 boxSize="5"
1798 color="#c5c0d0"
1799 mt="2px"
1800 flexShrink={0}
1801 />
1802 <Textarea
1803 ref={promptInputRef}
1804 autoFocus
1805 value={prompt}
1806 onChange={(e) => setPrompt(e.target.value.slice(0, MAX_CHARS))}
1807 placeholder={__(
1808 'A feedback form to learn how onboarding felt, with a 1–5 rating and one optional comment…',
1809 'everest-forms',
1810 )}
1811 border="none"
1812 resize="none"
1813 fontSize="15px"
1814 lineHeight="1.6"
1815 color="#181818"
1816 p="0"
1817 minH="90px"
1818 bg="transparent"
1819 _focus={{ boxShadow: 'none', outline: 'none', border: 'none' }}
1820 _placeholder={{ color: '#9a9a9a' }}
1821 />
1822 </Flex>
1823
1824 <Box mx="20px" borderTop="1px solid #f1f5f9" />
1825
1826 <Flex align="center" justify="space-between" px="16px" py="12px" gap="12px">
1827 <CreditsPill usage={usage} loading={usageLoading} />
1828 {isRateLimited ? (
1829 <Popover trigger="hover" placement="top" isLazy>
1830 <PopoverTrigger>
1831 <Box
1832 as="button"
1833 display="inline-flex"
1834 alignItems="center"
1835 gap="8px"
1836 fontSize="14px"
1837 fontWeight="500"
1838 pl="16px"
1839 pr="8px"
1840 py="8px"
1841 ml="auto"
1842 borderRadius="8px"
1843 bg="#e6e3ee"
1844 color="#9a9a9a"
1845 cursor="not-allowed"
1846 border="none"
1847 transition="background 0.2s"
1848 >
1849 <Text margin="0" color="#9a9a9a">
1850 {__('Generate', 'everest-forms')}
1851 </Text>
1852 <Box
1853 w="24px"
1854 h="24px"
1855 borderRadius="6px"
1856 display="inline-flex"
1857 alignItems="center"
1858 justifyContent="center"
1859 bg="#d5d0e0"
1860 >
1861 <Icon as={FiArrowUp} boxSize="3.5" color="#9a9a9a" />
1862 </Box>
1863 </Box>
1864 </PopoverTrigger>
1865 <PopoverContent
1866 w="auto"
1867 maxW="220px"
1868 zIndex={200000}
1869 _focus={{ outline: 'none' }}
1870 >
1871 <PopoverArrow />
1872 <PopoverBody p={3}>
1873 <LimitReachedBody tier={limitTier} />
1874 </PopoverBody>
1875 </PopoverContent>
1876 </Popover>
1877 ) : (
1878 <Box
1879 as="button"
1880 display="inline-flex"
1881 alignItems="center"
1882 gap="8px"
1883 fontSize="14px"
1884 fontWeight="500"
1885 pl="16px"
1886 pr="8px"
1887 py="8px"
1888 ml="auto"
1889 borderRadius="8px"
1890 bg={hasPrompt ? '#7545BB' : '#e6e3ee'}
1891 color={hasPrompt ? 'white' : '#9a9a9a'}
1892 cursor={hasPrompt ? 'pointer' : 'not-allowed'}
1893 border="none"
1894 onClick={hasPrompt ? handleGenerate : undefined}
1895 _hover={hasPrompt ? { bg: '#6a3daa' } : {}}
1896 transition="background 0.2s"
1897 >
1898 <Text margin="0" color={hasPrompt ? 'white' : '#9a9a9a'}>
1899 {__('Generate', 'everest-forms')}
1900 </Text>
1901 <Box
1902 w="24px"
1903 h="24px"
1904 borderRadius="6px"
1905 display="inline-flex"
1906 alignItems="center"
1907 justifyContent="center"
1908 bg={hasPrompt ? '#6a3daa' : '#d5d0e0'}
1909 >
1910 <Icon
1911 as={FiArrowUp}
1912 boxSize="3.5"
1913 color={hasPrompt ? 'white' : '#9a9a9a'}
1914 />
1915 </Box>
1916 </Box>
1917 )}
1918 </Flex>
1919 </Box>
1920
1921 <Text
1922 textAlign="right"
1923 fontSize="11px"
1924 color="#9a9a9a"
1925 m="0"
1926 pr="4px"
1927 mb="14px"
1928 >
1929 {prompt.length}/{MAX_CHARS}
1930 </Text>
1931
1932 <Box>
1933 <Flex align="flex-end" justify="space-between" mb="10px">
1934 <Heading
1935 as="h2"
1936 fontSize="20px"
1937 fontWeight="600"
1938 color="#0e0e0e"
1939 m="0"
1940 letterSpacing="-0.01em"
1941 >
1942 {__('Need inspiration?', 'everest-forms')}
1943 </Heading>
1944 <Text fontSize="13px" fontWeight="500" color="#9a9a9a" m="0">
1945 {__('Tap a card to use as starting point', 'everest-forms')}
1946 </Text>
1947 </Flex>
1948
1949 <SimpleGrid columns={{ base: 1, sm: 2 }} spacing="8px">
1950 {INSPIRATION_CARDS.map((card) => (
1951 <Box
1952 key={card.title}
1953 as="button"
1954 textAlign="left"
1955 display="block"
1956 width="100%"
1957 border="1px solid rgba(226,232,240,0.8)"
1958 bg="white"
1959 px="16px"
1960 py="12px"
1961 borderRadius="8px"
1962 cursor="pointer"
1963 onClick={() => setPrompt(card.prompt)}
1964 _hover={{
1965 borderColor: 'rgba(117,69,187,0.4)',
1966 bg: 'rgba(117,69,187,0.02)',
1967 }}
1968 >
1969 <Text
1970 fontSize="14px"
1971 fontWeight="600"
1972 color="#0e0e0e"
1973 m="0 0 4px"
1974 lineHeight="1.4"
1975 >
1976 {card.title}
1977 </Text>
1978 <Text
1979 fontSize="12.5px"
1980 color="#6b6b6b"
1981 m="0"
1982 lineHeight="1.4"
1983 >
1984 {card.description}
1985 </Text>
1986 </Box>
1987 ))}
1988 </SimpleGrid>
1989 </Box>
1990 </Box>
1991 </Flex>
1992 </PageShell>
1993 );
1994 };
1995
1996 export default CreateWithAI;
1997