PluginProbe
Extendify / 3.1.1
Extendify v3.1.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
extendify / src / AutoLaunch / components / DescriptionGathering.jsx

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

293 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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';
20
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 };
293