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