PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / AutoLaunch / hooks / useDescriptionForm.js

useDescriptionForm.js in Extendify 3.2.1, at src/AutoLaunch/hooks/useDescriptionForm.js

177 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { getExtendifyCodeRecommendation } from '@auto-launch/functions/extendify-code';
2 import { getAbTest } from '@auto-launch/functions/getAbTest';
3 import { fetchWithTimeout } from '@auto-launch/functions/helpers';
4 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
5 import { launchStrings } from '@auto-launch/strings';
6 import { AI_HOST } from '@constants';
7 import { reqDataBasics } from '@shared/lib/data';
8 import { useAIConsentStore } from '@shared/state/ai-consent';
9 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
10 import { decodeEntities } from '@wordpress/html-entities';
11 import { isURL } from '@wordpress/url';
12
13 const getShowTitle = () => Boolean(window.extLaunchData?.showLaunchTitle);
14
15 // A round trip can finish in under a frame, and the loader reads as a glitch.
16 const MIN_LOADER_MS = 2000;
17 const heldFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
19 const getSubmitLabel = () =>
20 getAbTest('AutoLaunch.SubmitCreateWebsite').variant === 'B'
21 ? launchStrings().submitAlternate
22 : launchStrings().submit;
23
24 const getPlaceholder = () =>
25 getAbTest('AutoLaunch.DescriptionPlaceholderLaw').variant === 'B'
26 ? launchStrings().placeholderAlternate
27 : launchStrings().placeholder;
28
29 export const useDescriptionForm = () => {
30 const { setData, descriptionBackup, urlParams } = useLaunchDataStore();
31 const [input, setInput] = useState(
32 urlParams.description ||
33 (!getShowTitle() && urlParams.title) ||
34 descriptionBackup ||
35 '',
36 );
37 const blogname = window.extSharedData?.siteTitle || '';
38 const titlePrefill =
39 !blogname || isURL(blogname) ? '' : decodeEntities(blogname);
40 const [title, setTitle] = useState(urlParams.title || titlePrefill);
41 const [improving, setImproving] = useState(false);
42 const [checking, setChecking] = useState(false);
43 const [lastImproved, setLastImproved] = useState(null);
44 const textareaRef = useRef(null);
45 const { consentTerms } = useAIConsentStore();
46 // The title field makes the description optional, so the submit gate follows it.
47 const showTitle = getShowTitle();
48 const waiting = checking || improving;
49 // The overlay stops a pointer, not Enter on a still-enabled button.
50 const submitDisabled =
51 waiting ||
52 (showTitle ? title.trim().length === 0 : input.trim().length === 0);
53 const waitingMessage = checking
54 ? launchStrings().reviewing
55 : launchStrings().enhancing;
56
57 const adjustHeight = useCallback(() => {
58 const el = textareaRef.current;
59 if (!el) return;
60 const bottomPadding = 120;
61 // Reset to measure natural height
62 el.style.height = 'auto';
63
64 const rect = el.getBoundingClientRect();
65 const viewportHeight = window.innerHeight;
66
67 const maxAvailable = Math.max(0, viewportHeight - rect.top - bottomPadding);
68 const desired = el.scrollHeight;
69 const nextHeight = Math.min(desired, maxAvailable);
70
71 el.style.height = `${nextHeight}px`;
72 el.style.overflowY = desired > maxAvailable ? 'auto' : 'hidden';
73 }, []);
74
75 const submitForm = async (e) => {
76 e.preventDefault();
77 const trimmedTitle = title.trim();
78 const trimmedInput = input.trim();
79 if (showTitle) setData('title', trimmedTitle);
80 setData('descriptionRaw', trimmedInput);
81
82 // Only `showExtendifyCode` partners pay the classification latency; on a
83 // `1` we divert to the connector screen instead of starting site creation.
84 if (window.extSharedData?.showExtendifyCode) {
85 setChecking(true);
86 const [recommend] = await Promise.all([
87 getExtendifyCodeRecommendation(trimmedInput || trimmedTitle),
88 heldFor(MIN_LOADER_MS),
89 ]);
90 // Left on either way: clearing it first shows the form again for the
91 // length of the page fade, which reads as a second, wrong screen.
92 if (recommend === 1) {
93 setData('showExtendifyCodeScreen', true);
94 return;
95 }
96 }
97 setData('go', true);
98 };
99
100 const handleImprove = async () => {
101 setImproving(true);
102 const url = `${AI_HOST}/api/prompt/improve`;
103 const method = 'POST';
104 const headers = { 'Content-Type': 'application/json' };
105 const response = await fetchWithTimeout(url, {
106 method,
107 headers,
108 body: JSON.stringify({
109 ...reqDataBasics,
110 description: input.trim(),
111 title: window.extSharedData.siteTitle,
112 }),
113 })
114 .then((res) => res.ok && res.json())
115 .catch(() => null);
116 const nextValue = response?.improvedPrompt;
117 setImproving(false);
118 if (nextValue) {
119 setLastImproved(nextValue);
120 const el = textareaRef.current;
121 if (!el) return setInput(nextValue);
122 requestAnimationFrame(() => {
123 // Preserve undo ability by using native events instead of React state
124 el.focus();
125 el.select();
126 const ok = document.execCommand('insertText', false, nextValue);
127 if (!ok) setInput(nextValue);
128 });
129 }
130 };
131
132 useEffect(() => {
133 setData('descriptionBackup', input.trim());
134 const raf = requestAnimationFrame(() => {
135 adjustHeight();
136 });
137 return () => cancelAnimationFrame(raf);
138 }, [input, setData]);
139
140 useEffect(() => {
141 const controller = new AbortController();
142 const { signal } = controller;
143 const handleResize = () => {
144 adjustHeight();
145 const c = textareaRef.current;
146 c?.scrollTo(0, c.scrollHeight);
147 };
148 window.addEventListener('resize', handleResize, { signal });
149 window.addEventListener('orientationchange', handleResize, { signal });
150 adjustHeight();
151 return () => controller.abort();
152 }, [adjustHeight]);
153
154 return {
155 showTitle,
156 title,
157 onTitleChange: (value) => {
158 setTitle(value);
159 setData('title', value);
160 },
161 description: input,
162 onDescriptionChange: setInput,
163 descriptionRef: textareaRef,
164 placeholder: getPlaceholder(),
165 submitLabel: getSubmitLabel(),
166 submitDisabled,
167 onSubmit: submitForm,
168 showEnhance: getAbTest('AutoLaunch.HideEnhanceAI').variant !== 'B',
169 enhanceDisabled: input.trim().length === 0 || input.trim() === lastImproved,
170 onEnhance: handleImprove,
171 consentTerms,
172 strings: launchStrings(),
173 waiting,
174 waitingMessage,
175 };
176 };
177