build_available_tools_note(); if ( 'edit' !== $request_mode ) { $insert_instructions = <<<'EOT' Insertion: core/* blocks only. Do not insert custom/third-party blocks. Images: blank placeholders only. No external URLs, Unsplash, generated sources, or media content. Text blocks must not be empty. Use filler copy unless empty placeholders are explicitly requested. No Markdown fences, unsupported blocks, raw HTML outside Gutenberg block comments, inline styles, style attributes, arbitrary classes, or unsupported settings. Large layouts: use clear top-level sections. Prefer separate full-width Group sections with constrained inner layouts. Do not wrap all sections in one outer Group unless requested. Use columns/groups/buttons/presets/gradients/colors/spacing/aspect ratios only when supported and useful. Avoid card-heavy layouts unless the content clearly benefits from cards or the user asks for them; prefer sections, spacing, columns, and typography for structure. Keep side-by-side layouts visually balanced unless asymmetry is requested. EOT; $system_message .= "\n\n" . $insert_instructions; $long_copy_insert_note = $this->build_long_copy_insert_note( $request_mode, $messages ); if ( '' !== $long_copy_insert_note ) { $system_message .= "\n\n" . $long_copy_insert_note; } $insert_canonical_examples = $this->get_base_insert_canonical_examples( $selection ); if ( ! empty( $insert_canonical_examples ) ) { $system_message .= "\nPrefer the Cover block for hero sections unless the user asks for a different structure.\n"; $system_message .= "\n\nCanonical Gutenberg examples:\n"; $system_message .= "Examples show full markup shapes with representative supported properties and illustrative values. Use them to understand what is possible, not as exact values to copy.\n"; $system_message .= "For the actual result, use only supported properties needed for the request. Choose values that fit the content/layout, and avoid combining competing visual properties.\n\n"; $system_message .= implode( "\n\n", $insert_canonical_examples ); } } if ( 'edit' === $request_mode ) { $edit_instructions = <<<'EOT' Edit only the current Gutenberg selection, whether one block or multiple blocks. Return the full updated markup for that selected subtree only. Use the provided selected markup as context. Do not invent surrounding layout, siblings, section context, or content outside the selection. Treat each turn as an incremental edit. Follow only the latest request unless the user clearly refers back to earlier instructions. Preserve unrelated content, structure, language, styling, settings, media, and background-image sources. Add or change only what the latest request requires. Keep canonical Gutenberg structure: block comments, JSON attributes, and saved HTML must stay aligned. Use real block API properties and canonical attribute names only. Any new image blocks must be blank placeholders with no media, external URL, generated source, or Unsplash image. EOT; $system_message .= "\n\n" . $edit_instructions; } $theme_context_note = $this->build_theme_context_note(); if ( '' !== $theme_context_note ) { $system_message .= "\n\n" . $theme_context_note; } $custom_instructions = $this->build_custom_system_instructions( $model, $request_mode, $selection, $messages ); if ( '' !== $custom_instructions ) { $system_message .= "\n\n" . $custom_instructions; } $style_guide_note = $this->build_style_guide_note( $selection ); if ( '' !== $style_guide_note ) { $system_message .= "\n\n" . $style_guide_note; } $selection_note = $this->build_selection_note( $selection, $request_mode ); if ( '' !== $selection_note ) { $system_message .= "\n\n" . $selection_note; } if ( 'edit' === $request_mode && ! empty( $selection['keywordMatchedCanonicalExamples'] ) && is_array( $selection['keywordMatchedCanonicalExamples'] ) ) { $matched_canonical_examples = array_values( array_filter( $selection['keywordMatchedCanonicalExamples'], static function ( $example ) { return is_string( $example ) && '' !== trim( $example ); } ) ); if ( ! empty( $matched_canonical_examples ) ) { $system_message .= "\n\nAdditional canonical Gutenberg examples for other blocks:\n"; $system_message .= implode( "\n\n", $matched_canonical_examples ); } } if ( 'edit' === $request_mode && ! empty( $selection['containsImageBlock'] ) ) { $system_message .= "\n\nThe selection contains image blocks. New image blocks must stay blank placeholders. Preserve existing image URLs or media, and do not add Unsplash, external URLs, generated sources, or new media."; } return $system_message; } /** * Build the available tools note. * * @return string */ private function build_available_tools_note() { $tools = array( 'Available tools:', '- insert_block_markup: add new core/* blocks using canonical Gutenberg markup.', '- update_selected_block_markup: edit the selected block or block range, including custom/third-party blocks only when already selected.', '- change_editor_history: undo or redo an editor change.', '- request_clarification: reply without block markup when you need the user to clarify the request, select a different block, provide missing block or selection context, or take a manual editor step.', ); $ability_tools = array(); if ( class_exists( 'AIEditor_Ability_Tools' ) && AIEditor_Ability_Tools::is_supported() ) { $ability_tools = AIEditor_Ability_Tools::get_openai_tools(); if ( $this->has_openai_tool( 'query_wordpress_content', $ability_tools ) ) { $tools[] = '- query_wordpress_content: look up existing WordPress content when page/post context is needed for the request. Returns text, not block markup.'; } } if ( $this->has_openai_tools_other_than( array( 'query_wordpress_content' ), $ability_tools ) ) { $tools[] = '- Other tools may be available.'; } return implode( "\n", $tools ); } /** * Whether an OpenAI tool definition list contains a tool name. * * @param string $tool_name OpenAI tool name. * @param array $tools OpenAI tool definitions. * @return bool */ private function has_openai_tool( $tool_name, $tools ) { if ( ! is_array( $tools ) ) { return false; } foreach ( $tools as $tool ) { if ( is_array( $tool ) && isset( $tool['name'] ) && $tool_name === $tool['name'] ) { return true; } } return false; } /** * Whether an OpenAI tool definition list contains tools outside an ignored set. * * @param array $ignored_tool_names Tool names to ignore. * @param array $tools OpenAI tool definitions. * @return bool */ private function has_openai_tools_other_than( $ignored_tool_names, $tools ) { if ( ! is_array( $tools ) ) { return false; } foreach ( $tools as $tool ) { if ( ! is_array( $tool ) || empty( $tool['name'] ) || ! is_string( $tool['name'] ) ) { continue; } if ( ! in_array( $tool['name'], $ignored_tool_names, true ) ) { return true; } } return false; } /** * Build optional developer-provided system instructions. * * @param string $model Model identifier. * @param string $request_mode Request mode. * @param array|null $selection Selection snapshot from the editor. * @param array $messages Chat messages from the sidebar. * @return string */ private function build_custom_system_instructions( $model, $request_mode, $selection, $messages ) { $context = array( 'model' => $model, 'request_mode' => $request_mode, 'selection' => $selection, 'messages' => $messages, ); $extra = apply_filters( 'ai_editor_instructions_extra', '', $context ); if ( ! is_string( $extra ) ) { return ''; } $extra = sanitize_textarea_field( $extra ); $extra = trim( preg_replace( "/[ \t]+\n/", "\n", $extra ) ); if ( '' === $extra ) { return ''; } if ( strlen( $extra ) > 4000 ) { $extra = substr( $extra, 0, 4000 ); } return "Additional site instructions:\n" . $extra; } /** * Build a short active-theme context note for the system prompt. * * @return string */ private function build_theme_context_note() { $options = get_option( 'ai_editor_settings', array() ); $theme_style_context = ''; if ( class_exists( 'AIEditor_Settings_Page' ) ) { $theme_style_context = AIEditor_Settings_Page::get_saved_theme_style_context_for_slug( $options, $this->get_active_theme_slug() ); $theme_style_context = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $theme_style_context ) ) ); } if ( '' === $theme_style_context ) { $theme_style_context = $this->get_active_theme_description(); } if ( '' === $theme_style_context ) { return ''; } $note = "Active theme style context:\n"; $note .= $theme_style_context . "\n"; $note .= 'Use this as high-level stylistic direction when choosing layout, tone, and visual treatment.'; return trim( $note ); } /** * Get the active theme stylesheet slug. * * @return string */ private function get_active_theme_slug() { if ( ! function_exists( 'wp_get_theme' ) ) { return ''; } $theme = wp_get_theme(); if ( ! $theme || ! method_exists( $theme, 'get_stylesheet' ) ) { return ''; } return trim( (string) $theme->get_stylesheet() ); } /** * Get the active theme description. * * @return string */ private function get_active_theme_description() { if ( ! function_exists( 'wp_get_theme' ) ) { return ''; } $theme = wp_get_theme(); if ( ! $theme || ! method_exists( $theme, 'get' ) ) { return ''; } return trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $theme->get( 'Description' ) ) ) ); } /** * Add a short layout hint for long insert-copy requests. * * @param string $request_mode Current request mode. * @param array $messages Chat messages from the sidebar. * @return string */ private function build_long_copy_insert_note( $request_mode, $messages ) { if ( 'edit' === $request_mode ) { return ''; } $latest_user_message = $this->get_latest_user_message_text( $messages ); if ( '' === $latest_user_message ) { return ''; } $word_count = str_word_count( wp_strip_all_tags( $latest_user_message ) ); unset( $word_count ); return 'For full pages or large layouts, use clear separate top-level Group sections, usually full-width with constrained inner layouts. Use available presets where helpful. Keep layouts polished and readable; do not wrap all sections in one outer Group unless requested.'; } /** * Return the base insert canonical examples before keyword-driven additions. * * @param array|null $selection Selection snapshot from the editor. * @return array */ private function get_base_insert_canonical_examples( $selection ) { $insert_canonical_examples = array(); if ( ! empty( $selection['insertCanonicalExamples'] ) && is_array( $selection['insertCanonicalExamples'] ) ) { $insert_canonical_examples = array_values( array_filter( $selection['insertCanonicalExamples'], static function ( $example ) { return is_string( $example ) && '' !== trim( $example ); } ) ); } $insert_canonical_examples[] = <<<'EOT'
FruitColor
AppleRed
BananaYellow
Item TypePrimary Color
EOT; $insert_canonical_examples[] = <<<'EOT'
Accordion title

Accordion content.

EOT; return $insert_canonical_examples; } /** * Extract the latest plain user message text from chat history. * * @param array $messages Chat messages from the sidebar. * @return string */ private function get_latest_user_message_text( $messages ) { if ( ! is_array( $messages ) ) { return ''; } for ( $index = count( $messages ) - 1; $index >= 0; $index-- ) { $message = $messages[ $index ]; if ( is_array( $message ) && isset( $message['role'], $message['content'] ) && 'user' === $message['role'] && is_string( $message['content'] ) ) { return $message['content']; } } return ''; } /** * Build the simple preset list after filtering through per-block capabilities. * * @param array|null $selection Selection snapshot from the editor. * @return string */ private function build_style_guide_note( $selection ) { if ( ! is_array( $selection ) || empty( $selection['blockCapabilities'] ) || ! is_array( $selection['blockCapabilities'] ) ) { return ''; } $preset_groups = array( 'colors' => array(), 'gradients' => array(), 'duotones' => array(), 'fontSizes' => array(), 'spacingSizes' => array(), 'imageSizes' => array(), 'aspectRatios' => array(), ); $control_groups = array( 'textColor' => 'colors', 'backgroundColor' => 'colors', 'gradient' => 'gradients', 'duotone' => 'duotones', 'fontSize' => 'fontSizes', 'padding' => 'spacingSizes', 'margin' => 'spacingSizes', 'blockGap' => 'spacingSizes', 'imageSize' => 'imageSizes', 'aspectRatio' => 'aspectRatios', ); foreach ( $selection['blockCapabilities'] as $block_capabilities ) { if ( ! is_array( $block_capabilities ) || ( isset( $block_capabilities['settingsAvailable'] ) && false === $block_capabilities['settingsAvailable'] ) ) { continue; } foreach ( $control_groups as $control_name => $group_name ) { if ( empty( $block_capabilities[ $control_name ]['presets'] ) || ! is_array( $block_capabilities[ $control_name ]['presets'] ) ) { continue; } $preset_groups[ $group_name ] = array_merge( $preset_groups[ $group_name ], array_values( array_filter( $block_capabilities[ $control_name ]['presets'], 'is_string' ) ) ); } } $preset_groups = array_map( static function ( $values ) { return array_values( array_unique( $values ) ); }, $preset_groups ); if ( empty( array_filter( $preset_groups ) ) ) { return ''; } $note = "Available preset values:\n\n"; $note .= "Use these presets when they fit. If a section has a background color, explicitly set colors on its headings, buttons, and other text so they are not the same as the background.\n\n"; foreach ( $preset_groups as $key => $values ) { if ( empty( $values ) ) { continue; } $label = $key; if ( 'colors' === $key ) { $label .= ' (prefer these unless the user asks for custom colors)'; } elseif ( 'spacingSizes' === $key ) { $label .= ' (for padding, margin, gap, and block spacing)'; } elseif ( 'aspectRatios' === $key ) { $label .= ' (for images)'; } elseif ( 'duotones' === $key ) { $label .= ' (used on images and other media blocks)'; } $note .= $label . ":\n- " . implode( "\n- ", $values ) . "\n\n"; } return trim( $note ); } /** * Build a plain-text selection note for the model instructions. * * @param array|null $selection Selection snapshot from the editor. * @param string $request_mode Current request mode. * @return string */ private function build_selection_note( $selection, $request_mode ) { if ( ! is_array( $selection ) || empty( $selection['selectedClientIds'] ) ) { return ''; } $request_mode_label = $request_mode; if ( 'edit' === $request_mode ) { $request_mode_label = 'edit (or another tool if the user message should be handled by request_clarification or change_editor_history instead of a block update)'; } $note = 'Selection context:' . "\n\n"; $note .= 'Request mode: ' . $request_mode_label . "\n"; $note .= 'Selected block count: ' . count( $selection['selectedClientIds'] ) . "\n"; $selection_path_labels = array(); if ( ! empty( $selection['parentBlockTree'] ) && is_array( $selection['parentBlockTree'] ) ) { foreach ( $selection['parentBlockTree'] as $parent_block ) { if ( is_array( $parent_block ) && ! empty( $parent_block['label'] ) ) { $selection_path_labels[] = $parent_block['label']; } } } if ( ! empty( $selection['selectedBlockLabels'] ) && is_array( $selection['selectedBlockLabels'] ) ) { $selection_path_labels = array_merge( $selection_path_labels, $selection['selectedBlockLabels'] ); } if ( ! empty( $selection_path_labels ) ) { $note .= 'Selected block path: ' . implode( ' > ', array_unique( $selection_path_labels ) ) . ' (If the user asks for another block, the selected block is unrelated, or more context is needed, use request_clarification.)' . "\n"; } if ( ! empty( $selection['selectedBlockLabels'] ) && is_array( $selection['selectedBlockLabels'] ) ) { $note .= 'Selected blocks: ' . implode( ', ', $selection['selectedBlockLabels'] ) . "\n"; } if ( ! empty( $selection['selectedInnerBlockLabels'] ) && is_array( $selection['selectedInnerBlockLabels'] ) ) { $note .= 'Originally selected inner blocks: ' . implode( ', ', $selection['selectedInnerBlockLabels'] ) . "\n"; } if ( ! empty( $selection['selectedBlockStyles'] ) && is_array( $selection['selectedBlockStyles'] ) ) { $note .= "\nAvailable block styles for the selected block:\n"; foreach ( $selection['selectedBlockStyles'] as $style ) { if ( ! is_array( $style ) || empty( $style['name'] ) || empty( $style['label'] ) ) { continue; } $style_line = '- ' . $style['label'] . ' (' . $style['name'] . ')'; if ( ! empty( $style['isDefault'] ) ) { $style_line .= ' [default]'; } if ( ! empty( $selection['activeSelectedBlockStyle'] ) && $selection['activeSelectedBlockStyle'] === $style['name'] ) { $style_line .= ' [active]'; } $note .= $style_line . "\n"; } } if ( ! empty( $selection['selectedMarkup'] ) ) { $note .= "\nSelected Gutenberg block markup:\n"; $note .= "---\n"; $note .= $selection['selectedMarkup'] . "\n"; $note .= "---\n"; } $is_single_acf_selection = false; if ( 1 === count( $selection['selectedClientIds'] ) && ! empty( $selection['selectedBlocks'] ) && is_array( $selection['selectedBlocks'] ) && ! empty( $selection['selectedBlocks'][0]['name'] ) && 0 === strpos( (string) $selection['selectedBlocks'][0]['name'], 'acf/' ) ) { $is_single_acf_selection = true; } elseif ( 1 === count( $selection['selectedClientIds'] ) && ! empty( $selection['selectedMarkup'] ) && is_string( $selection['selectedMarkup'] ) && 1 === preg_match( '/^\s*