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 +80 -60 3.0.6 → trunk 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 -};