PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
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.0, at src/AutoLaunch/components/DescriptionGathering.jsx

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