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

195 lines 6.5 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 { __ } from '@wordpress/i18n';
16 import { chevronRight, Icon, pencil } from '@wordpress/icons';
17
18 export const DescriptionGathering = () => {
19 const { setData, descriptionBackup, urlParams } = useLaunchDataStore();
20 useInstallRequiredPlugins();
21 const [input, setInput] = useState(
22 urlParams.description || urlParams.title || descriptionBackup || '',
23 );
24 const [improving, setImproving] = useState(false);
25 const [lastImproved, setLastImproved] = useState(null);
26 const textareaRef = useRef(null);
27 const { consentTerms } = useAIConsentStore();
28
29 // resize the height of the textarea based on the content
30 const adjustHeight = useCallback(() => {
31 const el = textareaRef.current;
32 if (!el) return;
33 const bottomPadding = 120; // tweak as needed
34 // Reset to measure natural height
35 el.style.height = 'auto';
36
37 const rect = el.getBoundingClientRect();
38 const viewportHeight = window.innerHeight;
39
40 const maxAvailable = Math.max(0, viewportHeight - rect.top - bottomPadding);
41 const desired = el.scrollHeight;
42 const nextHeight = Math.min(desired, maxAvailable);
43
44 el.style.height = `${nextHeight}px`;
45 el.style.overflowY = desired > maxAvailable ? 'auto' : 'hidden';
46
47 // Notify others
48 window.dispatchEvent(new Event('launch-textarea-resize'));
49 }, []);
50
51 const submitForm = (e) => {
52 e.preventDefault();
53 setData('descriptionRaw', input.trim());
54 setData('go', true);
55 };
56
57 const handleImprove = async () => {
58 setImproving(true);
59 const url = `${AI_HOST}/api/prompt/improve`;
60 const method = 'POST';
61 const headers = { 'Content-Type': 'application/json' };
62 const response = await fetchWithTimeout(url, {
63 method,
64 headers,
65 body: JSON.stringify({
66 ...reqDataBasics,
67 description: input.trim(),
68 title: window.extSharedData.siteTitle,
69 }),
70 })
71 .then((res) => res.ok && res.json())
72 .catch(() => null);
73 const nextValue = response?.improvedPrompt;
74 setImproving(false);
75 if (nextValue) {
76 setLastImproved(nextValue);
77 const el = textareaRef.current;
78 if (!el) return setInput(nextValue);
79 requestAnimationFrame(() => {
80 // Preserve undo ability by using native events instead of React state
81 el.focus();
82 el.select();
83 const ok = document.execCommand('insertText', false, nextValue);
84 if (!ok) setInput(nextValue);
85 });
86 }
87 };
88
89 useEffect(() => {
90 setData('descriptionBackup', input.trim());
91 const raf = requestAnimationFrame(() => {
92 adjustHeight();
93 });
94 return () => cancelAnimationFrame(raf);
95 }, [input, setData]);
96
97 useEffect(() => {
98 const controller = new AbortController();
99 const { signal } = controller;
100 const handleResize = () => {
101 adjustHeight();
102 const c = textareaRef.current;
103 c?.scrollTo(0, c.scrollHeight);
104 };
105 window.addEventListener('resize', handleResize, { signal });
106 window.addEventListener('orientationchange', handleResize, { signal });
107 adjustHeight();
108 return () => controller.abort();
109 }, [adjustHeight]);
110
111 return (
112 <>
113 {/* biome-ignore lint: allow onClick without keyboard */}
114 <form
115 onSubmit={submitForm}
116 onClick={() => textareaRef.current?.focus()}
117 className="relative flex w-full flex-col"
118 >
119 <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">
120 {improving ? (
121 <div className="flex h-49 flex-col items-center justify-center gap-4">
122 <div className="h-12 w-12 text-design-main">
123 {loaderThreeDots}
124 </div>
125 <p className="m-0 text-base leading-6 text-center text-gray-800">
126 {__('Enhancing the website description...', 'extendify-local')}
127 </p>
128 </div>
129 ) : (
130 <>
131 <textarea
132 ref={textareaRef}
133 id="extendify-launch-chat-textarea"
134 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"
135 rows="1"
136 // biome-ignore lint: Allow autofocus here
137 autoFocus
138 value={input}
139 onChange={(e) => {
140 setInput(e.target.value);
141 }}
142 placeholder={__(
143 'E.g., A personal photography portfolio featuring a collection of landscape, portrait, and street photography, capturing moments from around the world.',
144 'extendify-local',
145 )}
146 />
147 <div className="flex justify-between items-end gap-4 p-6">
148 <div>
149 <ImprovePrompt
150 disabled={
151 input.trim().length === 0 || input.trim() === lastImproved
152 }
153 onClick={handleImprove}
154 />
155 </div>
156 <SubmitButton disabled={input.trim().length === 0} />
157 </div>
158 </>
159 )}
160 </div>
161 </form>
162 <div
163 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"
164 dangerouslySetInnerHTML={{ __html: consentTerms }}
165 />
166 </>
167 );
168 };
169
170 const SubmitButton = forwardRef((props, ref) => (
171 <button
172 ref={ref}
173 type="submit"
174 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"
175 {...props}
176 >
177 <span className="px-1">{__('Next', 'extendify-local')}</span>
178 <Icon fill="currentColor" icon={chevronRight} size={24} />
179 </button>
180 ));
181
182 const ImprovePrompt = (props) => {
183 return (
184 <button
185 type="button"
186 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"
187 {...props}
188 >
189 <Icon icon={pencil} size={24} />
190 {/* translators: "Enhance with AI" refers to improving the current input using AI. */}
191 <span className="px-1">{__('Enhance with AI', 'extendify-local')}</span>
192 </button>
193 );
194 };
195