PluginProbe
Extendify / 3.2.1
Extendify v3.2.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
← All changes | src/Agent/components/ChatInput.jsx +80 -60 3.0.63.2.1 View file →
@@ -1,8 +1,11 @@
1 -import { ChatTools } from '@agent/components/ChatTools';
1 +import { useCanvasAssist } from '@agent/components/Canvas';
2 +import { PageDocument } from '@agent/components/PageDocument';
2 3 import { cancelRequest } from '@agent/icons';
3 -import { useGlobalStore } from '@agent/state/global';
4 +import { useChatStore } from '@agent/state/chat';
4 5 import { useWorkflowStore } from '@agent/state/workflows';
6 +import { askAiTarget } from '@quick-edit/lib/hover-bar';
7 +import { useQuickEditStore } from '@quick-edit/state/store';
5 8 import {
6 9 useCallback,
7 10 useEffect,
8 11 useLayoutEffect,
@@ -12,8 +15,24 @@
12 15 import { __ } from '@wordpress/i18n';
13 16 import { arrowUp, Icon } from '@wordpress/icons';
14 17 import classNames from 'classnames';
15 18
19 +const placeholderFor = ({ disabled, editing, block }) => {
20 + // `disabled` also covers being out of credits, so it outranks the rest.
21 + if (disabled) return __('Ask anything', 'extendify-local');
22 + if (editing) {
23 + // translators: Shown in the AI chat box while a Quick Edit editor is open on a block.
24 + return __('Finish editing first', 'extendify-local');
25 + }
26 + if (block) {
27 + return __(
28 + 'What do you want to change in the selected content?',
29 + 'extendify-local',
30 + );
31 + }
32 + return __('Ask anything', 'extendify-local');
33 +};
34 +
16 35 export const ChatInput = ({ disabled, handleSubmit }) => {
17 36 const textareaRef = useRef(null);
18 37 const [input, setInput] = useState('');
19 38 const [history, setHistory] = useState([]);
@@ -18,20 +37,26 @@
18 37 const [input, setInput] = useState('');
19 38 const [history, setHistory] = useState([]);
20 39 const dirtyRef = useRef(false);
21 40 const [historyIndex, setHistoryIndex] = useState(null);
22 - const { getWorkflowsByFeature, block } = useWorkflowStore();
23 - const { isMobile } = useGlobalStore();
24 - const domTool =
25 - getWorkflowsByFeature({ requires: ['block'] })?.length > 0 && !isMobile;
41 + const { workflow } = useWorkflowStore();
42 + const canvasAssist = useCanvasAssist();
43 + const block = useQuickEditStore((s) => s.agentBlock);
44 + // Quick Edit's modals mount off `selected` too, so this covers them.
45 + const editing = useQuickEditStore((s) => Boolean(s.selected));
26 46 const INPUT_LIMIT = 1500;
27 47 const inputTrimmed = input.trim();
28 48 const overLimit = inputTrimmed.length > INPUT_LIMIT;
49 + // The workflow stays set with a canvas open, so this would show cancel.
50 + const busy = disabled || (Boolean(workflow?.id) && !canvasAssist);
51 + const inputDisabled = disabled || editing;
29 52
30 53 // resize the height of the textarea based on the content
31 54 const adjustHeight = useCallback(() => {
32 55 if (!textareaRef.current) return;
33 56 textareaRef.current.style.height = 'auto';
57 + // while empty, scrollHeight measures the wrapped placeholder and overshoots
58 + if (!textareaRef.current.value) return;
34 59 const chat =
35 60 textareaRef.current.closest('#extendify-agent-chat').offsetHeight * 0.55;
36 61 const h = Math.min(chat, textareaRef.current.scrollHeight);
37 62 textareaRef.current.style.height = `${block && h < 60 ? 60 : h}px`;
@@ -44,36 +69,23 @@
44 69 window.removeEventListener('extendify-agent:resize-end', adjustHeight);
45 70 }, [adjustHeight]);
46 71
47 72 useEffect(() => {
48 - const watchForSubmit = ({ detail }) => {
49 - setHistory((prev) => {
50 - // avoid duplicates
51 - if (prev?.at(-1) === detail.message) return prev;
52 - return [...prev, detail.message];
53 - });
54 - setHistoryIndex(null);
55 - };
56 - window.addEventListener('extendify-agent:chat-submit', watchForSubmit);
57 - return () =>
58 - window.removeEventListener('extendify-agent:chat-submit', watchForSubmit);
59 - }, []);
60 -
61 - useEffect(() => {
62 73 adjustHeight();
63 74 }, [input, adjustHeight]);
64 75
76 + // Derive the up-arrow history from the chat store so it survives reload —
77 + // a one-shot DOM read missed messages that hydrate in asynchronously.
78 + const messages = useChatStore((s) => s.messages);
65 79 useEffect(() => {
66 - const userMessages = Array.from(
67 - document.querySelectorAll(
68 - '#extendify-agent-chat-scroll-area > [data-agent-message-role="user"]',
69 - ),
70 - )?.map((el) => el.textContent || '');
71 - const deduped = userMessages.filter(
72 - (msg, i, arr) => i === 0 || msg !== arr[i - 1],
80 + const userMessages = messages
81 + .filter((m) => m.type === 'message' && m.details?.role === 'user')
82 + .map((m) => m.details.content ?? '');
83 + setHistory(
84 + userMessages.filter((msg, i, arr) => i === 0 || msg !== arr[i - 1]),
73 85 );
74 - setHistory(deduped);
75 - }, []);
86 + setHistoryIndex(null);
87 + }, [messages]);
76 88
77 89 const submitForm = useCallback(
78 90 (e) => {
79 91 e?.preventDefault();
@@ -141,8 +153,15 @@
141 153 },
142 154 [history, historyIndex, submitForm, overLimit],
143 155 );
144 156
157 + // Typing beside a pinned bar is a request about that block.
158 + const stagePinnedSelection = useCallback(() => {
159 + const { committedSelection, agentBlock } = useQuickEditStore.getState();
160 + if (!committedSelection?.el || agentBlock) return;
161 + askAiTarget(committedSelection.el);
162 + }, []);
163 +
145 164 const handleCancel = useCallback((e) => {
146 165 e.stopPropagation();
147 166 window.dispatchEvent(new CustomEvent('extendify-agent:cancel-workflow'));
148 167 }, []);
@@ -154,28 +173,26 @@
154 173 onClick={() => textareaRef.current?.focus()}
155 174 className={classNames(
156 175 'relative flex w-full flex-col rounded-sm border border-gray-300 focus-within:outline-design-main focus:rounded-sm focus:border-design-main focus:ring-design-main',
157 176 {
158 - 'bg-gray-300': disabled,
159 - 'bg-gray-50': !disabled,
177 + 'bg-gray-300': inputDisabled,
178 + 'bg-gray-50': !inputDisabled,
160 179 },
161 180 )}
162 181 >
182 + {block ? (
183 + <div className="px-2 pt-2">
184 + <PageDocument busy={busy} />
185 + </div>
186 + ) : null}
163 187 <textarea
164 188 ref={textareaRef}
165 189 id="extendify-agent-chat-textarea"
166 - disabled={disabled}
190 + disabled={inputDisabled}
167 191 className={classNames(
168 192 'flex max-h-[calc(75dvh)] min-h-16 w-full resize-none overflow-y-auto bg-transparent px-2 pb-4 pt-2.5 text-base placeholder:text-gray-700 focus:shadow-none focus:outline-hidden disabled:opacity-50 md:text-sm border-none text-gray-900',
169 193 )}
170 - placeholder={
171 - block
172 - ? __(
173 - 'What do you want to change in the selected content?',
174 - 'extendify-local',
175 - )
176 - : __('Ask anything', 'extendify-local')
177 - }
194 + placeholder={placeholderFor({ disabled, editing, block })}
178 195 rows="1"
179 196 // biome-ignore lint: Allow autofocus here
180 197 autoFocus
181 198 value={input}
@@ -184,12 +201,12 @@
184 201 setHistoryIndex(null);
185 202 adjustHeight();
186 203 }}
187 204 onKeyDown={handleKeyDown}
205 + onFocus={stagePinnedSelection}
188 206 />
189 207 <div className="flex justify-between gap-4 px-2 pb-2">
190 - {domTool ? <ChatTools disabled={disabled} /> : null}
191 - <div className="ms-auto flex items-center gap-2">
208 + <div className="ms-auto flex items-center gap-1">
192 209 <span
193 210 className={classNames(
194 211 'text-xs font-medium',
195 212 overLimit ? 'text-red-600' : 'invisible',
@@ -198,11 +215,12 @@
198 215 >
199 216 {overLimit && __('Message too long', 'extendify-local')}
200 217 </span>
201 218 <SubmitButton
202 - disabled={disabled}
219 + disabled={inputDisabled}
203 220 noInput={input.trim().length === 0}
204 221 overLimit={overLimit}
222 + showCancel={busy}
205 223 handleCancel={handleCancel}
206 224 />
207 225 </div>
208 226 </div>
@@ -209,28 +227,30 @@
209 227 </form>
210 228 );
211 229 };
212 230
213 -const SubmitButton = ({ disabled, noInput, overLimit, handleCancel }) => {
214 - if (disabled) {
215 - return (
216 - <button
217 - type="button"
218 - onClick={handleCancel}
219 - className="inline-flex h-fit items-center justify-center gap-2 whitespace-nowrap rounded-full border-0 bg-design-main p-1 text-sm font-medium text-white transition-colors focus-visible:ring-design-main disabled:opacity-20"
220 - >
221 - <Icon fill="currentColor" icon={cancelRequest} size={18} />
222 - <span className="sr-only">{__('Cancel', 'extendify-local')}</span>
223 - </button>
224 - );
225 - }
226 - return (
231 +const SubmitButton = ({
232 + disabled,
233 + noInput,
234 + overLimit,
235 + showCancel,
236 + handleCancel,
237 +}) =>
238 + showCancel ? (
227 239 <button
240 + type="button"
241 + onClick={handleCancel}
242 + className="inline-flex h-7 w-7 items-center justify-center rounded-full border-0 bg-design-main p-0 text-white transition-colors focus-visible:ring-design-main disabled:opacity-20"
243 + >
244 + <Icon fill="currentColor" icon={cancelRequest} size={14} />
245 + <span className="sr-only">{__('Cancel', 'extendify-local')}</span>
246 + </button>
247 + ) : (
248 + <button
228 249 type="submit"
229 - className="inline-flex h-fit items-center justify-center gap-2 whitespace-nowrap rounded-full border-0 bg-design-main p-0.5 text-sm font-medium text-white transition-colors focus-visible:ring-design-main disabled:opacity-20"
250 + className="inline-flex h-7 w-7 items-center justify-center rounded-full border-0 bg-design-main p-0 text-white transition-colors focus-visible:ring-design-main disabled:opacity-20"
230 251 disabled={disabled || noInput || overLimit}
231 252 >
232 - <Icon fill="currentColor" icon={arrowUp} size={24} />
253 + <Icon fill="currentColor" icon={arrowUp} size={20} />
233 254 <span className="sr-only">{__('Send message', 'extendify-local')}</span>
234 255 </button>
235 256 );
236 -};