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
extendify / src / Agent / workflows / block-selector / components / ReplaceImageConfirm.jsx

ReplaceImageConfirm.jsx in Extendify trunk, at src/Agent/workflows/block-selector/components/ReplaceImageConfirm.jsx

128 lines 3.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 ImagePicker,
3 PickerActions,
4 PickerButton,
5 } from '@agent/components/ImagePicker';
6 import { SharedBlockNotice } from '@agent/components/SharedBlockNotice';
7 import { targetFor } from '@agent/hooks/useImageAcquisition';
8 import { BLOCK_ID_SEL, findBlockEl } from '@agent/lib/block-el';
9 import { useChatStore } from '@agent/state/chat';
10 import { useQuickEditStore } from '@quick-edit/state/store';
11 import { useCallback, useEffect, useRef } from '@wordpress/element';
12 import { __ } from '@wordpress/i18n';
13
14 // The block's own image — never one belonging to a nested tagged block.
15 const findBlockImage = (blockId, partSlug) => {
16 const scope = findBlockEl(blockId, document, { partSlug });
17 if (!scope) return null;
18 return (
19 [...scope.querySelectorAll('img')].find(
20 (img) => img.closest(BLOCK_ID_SEL) === scope,
21 ) ?? null
22 );
23 };
24
25 export const ReplaceImageConfirm = ({ inputs, onConfirm, onCancel }) => {
26 // The backend caps a patch at one image; a second would have no picker.
27 const operation = (
28 Array.isArray(inputs.operations) ? inputs.operations : []
29 ).find(({ op }) => op === 'replace-image');
30 const blockId = operation?.blockId;
31 const original = useRef(null);
32 const confirmed = useRef(false);
33 const addMessage = useChatStore((state) => state.addMessage);
34 const updateMessage = useChatStore((state) => state.updateMessage);
35 // A part block's id repeats in post content, so the scope picks the instance.
36 const partSlug =
37 useQuickEditStore((state) => state.agentBlock?.source?.partSlug) ?? null;
38
39 const restoreOriginal = useCallback(() => {
40 const saved = original.current;
41 if (!saved) return;
42 saved.el.src = saved.src;
43 if (saved.srcset) saved.el.setAttribute('srcset', saved.srcset);
44 else saved.el.removeAttribute('srcset');
45 }, []);
46
47 const previewOnPage = useCallback(
48 (url) => {
49 const img = findBlockImage(blockId, partSlug);
50 if (!img) return;
51 original.current ??= {
52 el: img,
53 src: img.getAttribute('src') ?? '',
54 srcset: img.getAttribute('srcset') ?? '',
55 };
56 if (!url) {
57 restoreOriginal();
58 return;
59 }
60 img.removeAttribute('srcset');
61 img.src = url;
62 },
63 [blockId, partSlug, restoreOriginal],
64 );
65
66 // Nothing on the page means there is nothing to replace.
67 const receipt = useRef(null);
68 useEffect(() => {
69 if (!findBlockImage(blockId, partSlug)) return onCancel();
70 receipt.current ??= addMessage('image', {});
71 }, [blockId, partSlug, onCancel, addMessage]);
72
73 useEffect(() => {
74 return () => {
75 if (confirmed.current) return;
76 restoreOriginal();
77 };
78 }, [restoreOriginal]);
79
80 if (!operation) return null;
81
82 return (
83 <ImagePicker
84 prompt={operation.prompt ?? ''}
85 search={operation.prompt ?? ''}
86 tab={operation.source === 'generate' ? 'generate' : 'media'}
87 autoGenerate={operation.source === 'generate'}
88 target={() => targetFor(findBlockImage(blockId, partSlug))}
89 onSelect={previewOnPage}
90 onSubmit={async (image) => {
91 confirmed.current = true;
92 const url = image?.source_url || image?.url;
93 if (url) updateMessage(receipt.current, { url });
94 // The full attachment is ~800 tokens replayed every turn.
95 // swapBlockImage builds the block's alt from alt_text.
96 const picked = image && {
97 id: image.id,
98 url,
99 alt_text: image.alt_text ?? '',
100 };
101 await onConfirm({
102 // No image resolved — the tool reports it as no-change.
103 data: {
104 ...inputs,
105 operations: [picked ? { ...operation, image: picked } : operation],
106 },
107 shouldRefreshPage: true,
108 });
109 }}
110 footer={({ ready, busy, submit }) => (
111 <>
112 <SharedBlockNotice blockIds={[blockId]} />
113 <PickerActions>
114 <PickerButton disabled={busy} onClick={onCancel}>
115 {__('Cancel', 'extendify-local')}
116 </PickerButton>
117 <PickerButton primary disabled={!ready || busy} onClick={submit}>
118 {busy
119 ? __('Saving...', 'extendify-local')
120 : __('Save', 'extendify-local')}
121 </PickerButton>
122 </PickerActions>
123 </>
124 )}
125 />
126 );
127 };
128