CreateFormCTA.tsx
2 weeks ago
CreateWithAI.tsx
3 weeks ago
Main.tsx
3 weeks ago
PluginStatus.tsx
3 weeks ago
Sidebar.tsx
3 weeks ago
TemplateList.tsx
3 weeks ago
TemplatesSkeleton.tsx
4 months ago
CreateWithAI.tsx
1794 lines
| 1 | import { |
| 2 | Box, |
| 3 | Flex, |
| 4 | HStack, |
| 5 | Heading, |
| 6 | Icon, |
| 7 | Popover, |
| 8 | PopoverArrow, |
| 9 | PopoverBody, |
| 10 | PopoverContent, |
| 11 | PopoverTrigger, |
| 12 | SimpleGrid, |
| 13 | Spinner, |
| 14 | Text, |
| 15 | Textarea, |
| 16 | VStack, |
| 17 | keyframes, |
| 18 | useToast, |
| 19 | } from '@chakra-ui/react'; |
| 20 | import apiFetch from '@wordpress/api-fetch'; |
| 21 | import { __, sprintf } from '@wordpress/i18n'; |
| 22 | import React, { useEffect, useRef, useState } from 'react'; |
| 23 | import { BsStars } from 'react-icons/bs'; |
| 24 | import { |
| 25 | FiArrowLeft, |
| 26 | FiArrowUp, |
| 27 | FiCheck, |
| 28 | FiEdit3, |
| 29 | FiRefreshCw, |
| 30 | } from 'react-icons/fi'; |
| 31 | import { templatesScriptData } from '../utils/global'; |
| 32 | |
| 33 | const pulseGlow = keyframes` |
| 34 | 0% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); } |
| 35 | 50% { box-shadow: 0 0 28px 10px rgba(117,69,187,0.12); transform: scale(1.06); } |
| 36 | 100% { box-shadow: 0 0 0 0 rgba(117,69,187,0.3); transform: scale(1); } |
| 37 | `; |
| 38 | |
| 39 | const dotBounce = keyframes` |
| 40 | 0%, 80%, 100% { transform: scale(0.35); opacity: 0.3; } |
| 41 | 40% { transform: scale(1); opacity: 1; } |
| 42 | `; |
| 43 | |
| 44 | const fadeUp = keyframes` |
| 45 | from { opacity: 0; transform: translateY(12px); } |
| 46 | to { opacity: 1; transform: translateY(0); } |
| 47 | `; |
| 48 | |
| 49 | const shimmer = keyframes` |
| 50 | 0% { background-position: -400px 0; } |
| 51 | 100% { background-position: 400px 0; } |
| 52 | `; |
| 53 | |
| 54 | const INSPIRATION_CARDS = [ |
| 55 | { |
| 56 | title: __('Online store checkout', 'everest-forms'), |
| 57 | description: __( |
| 58 | 'Product selection with prices, quantity, coupon code, order total, and payment gateway.', |
| 59 | 'everest-forms', |
| 60 | ), |
| 61 | prompt: |
| 62 | '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.', |
| 63 | }, |
| 64 | { |
| 65 | title: __('Subscription sign-up', 'everest-forms'), |
| 66 | description: __( |
| 67 | 'Subscription plan selection, billing details, and payment gateway for a recurring membership.', |
| 68 | 'everest-forms', |
| 69 | ), |
| 70 | prompt: |
| 71 | '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.', |
| 72 | }, |
| 73 | { |
| 74 | title: __('Event ticket purchase', 'everest-forms'), |
| 75 | description: __( |
| 76 | 'Ticket type with pricing, quantity, coupon code, total, and payment gateway.', |
| 77 | 'everest-forms', |
| 78 | ), |
| 79 | prompt: |
| 80 | '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.', |
| 81 | }, |
| 82 | { |
| 83 | title: __('Freelance contract & NDA', 'everest-forms'), |
| 84 | description: __( |
| 85 | 'Client details, project scope, agreed rate, contract terms, and digital signature.', |
| 86 | 'everest-forms', |
| 87 | ), |
| 88 | prompt: |
| 89 | '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.', |
| 90 | }, |
| 91 | { |
| 92 | title: __('Health & wellness check-in', 'everest-forms'), |
| 93 | description: __( |
| 94 | 'Pain, energy, and stress levels via range sliders, symptoms checklist, and notes.', |
| 95 | 'everest-forms', |
| 96 | ), |
| 97 | prompt: |
| 98 | '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.', |
| 99 | }, |
| 100 | { |
| 101 | title: __('User account registration', 'everest-forms'), |
| 102 | description: __( |
| 103 | 'Username, email, password with confirmation, profile photo, and preferences.', |
| 104 | 'everest-forms', |
| 105 | ), |
| 106 | prompt: |
| 107 | '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.', |
| 108 | }, |
| 109 | ]; |
| 110 | |
| 111 | const GEN_STEPS = [ |
| 112 | __('Understanding your prompt', 'everest-forms'), |
| 113 | __('Designing field structure', 'everest-forms'), |
| 114 | __('Configuring validation & logic', 'everest-forms'), |
| 115 | __('Finalizing your form', 'everest-forms'), |
| 116 | ]; |
| 117 | |
| 118 | const MAX_CHARS = 500; |
| 119 | |
| 120 | const UPGRADE_URL = |
| 121 | 'https://everestforms.net/upgrade/?utm_source=evf-free&utm_medium=ai-form-builder&utm_campaign=ai-rate-limit&utm_content=Upgrade+to+Pro'; |
| 122 | |
| 123 | const SkeletonField: React.FC<{ delay?: string }> = ({ delay = '0s' }) => ( |
| 124 | <Box sx={{ animation: `${fadeUp} 0.4s ease ${delay} both` }}> |
| 125 | <Box |
| 126 | h="12px" |
| 127 | w="30%" |
| 128 | mb="8px" |
| 129 | borderRadius="4px" |
| 130 | sx={{ |
| 131 | background: |
| 132 | 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)', |
| 133 | backgroundSize: '400px 100%', |
| 134 | animation: `${shimmer} 1.6s ease-in-out infinite`, |
| 135 | }} |
| 136 | /> |
| 137 | <Box |
| 138 | h="36px" |
| 139 | borderRadius="6px" |
| 140 | sx={{ |
| 141 | background: |
| 142 | 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)', |
| 143 | backgroundSize: '400px 100%', |
| 144 | animation: `${shimmer} 1.6s ease-in-out infinite`, |
| 145 | }} |
| 146 | /> |
| 147 | </Box> |
| 148 | ); |
| 149 | |
| 150 | const PageShell: React.FC<{ |
| 151 | onBack: () => void; |
| 152 | backLabel?: string; |
| 153 | headerRight?: React.ReactNode; |
| 154 | children: React.ReactNode; |
| 155 | }> = ({ onBack, backLabel, headerRight, children }) => { |
| 156 | useEffect(() => { |
| 157 | const prev = document.body.style.overflow; |
| 158 | document.body.style.overflow = 'hidden'; |
| 159 | return () => { |
| 160 | document.body.style.overflow = prev; |
| 161 | }; |
| 162 | }, []); |
| 163 | return ( |
| 164 | <Flex |
| 165 | position="fixed" |
| 166 | top="0" |
| 167 | left="0" |
| 168 | right="0" |
| 169 | bottom="0" |
| 170 | zIndex={100000} |
| 171 | direction="column" |
| 172 | overflow="hidden" |
| 173 | bg="#f6f6f8" |
| 174 | > |
| 175 | <Box h="4px" bg="#7545BB" flexShrink={0} /> |
| 176 | <Flex |
| 177 | as="header" |
| 178 | align="center" |
| 179 | h="56px" |
| 180 | bg="white" |
| 181 | borderBottom="1px solid #e2e8f0" |
| 182 | px="24px" |
| 183 | flexShrink={0} |
| 184 | > |
| 185 | <HStack spacing="12px" flex="1"> |
| 186 | <Box |
| 187 | as="button" |
| 188 | w="32px" |
| 189 | h="32px" |
| 190 | display="inline-flex" |
| 191 | alignItems="center" |
| 192 | justifyContent="center" |
| 193 | borderRadius="8px" |
| 194 | color="#383838" |
| 195 | bg="transparent" |
| 196 | border="none" |
| 197 | cursor="pointer" |
| 198 | onClick={onBack} |
| 199 | _hover={{ bg: '#f8fafc' }} |
| 200 | transition="background 0.2s" |
| 201 | > |
| 202 | <Icon as={FiArrowLeft} boxSize="4" /> |
| 203 | </Box> |
| 204 | <Heading |
| 205 | as="h1" |
| 206 | fontSize="16px" |
| 207 | fontWeight="500" |
| 208 | color="#0e0e0e" |
| 209 | m="0" |
| 210 | > |
| 211 | {backLabel || __('Create with AI', 'everest-forms')} |
| 212 | </Heading> |
| 213 | </HStack> |
| 214 | {headerRight} |
| 215 | </Flex> |
| 216 | <Box flex={1} overflowY="auto" display="flex" flexDirection="column"> |
| 217 | {children} |
| 218 | </Box> |
| 219 | </Flex> |
| 220 | ); |
| 221 | }; |
| 222 | |
| 223 | interface CreateWithAIProps { |
| 224 | onBack: () => void; |
| 225 | initialFormId?: number; |
| 226 | initialTitle?: string; |
| 227 | } |
| 228 | |
| 229 | interface ChatMessage { |
| 230 | role: 'user' | 'assistant'; |
| 231 | text: string; |
| 232 | loading?: boolean; |
| 233 | error?: boolean; |
| 234 | notice?: boolean; |
| 235 | noticeUrl?: string; |
| 236 | } |
| 237 | |
| 238 | const { restURL, security, ajaxUrl, aiNonce } = templatesScriptData; |
| 239 | |
| 240 | const callAi = async ( |
| 241 | action: string, |
| 242 | data: Record<string, string | number> = {}, |
| 243 | ): Promise<any> => { |
| 244 | const body = new URLSearchParams(); |
| 245 | body.append('action', action); |
| 246 | body.append('nonce', aiNonce); |
| 247 | Object.entries(data).forEach(([key, value]) => |
| 248 | body.append(key, String(value)), |
| 249 | ); |
| 250 | |
| 251 | const response = await fetch(ajaxUrl, { |
| 252 | method: 'POST', |
| 253 | credentials: 'same-origin', |
| 254 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 255 | body: body.toString(), |
| 256 | }); |
| 257 | |
| 258 | return response.json(); |
| 259 | }; |
| 260 | |
| 261 | const CreateWithAI: React.FC<CreateWithAIProps> = ({ |
| 262 | onBack, |
| 263 | initialFormId, |
| 264 | initialTitle, |
| 265 | }) => { |
| 266 | const toast = useToast(); |
| 267 | const [prompt, setPrompt] = useState(''); |
| 268 | const [isRateLimited, setIsRateLimited] = useState(false); |
| 269 | const [genState, setGenState] = useState<'idle' | 'generating' | 'generated'>( |
| 270 | 'idle', |
| 271 | ); |
| 272 | const [genStep, setGenStep] = useState(-1); |
| 273 | const [hint, setHint] = useState({ show: false, x: 0, y: 0 }); |
| 274 | const [isRegenerating, setIsRegenerating] = useState(false); |
| 275 | const [isCreatingForm, setIsCreatingForm] = useState(false); |
| 276 | const [previewHTML, setPreviewHTML] = useState(''); |
| 277 | const [isPreviewLoading, setIsPreviewLoading] = useState(false); |
| 278 | const [formId, setFormId] = useState(0); |
| 279 | const [formTitle, setFormTitle] = useState(initialTitle || ''); |
| 280 | const [editUrl, setEditUrl] = useState(''); |
| 281 | const [multiPartSteps, setMultiPartSteps] = useState<string[]>([]); |
| 282 | const [activePartTab, setActivePartTab] = useState(0); |
| 283 | const [refinePrompt, setRefinePrompt] = useState(''); |
| 284 | const [previewVersion, setPreviewVersion] = useState(0); |
| 285 | const [messages, setMessages] = useState<ChatMessage[]>([]); |
| 286 | const previewHintTimer = React.useRef<ReturnType<typeof setTimeout> | null>( |
| 287 | null, |
| 288 | ); |
| 289 | const aiResponseRef = React.useRef<any>(null); |
| 290 | const promptInputRef = React.useRef<HTMLTextAreaElement | null>(null); |
| 291 | const canvasRef = useRef<HTMLDivElement | null>(null); |
| 292 | const previewHTMLRef = React.useRef(''); |
| 293 | const previewFetchStartedRef = React.useRef(false); |
| 294 | |
| 295 | useEffect(() => { |
| 296 | if (!canvasRef.current || multiPartSteps.length === 0) return; |
| 297 | const rows = canvasRef.current.querySelectorAll<HTMLElement>( |
| 298 | '.evf-admin-row[data-part-id]', |
| 299 | ); |
| 300 | const target = activePartTab + 1; |
| 301 | rows.forEach((row) => { |
| 302 | const pid = parseInt(row.dataset.partId || '1', 10); |
| 303 | row.style.display = pid === target ? '' : 'none'; |
| 304 | }); |
| 305 | }, [activePartTab, previewHTML, multiPartSteps]); |
| 306 | |
| 307 | useEffect(() => { |
| 308 | if (genState !== 'idle') return; |
| 309 | const t = setTimeout(() => promptInputRef.current?.focus(), 50); |
| 310 | return () => clearTimeout(t); |
| 311 | }, [genState]); |
| 312 | |
| 313 | useEffect(() => { |
| 314 | if (typeof initialFormId !== 'number' || initialFormId <= 0) return; |
| 315 | setFormId(initialFormId); |
| 316 | setPrompt(initialTitle || __('this form', 'everest-forms')); |
| 317 | setMessages([ |
| 318 | { |
| 319 | role: 'assistant', |
| 320 | text: initialTitle |
| 321 | ? /* translators: %s: template name. */ |
| 322 | sprintf( |
| 323 | __( |
| 324 | 'Loaded the “%s” template. Tell me what to change — add fields, reword labels, or anything else.', |
| 325 | 'everest-forms', |
| 326 | ), |
| 327 | initialTitle, |
| 328 | ) |
| 329 | : __('Loaded your form. Tell me what to change.', 'everest-forms'), |
| 330 | }, |
| 331 | ]); |
| 332 | setGenState('generated'); |
| 333 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 334 | }, [initialFormId]); |
| 335 | |
| 336 | const handleUseThisForm = async () => { |
| 337 | if (isCreatingForm || !formId) return; |
| 338 | setIsCreatingForm(true); |
| 339 | try { |
| 340 | const res = await callAi('evf_ai_activate_form', { form_id: formId }); |
| 341 | |
| 342 | if (res?.success && res.data?.edit_url) { |
| 343 | window.location.href = res.data.edit_url; |
| 344 | } else if (editUrl) { |
| 345 | window.location.href = editUrl; |
| 346 | } else { |
| 347 | throw new Error(res?.data?.message || 'Unexpected response'); |
| 348 | } |
| 349 | } catch (e: any) { |
| 350 | setIsCreatingForm(false); |
| 351 | toast({ |
| 352 | title: __('Error', 'everest-forms'), |
| 353 | description: |
| 354 | e?.message || |
| 355 | __('Could not open the form. Please try again.', 'everest-forms'), |
| 356 | status: 'error', |
| 357 | position: 'bottom', |
| 358 | duration: 5000, |
| 359 | isClosable: true, |
| 360 | variant: 'subtle', |
| 361 | }); |
| 362 | } |
| 363 | }; |
| 364 | |
| 365 | const resolveLoading = ( |
| 366 | text: string, |
| 367 | isError = false, |
| 368 | isNotice = false, |
| 369 | noticeUrl = '', |
| 370 | ) => |
| 371 | setMessages((m) => { |
| 372 | const next = [...m]; |
| 373 | for (let i = next.length - 1; i >= 0; i--) { |
| 374 | if (next[i].role === 'assistant' && next[i].loading) { |
| 375 | next[i] = { |
| 376 | role: 'assistant', |
| 377 | text, |
| 378 | error: isError, |
| 379 | notice: isNotice, |
| 380 | noticeUrl, |
| 381 | }; |
| 382 | break; |
| 383 | } |
| 384 | } |
| 385 | return next; |
| 386 | }); |
| 387 | |
| 388 | const handleUpdate = async (refinePromptText: string = '') => { |
| 389 | if (isRegenerating || !formId || !prompt.trim()) return; |
| 390 | const refine = (refinePromptText || '').trim(); |
| 391 | const userText = refine || __('Regenerate the form', 'everest-forms'); |
| 392 | |
| 393 | setMessages((m) => [ |
| 394 | ...m, |
| 395 | { role: 'user', text: userText }, |
| 396 | { role: 'assistant', text: '', loading: true }, |
| 397 | ]); |
| 398 | setRefinePrompt(''); |
| 399 | setIsRegenerating(true); |
| 400 | try { |
| 401 | const res = await callAi('evf_ai_update_form', { |
| 402 | form_id: formId, |
| 403 | prompt, |
| 404 | refine_prompt: refine, |
| 405 | }); |
| 406 | if (res?.success) { |
| 407 | if (res.data?.form_title) setFormTitle(res.data.form_title); |
| 408 | if (res.data?.multi_part_steps) { |
| 409 | setMultiPartSteps(res.data.multi_part_steps); |
| 410 | setActivePartTab(0); |
| 411 | } |
| 412 | const notice = res.data?.notice || ''; |
| 413 | const noticeUrl = res.data?.notice_url || ''; |
| 414 | const doneText = refine |
| 415 | ? __( |
| 416 | "Done — I've updated your form. Check the preview on the right.", |
| 417 | 'everest-forms', |
| 418 | ) |
| 419 | : __("Here's a fresh version of your form.", 'everest-forms'); |
| 420 | |
| 421 | if (notice && !messages.some((m) => m.notice && m.text === notice)) { |
| 422 | setMessages((m) => { |
| 423 | const next = [...m]; |
| 424 | for (let i = next.length - 1; i >= 0; i--) { |
| 425 | if (next[i].role === 'assistant' && next[i].loading) { |
| 426 | next[i] = { role: 'assistant', text: doneText }; |
| 427 | break; |
| 428 | } |
| 429 | } |
| 430 | return [ |
| 431 | ...next, |
| 432 | { role: 'assistant', text: notice, notice: true, noticeUrl }, |
| 433 | ]; |
| 434 | }); |
| 435 | } else { |
| 436 | resolveLoading(doneText, false, false, ''); |
| 437 | } |
| 438 | if (res.data?.preview_html) { |
| 439 | previewHTMLRef.current = res.data.preview_html; |
| 440 | setPreviewHTML(res.data.preview_html); |
| 441 | } else { |
| 442 | setPreviewVersion((v) => v + 1); |
| 443 | } |
| 444 | } else { |
| 445 | if (res?.data?.code === 'daily_limit_reached') { |
| 446 | setIsRateLimited(true); |
| 447 | } |
| 448 | throw new Error( |
| 449 | res?.data?.message || |
| 450 | __('Could not update the form. Please try again.', 'everest-forms'), |
| 451 | ); |
| 452 | } |
| 453 | } catch (e: any) { |
| 454 | const message = |
| 455 | e?.message || |
| 456 | __('Could not update the form. Please try again.', 'everest-forms'); |
| 457 | resolveLoading(message, true); |
| 458 | } finally { |
| 459 | setIsRegenerating(false); |
| 460 | } |
| 461 | }; |
| 462 | |
| 463 | const handleRegenerate = () => handleUpdate(); |
| 464 | |
| 465 | const handleFieldClick = (e: React.MouseEvent) => { |
| 466 | const { clientX, clientY } = e; |
| 467 | setHint({ show: true, x: clientX, y: clientY }); |
| 468 | if (previewHintTimer.current) clearTimeout(previewHintTimer.current); |
| 469 | previewHintTimer.current = setTimeout( |
| 470 | () => setHint((h) => ({ ...h, show: false })), |
| 471 | 2600, |
| 472 | ); |
| 473 | }; |
| 474 | |
| 475 | const hasPrompt = prompt.trim().length > 0; |
| 476 | |
| 477 | useEffect(() => { |
| 478 | if (genState !== 'generating') return; |
| 479 | setGenStep(-1); |
| 480 | aiResponseRef.current = null; |
| 481 | previewHTMLRef.current = ''; |
| 482 | previewFetchStartedRef.current = false; |
| 483 | setPreviewHTML(''); |
| 484 | |
| 485 | let cancelled = false; |
| 486 | let intervalId: ReturnType<typeof setInterval> | null = null; |
| 487 | |
| 488 | const showError = (message: string, isRateLimit = false) => { |
| 489 | if (cancelled) return; |
| 490 | if (intervalId) clearInterval(intervalId); |
| 491 | if (isRateLimit) setIsRateLimited(true); |
| 492 | setGenState('idle'); |
| 493 | toast({ |
| 494 | title: isRateLimit |
| 495 | ? __('Daily limit reached', 'everest-forms') |
| 496 | : __('AI generation failed', 'everest-forms'), |
| 497 | description: isRateLimit ? ( |
| 498 | <Box> |
| 499 | <Text mb={1}>{message}</Text> |
| 500 | <Box |
| 501 | as="a" |
| 502 | href={UPGRADE_URL} |
| 503 | target="_blank" |
| 504 | rel="noopener noreferrer" |
| 505 | fontWeight="600" |
| 506 | textDecoration="underline" |
| 507 | _hover={{ opacity: 0.8 }} |
| 508 | > |
| 509 | {__('Upgrade to Pro →', 'everest-forms')} |
| 510 | </Box> |
| 511 | </Box> |
| 512 | ) : ( |
| 513 | message |
| 514 | ), |
| 515 | status: 'error', |
| 516 | position: 'bottom', |
| 517 | duration: 5000, |
| 518 | isClosable: true, |
| 519 | variant: 'subtle', |
| 520 | }); |
| 521 | }; |
| 522 | |
| 523 | callAi('evf_ai_generate_form', { prompt }) |
| 524 | .then((res: any) => { |
| 525 | if (res?.success && res.data?.form_id) { |
| 526 | const html = res.data.preview_html || ''; |
| 527 | if (html) { |
| 528 | previewHTMLRef.current = html; |
| 529 | previewFetchStartedRef.current = true; |
| 530 | setPreviewHTML(html); |
| 531 | setIsPreviewLoading(false); |
| 532 | } |
| 533 | aiResponseRef.current = { |
| 534 | ok: true, |
| 535 | formId: res.data.form_id, |
| 536 | formTitle: res.data.form_title || '', |
| 537 | editUrl: res.data.edit_url || '', |
| 538 | multiPartSteps: res.data.multi_part_steps || [], |
| 539 | notice: res.data.notice || '', |
| 540 | noticeUrl: res.data.notice_url || '', |
| 541 | }; |
| 542 | } else { |
| 543 | showError( |
| 544 | res?.data?.message || |
| 545 | __('Something went wrong. Please try again.', 'everest-forms'), |
| 546 | res?.data?.code === 'daily_limit_reached', |
| 547 | ); |
| 548 | } |
| 549 | }) |
| 550 | .catch(() => { |
| 551 | showError( |
| 552 | __( |
| 553 | 'Could not reach the AI service. Please try again.', |
| 554 | 'everest-forms', |
| 555 | ), |
| 556 | ); |
| 557 | }); |
| 558 | |
| 559 | let step = -1; |
| 560 | intervalId = setInterval(() => { |
| 561 | step += 1; |
| 562 | setGenStep(step); |
| 563 | if (step >= GEN_STEPS.length - 1) { |
| 564 | if (intervalId) clearInterval(intervalId); |
| 565 | const finish = () => { |
| 566 | if (cancelled) return; |
| 567 | const result = aiResponseRef.current; |
| 568 | if (!result) { |
| 569 | setTimeout(finish, 200); |
| 570 | return; |
| 571 | } |
| 572 | if (result.ok) { |
| 573 | setFormId(result.formId); |
| 574 | setFormTitle(result.formTitle || ''); |
| 575 | setEditUrl(result.editUrl); |
| 576 | setMultiPartSteps(result.multiPartSteps || []); |
| 577 | setActivePartTab(0); |
| 578 | setMessages([ |
| 579 | { role: 'user', text: prompt }, |
| 580 | { |
| 581 | role: 'assistant', |
| 582 | text: |
| 583 | result.notice || |
| 584 | __( |
| 585 | 'Here\'s your form! Review the preview on the right and click "Use This Form" when you\'re happy with it.', |
| 586 | 'everest-forms', |
| 587 | ), |
| 588 | notice: !!result.notice, |
| 589 | noticeUrl: result.noticeUrl || '', |
| 590 | }, |
| 591 | ]); |
| 592 | setGenState('generated'); |
| 593 | } |
| 594 | }; |
| 595 | setTimeout(finish, 700); |
| 596 | } |
| 597 | }, 950); |
| 598 | |
| 599 | return () => { |
| 600 | cancelled = true; |
| 601 | if (intervalId) clearInterval(intervalId); |
| 602 | }; |
| 603 | }, [genState]); |
| 604 | |
| 605 | useEffect(() => { |
| 606 | if (genState !== 'generated' || !formId) return; |
| 607 | if ( |
| 608 | previewVersion === 0 && |
| 609 | (previewHTMLRef.current || previewFetchStartedRef.current) |
| 610 | ) |
| 611 | return; |
| 612 | let cancelled = false; |
| 613 | setIsPreviewLoading(true); |
| 614 | apiFetch({ |
| 615 | path: `${restURL}everest-forms/v1/templates/ai-preview`, |
| 616 | method: 'POST', |
| 617 | data: { form_id: formId }, |
| 618 | headers: { 'X-WP-Nonce': security }, |
| 619 | }) |
| 620 | .then((res: any) => { |
| 621 | if (!cancelled && res?.success && res?.data?.html) { |
| 622 | previewHTMLRef.current = res.data.html; |
| 623 | setPreviewHTML(res.data.html); |
| 624 | } |
| 625 | }) |
| 626 | .catch(() => {}) |
| 627 | .finally(() => { |
| 628 | if (!cancelled) setIsPreviewLoading(false); |
| 629 | }); |
| 630 | return () => { |
| 631 | cancelled = true; |
| 632 | }; |
| 633 | }, [genState, formId, previewVersion]); |
| 634 | |
| 635 | const handleGenerate = () => { |
| 636 | if (!hasPrompt) return; |
| 637 | setGenState('generating'); |
| 638 | }; |
| 639 | |
| 640 | if (genState === 'generating') { |
| 641 | return ( |
| 642 | <PageShell onBack={onBack}> |
| 643 | <Flex |
| 644 | flex="1" |
| 645 | overflow="hidden" |
| 646 | sx={{ animation: `${fadeUp} 0.3s ease` }} |
| 647 | > |
| 648 | <Flex |
| 649 | w="480px" |
| 650 | flexShrink={0} |
| 651 | bg="white" |
| 652 | borderRight="1px solid #e2e8f0" |
| 653 | direction="column" |
| 654 | align="center" |
| 655 | justify="center" |
| 656 | px="36px" |
| 657 | py="40px" |
| 658 | gap="0" |
| 659 | > |
| 660 | <Box |
| 661 | w="60px" |
| 662 | h="60px" |
| 663 | borderRadius="full" |
| 664 | background="linear-gradient(135deg, #9c6de8 0%, #7545BB 100%)" |
| 665 | display="flex" |
| 666 | alignItems="center" |
| 667 | justifyContent="center" |
| 668 | mb="20px" |
| 669 | sx={{ animation: `${pulseGlow} 2s ease-in-out infinite` }} |
| 670 | > |
| 671 | <Icon as={BsStars} boxSize={6} color="white" /> |
| 672 | </Box> |
| 673 | |
| 674 | <VStack spacing="4px" textAlign="center" mb="24px"> |
| 675 | <Heading |
| 676 | as="h2" |
| 677 | fontSize="20px" |
| 678 | fontWeight="700" |
| 679 | color="#0e0e0e" |
| 680 | margin="0" |
| 681 | letterSpacing="-0.3px" |
| 682 | > |
| 683 | {__('Building your form…', 'everest-forms')} |
| 684 | </Heading> |
| 685 | <Text fontSize="13px" color="#9a9a9a" margin="0"> |
| 686 | {__('This usually takes a few seconds', 'everest-forms')} |
| 687 | </Text> |
| 688 | </VStack> |
| 689 | |
| 690 | <Box |
| 691 | bg="#faf9ff" |
| 692 | borderRadius="12px" |
| 693 | border="1px solid #ede8f8" |
| 694 | p="20px 24px" |
| 695 | width="100%" |
| 696 | mb="20px" |
| 697 | > |
| 698 | <VStack align="stretch" spacing="14px"> |
| 699 | {GEN_STEPS.map((step, i) => { |
| 700 | const isDone = i < genStep; |
| 701 | const isActive = i === genStep; |
| 702 | const isPending = i > genStep; |
| 703 | return ( |
| 704 | <HStack |
| 705 | key={i} |
| 706 | spacing="12px" |
| 707 | opacity={isPending ? 0.3 : 1} |
| 708 | transition="opacity 0.4s" |
| 709 | > |
| 710 | <Flex |
| 711 | w="20px" |
| 712 | h="20px" |
| 713 | borderRadius="full" |
| 714 | flexShrink={0} |
| 715 | bg={isDone ? '#7545BB' : 'transparent'} |
| 716 | border={isDone ? 'none' : '2px solid'} |
| 717 | borderColor={isActive ? '#7545BB' : '#ddd'} |
| 718 | align="center" |
| 719 | justify="center" |
| 720 | transition="all 0.35s" |
| 721 | > |
| 722 | {isDone && ( |
| 723 | <Icon |
| 724 | as={FiCheck} |
| 725 | color="white" |
| 726 | sx={{ width: '9px', height: '9px', strokeWidth: 3 }} |
| 727 | /> |
| 728 | )} |
| 729 | {isActive && ( |
| 730 | <Box |
| 731 | w="6px" |
| 732 | h="6px" |
| 733 | borderRadius="full" |
| 734 | bg="#7545BB" |
| 735 | sx={{ |
| 736 | animation: `${dotBounce} 1.1s ease-in-out infinite`, |
| 737 | }} |
| 738 | /> |
| 739 | )} |
| 740 | </Flex> |
| 741 | <Text |
| 742 | flex={1} |
| 743 | fontSize="13px" |
| 744 | margin="0" |
| 745 | fontWeight={isActive ? '600' : isDone ? '500' : '400'} |
| 746 | color={ |
| 747 | isActive ? '#7545BB' : isDone ? '#1a1a1a' : '#c0c0cc' |
| 748 | } |
| 749 | transition="all 0.35s" |
| 750 | > |
| 751 | {step} |
| 752 | </Text> |
| 753 | {isActive && ( |
| 754 | <HStack spacing="3px"> |
| 755 | {[0, 1, 2].map((d) => ( |
| 756 | <Box |
| 757 | key={d} |
| 758 | w="4px" |
| 759 | h="4px" |
| 760 | borderRadius="full" |
| 761 | bg="#7545BB" |
| 762 | sx={{ |
| 763 | animation: `${dotBounce} 1.1s ease-in-out ${d * 0.18}s infinite`, |
| 764 | }} |
| 765 | /> |
| 766 | ))} |
| 767 | </HStack> |
| 768 | )} |
| 769 | </HStack> |
| 770 | ); |
| 771 | })} |
| 772 | </VStack> |
| 773 | </Box> |
| 774 | </Flex> |
| 775 | |
| 776 | <Box flex={1} bg="#f6f6f8" overflowY="auto" p="24px"> |
| 777 | <Box |
| 778 | bg="white" |
| 779 | border="1px solid #e2e8f0" |
| 780 | borderRadius="16px" |
| 781 | overflow="hidden" |
| 782 | sx={{ animation: `${fadeUp} 0.4s ease 0.15s both` }} |
| 783 | > |
| 784 | <Flex |
| 785 | align="center" |
| 786 | px="24px" |
| 787 | py="16px" |
| 788 | borderBottom="1px solid #e2e8f0" |
| 789 | gap="10px" |
| 790 | > |
| 791 | <Box |
| 792 | w="32px" |
| 793 | h="32px" |
| 794 | borderRadius="8px" |
| 795 | sx={{ |
| 796 | background: |
| 797 | 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)', |
| 798 | backgroundSize: '400px 100%', |
| 799 | animation: `${shimmer} 1.6s ease-in-out infinite`, |
| 800 | }} |
| 801 | /> |
| 802 | <Box |
| 803 | h="14px" |
| 804 | w="40%" |
| 805 | borderRadius="4px" |
| 806 | sx={{ |
| 807 | background: |
| 808 | 'linear-gradient(90deg, #eeeeef 25%, #e4e4e8 50%, #eeeeef 75%)', |
| 809 | backgroundSize: '400px 100%', |
| 810 | animation: `${shimmer} 1.6s ease-in-out infinite`, |
| 811 | }} |
| 812 | /> |
| 813 | </Flex> |
| 814 | <Box p="24px"> |
| 815 | <VStack spacing="16px" align="stretch"> |
| 816 | <SkeletonField delay="0.1s" /> |
| 817 | <SkeletonField delay="0.22s" /> |
| 818 | <SkeletonField delay="0.34s" /> |
| 819 | <SkeletonField delay="0.46s" /> |
| 820 | <SkeletonField delay="0.58s" /> |
| 821 | </VStack> |
| 822 | </Box> |
| 823 | </Box> |
| 824 | </Box> |
| 825 | </Flex> |
| 826 | </PageShell> |
| 827 | ); |
| 828 | } |
| 829 | |
| 830 | if (genState === 'generated') { |
| 831 | let lastAssistantIdx = -1; |
| 832 | let useThisFormIdx = -1; |
| 833 | messages.forEach((m, i) => { |
| 834 | if (m.role === 'assistant' && !m.loading) { |
| 835 | lastAssistantIdx = i; |
| 836 | if (!m.error) useThisFormIdx = i; |
| 837 | } |
| 838 | }); |
| 839 | return ( |
| 840 | <PageShell |
| 841 | onBack={() => setGenState('idle')} |
| 842 | backLabel={__('New Prompt', 'everest-forms')} |
| 843 | headerRight={ |
| 844 | <Box |
| 845 | as="button" |
| 846 | display="inline-flex" |
| 847 | alignItems="center" |
| 848 | gap="6px" |
| 849 | h="32px" |
| 850 | px="14px" |
| 851 | borderRadius="8px" |
| 852 | border="none" |
| 853 | bg={isCreatingForm ? '#9660db' : '#7545BB'} |
| 854 | color="white" |
| 855 | fontSize="13px" |
| 856 | fontWeight="600" |
| 857 | cursor={isCreatingForm ? 'not-allowed' : 'pointer'} |
| 858 | opacity={isCreatingForm ? 0.85 : 1} |
| 859 | onClick={handleUseThisForm} |
| 860 | _hover={{ bg: isCreatingForm ? '#9660db' : '#6a3daa' }} |
| 861 | transition="background 0.2s, opacity 0.2s" |
| 862 | > |
| 863 | {isCreatingForm && ( |
| 864 | <Spinner size="xs" color="white" thickness="2px" speed="0.65s" /> |
| 865 | )} |
| 866 | {isCreatingForm |
| 867 | ? __('Creating…', 'everest-forms') |
| 868 | : __('Use This Form', 'everest-forms')} |
| 869 | </Box> |
| 870 | } |
| 871 | > |
| 872 | {hint.show && ( |
| 873 | <Box |
| 874 | position="fixed" |
| 875 | top={`${hint.y + 14}px`} |
| 876 | left={`${hint.x}px`} |
| 877 | transform="translateX(-50%)" |
| 878 | bg="rgba(18,18,18,0.93)" |
| 879 | color="white" |
| 880 | borderRadius="8px" |
| 881 | px="14px" |
| 882 | py="10px" |
| 883 | textAlign="center" |
| 884 | pointerEvents="none" |
| 885 | zIndex={9999} |
| 886 | boxShadow="0 4px 20px rgba(0,0,0,0.2)" |
| 887 | maxW="260px" |
| 888 | sx={{ animation: `${fadeUp} 0.15s ease` }} |
| 889 | > |
| 890 | <Box |
| 891 | position="absolute" |
| 892 | top="-5px" |
| 893 | left="50%" |
| 894 | transform="translateX(-50%)" |
| 895 | width="0" |
| 896 | height="0" |
| 897 | borderLeft="5px solid transparent" |
| 898 | borderRight="5px solid transparent" |
| 899 | borderBottom="5px solid rgba(18,18,18,0.93)" |
| 900 | /> |
| 901 | <Text |
| 902 | fontSize="12px" |
| 903 | fontWeight="600" |
| 904 | margin="0 0 3px" |
| 905 | lineHeight="1.45" |
| 906 | > |
| 907 | {__('This is just a preview of your form.', 'everest-forms')} |
| 908 | </Text> |
| 909 | <Text |
| 910 | fontSize="12px" |
| 911 | color="rgba(255,255,255,0.65)" |
| 912 | margin="0" |
| 913 | lineHeight="1.45" |
| 914 | > |
| 915 | {__('Click "Use This Form" to start editing.', 'everest-forms')} |
| 916 | </Text> |
| 917 | </Box> |
| 918 | )} |
| 919 | |
| 920 | <Flex |
| 921 | flex="1" |
| 922 | overflow="hidden" |
| 923 | sx={{ animation: `${fadeUp} 0.35s ease` }} |
| 924 | > |
| 925 | <Flex |
| 926 | w="480px" |
| 927 | flexShrink={0} |
| 928 | bg="white" |
| 929 | borderRight="1px solid #e2e8f0" |
| 930 | direction="column" |
| 931 | overflow="hidden" |
| 932 | > |
| 933 | <Box flex={1} p="20px" overflowY="auto"> |
| 934 | {messages.map((msg, idx) => { |
| 935 | if (msg.role === 'user') { |
| 936 | return ( |
| 937 | <Flex key={idx} justify="flex-end" mb="16px"> |
| 938 | <Box |
| 939 | bg="#7545BB" |
| 940 | color="white" |
| 941 | borderRadius="16px 16px 4px 16px" |
| 942 | px="14px" |
| 943 | py="10px" |
| 944 | fontSize="14px" |
| 945 | lineHeight="1.6" |
| 946 | maxW="340px" |
| 947 | boxShadow="0 2px 8px rgba(117,69,187,0.2)" |
| 948 | > |
| 949 | {msg.text} |
| 950 | </Box> |
| 951 | </Flex> |
| 952 | ); |
| 953 | } |
| 954 | |
| 955 | const isLastAssistant = idx === lastAssistantIdx; |
| 956 | const isUseThisFormMsg = idx === useThisFormIdx; |
| 957 | return ( |
| 958 | <HStack key={idx} align="flex-start" spacing="10px" mb="16px"> |
| 959 | <Flex |
| 960 | w="28px" |
| 961 | h="28px" |
| 962 | borderRadius="full" |
| 963 | bg="rgba(117,69,187,0.1)" |
| 964 | align="center" |
| 965 | justify="center" |
| 966 | flexShrink={0} |
| 967 | mt="2px" |
| 968 | > |
| 969 | <Icon as={BsStars} boxSize="13px" color="#7545BB" /> |
| 970 | </Flex> |
| 971 | |
| 972 | <Box |
| 973 | flex={1} |
| 974 | bg={msg.error || msg.notice ? '#fff8f8' : '#faf9ff'} |
| 975 | border={ |
| 976 | msg.error || msg.notice |
| 977 | ? '1px solid #fcd5d5' |
| 978 | : '1px solid #ede8f8' |
| 979 | } |
| 980 | borderRadius="4px 16px 16px 16px" |
| 981 | p="16px" |
| 982 | boxShadow={ |
| 983 | msg.error || msg.notice |
| 984 | ? 'none' |
| 985 | : '0 2px 10px rgba(117,69,187,0.06)' |
| 986 | } |
| 987 | > |
| 988 | {msg.loading ? ( |
| 989 | <HStack spacing="8px"> |
| 990 | <Spinner |
| 991 | size="xs" |
| 992 | color="#7545BB" |
| 993 | thickness="2px" |
| 994 | speed="0.65s" |
| 995 | /> |
| 996 | <Text |
| 997 | fontSize="14px" |
| 998 | color="#7545BB" |
| 999 | margin="0" |
| 1000 | fontWeight="500" |
| 1001 | > |
| 1002 | {__('Updating your form…', 'everest-forms')} |
| 1003 | </Text> |
| 1004 | </HStack> |
| 1005 | ) : ( |
| 1006 | <> |
| 1007 | <Text |
| 1008 | fontSize="14px" |
| 1009 | color={msg.error || msg.notice ? '#c0392b' : '#444'} |
| 1010 | lineHeight="1.65" |
| 1011 | margin={ |
| 1012 | isUseThisFormMsg |
| 1013 | ? '0 0 14px' |
| 1014 | : msg.noticeUrl |
| 1015 | ? '0 0 10px' |
| 1016 | : '0' |
| 1017 | } |
| 1018 | > |
| 1019 | {msg.text} |
| 1020 | </Text> |
| 1021 | |
| 1022 | {msg.noticeUrl && ( |
| 1023 | <Box |
| 1024 | as="a" |
| 1025 | href={msg.noticeUrl} |
| 1026 | target="_blank" |
| 1027 | rel="noopener noreferrer" |
| 1028 | display="inline-block" |
| 1029 | mb={isUseThisFormMsg ? '10px' : '0'} |
| 1030 | color="#7545BB" |
| 1031 | fontSize="12px" |
| 1032 | fontWeight="600" |
| 1033 | _hover={{ textDecoration: 'underline' }} |
| 1034 | > |
| 1035 | {__('Upgrade to Pro →', 'everest-forms')} |
| 1036 | </Box> |
| 1037 | )} |
| 1038 | |
| 1039 | {isUseThisFormMsg && ( |
| 1040 | <Box |
| 1041 | as="button" |
| 1042 | w="100%" |
| 1043 | display="flex" |
| 1044 | alignItems="center" |
| 1045 | justifyContent="center" |
| 1046 | gap="8px" |
| 1047 | h="36px" |
| 1048 | borderRadius="8px" |
| 1049 | bg={isCreatingForm ? '#9660db' : '#7545BB'} |
| 1050 | color="white" |
| 1051 | fontSize="13px" |
| 1052 | fontWeight="600" |
| 1053 | border="none" |
| 1054 | cursor={ |
| 1055 | isCreatingForm ? 'not-allowed' : 'pointer' |
| 1056 | } |
| 1057 | mb="12px" |
| 1058 | opacity={isCreatingForm ? 0.85 : 1} |
| 1059 | onClick={handleUseThisForm} |
| 1060 | _hover={{ |
| 1061 | bg: isCreatingForm ? '#9660db' : '#6a3daa', |
| 1062 | }} |
| 1063 | transition="background 0.2s, opacity 0.2s" |
| 1064 | > |
| 1065 | {isCreatingForm && ( |
| 1066 | <Spinner |
| 1067 | size="xs" |
| 1068 | color="white" |
| 1069 | thickness="2px" |
| 1070 | speed="0.65s" |
| 1071 | /> |
| 1072 | )} |
| 1073 | {isCreatingForm |
| 1074 | ? __('Creating…', 'everest-forms') |
| 1075 | : __('Use This Form', 'everest-forms')} |
| 1076 | </Box> |
| 1077 | )} |
| 1078 | |
| 1079 | <HStack justify="flex-end" pt="2px"> |
| 1080 | {isRateLimited ? ( |
| 1081 | <Popover trigger="hover" placement="top" isLazy> |
| 1082 | <PopoverTrigger> |
| 1083 | <HStack |
| 1084 | spacing="5px" |
| 1085 | cursor="not-allowed" |
| 1086 | opacity={0.5} |
| 1087 | > |
| 1088 | <Icon |
| 1089 | as={FiRefreshCw} |
| 1090 | boxSize="12px" |
| 1091 | color="#9ca3af" |
| 1092 | /> |
| 1093 | <Text |
| 1094 | fontSize="12px" |
| 1095 | color="#9ca3af" |
| 1096 | margin="0" |
| 1097 | fontWeight="500" |
| 1098 | > |
| 1099 | {__('Redo', 'everest-forms')} |
| 1100 | </Text> |
| 1101 | </HStack> |
| 1102 | </PopoverTrigger> |
| 1103 | <PopoverContent |
| 1104 | w="auto" |
| 1105 | maxW="220px" |
| 1106 | zIndex={200000} |
| 1107 | _focus={{ outline: 'none' }} |
| 1108 | > |
| 1109 | <PopoverArrow /> |
| 1110 | <PopoverBody p={3}> |
| 1111 | <Text |
| 1112 | fontSize="12px" |
| 1113 | color="#555" |
| 1114 | mb={2} |
| 1115 | lineHeight="1.5" |
| 1116 | > |
| 1117 | {__( |
| 1118 | "You've reached your daily free limit.", |
| 1119 | 'everest-forms', |
| 1120 | )} |
| 1121 | </Text> |
| 1122 | <Box |
| 1123 | as="a" |
| 1124 | href={UPGRADE_URL} |
| 1125 | target="_blank" |
| 1126 | rel="noopener noreferrer" |
| 1127 | color="#7545BB" |
| 1128 | fontSize="12px" |
| 1129 | fontWeight="600" |
| 1130 | _hover={{ textDecoration: 'underline' }} |
| 1131 | > |
| 1132 | {__('Upgrade to Pro →', 'everest-forms')} |
| 1133 | </Box> |
| 1134 | </PopoverBody> |
| 1135 | </PopoverContent> |
| 1136 | </Popover> |
| 1137 | ) : ( |
| 1138 | <HStack |
| 1139 | spacing="5px" |
| 1140 | cursor={ |
| 1141 | isRegenerating ? 'not-allowed' : 'pointer' |
| 1142 | } |
| 1143 | opacity={isRegenerating ? 0.5 : 1} |
| 1144 | _hover={{ |
| 1145 | opacity: isRegenerating ? 0.5 : 0.65, |
| 1146 | }} |
| 1147 | onClick={ |
| 1148 | isRegenerating ? undefined : handleRegenerate |
| 1149 | } |
| 1150 | > |
| 1151 | {isRegenerating && isLastAssistant ? ( |
| 1152 | <Spinner |
| 1153 | size="xs" |
| 1154 | color="#9ca3af" |
| 1155 | thickness="2px" |
| 1156 | speed="0.65s" |
| 1157 | /> |
| 1158 | ) : ( |
| 1159 | <Icon |
| 1160 | as={FiRefreshCw} |
| 1161 | boxSize="12px" |
| 1162 | color="#9ca3af" |
| 1163 | /> |
| 1164 | )} |
| 1165 | <Text |
| 1166 | fontSize="12px" |
| 1167 | color="#9ca3af" |
| 1168 | margin="0" |
| 1169 | fontWeight="500" |
| 1170 | > |
| 1171 | {__('Redo', 'everest-forms')} |
| 1172 | </Text> |
| 1173 | </HStack> |
| 1174 | )} |
| 1175 | </HStack> |
| 1176 | </> |
| 1177 | )} |
| 1178 | </Box> |
| 1179 | </HStack> |
| 1180 | ); |
| 1181 | })} |
| 1182 | </Box> |
| 1183 | |
| 1184 | <Box borderTop="1px solid #e2e8f0" p="16px"> |
| 1185 | <Box |
| 1186 | border="1px solid #e2e8f0" |
| 1187 | borderRadius="12px" |
| 1188 | bg="white" |
| 1189 | overflow="hidden" |
| 1190 | transition="border-color 0.2s" |
| 1191 | _focusWithin={{ borderColor: '#7545BB' }} |
| 1192 | > |
| 1193 | <Textarea |
| 1194 | placeholder={__('Refine or follow up…', 'everest-forms')} |
| 1195 | border="none" |
| 1196 | fontSize="14px" |
| 1197 | color="#333" |
| 1198 | _focus={{ boxShadow: 'none' }} |
| 1199 | p="12px 16px" |
| 1200 | minHeight="56px" |
| 1201 | resize="none" |
| 1202 | _placeholder={{ color: '#c0c0cc' }} |
| 1203 | value={refinePrompt} |
| 1204 | isDisabled={isRegenerating} |
| 1205 | onChange={(e) => |
| 1206 | setRefinePrompt(e.target.value.slice(0, MAX_CHARS)) |
| 1207 | } |
| 1208 | onKeyDown={(e) => { |
| 1209 | if (e.key === 'Enter' && !e.shiftKey && !isRateLimited) { |
| 1210 | e.preventDefault(); |
| 1211 | handleUpdate(refinePrompt); |
| 1212 | } |
| 1213 | }} |
| 1214 | /> |
| 1215 | <Flex justify="flex-end" px="12px" pb="10px"> |
| 1216 | {isRateLimited ? ( |
| 1217 | <Popover trigger="hover" placement="top" isLazy> |
| 1218 | <PopoverTrigger> |
| 1219 | <Box |
| 1220 | as="button" |
| 1221 | w="28px" |
| 1222 | h="28px" |
| 1223 | bg="#c9bce4" |
| 1224 | borderRadius="6px" |
| 1225 | display="inline-flex" |
| 1226 | alignItems="center" |
| 1227 | justifyContent="center" |
| 1228 | border="none" |
| 1229 | cursor="not-allowed" |
| 1230 | transition="background 0.2s" |
| 1231 | > |
| 1232 | <Icon as={FiArrowUp} boxSize="3.5" color="white" /> |
| 1233 | </Box> |
| 1234 | </PopoverTrigger> |
| 1235 | <PopoverContent |
| 1236 | w="auto" |
| 1237 | maxW="220px" |
| 1238 | zIndex={200000} |
| 1239 | _focus={{ outline: 'none' }} |
| 1240 | > |
| 1241 | <PopoverArrow /> |
| 1242 | <PopoverBody p={3}> |
| 1243 | <Text |
| 1244 | fontSize="12px" |
| 1245 | color="#555" |
| 1246 | mb={2} |
| 1247 | lineHeight="1.5" |
| 1248 | > |
| 1249 | {__( |
| 1250 | "You've reached your daily free limit.", |
| 1251 | 'everest-forms', |
| 1252 | )} |
| 1253 | </Text> |
| 1254 | <Box |
| 1255 | as="a" |
| 1256 | href={UPGRADE_URL} |
| 1257 | target="_blank" |
| 1258 | rel="noopener noreferrer" |
| 1259 | color="#7545BB" |
| 1260 | fontSize="12px" |
| 1261 | fontWeight="600" |
| 1262 | _hover={{ textDecoration: 'underline' }} |
| 1263 | > |
| 1264 | {__('Upgrade to Pro →', 'everest-forms')} |
| 1265 | </Box> |
| 1266 | </PopoverBody> |
| 1267 | </PopoverContent> |
| 1268 | </Popover> |
| 1269 | ) : ( |
| 1270 | <Box |
| 1271 | as="button" |
| 1272 | w="28px" |
| 1273 | h="28px" |
| 1274 | bg={ |
| 1275 | refinePrompt.trim() && !isRegenerating |
| 1276 | ? '#7545BB' |
| 1277 | : '#c9bce4' |
| 1278 | } |
| 1279 | borderRadius="6px" |
| 1280 | display="inline-flex" |
| 1281 | alignItems="center" |
| 1282 | justifyContent="center" |
| 1283 | border="none" |
| 1284 | cursor={ |
| 1285 | refinePrompt.trim() && !isRegenerating |
| 1286 | ? 'pointer' |
| 1287 | : 'not-allowed' |
| 1288 | } |
| 1289 | _hover={{ |
| 1290 | bg: |
| 1291 | refinePrompt.trim() && !isRegenerating |
| 1292 | ? '#6a3daa' |
| 1293 | : '#c9bce4', |
| 1294 | }} |
| 1295 | transition="background 0.2s" |
| 1296 | onClick={() => handleUpdate(refinePrompt)} |
| 1297 | > |
| 1298 | {isRegenerating ? ( |
| 1299 | <Spinner |
| 1300 | size="xs" |
| 1301 | color="white" |
| 1302 | thickness="2px" |
| 1303 | speed="0.65s" |
| 1304 | /> |
| 1305 | ) : ( |
| 1306 | <Icon as={FiArrowUp} boxSize="3.5" color="white" /> |
| 1307 | )} |
| 1308 | </Box> |
| 1309 | )} |
| 1310 | </Flex> |
| 1311 | </Box> |
| 1312 | </Box> |
| 1313 | </Flex> |
| 1314 | |
| 1315 | <Box |
| 1316 | flex={1} |
| 1317 | bg="#f6f6f8" |
| 1318 | display="flex" |
| 1319 | flexDirection="column" |
| 1320 | overflow="hidden" |
| 1321 | opacity={isRegenerating ? 0.5 : 1} |
| 1322 | transition="opacity 0.3s" |
| 1323 | > |
| 1324 | <Box |
| 1325 | flex={1} |
| 1326 | overflowY="auto" |
| 1327 | p="24px" |
| 1328 | pb={multiPartSteps.length > 0 ? '0' : '24px'} |
| 1329 | > |
| 1330 | <Box |
| 1331 | bg="white" |
| 1332 | border="1px solid #e2e8f0" |
| 1333 | borderRadius={ |
| 1334 | multiPartSteps.length > 0 ? '16px 16px 0 0' : '16px' |
| 1335 | } |
| 1336 | borderBottom={ |
| 1337 | multiPartSteps.length > 0 ? 'none' : '1px solid #e2e8f0' |
| 1338 | } |
| 1339 | overflow="hidden" |
| 1340 | mb="0" |
| 1341 | position="relative" |
| 1342 | transition="border-color 0.3s" |
| 1343 | > |
| 1344 | <Flex |
| 1345 | align="center" |
| 1346 | px="24px" |
| 1347 | py="16px" |
| 1348 | borderBottom="1px solid #e2e8f0" |
| 1349 | justify="space-between" |
| 1350 | > |
| 1351 | <HStack spacing="10px"> |
| 1352 | <Box |
| 1353 | w="32px" |
| 1354 | h="32px" |
| 1355 | bg="rgba(117,69,187,0.1)" |
| 1356 | borderRadius="8px" |
| 1357 | display="flex" |
| 1358 | alignItems="center" |
| 1359 | justifyContent="center" |
| 1360 | > |
| 1361 | <Icon as={FiEdit3} boxSize="15px" color="#7545BB" /> |
| 1362 | </Box> |
| 1363 | <Text |
| 1364 | fontSize="16px" |
| 1365 | fontWeight="600" |
| 1366 | color="#0e0e0e" |
| 1367 | margin="0" |
| 1368 | > |
| 1369 | {formTitle || __('AI Generated Form', 'everest-forms')} |
| 1370 | </Text> |
| 1371 | </HStack> |
| 1372 | {isRegenerating && ( |
| 1373 | <HStack |
| 1374 | spacing="5px" |
| 1375 | sx={{ animation: `${fadeUp} 0.2s ease` }} |
| 1376 | > |
| 1377 | {[0, 1, 2].map((d) => ( |
| 1378 | <Box |
| 1379 | key={d} |
| 1380 | w="5px" |
| 1381 | h="5px" |
| 1382 | borderRadius="full" |
| 1383 | bg="#7545BB" |
| 1384 | sx={{ |
| 1385 | animation: `${dotBounce} 1.1s ease-in-out ${d * 0.2}s infinite`, |
| 1386 | }} |
| 1387 | /> |
| 1388 | ))} |
| 1389 | <Text |
| 1390 | fontSize="11px" |
| 1391 | color="#7545BB" |
| 1392 | margin="0" |
| 1393 | fontWeight="500" |
| 1394 | > |
| 1395 | {__('Regenerating', 'everest-forms')} |
| 1396 | </Text> |
| 1397 | </HStack> |
| 1398 | )} |
| 1399 | </Flex> |
| 1400 | |
| 1401 | <style |
| 1402 | dangerouslySetInnerHTML={{ |
| 1403 | __html: ` |
| 1404 | .evf-ai-preview-canvas .everest-forms-panel-content { |
| 1405 | height: auto !important; |
| 1406 | overflow: visible !important; |
| 1407 | } |
| 1408 | `, |
| 1409 | }} |
| 1410 | /> |
| 1411 | {multiPartSteps.length > 0 && ( |
| 1412 | <style |
| 1413 | dangerouslySetInnerHTML={{ |
| 1414 | __html: ` |
| 1415 | .evf-ai-preview-canvas .evf-admin-row[data-part-id] { display: none !important; } |
| 1416 | .evf-ai-preview-canvas .evf-admin-row[data-part-id="${activePartTab + 1}"] { display: flex !important; margin-bottom: 15px !important; } |
| 1417 | `, |
| 1418 | }} |
| 1419 | /> |
| 1420 | )} |
| 1421 | <Box |
| 1422 | p="0" |
| 1423 | pointerEvents={ |
| 1424 | isRegenerating || isCreatingForm ? 'none' : 'auto' |
| 1425 | } |
| 1426 | > |
| 1427 | {previewHTML ? ( |
| 1428 | <div |
| 1429 | ref={canvasRef} |
| 1430 | className="evf-ai-preview-canvas" |
| 1431 | onClick={(e) => handleFieldClick(e)} |
| 1432 | dangerouslySetInnerHTML={{ __html: previewHTML }} |
| 1433 | /> |
| 1434 | ) : ( |
| 1435 | <Box p="24px"> |
| 1436 | <VStack spacing="20px" align="stretch"> |
| 1437 | {Array.from({ length: 5 }).map((_, idx) => ( |
| 1438 | <SkeletonField key={idx} delay={`${idx * 0.1}s`} /> |
| 1439 | ))} |
| 1440 | </VStack> |
| 1441 | </Box> |
| 1442 | )} |
| 1443 | </Box> |
| 1444 | </Box> |
| 1445 | </Box> |
| 1446 | |
| 1447 | {multiPartSteps.length > 0 && ( |
| 1448 | <Box |
| 1449 | bg="#fafafa" |
| 1450 | borderTop="1px solid #d5d9e2" |
| 1451 | borderLeft="1px solid #d5d9e2" |
| 1452 | borderRight="1px solid #d5d9e2" |
| 1453 | borderBottom="1px solid #d5d9e2" |
| 1454 | borderRadius="0 0 16px 16px" |
| 1455 | display="flex" |
| 1456 | alignItems="stretch" |
| 1457 | h="42px" |
| 1458 | mx="24px" |
| 1459 | mb="24px" |
| 1460 | overflow="hidden" |
| 1461 | flexShrink={0} |
| 1462 | pointerEvents={ |
| 1463 | isRegenerating || isCreatingForm ? 'none' : 'auto' |
| 1464 | } |
| 1465 | > |
| 1466 | <Box |
| 1467 | as="ul" |
| 1468 | display="flex" |
| 1469 | listStyleType="none" |
| 1470 | m="0" |
| 1471 | p="0" |
| 1472 | flex="1" |
| 1473 | h="41px" |
| 1474 | alignSelf="flex-start" |
| 1475 | overflow="hidden" |
| 1476 | borderBottom="1px solid #cccccc" |
| 1477 | > |
| 1478 | {multiPartSteps.map((step, idx) => ( |
| 1479 | <Box |
| 1480 | as="li" |
| 1481 | key={idx} |
| 1482 | display="block" |
| 1483 | flexShrink={0} |
| 1484 | h="40px" |
| 1485 | bg={activePartTab === idx ? '#eeeeee' : '#f7f7f7'} |
| 1486 | borderRadius={ |
| 1487 | idx === 0 |
| 1488 | ? '0' |
| 1489 | : idx === multiPartSteps.length - 1 |
| 1490 | ? '0 0 16px 0' |
| 1491 | : '0' |
| 1492 | } |
| 1493 | cursor="pointer" |
| 1494 | onClick={() => setActivePartTab(idx)} |
| 1495 | > |
| 1496 | <Box |
| 1497 | as="a" |
| 1498 | href="#" |
| 1499 | onClick={(e: React.MouseEvent) => e.preventDefault()} |
| 1500 | display="inline-flex" |
| 1501 | alignItems="center" |
| 1502 | h="40px" |
| 1503 | px="10px" |
| 1504 | fontSize="13px" |
| 1505 | fontWeight="600" |
| 1506 | lineHeight="1" |
| 1507 | color={activePartTab === idx ? '#7e3bd0' : '#555555'} |
| 1508 | textDecoration="none" |
| 1509 | _hover={{ |
| 1510 | color: activePartTab === idx ? '#7e3bd0' : '#333333', |
| 1511 | textDecoration: 'none', |
| 1512 | }} |
| 1513 | > |
| 1514 | <Text |
| 1515 | margin="0" |
| 1516 | fontSize="13px" |
| 1517 | fontWeight="600" |
| 1518 | color="inherit" |
| 1519 | > |
| 1520 | {step} |
| 1521 | </Text> |
| 1522 | </Box> |
| 1523 | </Box> |
| 1524 | ))} |
| 1525 | </Box> |
| 1526 | </Box> |
| 1527 | )} |
| 1528 | </Box> |
| 1529 | </Flex> |
| 1530 | </PageShell> |
| 1531 | ); |
| 1532 | } |
| 1533 | |
| 1534 | return ( |
| 1535 | <PageShell onBack={onBack}> |
| 1536 | <Flex |
| 1537 | flex="1" |
| 1538 | direction="column" |
| 1539 | align="center" |
| 1540 | justify="center" |
| 1541 | px="24px" |
| 1542 | py="16px" |
| 1543 | > |
| 1544 | <Box width="100%" maxW="896px"> |
| 1545 | <VStack spacing="8px" mb="14px" textAlign="center"> |
| 1546 | <Heading |
| 1547 | as="h1" |
| 1548 | fontSize={{ base: '24px', md: '32px' }} |
| 1549 | fontWeight="600" |
| 1550 | color="#0e0e0e" |
| 1551 | lineHeight="1.15" |
| 1552 | m="0" |
| 1553 | letterSpacing="-0.02em" |
| 1554 | > |
| 1555 | {__('What should we build today?', 'everest-forms')} |
| 1556 | </Heading> |
| 1557 | <Text fontSize="15px" color="#6b6b6b" m="0" lineHeight="1.55"> |
| 1558 | {__( |
| 1559 | "Describe your form in your own words. We'll handle the fields, logic and layout — in seconds.", |
| 1560 | 'everest-forms', |
| 1561 | )} |
| 1562 | </Text> |
| 1563 | </VStack> |
| 1564 | |
| 1565 | <Box |
| 1566 | borderRadius="16px" |
| 1567 | border="1px solid #e2e8f0" |
| 1568 | bg="white" |
| 1569 | boxShadow="0 1px 2px rgba(15,15,15,0.04)" |
| 1570 | mb="8px" |
| 1571 | > |
| 1572 | <Flex align="flex-start" gap="12px" px="20px" pt="20px"> |
| 1573 | <Icon |
| 1574 | as={BsStars} |
| 1575 | boxSize="5" |
| 1576 | color="#c5c0d0" |
| 1577 | mt="2px" |
| 1578 | flexShrink={0} |
| 1579 | /> |
| 1580 | <Textarea |
| 1581 | ref={promptInputRef} |
| 1582 | autoFocus |
| 1583 | value={prompt} |
| 1584 | onChange={(e) => setPrompt(e.target.value.slice(0, MAX_CHARS))} |
| 1585 | placeholder={__( |
| 1586 | 'A feedback form to learn how onboarding felt, with a 1–5 rating and one optional comment…', |
| 1587 | 'everest-forms', |
| 1588 | )} |
| 1589 | border="none" |
| 1590 | resize="none" |
| 1591 | fontSize="15px" |
| 1592 | lineHeight="1.6" |
| 1593 | color="#181818" |
| 1594 | p="0" |
| 1595 | minH="90px" |
| 1596 | bg="transparent" |
| 1597 | _focus={{ boxShadow: 'none', outline: 'none', border: 'none' }} |
| 1598 | _placeholder={{ color: '#9a9a9a' }} |
| 1599 | /> |
| 1600 | </Flex> |
| 1601 | |
| 1602 | <Box mx="20px" borderTop="1px solid #f1f5f9" /> |
| 1603 | |
| 1604 | <Flex align="center" justify="flex-end" px="16px" py="12px"> |
| 1605 | {isRateLimited ? ( |
| 1606 | <Popover trigger="hover" placement="top" isLazy> |
| 1607 | <PopoverTrigger> |
| 1608 | <Box |
| 1609 | as="button" |
| 1610 | display="inline-flex" |
| 1611 | alignItems="center" |
| 1612 | gap="8px" |
| 1613 | fontSize="14px" |
| 1614 | fontWeight="500" |
| 1615 | pl="16px" |
| 1616 | pr="8px" |
| 1617 | py="8px" |
| 1618 | borderRadius="8px" |
| 1619 | bg="#e6e3ee" |
| 1620 | color="#9a9a9a" |
| 1621 | cursor="not-allowed" |
| 1622 | border="none" |
| 1623 | transition="background 0.2s" |
| 1624 | > |
| 1625 | <Text margin="0" color="#9a9a9a"> |
| 1626 | {__('Generate', 'everest-forms')} |
| 1627 | </Text> |
| 1628 | <Box |
| 1629 | w="24px" |
| 1630 | h="24px" |
| 1631 | borderRadius="6px" |
| 1632 | display="inline-flex" |
| 1633 | alignItems="center" |
| 1634 | justifyContent="center" |
| 1635 | bg="#d5d0e0" |
| 1636 | > |
| 1637 | <Icon as={FiArrowUp} boxSize="3.5" color="#9a9a9a" /> |
| 1638 | </Box> |
| 1639 | </Box> |
| 1640 | </PopoverTrigger> |
| 1641 | <PopoverContent |
| 1642 | w="auto" |
| 1643 | maxW="220px" |
| 1644 | zIndex={200000} |
| 1645 | _focus={{ outline: 'none' }} |
| 1646 | > |
| 1647 | <PopoverArrow /> |
| 1648 | <PopoverBody p={3}> |
| 1649 | <Text |
| 1650 | fontSize="12px" |
| 1651 | color="#555" |
| 1652 | mb={2} |
| 1653 | lineHeight="1.5" |
| 1654 | > |
| 1655 | {__( |
| 1656 | "You've reached your daily free limit.", |
| 1657 | 'everest-forms', |
| 1658 | )} |
| 1659 | </Text> |
| 1660 | <Box |
| 1661 | as="a" |
| 1662 | href={UPGRADE_URL} |
| 1663 | target="_blank" |
| 1664 | rel="noopener noreferrer" |
| 1665 | color="#7545BB" |
| 1666 | fontSize="12px" |
| 1667 | fontWeight="600" |
| 1668 | _hover={{ textDecoration: 'underline' }} |
| 1669 | > |
| 1670 | {__('Upgrade to Pro →', 'everest-forms')} |
| 1671 | </Box> |
| 1672 | </PopoverBody> |
| 1673 | </PopoverContent> |
| 1674 | </Popover> |
| 1675 | ) : ( |
| 1676 | <Box |
| 1677 | as="button" |
| 1678 | display="inline-flex" |
| 1679 | alignItems="center" |
| 1680 | gap="8px" |
| 1681 | fontSize="14px" |
| 1682 | fontWeight="500" |
| 1683 | pl="16px" |
| 1684 | pr="8px" |
| 1685 | py="8px" |
| 1686 | borderRadius="8px" |
| 1687 | bg={hasPrompt ? '#7545BB' : '#e6e3ee'} |
| 1688 | color={hasPrompt ? 'white' : '#9a9a9a'} |
| 1689 | cursor={hasPrompt ? 'pointer' : 'not-allowed'} |
| 1690 | border="none" |
| 1691 | onClick={hasPrompt ? handleGenerate : undefined} |
| 1692 | _hover={hasPrompt ? { bg: '#6a3daa' } : {}} |
| 1693 | transition="background 0.2s" |
| 1694 | > |
| 1695 | <Text margin="0" color={hasPrompt ? 'white' : '#9a9a9a'}> |
| 1696 | {__('Generate', 'everest-forms')} |
| 1697 | </Text> |
| 1698 | <Box |
| 1699 | w="24px" |
| 1700 | h="24px" |
| 1701 | borderRadius="6px" |
| 1702 | display="inline-flex" |
| 1703 | alignItems="center" |
| 1704 | justifyContent="center" |
| 1705 | bg={hasPrompt ? '#6a3daa' : '#d5d0e0'} |
| 1706 | > |
| 1707 | <Icon |
| 1708 | as={FiArrowUp} |
| 1709 | boxSize="3.5" |
| 1710 | color={hasPrompt ? 'white' : '#9a9a9a'} |
| 1711 | /> |
| 1712 | </Box> |
| 1713 | </Box> |
| 1714 | )} |
| 1715 | </Flex> |
| 1716 | </Box> |
| 1717 | |
| 1718 | <Text |
| 1719 | textAlign="right" |
| 1720 | fontSize="11px" |
| 1721 | color="#9a9a9a" |
| 1722 | m="0" |
| 1723 | pr="4px" |
| 1724 | mb="14px" |
| 1725 | > |
| 1726 | {prompt.length}/{MAX_CHARS} |
| 1727 | </Text> |
| 1728 | |
| 1729 | <Box> |
| 1730 | <Flex align="flex-end" justify="space-between" mb="10px"> |
| 1731 | <Heading |
| 1732 | as="h2" |
| 1733 | fontSize="20px" |
| 1734 | fontWeight="600" |
| 1735 | color="#0e0e0e" |
| 1736 | m="0" |
| 1737 | letterSpacing="-0.01em" |
| 1738 | > |
| 1739 | {__('Need inspiration?', 'everest-forms')} |
| 1740 | </Heading> |
| 1741 | <Text fontSize="13px" fontWeight="500" color="#9a9a9a" m="0"> |
| 1742 | {__('Tap a card to use as starting point', 'everest-forms')} |
| 1743 | </Text> |
| 1744 | </Flex> |
| 1745 | |
| 1746 | <SimpleGrid columns={{ base: 1, sm: 2 }} spacing="8px"> |
| 1747 | {INSPIRATION_CARDS.map((card) => ( |
| 1748 | <Box |
| 1749 | key={card.title} |
| 1750 | as="button" |
| 1751 | textAlign="left" |
| 1752 | display="block" |
| 1753 | width="100%" |
| 1754 | border="1px solid rgba(226,232,240,0.8)" |
| 1755 | bg="white" |
| 1756 | px="16px" |
| 1757 | py="12px" |
| 1758 | borderRadius="8px" |
| 1759 | cursor="pointer" |
| 1760 | onClick={() => setPrompt(card.prompt)} |
| 1761 | _hover={{ |
| 1762 | borderColor: 'rgba(117,69,187,0.4)', |
| 1763 | bg: 'rgba(117,69,187,0.02)', |
| 1764 | }} |
| 1765 | > |
| 1766 | <Text |
| 1767 | fontSize="14px" |
| 1768 | fontWeight="600" |
| 1769 | color="#0e0e0e" |
| 1770 | m="0 0 4px" |
| 1771 | lineHeight="1.4" |
| 1772 | > |
| 1773 | {card.title} |
| 1774 | </Text> |
| 1775 | <Text |
| 1776 | fontSize="12.5px" |
| 1777 | color="#6b6b6b" |
| 1778 | m="0" |
| 1779 | lineHeight="1.4" |
| 1780 | > |
| 1781 | {card.description} |
| 1782 | </Text> |
| 1783 | </Box> |
| 1784 | ))} |
| 1785 | </SimpleGrid> |
| 1786 | </Box> |
| 1787 | </Box> |
| 1788 | </Flex> |
| 1789 | </PageShell> |
| 1790 | ); |
| 1791 | }; |
| 1792 | |
| 1793 | export default CreateWithAI; |
| 1794 |