ai_service = $ai_service; } public function name(): string { return 'edit_full_page'; } public function description(): string { return 'Edit the entire current page at once by regenerating its full content. ' . 'Use this ONLY when the requested changes affect more than one place on the page ' . '(e.g. "reorder the sections", "change all headings to blue", "add a CTA after every section", "swap the hero and the features section", "rewrite the whole page"). ' . 'For adding a single new section use add_blocks_to_page instead. ' . 'For changing the currently selected block use replace_selected_blocks instead. ' . 'Provide a description of all the edits to make; the current page content is read automatically and the edited page is returned.'; } public function input_schema(): array { return array( 'type' => 'object', 'properties' => array( 'description' => array( 'type' => 'string', 'description' => 'Detailed description of every edit to apply to the page. Describe all changes — the current page content is provided automatically as context.', ), ), 'required' => array( 'description' ), ); } public function execute( array $input, array $context = array() ): array { $post_id = intval( $context['post_id'] ?? 0 ) ?: null; if ( ! $post_id ) { return array( 'type' => 'error', 'message' => __( 'No current post ID available.', 'advanced-animation' ), ); } $description = $input['description'] ?? ''; if ( empty( $description ) ) { return array( 'type' => 'error', 'message' => __( 'No edit description provided.', 'advanced-animation' ), ); } $selection = $context['selection'] ?? null; $edited_html = $this->ai_service->edit_full_page( $description, $selection, $post_id ); if ( is_wp_error( $edited_html ) ) { return array( 'type' => 'error', 'message' => $edited_html->get_error_message(), ); } $edited_html = $this->clean_content( $edited_html ); $blocks = HtmlToBlocks::convert( $edited_html ); if ( empty( $blocks ) ) { return array( 'type' => 'error', 'message' => sprintf( /* translators: %s: received HTML snippet */ __( 'Could not convert the edited HTML to blocks. Received: %s', 'advanced-animation' ), mb_strimwidth( $edited_html, 0, 200, '...' ) ), ); } $new_content = serialize_blocks( \AdvancedAddons\BlocksToHtml::normalize_blocks( $blocks ) ); $result_html = \AdvancedAddons\BlocksToHtml::convert_blocks( $blocks ); $result = array( 'action' => 'full_page_edit', 'editor' => 'gutenberg', 'post_id' => $post_id, 'blocks' => $blocks, 'html' => $result_html, 'post_content' => $new_content, ); if ( 'chat' !== ( $context['request_source'] ?? '' ) ) { SelectionBridge::store_result( $result ); } return array_merge( $result, array( 'type' => 'page_updated', 'message' => __( 'Full page updated.', 'advanced-animation' ), ) ); } /** * Strip markdown fences and explanatory text that breaks conversion. * * Unlike the single-section tools, we intentionally do NOT strip * tags — HtmlToBlocks::convert() needs the full document structure to * locate the element and iterate all of its children. Stripping * body tags leaves a fragment with multiple root elements, which * loadHTML(LIBXML_HTML_NOIMPLIED) does not wrap in , causing only * the first element to be converted. */ private function clean_content( string $content ): string { $content = trim( $content ); // Strip markdown fences — the AI may wrap the HTML in ```html ... ``` // possibly with explanatory text before/after. Extract the largest // HTML block between the first opening fence and the last closing fence. if ( preg_match( '/```(?:html|php|json)?\s*(.+?)\s*```/s', $content, $matches ) ) { $content = trim( $matches[1] ); } // If the content is a full HTML document, extract everything from // the first or onward, discarding any leading text. if ( preg_match('/(])/i', $content, $m, PREG_OFFSET_CAPTURE) ) { $content = substr( $content, $m[0][1] ); } return $content; } }