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