PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/AutoLaunch/components/DescriptionGathering.jsx +5 -256 3.1.0 → 3.2.2 View file →
@@ -1,257 +1,6 @@
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';
1 +import { DescriptionForm } from '@auto-launch/components/DescriptionForm';
2 +import { useDescriptionForm } from '@auto-launch/hooks/useDescriptionForm';
19 3
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 -};
4 +export const DescriptionGathering = ({ autoFocus }) => (
5 + <DescriptionForm {...useDescriptionForm()} autoFocus={autoFocus} />
6 +);