import { Box, Flex, HStack, Heading, Icon, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, SimpleGrid, Spinner, Text, Textarea, VStack, keyframes, useToast, } from '@chakra-ui/react'; import apiFetch from '@wordpress/api-fetch'; import { __, sprintf } from '@wordpress/i18n'; import React, { useEffect, useRef, useState } from 'react'; import { BsStars } from 'react-icons/bs'; import { FiArrowLeft, FiArrowUp, FiCheck, FiEdit3, FiRefreshCw, } from 'react-icons/fi'; import { templatesScriptData } from '../utils/global'; const pulseGlow = keyframes` 0% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); } 50% { box-shadow: 0 0 28px 10px rgba(117,69,187,0.12); transform: scale(1.06); } 100% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); } `; const dotBounce = keyframes` 0%, 80%, 100% { transform: scale(0.35); opacity: 0.3; } 40% { transform: scale(1); opacity: 1; } `; const fadeUp = keyframes` from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } `; const shimmer = keyframes` 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } `; const INSPIRATION_CARDS = [ { title: __('Online store checkout', 'everest-forms'), description: __( 'Product selection with prices, quantity, coupon code, order total, and payment gateway.', 'everest-forms', ), prompt: '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.', }, { title: __('Subscription sign-up', 'everest-forms'), description: __( 'Subscription plan selection, billing details, and payment gateway for a recurring membership.', 'everest-forms', ), prompt: '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.', }, { title: __('Event ticket purchase', 'everest-forms'), description: __( 'Ticket type with pricing, quantity, coupon code, total, and payment gateway.', 'everest-forms', ), prompt: '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.', }, { title: __('Freelance contract & NDA', 'everest-forms'), description: __( 'Client details, project scope, agreed rate, contract terms, and digital signature.', 'everest-forms', ), prompt: '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.', }, { title: __('Health & wellness check-in', 'everest-forms'), description: __( 'Pain, energy, and stress levels via range sliders, symptoms checklist, and notes.', 'everest-forms', ), prompt: '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.', }, { title: __('User account registration', 'everest-forms'), description: __( 'Username, email, password with confirmation, profile photo, and preferences.', 'everest-forms', ), prompt: '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.', }, ]; const GEN_STEPS = [ __('Understanding your prompt', 'everest-forms'), __('Designing field structure', 'everest-forms'), __('Configuring validation & logic', 'everest-forms'), __('Finalizing your form', 'everest-forms'), ]; const MAX_CHARS = 500; const UPGRADE_URL = 'https://everestforms.net/upgrade/?utm_source=evf-free&utm_medium=ai-form-builder&utm_campaign=ai-rate-limit&utm_content=Upgrade+to+Pro'; const SkeletonField: React.FC<{ delay?: string }> = ({ delay = '0s' }) => ( ); const PageShell: React.FC<{ onBack: () => void; backLabel?: string; headerRight?: React.ReactNode; children: React.ReactNode; }> = ({ onBack, backLabel, headerRight, children }) => { useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, []); return ( {backLabel || __('Create with AI', 'everest-forms')} {headerRight} {children} ); }; interface CreateWithAIProps { onBack: () => void; initialFormId?: number; initialTitle?: string; } interface ChatMessage { role: 'user' | 'assistant'; text: string; loading?: boolean; error?: boolean; notice?: boolean; noticeUrl?: string; } const { restURL, security, ajaxUrl, aiNonce } = templatesScriptData; const callAi = async ( action: string, data: Record = {}, ): Promise => { const body = new URLSearchParams(); body.append('action', action); body.append('nonce', aiNonce); Object.entries(data).forEach(([key, value]) => body.append(key, String(value)), ); const response = await fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }); return response.json(); }; const CreateWithAI: React.FC = ({ onBack, initialFormId, initialTitle, }) => { const toast = useToast(); const [prompt, setPrompt] = useState(''); const [isRateLimited, setIsRateLimited] = useState(false); const [genState, setGenState] = useState<'idle' | 'generating' | 'generated'>( 'idle', ); const [genStep, setGenStep] = useState(-1); const [hint, setHint] = useState({ show: false, x: 0, y: 0 }); const [isRegenerating, setIsRegenerating] = useState(false); const [isCreatingForm, setIsCreatingForm] = useState(false); const [previewHTML, setPreviewHTML] = useState(''); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [formId, setFormId] = useState(0); const [formTitle, setFormTitle] = useState(initialTitle || ''); const [editUrl, setEditUrl] = useState(''); const [multiPartSteps, setMultiPartSteps] = useState([]); const [activePartTab, setActivePartTab] = useState(0); const [refinePrompt, setRefinePrompt] = useState(''); const [previewVersion, setPreviewVersion] = useState(0); const [messages, setMessages] = useState([]); const previewHintTimer = React.useRef | null>( null, ); const aiResponseRef = React.useRef(null); const promptInputRef = React.useRef(null); const canvasRef = useRef(null); const previewHTMLRef = React.useRef(''); const previewFetchStartedRef = React.useRef(false); useEffect(() => { if (!canvasRef.current || multiPartSteps.length === 0) return; const rows = canvasRef.current.querySelectorAll( '.evf-admin-row[data-part-id]', ); const target = activePartTab + 1; rows.forEach((row) => { const pid = parseInt(row.dataset.partId || '1', 10); row.style.display = pid === target ? '' : 'none'; }); }, [activePartTab, previewHTML, multiPartSteps]); useEffect(() => { if (genState !== 'idle') return; const t = setTimeout(() => promptInputRef.current?.focus(), 50); return () => clearTimeout(t); }, [genState]); useEffect(() => { if (typeof initialFormId !== 'number' || initialFormId <= 0) return; setFormId(initialFormId); setPrompt(initialTitle || __('this form', 'everest-forms')); setMessages([ { role: 'assistant', text: initialTitle ? /* translators: %s: template name. */ sprintf( __( 'Loaded the “%s” template. Tell me what to change — add fields, reword labels, or anything else.', 'everest-forms', ), initialTitle, ) : __('Loaded your form. Tell me what to change.', 'everest-forms'), }, ]); setGenState('generated'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialFormId]); const handleUseThisForm = async () => { if (isCreatingForm || !formId) return; setIsCreatingForm(true); try { const res = await callAi('evf_ai_activate_form', { form_id: formId }); if (res?.success && res.data?.edit_url) { window.location.href = res.data.edit_url; } else if (editUrl) { window.location.href = editUrl; } else { throw new Error(res?.data?.message || 'Unexpected response'); } } catch (e: any) { setIsCreatingForm(false); toast({ title: __('Error', 'everest-forms'), description: e?.message || __('Could not open the form. Please try again.', 'everest-forms'), status: 'error', position: 'bottom', duration: 5000, isClosable: true, variant: 'subtle', }); } }; const resolveLoading = ( text: string, isError = false, isNotice = false, noticeUrl = '', ) => setMessages((m) => { const next = [...m]; for (let i = next.length - 1; i >= 0; i--) { if (next[i].role === 'assistant' && next[i].loading) { next[i] = { role: 'assistant', text, error: isError, notice: isNotice, noticeUrl, }; break; } } return next; }); const handleUpdate = async (refinePromptText: string = '') => { if (isRegenerating || !formId || !prompt.trim()) return; const refine = (refinePromptText || '').trim(); const userText = refine || __('Regenerate the form', 'everest-forms'); setMessages((m) => [ ...m, { role: 'user', text: userText }, { role: 'assistant', text: '', loading: true }, ]); setRefinePrompt(''); setIsRegenerating(true); try { const res = await callAi('evf_ai_update_form', { form_id: formId, prompt, refine_prompt: refine, }); if (res?.success) { if (res.data?.form_title) setFormTitle(res.data.form_title); if (res.data?.multi_part_steps) { setMultiPartSteps(res.data.multi_part_steps); setActivePartTab(0); } const notice = res.data?.notice || ''; const noticeUrl = res.data?.notice_url || ''; const doneText = refine ? __( "Done — I've updated your form. Check the preview on the right.", 'everest-forms', ) : __("Here's a fresh version of your form.", 'everest-forms'); if (notice && !messages.some((m) => m.notice && m.text === notice)) { setMessages((m) => { const next = [...m]; for (let i = next.length - 1; i >= 0; i--) { if (next[i].role === 'assistant' && next[i].loading) { next[i] = { role: 'assistant', text: doneText }; break; } } return [ ...next, { role: 'assistant', text: notice, notice: true, noticeUrl }, ]; }); } else { resolveLoading(doneText, false, false, ''); } if (res.data?.preview_html) { previewHTMLRef.current = res.data.preview_html; setPreviewHTML(res.data.preview_html); } else { setPreviewVersion((v) => v + 1); } } else { if (res?.data?.code === 'daily_limit_reached') { setIsRateLimited(true); } throw new Error( res?.data?.message || __('Could not update the form. Please try again.', 'everest-forms'), ); } } catch (e: any) { const message = e?.message || __('Could not update the form. Please try again.', 'everest-forms'); resolveLoading(message, true); } finally { setIsRegenerating(false); } }; const handleRegenerate = () => handleUpdate(); const handleFieldClick = (e: React.MouseEvent) => { const { clientX, clientY } = e; setHint({ show: true, x: clientX, y: clientY }); if (previewHintTimer.current) clearTimeout(previewHintTimer.current); previewHintTimer.current = setTimeout( () => setHint((h) => ({ ...h, show: false })), 2600, ); }; const hasPrompt = prompt.trim().length > 0; useEffect(() => { if (genState !== 'generating') return; setGenStep(-1); aiResponseRef.current = null; previewHTMLRef.current = ''; previewFetchStartedRef.current = false; setPreviewHTML(''); let cancelled = false; let intervalId: ReturnType | null = null; const showError = (message: string, isRateLimit = false) => { if (cancelled) return; if (intervalId) clearInterval(intervalId); if (isRateLimit) setIsRateLimited(true); setGenState('idle'); toast({ title: isRateLimit ? __('Daily limit reached', 'everest-forms') : __('AI generation failed', 'everest-forms'), description: isRateLimit ? ( {message} {__('Upgrade to Pro →', 'everest-forms')} ) : ( message ), status: 'error', position: 'bottom', duration: 5000, isClosable: true, variant: 'subtle', }); }; callAi('evf_ai_generate_form', { prompt }) .then((res: any) => { if (res?.success && res.data?.form_id) { const html = res.data.preview_html || ''; if (html) { previewHTMLRef.current = html; previewFetchStartedRef.current = true; setPreviewHTML(html); setIsPreviewLoading(false); } aiResponseRef.current = { ok: true, formId: res.data.form_id, formTitle: res.data.form_title || '', editUrl: res.data.edit_url || '', multiPartSteps: res.data.multi_part_steps || [], notice: res.data.notice || '', noticeUrl: res.data.notice_url || '', }; } else { showError( res?.data?.message || __('Something went wrong. Please try again.', 'everest-forms'), res?.data?.code === 'daily_limit_reached', ); } }) .catch(() => { showError( __( 'Could not reach the AI service. Please try again.', 'everest-forms', ), ); }); let step = -1; intervalId = setInterval(() => { step += 1; setGenStep(step); if (step >= GEN_STEPS.length - 1) { if (intervalId) clearInterval(intervalId); const finish = () => { if (cancelled) return; const result = aiResponseRef.current; if (!result) { setTimeout(finish, 200); return; } if (result.ok) { setFormId(result.formId); setFormTitle(result.formTitle || ''); setEditUrl(result.editUrl); setMultiPartSteps(result.multiPartSteps || []); setActivePartTab(0); setMessages([ { role: 'user', text: prompt }, { role: 'assistant', text: result.notice || __( 'Here\'s your form! Review the preview on the right and click "Use This Form" when you\'re happy with it.', 'everest-forms', ), notice: !!result.notice, noticeUrl: result.noticeUrl || '', }, ]); setGenState('generated'); } }; setTimeout(finish, 700); } }, 950); return () => { cancelled = true; if (intervalId) clearInterval(intervalId); }; }, [genState]); useEffect(() => { if (genState !== 'generated' || !formId) return; if ( previewVersion === 0 && (previewHTMLRef.current || previewFetchStartedRef.current) ) return; let cancelled = false; setIsPreviewLoading(true); apiFetch({ path: `${restURL}everest-forms/v1/templates/ai-preview`, method: 'POST', data: { form_id: formId }, headers: { 'X-WP-Nonce': security }, }) .then((res: any) => { if (!cancelled && res?.success && res?.data?.html) { previewHTMLRef.current = res.data.html; setPreviewHTML(res.data.html); } }) .catch(() => {}) .finally(() => { if (!cancelled) setIsPreviewLoading(false); }); return () => { cancelled = true; }; }, [genState, formId, previewVersion]); const handleGenerate = () => { if (!hasPrompt) return; setGenState('generating'); }; if (genState === 'generating') { return ( {__('Building your form…', 'everest-forms')} {__('This usually takes a few seconds', 'everest-forms')} {GEN_STEPS.map((step, i) => { const isDone = i < genStep; const isActive = i === genStep; const isPending = i > genStep; return ( {isDone && ( )} {isActive && ( )} {step} {isActive && ( {[0, 1, 2].map((d) => ( ))} )} ); })} ); } if (genState === 'generated') { let lastAssistantIdx = -1; let useThisFormIdx = -1; messages.forEach((m, i) => { if (m.role === 'assistant' && !m.loading) { lastAssistantIdx = i; if (!m.error) useThisFormIdx = i; } }); return ( setGenState('idle')} backLabel={__('New Prompt', 'everest-forms')} headerRight={ {isCreatingForm && ( )} {isCreatingForm ? __('Creating…', 'everest-forms') : __('Use This Form', 'everest-forms')} } > {hint.show && ( {__('This is just a preview of your form.', 'everest-forms')} {__('Click "Use This Form" to start editing.', 'everest-forms')} )} {messages.map((msg, idx) => { if (msg.role === 'user') { return ( {msg.text} ); } const isLastAssistant = idx === lastAssistantIdx; const isUseThisFormMsg = idx === useThisFormIdx; return ( {msg.loading ? ( {__('Updating your form…', 'everest-forms')} ) : ( <> {msg.text} {msg.noticeUrl && ( {__('Upgrade to Pro →', 'everest-forms')} )} {isUseThisFormMsg && ( {isCreatingForm && ( )} {isCreatingForm ? __('Creating…', 'everest-forms') : __('Use This Form', 'everest-forms')} )} {isRateLimited ? ( {__('Redo', 'everest-forms')} {__( "You've reached your daily free limit.", 'everest-forms', )} {__('Upgrade to Pro →', 'everest-forms')} ) : ( {isRegenerating && isLastAssistant ? ( ) : ( )} {__('Redo', 'everest-forms')} )} )} ); })}