PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.3, at src/AutoLaunch/components/DescriptionGathering.jsx

319 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 = () => 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 };
319