PluginProbe
Extendify / trunk
Extendify vtrunk
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 +78 -60 3.1.1 → trunk View file →
@@ -1,8 +1,10 @@
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';
5 7 import { useQuickEditStore } from '@quick-edit/state/store';
6 8 import {
7 9 useCallback,
8 10 useEffect,
@@ -13,8 +15,24 @@
13 15 import { __ } from '@wordpress/i18n';
14 16 import { arrowUp, Icon } from '@wordpress/icons';
15 17 import classNames from 'classnames';
16 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 +
17 35 export const ChatInput = ({ disabled, handleSubmit }) => {
18 36 const textareaRef = useRef(null);
19 37 const [input, setInput] = useState('');
20 38 const [history, setHistory] = useState([]);
@@ -19,21 +37,26 @@
19 37 const [input, setInput] = useState('');
20 38 const [history, setHistory] = useState([]);
21 39 const dirtyRef = useRef(false);
22 40 const [historyIndex, setHistoryIndex] = useState(null);
23 - const { getWorkflowsByFeature } = useWorkflowStore();
41 + const { workflow } = useWorkflowStore();
42 + const canvasAssist = useCanvasAssist();
24 43 const block = useQuickEditStore((s) => s.agentBlock);
25 - const { isMobile } = useGlobalStore();
26 - const domTool =
27 - getWorkflowsByFeature({ requires: ['block'] })?.length > 0 && !isMobile;
44 + // Quick Edit's modals mount off `selected` too, so this covers them.
45 + const editing = useQuickEditStore((s) => Boolean(s.selected));
28 46 const INPUT_LIMIT = 1500;
29 47 const inputTrimmed = input.trim();
30 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;
31 52
32 53 // resize the height of the textarea based on the content
33 54 const adjustHeight = useCallback(() => {
34 55 if (!textareaRef.current) return;
35 56 textareaRef.current.style.height = 'auto';
57 + // while empty, scrollHeight measures the wrapped placeholder and overshoots
58 + if (!textareaRef.current.value) return;
36 59 const chat =
37 60 textareaRef.current.closest('#extendify-agent-chat').offsetHeight * 0.55;
38 61 const h = Math.min(chat, textareaRef.current.scrollHeight);
39 62 textareaRef.current.style.height = `${block && h < 60 ? 60 : h}px`;
@@ -46,36 +69,23 @@
46 69 window.removeEventListener('extendify-agent:resize-end', adjustHeight);
47 70 }, [adjustHeight]);
48 71
49 72 useEffect(() => {
50 - const watchForSubmit = ({ detail }) => {
51 - setHistory((prev) => {
52 - // avoid duplicates
53 - if (prev?.at(-1) === detail.message) return prev;
54 - return [...prev, detail.message];
55 - });
56 - setHistoryIndex(null);
57 - };
58 - window.addEventListener('extendify-agent:chat-submit', watchForSubmit);
59 - return () =>
60 - window.removeEventListener('extendify-agent:chat-submit', watchForSubmit);
61 - }, []);
62 -
63 - useEffect(() => {
64 73 adjustHeight();
65 74 }, [input, adjustHeight]);
66 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);
67 79 useEffect(() => {
68 - const userMessages = Array.from(
69 - document.querySelectorAll(
70 - '#extendify-agent-chat-scroll-area > [data-agent-message-role="user"]',
71 - ),
72 - )?.map((el) => el.textContent || '');
73 - const deduped = userMessages.filter(
74 - (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]),
75 85 );
76 - setHistory(deduped);
77 - }, []);
86 + setHistoryIndex(null);
87 + }, [messages]);
78 88
79 89 const submitForm = useCallback(
80 90 (e) => {
81 91 e?.preventDefault();
@@ -143,8 +153,15 @@
143 153 },
144 154 [history, historyIndex, submitForm, overLimit],
145 155 );
146 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 +
147 164 const handleCancel = useCallback((e) => {
148 165 e.stopPropagation();
149 166 window.dispatchEvent(new CustomEvent('extendify-agent:cancel-workflow'));
150 167 }, []);
@@ -156,28 +173,26 @@
156 173 onClick={() => textareaRef.current?.focus()}
157 174 className={classNames(
158 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',
159 176 {
160 - 'bg-gray-300': disabled,
161 - 'bg-gray-50': !disabled,
177 + 'bg-gray-300': inputDisabled,
178 + 'bg-gray-50': !inputDisabled,
162 179 },
163 180 )}
164 181 >
182 + {block ? (
183 + <div className="px-2 pt-2">
184 + <PageDocument busy={busy} />
185 + </div>
186 + ) : null}
165 187 <textarea
166 188 ref={textareaRef}
167 189 id="extendify-agent-chat-textarea"
168 - disabled={disabled}
190 + disabled={inputDisabled}
169 191 className={classNames(
170 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',
171 193 )}
172 - placeholder={
173 - block
174 - ? __(
175 - 'What do you want to change in the selected content?',
176 - 'extendify-local',
177 - )
178 - : __('Ask anything', 'extendify-local')
179 - }
194 + placeholder={placeholderFor({ disabled, editing, block })}
180 195 rows="1"
181 196 // biome-ignore lint: Allow autofocus here
182 197 autoFocus
183 198 value={input}
@@ -186,12 +201,12 @@
186 201 setHistoryIndex(null);
187 202 adjustHeight();
188 203 }}
189 204 onKeyDown={handleKeyDown}
205 + onFocus={stagePinnedSelection}
190 206 />
191 207 <div className="flex justify-between gap-4 px-2 pb-2">
192 - {domTool ? <ChatTools disabled={disabled} /> : null}
193 - <div className="ms-auto flex items-center gap-2">
208 + <div className="ms-auto flex items-center gap-1">
194 209 <span
195 210 className={classNames(
196 211 'text-xs font-medium',
197 212 overLimit ? 'text-red-600' : 'invisible',
@@ -200,11 +215,12 @@
200 215 >
201 216 {overLimit && __('Message too long', 'extendify-local')}
202 217 </span>
203 218 <SubmitButton
204 - disabled={disabled}
219 + disabled={inputDisabled}
205 220 noInput={input.trim().length === 0}
206 221 overLimit={overLimit}
222 + showCancel={busy}
207 223 handleCancel={handleCancel}
208 224 />
209 225 </div>
210 226 </div>
@@ -211,28 +227,30 @@
211 227 </form>
212 228 );
213 229 };
214 230
215 -const SubmitButton = ({ disabled, noInput, overLimit, handleCancel }) => {
216 - if (disabled) {
217 - return (
218 - <button
219 - type="button"
220 - onClick={handleCancel}
221 - 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"
222 - >
223 - <Icon fill="currentColor" icon={cancelRequest} size={18} />
224 - <span className="sr-only">{__('Cancel', 'extendify-local')}</span>
225 - </button>
226 - );
227 - }
228 - return (
231 +const SubmitButton = ({
232 + disabled,
233 + noInput,
234 + overLimit,
235 + showCancel,
236 + handleCancel,
237 +}) =>
238 + showCancel ? (
229 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
230 249 type="submit"
231 - 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"
232 251 disabled={disabled || noInput || overLimit}
233 252 >
234 - <Icon fill="currentColor" icon={arrowUp} size={24} />
253 + <Icon fill="currentColor" icon={arrowUp} size={20} />
235 254 <span className="sr-only">{__('Send message', 'extendify-local')}</span>
236 255 </button>
237 256 );
238 -};