PluginProbe
Extendify / trunk
Extendify vtrunk
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 -323 3.1.4 → trunk View file →
@@ -1,324 +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 = () => 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 -};
4 +export const DescriptionGathering = ({ autoFocus }) => (
5 + <DescriptionForm {...useDescriptionForm()} autoFocus={autoFocus} />
6 +);