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
← All changes | src/AutoLaunch/components/DescriptionGathering.jsx +5 -317 3.1.23.2.1 View file →
@@ -1,318 +1,6 @@
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 { useInstallRequiredPlugins } from '@auto-launch/hooks/useInstallRequiredPlugins';
5 -import { loaderThreeDots } from '@auto-launch/icons';
6 -import { useLaunchDataStore } from '@auto-launch/state/launch-data';
7 -import { AI_HOST } from '@constants';
8 -import { reqDataBasics } from '@shared/lib/data';
9 -import { useAIConsentStore } from '@shared/state/ai-consent';
10 -import {
11 - forwardRef,
12 - useCallback,
13 - useEffect,
14 - useRef,
15 - useState,
16 -} from '@wordpress/element';
17 -import { decodeEntities } from '@wordpress/html-entities';
18 -import { __ } from '@wordpress/i18n';
19 -import { chevronRight, Icon, pencil } from '@wordpress/icons';
20 -import { isURL } from '@wordpress/url';
1 +import { DescriptionForm } from '@auto-launch/components/DescriptionForm';
2 +import { useDescriptionForm } from '@auto-launch/hooks/useDescriptionForm';
21 3
22 -const getShowTitle = () => getAbTest('AutoLaunch.ShowTitle').variant === 'B';
23 -
24 -export const DescriptionGathering = () => {
25 - const { setData, descriptionBackup, urlParams } = useLaunchDataStore();
26 - useInstallRequiredPlugins();
27 - const [input, setInput] = useState(
28 - urlParams.description || urlParams.title || descriptionBackup || '',
29 - );
30 - const blogname = window.extSharedData?.siteTitle || '';
31 - const titlePrefill =
32 - !blogname || isURL(blogname) ? '' : decodeEntities(blogname);
33 - const [title, setTitle] = useState(urlParams.title || titlePrefill);
34 - const [improving, setImproving] = useState(false);
35 - const [checking, setChecking] = useState(false);
36 - const [lastImproved, setLastImproved] = useState(null);
37 - const textareaRef = useRef(null);
38 - const { consentTerms } = useAIConsentStore();
39 - // Showing the title field makes the description optional, so the submit
40 - // gate and the textarea autofocus both follow it.
41 - const showTitle = getShowTitle();
42 - const submitDisabled =
43 - checking ||
44 - (showTitle ? title.trim().length === 0 : input.trim().length === 0);
45 - const placeholder = useDescriptionPlaceholder();
46 -
47 - // resize the height of the textarea based on the content
48 - const adjustHeight = useCallback(() => {
49 - const el = textareaRef.current;
50 - if (!el) return;
51 - const bottomPadding = 120; // tweak as needed
52 - // Reset to measure natural height
53 - el.style.height = 'auto';
54 -
55 - const rect = el.getBoundingClientRect();
56 - const viewportHeight = window.innerHeight;
57 -
58 - const maxAvailable = Math.max(0, viewportHeight - rect.top - bottomPadding);
59 - const desired = el.scrollHeight;
60 - const nextHeight = Math.min(desired, maxAvailable);
61 -
62 - el.style.height = `${nextHeight}px`;
63 - el.style.overflowY = desired > maxAvailable ? 'auto' : 'hidden';
64 -
65 - // Notify others
66 - window.dispatchEvent(new Event('launch-textarea-resize'));
67 - }, []);
68 -
69 - const submitForm = async (e) => {
70 - e.preventDefault();
71 - const trimmedTitle = title.trim();
72 - const trimmedInput = input.trim();
73 - if (showTitle) setData('title', trimmedTitle);
74 - setData('descriptionRaw', trimmedInput);
75 -
76 - // Only `showExtendifyCode` partners pay the classification latency; on a
77 - // `1` we divert to the connector screen instead of starting site creation.
78 - if (window.extSharedData?.showExtendifyCode) {
79 - setChecking(true);
80 - const recommend = await getExtendifyCodeRecommendation(
81 - trimmedInput || trimmedTitle,
82 - );
83 - if (recommend === 1) {
84 - // Leave `checking` on so the loading state holds through the exit
85 - // transition instead of flashing the textarea before the connector.
86 - setData('showExtendifyCodeScreen', true);
87 - return;
88 - }
89 - setChecking(false);
90 - }
91 - setData('go', true);
92 - };
93 -
94 - const handleImprove = async () => {
95 - setImproving(true);
96 - const url = `${AI_HOST}/api/prompt/improve`;
97 - const method = 'POST';
98 - const headers = { 'Content-Type': 'application/json' };
99 - const response = await fetchWithTimeout(url, {
100 - method,
101 - headers,
102 - body: JSON.stringify({
103 - ...reqDataBasics,
104 - description: input.trim(),
105 - title: window.extSharedData.siteTitle,
106 - }),
107 - })
108 - .then((res) => res.ok && res.json())
109 - .catch(() => null);
110 - const nextValue = response?.improvedPrompt;
111 - setImproving(false);
112 - if (nextValue) {
113 - setLastImproved(nextValue);
114 - const el = textareaRef.current;
115 - if (!el) return setInput(nextValue);
116 - requestAnimationFrame(() => {
117 - // Preserve undo ability by using native events instead of React state
118 - el.focus();
119 - el.select();
120 - const ok = document.execCommand('insertText', false, nextValue);
121 - if (!ok) setInput(nextValue);
122 - });
123 - }
124 - };
125 -
126 - useEffect(() => {
127 - setData('descriptionBackup', input.trim());
128 - const raf = requestAnimationFrame(() => {
129 - adjustHeight();
130 - });
131 - return () => cancelAnimationFrame(raf);
132 - }, [input, setData]);
133 -
134 - useEffect(() => {
135 - const controller = new AbortController();
136 - const { signal } = controller;
137 - const handleResize = () => {
138 - adjustHeight();
139 - const c = textareaRef.current;
140 - c?.scrollTo(0, c.scrollHeight);
141 - };
142 - window.addEventListener('resize', handleResize, { signal });
143 - window.addEventListener('orientationchange', handleResize, { signal });
144 - adjustHeight();
145 - return () => controller.abort();
146 - }, [adjustHeight]);
147 -
148 - return (
149 - <>
150 - {/* biome-ignore lint: allow onClick without keyboard */}
151 - <form
152 - onSubmit={submitForm}
153 - onClick={() => textareaRef.current?.focus()}
154 - className="relative flex w-full flex-col"
155 - >
156 - <TitleField
157 - title={title}
158 - setTitle={setTitle}
159 - setData={setData}
160 - improving={improving}
161 - />
162 - <div className="w-full rounded-3xl border border-gray-300 bg-gray-100/80 text-gray-900 backdrop-blur-2xl focus-within:border-gray-500 focus-within:ring-gray-500 shadow-md overflow-hidden">
163 - {improving || checking ? (
164 - <div className="flex h-49 flex-col items-center justify-center gap-4">
165 - <div className="h-12 w-12 text-design-main">
166 - {loaderThreeDots}
167 - </div>
168 - <p className="m-0 text-base leading-6 text-center text-gray-800">
169 - {checking &&
170 - __('Reviewing your description...', 'extendify-local')}
171 - {improving &&
172 - __('Enhancing the website description...', 'extendify-local')}
173 - </p>
174 - </div>
175 - ) : (
176 - <>
177 - <textarea
178 - ref={textareaRef}
179 - id="extendify-launch-chat-textarea"
180 - className="flex min-h-20 md:min-h-24 w-full resize-none bg-transparent text-base leading-6 placeholder:text-gray-700 focus:shadow-none focus:outline-hidden border-none text-gray-900 p-6 pb-0"
181 - rows="1"
182 - // biome-ignore lint: Allow autofocus here
183 - autoFocus={!showTitle}
184 - autoComplete="off"
185 - data-1p-ignore
186 - value={input}
187 - onChange={(e) => {
188 - setInput(e.target.value);
189 - }}
190 - placeholder={placeholder}
191 - />
192 - <div className="flex justify-between items-end gap-4 p-6">
193 - <div>
194 - <EnhanceWithAIButton
195 - disabled={
196 - input.trim().length === 0 || input.trim() === lastImproved
197 - }
198 - onClick={handleImprove}
199 - />
200 - </div>
201 - <InlineSubmitButton disabled={submitDisabled} />
202 - </div>
203 - </>
204 - )}
205 - </div>
206 - <OutsideSubmitButton
207 - disabled={submitDisabled}
208 - improving={improving || checking}
209 - />
210 - </form>
211 - <div
212 - className="text-pretty mt-4 text-center text-xs leading-4 opacity-70 text-banner-text [&>a]:text-xs [&>a]:text-banner-text [&>a]:underline w-full"
213 - dangerouslySetInnerHTML={{ __html: consentTerms }}
214 - />
215 - </>
216 - );
217 -};
218 -
219 -const useDescriptionPlaceholder = () =>
220 - getAbTest('AutoLaunch.DescriptionPlaceholderLaw').variant === 'B'
221 - ? __(
222 - 'E.g., A boutique law firm specializing in family law, estate planning, and real estate, offering trusted, personalized counsel to clients across the region.',
223 - 'extendify-local',
224 - )
225 - : __(
226 - 'E.g., A personal photography portfolio featuring a collection of landscape, portrait, and street photography, capturing moments from around the world.',
227 - 'extendify-local',
228 - );
229 -
230 -const TitleField = ({ title, setTitle, setData, improving }) => {
231 - if (!getShowTitle() || improving) return null;
232 - return (
233 - <>
234 - <div className="mb-4 w-full">
235 - <label
236 - htmlFor="extendify-launch-site-title"
237 - className="mb-2 block px-2 text-base font-medium leading-6 text-banner-text"
238 - >
239 - {__('Website title (required)', 'extendify-local')}
240 - </label>
241 - <div className="w-full rounded-3xl border border-gray-300 bg-gray-100/80 text-gray-900 backdrop-blur-2xl focus-within:border-gray-500 focus-within:ring-gray-500 shadow-md overflow-hidden">
242 - <input
243 - id="extendify-launch-site-title"
244 - type="text"
245 - className="w-full bg-transparent text-base font-medium leading-6 placeholder:text-gray-700 placeholder:font-normal focus:shadow-none focus:outline-hidden border-none text-gray-900 px-6 py-4"
246 - // biome-ignore lint: Allow autofocus here
247 - autoFocus
248 - autoComplete="off"
249 - data-1p-ignore
250 - value={title}
251 - // the form's onClick refocuses the textarea; keep clicks here local
252 - onClick={(e) => e.stopPropagation()}
253 - onChange={(e) => {
254 - setTitle(e.target.value);
255 - setData('title', e.target.value);
256 - }}
257 - placeholder={__('Enter your website name', 'extendify-local')}
258 - />
259 - </div>
260 - </div>
261 - <label
262 - htmlFor="extendify-launch-chat-textarea"
263 - className="mb-2 block px-2 text-base font-medium leading-6 text-banner-text"
264 - >
265 - {__('Describe your website', 'extendify-local')}
266 - </label>
267 - </>
268 - );
269 -};
270 -
271 -const InlineSubmitButton = ({ disabled }) => {
272 - if (getAbTest('AutoLaunch.SubmitOutside').variant === 'B') return null;
273 - return <SubmitButton disabled={disabled} />;
274 -};
275 -
276 -const OutsideSubmitButton = ({ disabled, improving }) => {
277 - if (getAbTest('AutoLaunch.SubmitOutside').variant !== 'B' || improving) {
278 - return null;
279 - }
280 - return (
281 - <div className="mt-4 flex justify-end">
282 - <SubmitButton disabled={disabled} />
283 - </div>
284 - );
285 -};
286 -
287 -const SubmitButton = forwardRef((props, ref) => {
288 - const label =
289 - getAbTest('AutoLaunch.SubmitCreateWebsite').variant === 'B'
290 - ? __('Create website', 'extendify-local')
291 - : __('Next', 'extendify-local');
292 - return (
293 - <button
294 - ref={ref}
295 - type="submit"
296 - className="inline-flex items-center justify-center rounded-full border-0 bg-design-main px-3 py-2 text-sm leading-5 font-normal text-design-text focus-visible:ring-design-main disabled:opacity-40 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-2 group hover:opacity-90 transition-opacity"
297 - {...props}
298 - >
299 - <span className="px-1">{label}</span>
300 - <Icon fill="currentColor" icon={chevronRight} size={24} />
301 - </button>
302 - );
303 -});
304 -
305 -const EnhanceWithAIButton = (props) => {
306 - if (getAbTest('AutoLaunch.HideEnhanceAI').variant === 'B') return null;
307 - return (
308 - <button
309 - type="button"
310 - className="inline-flex items-center rounded-full ring-1 ring-gray-800 px-3 py-2 text-sm leading-5 font-normal text-gray-800 transition-colors hover:bg-gray-600/5 disabled:opacity-40"
311 - {...props}
312 - >
313 - <Icon icon={pencil} size={24} />
314 - {/* translators: "Enhance with AI" refers to improving the current input using AI. */}
315 - <span className="px-1">{__('Enhance with AI', 'extendify-local')}</span>
316 - </button>
317 - );
318 -};
4 +export const DescriptionGathering = ({ autoFocus }) => (
5 + <DescriptionForm {...useDescriptionForm()} autoFocus={autoFocus} />
6 +);