PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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 / components / DescriptionGathering.jsx

DescriptionGathering.jsx in Extendify 3.1.4, at src/AutoLaunch/components/DescriptionGathering.jsx

325 lines 10.9 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 { 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';
21
22 const getShowTitle = () => Boolean(window.extLaunchData?.showLaunchTitle);
23
24 const getSubmitOutside = () =>
25 getAbTest('AutoLaunch.SubmitOutside').variant === 'B' || getShowTitle();
26
27 export const DescriptionGathering = () => {
28 const { setData, descriptionBackup, urlParams } = useLaunchDataStore();
29 useInstallRequiredPlugins();
30 const [input, setInput] = useState(
31 urlParams.description ||
32 (!getShowTitle() && urlParams.title) ||
33 descriptionBackup ||
34 '',
35 );
36 const blogname = window.extSharedData?.siteTitle || '';
37 const titlePrefill =
38 !blogname || isURL(blogname) ? '' : decodeEntities(blogname);
39 const [title, setTitle] = useState(urlParams.title || titlePrefill);
40 const [improving, setImproving] = useState(false);
41 const [checking, setChecking] = useState(false);
42 const [lastImproved, setLastImproved] = useState(null);
43 const textareaRef = useRef(null);
44 const { consentTerms } = useAIConsentStore();
45 // Showing the title field makes the description optional, so the submit
46 // gate and the textarea autofocus both follow it.
47 const showTitle = getShowTitle();
48 const submitDisabled =
49 checking ||
50 (showTitle ? title.trim().length === 0 : input.trim().length === 0);
51 const placeholder = useDescriptionPlaceholder();
52
53 // resize the height of the textarea based on the content
54 const adjustHeight = useCallback(() => {
55 const el = textareaRef.current;
56 if (!el) return;
57 const bottomPadding = 120; // tweak as needed
58 // Reset to measure natural height
59 el.style.height = 'auto';
60
61 const rect = el.getBoundingClientRect();
62 const viewportHeight = window.innerHeight;
63
64 const maxAvailable = Math.max(0, viewportHeight - rect.top - bottomPadding);
65 const desired = el.scrollHeight;
66 const nextHeight = Math.min(desired, maxAvailable);
67
68 el.style.height = `${nextHeight}px`;
69 el.style.overflowY = desired > maxAvailable ? 'auto' : 'hidden';
70
71 // Notify others
72 window.dispatchEvent(new Event('launch-textarea-resize'));
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 getExtendifyCodeRecommendation(
87 trimmedInput || trimmedTitle,
88 );
89 if (recommend === 1) {
90 // Leave `checking` on so the loading state holds through the exit
91 // transition instead of flashing the textarea before the connector.
92 setData('showExtendifyCodeScreen', true);
93 return;
94 }
95 setChecking(false);
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 <>
156 {/* biome-ignore lint: allow onClick without keyboard */}
157 <form
158 onSubmit={submitForm}
159 onClick={() => textareaRef.current?.focus()}
160 className="relative flex w-full flex-col"
161 >
162 <TitleField
163 title={title}
164 setTitle={setTitle}
165 setData={setData}
166 improving={improving}
167 />
168 <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">
169 {improving || checking ? (
170 <div className="flex h-49 flex-col items-center justify-center gap-4">
171 <div className="h-12 w-12 text-design-main">
172 {loaderThreeDots}
173 </div>
174 <p className="m-0 text-base leading-6 text-center text-gray-800">
175 {checking &&
176 __('Reviewing your description...', 'extendify-local')}
177 {improving &&
178 __('Enhancing the website description...', 'extendify-local')}
179 </p>
180 </div>
181 ) : (
182 <>
183 <textarea
184 ref={textareaRef}
185 id="extendify-launch-chat-textarea"
186 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"
187 rows="1"
188 // biome-ignore lint: Allow autofocus here
189 autoFocus={!showTitle}
190 autoComplete="off"
191 data-1p-ignore
192 value={input}
193 onChange={(e) => {
194 setInput(e.target.value);
195 }}
196 placeholder={placeholder}
197 />
198 <div className="flex justify-between items-end gap-4 p-6">
199 <div>
200 <EnhanceWithAIButton
201 disabled={
202 input.trim().length === 0 || input.trim() === lastImproved
203 }
204 onClick={handleImprove}
205 />
206 </div>
207 <InlineSubmitButton disabled={submitDisabled} />
208 </div>
209 </>
210 )}
211 </div>
212 <OutsideSubmitButton
213 disabled={submitDisabled}
214 improving={improving || checking}
215 />
216 </form>
217 <div
218 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"
219 dangerouslySetInnerHTML={{ __html: consentTerms }}
220 />
221 </>
222 );
223 };
224
225 const useDescriptionPlaceholder = () =>
226 getAbTest('AutoLaunch.DescriptionPlaceholderLaw').variant === 'B'
227 ? __(
228 'E.g., A boutique law firm specializing in family law, estate planning, and real estate, offering trusted, personalized counsel to clients across the region.',
229 'extendify-local',
230 )
231 : __(
232 'E.g., A personal photography portfolio featuring a collection of landscape, portrait, and street photography, capturing moments from around the world.',
233 'extendify-local',
234 );
235
236 const TitleField = ({ title, setTitle, setData, improving }) => {
237 if (!getShowTitle() || improving) return null;
238 return (
239 <>
240 <div className="mb-4 w-full">
241 <label
242 htmlFor="extendify-launch-site-title"
243 className="mb-2 block px-2 text-base font-medium leading-6 text-banner-text"
244 >
245 {__('Website title (required)', 'extendify-local')}
246 </label>
247 <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">
248 <input
249 id="extendify-launch-site-title"
250 type="text"
251 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"
252 // biome-ignore lint: Allow autofocus here
253 autoFocus
254 autoComplete="off"
255 data-1p-ignore
256 value={title}
257 // the form's onClick refocuses the textarea; keep clicks here local
258 onClick={(e) => e.stopPropagation()}
259 onChange={(e) => {
260 setTitle(e.target.value);
261 setData('title', e.target.value);
262 }}
263 placeholder={__('Enter your website name', 'extendify-local')}
264 />
265 </div>
266 </div>
267 <label
268 htmlFor="extendify-launch-chat-textarea"
269 className="mb-2 block px-2 text-base font-medium leading-6 text-banner-text"
270 >
271 {__('Describe your website', 'extendify-local')}
272 </label>
273 </>
274 );
275 };
276
277 const InlineSubmitButton = ({ disabled }) => {
278 if (getSubmitOutside()) return null;
279 return <SubmitButton disabled={disabled} />;
280 };
281
282 const OutsideSubmitButton = ({ disabled, improving }) => {
283 if (!getSubmitOutside() || improving) {
284 return null;
285 }
286 return (
287 <div className="mt-4 flex justify-end">
288 <SubmitButton disabled={disabled} />
289 </div>
290 );
291 };
292
293 const SubmitButton = forwardRef((props, ref) => {
294 const label =
295 getAbTest('AutoLaunch.SubmitCreateWebsite').variant === 'B'
296 ? __('Create website', 'extendify-local')
297 : __('Next', 'extendify-local');
298 return (
299 <button
300 ref={ref}
301 type="submit"
302 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"
303 {...props}
304 >
305 <span className="px-1">{label}</span>
306 <Icon fill="currentColor" icon={chevronRight} size={24} />
307 </button>
308 );
309 });
310
311 const EnhanceWithAIButton = (props) => {
312 if (getAbTest('AutoLaunch.HideEnhanceAI').variant === 'B') return null;
313 return (
314 <button
315 type="button"
316 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"
317 {...props}
318 >
319 <Icon icon={pencil} size={24} />
320 {/* translators: "Enhance with AI" refers to improving the current input using AI. */}
321 <span className="px-1">{__('Enhance with AI', 'extendify-local')}</span>
322 </button>
323 );
324 };
325