settings = $settings; } /** * Hook the editor assets and the featured-image box. */ public function register() { add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); // Priority 9: registered BEFORE Topics' box (default 10), so in the // side column this box sits directly ABOVE "AI description & topics". add_action( 'add_meta_boxes', array( $this, 'featured_meta_box' ), 9 ); } /** * Whether the editor-side assistant can serve the current screen: block * editor on an agent-visible type, writes switch on, a user who may edit. * The TEXT features (per-section revise) need no more than this; the image * features add their own capability/provider checks on top. * * @return bool */ private function can_serve() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! in_array( $screen->post_type, Content::post_types(), true ) ) { return false; } if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) { return false; } if ( ! $this->settings->enabled( 'enable_agent_writes' ) ) { return false; } return current_user_can( 'edit_posts' ); } /** * Whether image generation can serve here — on top of {@see can_serve()}: * the user may upload and the provider can actually paint. * * @return bool */ private function images_ready() { return current_user_can( 'upload_files' ) && Assistant::image_ready(); } /** * Whether the AI-Readability "No featured image" row may offer its one-click * Generate — true only where the whole chain could deliver: a block-editor * context (the panel's REST refresh counts — only the block editor calls it), * the writes switch on, a user who may edit and upload, and a provider that * can actually paint. Static because the row renderer * ({@see PageCheckMetaBox::rows_html()}) has no AssistantEditor instance. * * @return bool */ public static function featured_one_click() { if ( ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) { return false; } } if ( ! ( new Settings() )->enabled( 'enable_agent_writes' ) ) { return false; } if ( ! current_user_can( 'edit_posts' ) || ! current_user_can( 'upload_files' ) ) { return false; } return Assistant::image_ready(); } /** * The "Featured image (AI)" box — a real side meta box (not a document * panel), so it sits in the meta-box column right above the * "AI description & topics" box, wearing the same Agentimus mark. */ public function featured_meta_box() { if ( ! $this->can_serve() || ! $this->images_ready() ) { return; } $screen = get_current_screen(); add_meta_box( 'agentimus-featured-ai', // Short title, like core's own panel — the button below says what the // box DOES, and the title stays one line in the 280px sidebar. self::meta_box_title( __( 'Featured Image', 'agentimus' ) ), array( $this, 'render_featured_box' ), $screen->post_type, 'side', 'default' ); } /** * The box body: one sentence, one full-width button. The behaviour lives * in the same inline editor script that powers the per-block Generate. * * @param \WP_Post $post Post being edited. */ public function render_featured_box( $post ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- WP passes the post; the JS reads editor state instead. echo '
' . esc_html__( 'Generates a featured image from the post title and saves it to your library.', 'agentimus' ) . '
'; echo ''; } /** * The shared Agentimus mark + title, identical to the one on the * "AI description & topics" box (see Topics::meta_box_title()). * * @param string $text The plain-text title (already translated). * @return string */ private static function meta_box_title( $text ) { return Admin::brand_title( $text ); // The one shared brand tile. } /** * Enqueue on block-editor screens for agent-visible types — and only when * every prerequisite holds, so a dead button never renders. * * @param string $hook Current admin page hook. */ public function assets( $hook ) { if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) { return; } if ( ! $this->can_serve() ) { return; } wp_register_script( self::HANDLE, false, array( 'wp-hooks', 'wp-compose', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-blocks', 'wp-data', 'wp-api-fetch' ), AGENTIMUS_VERSION, true ); wp_enqueue_script( self::HANDLE ); // The image affordances hide themselves when the provider can't paint // or the user can't upload; the text features stand on their own. wp_add_inline_script( self::HANDLE, 'var _agentimusEd = ' . wp_json_encode( array( 'imagesReady' => $this->images_ready() ) ) . ';', 'before' ); wp_add_inline_script( self::HANDLE, self::js() ); wp_register_style( self::HANDLE, false, array(), AGENTIMUS_VERSION ); wp_enqueue_style( self::HANDLE ); wp_add_inline_style( self::HANDLE, // Header reads like a core sidebar panel (Categories/Tags): title + // the collapse toggle only — Heera's call 2026-07-25. The classic // move-up/down arrows add ~88px of chrome (wider still since 7.1 // wrapped them in tooltips) and forced the title onto two lines in // the 280px sidebar; boxes stay drag-sortable by their headers. '#agentimus-featured-ai .handle-order-higher,#agentimus-featured-ai .handle-order-lower{display:none}' . '.agentimus-feat__hint{margin:0 0 10px;color:#646970;font-size:12px;line-height:1.5}' . '.agentimus-feat__btn{width:100%;text-align:center;justify-content:center}' . '.agentimus-feat__btn[disabled]{cursor:default}' ); } /** * The editor script: the per-block Generate button and the featured panel. * * @return string */ private static function js() { return <<<'JS' (function () { if (!window.wp || !wp.hooks || !wp.element || !wp.blockEditor || !wp.components) { return; } var el = wp.element.createElement; var Fragment = wp.element.Fragment; var useState = wp.element.useState; /* The brand tile — the same mark every Agentimus meta box header wears. */ var tileIcon = function (size) { return el('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': 'true', style: { flex: 'none' } }, el('rect', { x: 1.2, y: 1.2, width: 21.6, height: 21.6, rx: 6, fill: '#1b1913', stroke: '#146b64', strokeWidth: 1.5 }), el('path', { d: 'M7.35 17.3 12 6.7 16.65 17.3', stroke: '#f3f0e7', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }), el('path', { d: 'M9.5 13H14.5', stroke: '#ad7b18', strokeWidth: 1.9, strokeLinecap: 'round' }) ); }; var tile = tileIcon(16); function notice(kind, msg) { wp.data.dispatch('core/notices')[('ok' === kind) ? 'createSuccessNotice' : 'createErrorNotice'](msg, { type: 'snackbar' }); } function generate(alt, then, done) { var title = ''; try { title = wp.data.select('core/editor').getEditedPostAttribute('title') || ''; } catch (e) {} wp.apiFetch({ path: '/agentimus/v1/assistant/generate-image', method: 'POST', data: { alt: alt, title: title } }) .then(function (r) { then(r); notice('ok', 'Image generated and saved to your media library.'); }) .catch(function (e) { notice('err', (e && e.message) || 'The image didn’t come back — please try again.'); }) .then(function () { done(); }); } var imagesReady = !!(window._agentimusEd && window._agentimusEd.imagesReady); var REVISABLE = ['core/paragraph', 'core/heading', 'core/list', 'core/quote']; /* 1 — one BlockEdit filter carrying both affordances: image blocks get the toolbar "Generate image" (when the provider can paint), and text blocks get the "Revise with AI" sidebar panel — the selected block is the anchor; the instruction may rewrite it or ADD new blocks around it ("add a conclusion after this"), and the server answers with the section's final form as real block markup. */ wp.hooks.addFilter('editor.BlockEdit', 'agentimus/assistant', wp.compose.createHigherOrderComponent(function (BlockEdit) { return function (props) { var isImage = 'core/image' === props.name; var isText = -1 !== REVISABLE.indexOf(props.name); if ((!isImage || !imagesReady) && !isText) { return el(BlockEdit, props); } var state = useState(false); var busy = state[0]; var setBusy = state[1]; var ask = useState(''); var text = ask[0]; var setText = ask[1]; if (isImage) { var alt = (props.attributes && props.attributes.alt) || ''; return el(Fragment, null, el(BlockEdit, props), el(wp.blockEditor.BlockControls, { group: 'other' }, el(wp.components.ToolbarButton, { icon: tileIcon(20), label: busy ? 'Generating…' : 'Generate image from the alt text', isBusy: busy, disabled: busy, onClick: function () { if (busy) { return; } if (alt.trim().length < 5) { notice('err', 'Write the alt text first — it’s the description the image is painted from.'); return; } setBusy(true); generate(alt, function (r) { props.setAttributes({ id: r.id, url: r.fullUrl || r.url, alt: alt, sizeSlug: 'large' }); }, function () { setBusy(false); }); } }) ) ); } var revise = function () { var instruction = text.trim(); if (busy) { return; } if (instruction.length < 4) { notice('err', 'Say what to change or add — e.g. “make this shorter” or “add a conclusion after this”.'); return; } var sel = wp.data.select('core/block-editor').getBlock(props.clientId); if (!sel) { return; } var title = ''; var context = ''; try { title = wp.data.select('core/editor').getEditedPostAttribute('title') || ''; context = wp.data.select('core/editor').getEditedPostContent() || ''; } catch (e) {} setBusy(true); wp.apiFetch({ path: '/agentimus/v1/assistant/revise-block', method: 'POST', data: { block: wp.blocks.serialize(sel), instruction: instruction, context: context, title: title } }).then(function (r) { var blocks = wp.blocks.parse(r.content); if (!blocks.length) { throw new Error('The revision came back empty.'); } wp.data.dispatch('core/block-editor').replaceBlocks(props.clientId, blocks); setText(''); notice('ok', 'Section updated — undo (Ctrl+Z) brings the old version back.'); }).catch(function (e) { notice('err', (e && e.message) || 'The revision didn’t come back — please try again.'); }).then(function () { setBusy(false); }); }; return el(Fragment, null, el(BlockEdit, props), el(wp.blockEditor.InspectorControls, null, el(wp.components.PanelBody, { /* Deliberately generic: this panel rewrites the selected section AND inserts new blocks around it — "Revise" would undersell half of what it does. */ title: el('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } }, tile, 'Ask AI'), initialOpen: true }, el(wp.components.TextareaControl, { label: 'What should change here?', hideLabelFromVision: true, placeholder: 'e.g. make this shorter · turn it into a list · add a conclusion after this', value: text, disabled: busy, onChange: setText, rows: 3, __nextHasNoMarginBottom: true }), el('p', { style: { margin: '6px 0 10px', color: '#757575', fontSize: '12px' } }, 'Works on this section. It can also add new blocks before or after it — the whole post travels along as context.'), el(wp.components.Button, { variant: 'secondary', isBusy: busy, disabled: busy, style: { width: '100%', justifyContent: 'center' }, onClick: revise }, busy ? 'Working…' : 'Apply') ) ) ); }; }, 'withAgentimusAssistant') ); /* 2 — featured-image generation, shared by its two buttons: the side meta box's (server-rendered once, bound directly) and the AI-Readability "No featured image" row's one-click (that panel re-renders its rows after every save, so the click is delegated on the document — the Fix-with-AI pattern). Both seed from the post title and set the result as featured. */ function setFeaturedFromTitle(btn) { if (btn.disabled) { return; } var title = ''; try { title = (wp.data.select('core/editor').getEditedPostAttribute('title') || '').trim(); } catch (e) {} if (!title) { notice('err', 'Give the post a title first — the featured image is generated from it.'); return; } var idle = btn.innerHTML; btn.disabled = true; btn.textContent = 'Generating…'; generate('A featured image for the post “' + title + '”', function (r) { wp.data.dispatch('core/editor').editPost({ featured_media: r.id }); }, function () { btn.disabled = false; btn.innerHTML = idle; }); } function bindFeatured() { var btn = document.getElementById('agentimus-generate-featured'); if (btn) { btn.addEventListener('click', function () { setFeaturedFromTitle(btn); }); } } if ('loading' === document.readyState) { document.addEventListener('DOMContentLoaded', bindFeatured); } else { bindFeatured(); } if (imagesReady) { document.addEventListener('click', function (e) { var btn = e.target && e.target.closest ? e.target.closest('.agentimus-pc__genfeat') : null; if (btn) { setFeaturedFromTitle(btn); } }); } })(); JS; } }