$id, 'label_for' => $id, 'options' => $options, ) ); } /** * Renders a settings field. * * @param array $args Arguments for the settings field. */ public static function render_settings_field( $args ) { $options = $args['options']; $value = isset( $options[ $args['id'] ] ) ? esc_attr( $options[ $args['id'] ] ) : ''; $html = ''; $theme_description = self::get_active_theme_description(); $active_theme_slug = self::get_active_theme_slug(); $current_theme_style_context = self::get_saved_theme_style_context_for_slug( $options, $active_theme_slug ); $has_saved_theme_contexts = ! empty( self::get_saved_theme_style_contexts( $options ) ); $description_id = $args['id'] . '_description'; switch ( $args['id'] ) { case 'ai_editor_api_key': $html .= ''; $html .= '
' . sprintf( wp_kses( /* translators: %s: OpenAI API keys page URL. */ __( 'Enter your OpenAI API key. The API key will be stored in your database. You are responsible for charges and usage limits.', 'ai-editor' ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), 'aria-label' => array(), ), ) ), esc_url( 'https://platform.openai.com/api-keys' ) ) . '
'; break; case 'ai_editor_model': $value = $value !== '' ? $value : AIEditor_Model_Config::get_recommended_model_key(); $html .= ''; $html .= '' . esc_html( AIEditor_Model_Config::get_recommended_model_note() ) . '
'; break; case 'ai_editor_reasoning_effort': $value = $value !== '' ? $value : AIEditor_Model_Config::get_default_reasoning_effort(); $html .= ''; $html .= '' . esc_html( AIEditor_Model_Config::get_reasoning_effort_note() ) . '
'; $html .= '| ' . esc_html__( 'Effort', 'ai-editor' ) . ' | ' . esc_html__( 'Best for', 'ai-editor' ) . ' |
|---|---|
none | ' . esc_html__( 'You want the fastest replies for simple block edits, quick insertions, or straightforward text changes.', 'ai-editor' ) . ' |
low | ' . esc_html__( 'A little extra thinking may help the model follow your block instructions more reliably without adding much delay.', 'ai-editor' ) . ' |
medium | ' . esc_html__( 'Your request involves bigger layout changes, multiple sections, or more careful block restructuring.', 'ai-editor' ) . ' |
high | ' . esc_html__( 'Use only if tougher layout requests clearly improve enough to justify the extra time and token cost.', 'ai-editor' ) . ' |
' . esc_html__( 'The active theme changed, so this field is currently using the new theme description instead of the saved style context. Review the text below and rewrite it with AI if needed.', 'ai-editor' ) . '
' . esc_html__( 'Provide a short style instruction the AI can use as design direction for this theme. Until you save an edit or AI rewrite, this field uses the current theme description. Do not include preset values, token names, color hex codes, or similar raw theme data because AI Editor already provides that context.', 'ai-editor' ) . ' ' . sprintf( esc_html__( 'Maximum %d characters.', 'ai-editor' ), self::THEME_STYLE_CONTEXT_MAX_LENGTH ) . '
'; $html .= ''; break; case 'ai_editor_trim_chat_history_enabled': $checked = $value === '' ? '1' : $value; $html .= '
' . esc_html__( 'When enabled, AI Editor sends only the configured number of most recent chat messages with each request.', 'ai-editor' ) . '
'; $html .= '' . esc_html__( 'Controls how much recent chat history AI Editor remembers when continuing a conversation. Default: 50.', 'ai-editor' ) . '
'; break; } $allowed_html = array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), 'aria-label' => array(), ), 'input' => array( 'type' => array(), 'id' => array(), 'name' => array(), 'value' => array(), 'class' => array(), 'min' => array(), 'max' => array(), 'step' => array(), 'checked' => array(), 'aria-describedby' => array(), ), 'label' => array( 'for' => array(), ), 'select' => array( 'id' => array(), 'name' => array(), 'class' => array(), 'aria-describedby' => array(), ), 'option' => array( 'value' => array(), 'selected' => array(), ), 'p' => array( 'class' => array(), 'id' => array(), ), 'div' => array( 'class' => array(), 'role' => array(), 'aria-label' => array(), 'tabindex' => array(), ), 'table' => array( 'class' => array(), 'aria-label' => array(), ), 'caption' => array( 'class' => array(), ), 'thead' => array(), 'tbody' => array(), 'tr' => array(), 'th' => array( 'scope' => array(), ), 'td' => array(), 'strong' => array(), 'code' => array(), 'textarea' => array( 'id' => array(), 'name' => array(), 'rows' => array(), 'class' => array(), 'readonly' => array(), 'style' => array(), 'maxlength' => array(), 'aria-describedby' => array(), ), 'button' => array( 'type' => array(), 'class' => array(), 'id' => array(), ), 'span' => array( 'id' => array(), 'class' => array(), 'role' => array(), 'aria-live' => array(), 'aria-atomic' => array(), ), ); echo wp_kses($html, $allowed_html); } /** * Get the current active theme description for settings display. * * @return string */ private static 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' ) ) ) ); } /** * Sanitize and clamp the theme style context to the supported max length. * * @param string $value Raw submitted value. * @return string */ public static function sanitize_theme_style_context( $value ) { $sanitized = sanitize_textarea_field( $value ); if ( function_exists( 'mb_substr' ) ) { return mb_substr( $sanitized, 0, self::THEME_STYLE_CONTEXT_MAX_LENGTH ); } return substr( $sanitized, 0, self::THEME_STYLE_CONTEXT_MAX_LENGTH ); } /** * Return saved theme style contexts indexed by theme slug. * * Also folds the legacy single saved context into the map for backward compatibility. * * @param array $options Saved plugin options. * @return array */ public static function get_saved_theme_style_contexts( $options ) { if ( ! is_array( $options ) ) { return array(); } $contexts = array(); if ( ! empty( $options['ai_editor_theme_style_contexts'] ) && is_array( $options['ai_editor_theme_style_contexts'] ) ) { foreach ( $options['ai_editor_theme_style_contexts'] as $theme_slug => $theme_style_context ) { $normalized_theme_slug = sanitize_key( (string) $theme_slug ); $normalized_theme_style_context = self::sanitize_theme_style_context( $theme_style_context ); if ( '' !== $normalized_theme_slug && '' !== $normalized_theme_style_context ) { $contexts[ $normalized_theme_slug ] = $normalized_theme_style_context; } } } $legacy_theme_slug = isset( $options['ai_editor_theme_style_context_theme_slug'] ) ? sanitize_key( (string) $options['ai_editor_theme_style_context_theme_slug'] ) : ''; $legacy_theme_style_context = isset( $options['ai_editor_theme_style_context'] ) ? self::sanitize_theme_style_context( $options['ai_editor_theme_style_context'] ) : ''; if ( '' !== $legacy_theme_slug && '' !== $legacy_theme_style_context && ! isset( $contexts[ $legacy_theme_slug ] ) ) { $contexts[ $legacy_theme_slug ] = $legacy_theme_style_context; } return $contexts; } /** * Return the saved theme style context for a given theme slug. * * @param array $options Saved plugin options. * @param string|null $theme_slug Theme stylesheet slug. Defaults to active theme. * @return string */ public static function get_saved_theme_style_context_for_slug( $options, $theme_slug = null ) { $contexts = self::get_saved_theme_style_contexts( $options ); $theme_slug = null === $theme_slug ? self::get_active_theme_slug() : sanitize_key( (string) $theme_slug ); if ( '' === $theme_slug || empty( $contexts[ $theme_slug ] ) ) { return ''; } return $contexts[ $theme_slug ]; } /** * Build a compatibility checklist for the active theme. * * @param array $options Saved plugin options. * @return array */ private static function get_theme_compatibility_report( $options ) { $theme = function_exists( 'wp_get_theme' ) ? wp_get_theme() : null; $global_settings = function_exists( 'wp_get_global_settings' ) ? wp_get_global_settings() : array(); $report = array( 'status' => 'good', 'status_label' => __( 'Ready', 'ai-editor' ), 'summary' => __( 'This theme is compatible with AI Editor and supports Gutenberg layouts, presets, and editor settings.', 'ai-editor' ), 'items' => array(), ); $stylesheet_dir = ( $theme && method_exists( $theme, 'get_stylesheet_directory' ) ) ? (string) $theme->get_stylesheet_directory() : ''; $template_dir = ( $theme && method_exists( $theme, 'get_template_directory' ) ) ? (string) $theme->get_template_directory() : ''; $theme_json_present = ( '' !== $stylesheet_dir && file_exists( trailingslashit( $stylesheet_dir ) . 'theme.json' ) ) || ( '' !== $template_dir && $template_dir !== $stylesheet_dir && file_exists( trailingslashit( $template_dir ) . 'theme.json' ) ); $is_block_theme = function_exists( 'wp_is_block_theme' ) ? wp_is_block_theme() : false; $supports_editor_styles = current_theme_supports( 'editor-styles' ) || current_theme_supports( 'editor-style' ); $supports_align_wide = current_theme_supports( 'align-wide' ); $page_editor_enabled = function_exists( 'use_block_editor_for_post_type' ) ? use_block_editor_for_post_type( 'page' ) : false; $post_editor_enabled = function_exists( 'use_block_editor_for_post_type' ) ? use_block_editor_for_post_type( 'post' ) : false; $current_theme_style_context = self::get_saved_theme_style_context_for_slug( $options, self::get_active_theme_slug() ); $preset_group_count = 0; $preset_entry_count = 0; $preset_paths = array( array( 'color', 'palette' ), array( 'color', 'gradients' ), array( 'typography', 'fontSizes' ), array( 'typography', 'fontFamilies' ), array( 'spacing', 'spacingSizes' ), array( 'dimensions', 'aspectRatios' ), ); foreach ( $preset_paths as $preset_path ) { $group = $global_settings; foreach ( $preset_path as $path_key ) { if ( ! is_array( $group ) || ! isset( $group[ $path_key ] ) ) { $group = array(); break; } $group = $group[ $path_key ]; } if ( ! is_array( $group ) || empty( $group ) ) { continue; } $entries = self::flatten_theme_preset_groups( $group ); if ( empty( $entries ) ) { continue; } ++$preset_group_count; $preset_entry_count += count( $entries ); } $registered_blocks = array(); if ( class_exists( 'WP_Block_Type_Registry' ) ) { $registered_blocks = WP_Block_Type_Registry::get_instance()->get_all_registered(); } $custom_block_names = array(); $has_core_blocks = false; foreach ( $registered_blocks as $block_name => $block_type ) { if ( 0 === strpos( (string) $block_name, 'core/' ) ) { $has_core_blocks = true; } else { $custom_block_names[] = (string) $block_name; } } $custom_block_count = count( $custom_block_names ); $warning_count = 0; $has_hard_fail = false; $add_item = static function ( $status, $title, $message ) use ( &$report, &$warning_count, &$has_hard_fail ) { if ( 'warn' === $status ) { ++$warning_count; } if ( 'fail' === $status ) { $has_hard_fail = true; } $report['items'][] = array( 'status' => $status, 'title' => $title, 'message' => $message, ); }; if ( $is_block_theme ) { $add_item( 'pass', __( 'Block theme support', 'ai-editor' ), __( 'This is a block theme. AI Editor tends to work best here because layouts, templates, and editor settings are built around Gutenberg.', 'ai-editor' ) ); } elseif ( $theme_json_present ) { $add_item( 'pass', __( 'Hybrid block support', 'ai-editor' ), __( 'This theme includes a theme.json file, so it should still give AI Editor useful presets and editor-side design context even though it is not a full block theme.', 'ai-editor' ) ); } else { $add_item( 'warn', __( 'Theme structure', 'ai-editor' ), __( 'No theme.json file was detected. AI Editor can still generate core blocks, but styling and layout decisions may feel less connected to the active theme.', 'ai-editor' ) ); } if ( $page_editor_enabled || $post_editor_enabled ) { $add_item( 'pass', __( 'Gutenberg editor availability', 'ai-editor' ), __( 'The block editor is enabled for standard content types, so AI Editor can work in the environments it expects.', 'ai-editor' ) ); } else { $add_item( 'fail', __( 'Gutenberg editor availability', 'ai-editor' ), __( 'The block editor does not appear to be enabled for posts or pages. AI Editor depends on Gutenberg, so this theme or site setup may limit where it can be used.', 'ai-editor' ) ); } if ( $preset_group_count >= 3 && $preset_entry_count >= 8 ) { $add_item( 'pass', __( 'Theme presets and design tokens', 'ai-editor' ), sprintf( /* translators: 1: number of preset groups, 2: number of preset entries */ __( 'The theme exposes %1$d preset groups and %2$d preset values through global settings. That gives AI Editor useful colors, spacing, typography, and layout hints.', 'ai-editor' ), $preset_group_count, $preset_entry_count ) ); } else { $add_item( 'warn', __( 'Theme presets and design tokens', 'ai-editor' ), __( 'The theme exposes only a limited set of presets through global settings. AI Editor can still work, but generated layouts may rely more on generic core-block defaults.', 'ai-editor' ) ); } if ( $supports_editor_styles || $supports_align_wide ) { $editor_support_bits = array(); if ( $supports_editor_styles ) { $editor_support_bits[] = __( 'editor styles', 'ai-editor' ); } if ( $supports_align_wide ) { $editor_support_bits[] = __( 'wide alignment', 'ai-editor' ); } $add_item( 'pass', __( 'Editor support flags', 'ai-editor' ), sprintf( /* translators: %s: comma-separated support labels */ __( 'The active theme supports %s, which usually makes AI-generated layouts look more natural inside the editor and on the front end.', 'ai-editor' ), implode( ', ', $editor_support_bits ) ) ); } else { $add_item( 'warn', __( 'Editor support flags', 'ai-editor' ), __( 'No editor-style or wide-alignment support was detected. Core blocks will still work, but section layouts may feel more constrained.', 'ai-editor' ) ); } if ( 0 === $custom_block_count ) { $add_item( 'note', __( 'Custom block reminder', 'ai-editor' ), __( 'No custom blocks are registered. AI Editor is designed primarily around canonical core Gutenberg blocks.', 'ai-editor' ) ); } elseif ( $custom_block_count <= 8 ) { $add_item( 'note', __( 'Custom block reminder', 'ai-editor' ), sprintf( /* translators: %d: number of custom blocks */ __( '%d custom blocks are registered. AI Editor can update custom blocks after you add and select them, but custom block behavior depends on each block’s own contract.', 'ai-editor' ), $custom_block_count ) ); } else { $add_item( 'note', __( 'Custom block reminder', 'ai-editor' ), sprintf( /* translators: %d: number of custom blocks */ __( '%d custom blocks are registered. AI Editor is most predictable with core-block layouts, while custom block updates depend on each block’s own contract.', 'ai-editor' ), $custom_block_count ) ); } if ( $has_core_blocks ) { $add_item( 'pass', __( 'Core block coverage', 'ai-editor' ), __( 'Core Gutenberg blocks are available, which gives AI Editor the normal building blocks it expects for inserts and edits.', 'ai-editor' ) ); } if ( '' !== $current_theme_style_context ) { $add_item( 'pass', __( 'Saved theme direction', 'ai-editor' ), __( 'This theme already has a saved “Theme Style Context For AI”, so the assistant can follow a theme-specific design brief instead of relying only on the raw theme description.', 'ai-editor' ) ); } else { $add_item( 'warn', __( 'Saved theme direction', 'ai-editor' ), __( 'No saved “Theme Style Context For AI” exists for this theme yet. AI Editor can still work, but saving a short theme-specific design brief usually improves consistency.', 'ai-editor' ) ); } if ( $has_hard_fail ) { $report['status'] = 'fail'; $report['status_label'] = __( 'Not compatible in current setup', 'ai-editor' ); $report['summary'] = __( 'AI Editor depends on Gutenberg. Until the block editor is enabled for normal content types, this theme or site setup should be treated as incompatible.', 'ai-editor' ); } elseif ( $warning_count >= 4 ) { $report['status'] = 'limited'; $report['status_label'] = __( 'Compatibility needs review', 'ai-editor' ); $report['summary'] = __( 'AI Editor should still function, but this theme has several signals that make generated results less predictable.', 'ai-editor' ); } elseif ( $warning_count >= 2 ) { $report['status'] = 'mixed'; $report['status_label'] = __( 'Mostly compatible with caveats', 'ai-editor' ); $report['summary'] = __( 'AI Editor should work, but a few theme-level choices may make the output less polished or more dependent on manual cleanup.', 'ai-editor' ); } return $report; } /** * Flatten grouped preset structures from wp_get_global_settings(). * * @param array $items Raw preset items. * @return array */ private static function flatten_theme_preset_groups( $items ) { if ( ! is_array( $items ) ) { return array(); } $is_list = array_keys( $items ) === range( 0, count( $items ) - 1 ); if ( $is_list ) { return $items; } $flattened = array(); foreach ( $items as $group_items ) { if ( is_array( $group_items ) ) { $flattened = array_merge( $flattened, $group_items ); } } return $flattened; } /** * Build a plain-text preset summary from wp_get_global_settings() for AI prompts. * * @param array $global_settings Theme global settings. * @return string */ private static function build_ai_prompt_presets_text( $global_settings ) { if ( ! is_array( $global_settings ) || isset( $global_settings['error'] ) ) { return is_array( $global_settings ) && isset( $global_settings['error'] ) ? (string) $global_settings['error'] : 'wp_get_global_settings() is unavailable.'; } $lines = array(); $lines[] = 'Available preset values:'; $lines[] = ''; $layout_widths = array(); $colors = isset( $global_settings['color']['palette'] ) && is_array( $global_settings['color']['palette'] ) ? self::flatten_theme_preset_groups( $global_settings['color']['palette'] ) : array(); if ( ! empty( $colors ) ) { $lines[] = 'colors:'; foreach ( $colors as $color ) { if ( empty( $color['slug'] ) ) { continue; } $label = $color['slug']; if ( ! empty( $color['color'] ) ) { $label .= ' (' . $color['color'] . ')'; } $lines[] = '- ' . $label; } $lines[] = ''; } $gradients = isset( $global_settings['color']['gradients'] ) && is_array( $global_settings['color']['gradients'] ) ? self::flatten_theme_preset_groups( $global_settings['color']['gradients'] ) : array(); if ( ! empty( $gradients ) ) { $lines[] = 'gradients:'; foreach ( $gradients as $gradient ) { if ( empty( $gradient['slug'] ) ) { continue; } $label = $gradient['slug']; if ( ! empty( $gradient['gradient'] ) ) { $label .= ' (' . $gradient['gradient'] . ')'; } $lines[] = '- ' . $label; } $lines[] = ''; } $font_sizes = isset( $global_settings['typography']['fontSizes'] ) && is_array( $global_settings['typography']['fontSizes'] ) ? self::flatten_theme_preset_groups( $global_settings['typography']['fontSizes'] ) : array(); if ( ! empty( $font_sizes ) ) { $lines[] = 'fontSizes:'; foreach ( $font_sizes as $font_size ) { if ( empty( $font_size['slug'] ) ) { continue; } $label = $font_size['slug']; if ( ! empty( $font_size['size'] ) ) { $label .= ' (' . $font_size['size'] . ')'; } $lines[] = '- ' . $label; } $lines[] = ''; } $spacing_sizes = isset( $global_settings['spacing']['spacingSizes'] ) && is_array( $global_settings['spacing']['spacingSizes'] ) ? self::flatten_theme_preset_groups( $global_settings['spacing']['spacingSizes'] ) : array(); if ( ! empty( $spacing_sizes ) ) { $lines[] = 'spacingSizes:'; foreach ( $spacing_sizes as $spacing_size ) { if ( empty( $spacing_size['slug'] ) ) { continue; } $label = $spacing_size['slug']; if ( ! empty( $spacing_size['size'] ) ) { $label .= ' (' . $spacing_size['size'] . ')'; } $lines[] = '- ' . $label; } $lines[] = ''; } $aspect_ratios = isset( $global_settings['dimensions']['aspectRatios'] ) && is_array( $global_settings['dimensions']['aspectRatios'] ) ? self::flatten_theme_preset_groups( $global_settings['dimensions']['aspectRatios'] ) : array(); if ( ! empty( $aspect_ratios ) ) { $lines[] = 'aspectRatios (for images):'; foreach ( $aspect_ratios as $aspect_ratio ) { if ( empty( $aspect_ratio['ratio'] ) ) { continue; } $lines[] = '- ' . $aspect_ratio['ratio']; } $lines[] = ''; } $layout = isset( $global_settings['layout'] ) && is_array( $global_settings['layout'] ) ? $global_settings['layout'] : array(); if ( ! empty( $layout ) ) { $lines[] = 'layout:'; foreach ( $layout as $key => $value ) { if ( is_scalar( $value ) ) { $lines[] = '- ' . $key . ': ' . $value; } } } if ( current_theme_supports( 'align-wide' ) ) { $layout_widths[] = 'wide align supported'; $layout_widths[] = 'full align supported'; } if ( ! empty( $layout_widths ) ) { $lines[] = ''; $lines[] = 'layoutWidths:'; foreach ( array_unique( $layout_widths ) as $layout_width ) { $lines[] = '- ' . $layout_width; } } return trim( implode( "\n", $lines ) ); } /** * Get the current active theme stylesheet slug. * * @return string */ private static 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() ); } /** * Callback for the settings section description. */ public static function settings_section_callback() { echo ''; } /** * Renders the settings page in the WordPress admin. */ public static function render_settings_page() { $options = get_option( 'ai_editor_settings', array() ); $theme_compatibility_report = self::get_theme_compatibility_report( $options ); $theme_compatibility_failure_count = count( array_filter( $theme_compatibility_report['items'], static function ( $item ) { return is_array( $item ) && isset( $item['status'] ) && 'fail' === $item['status']; } ) ); $global_settings = function_exists( 'wp_get_global_settings' ) ? wp_get_global_settings() : array( 'error' => 'wp_get_global_settings() is unavailable.' ); $global_styles = function_exists( 'wp_get_global_styles' ) ? wp_get_global_styles() : array( 'error' => 'wp_get_global_styles() is unavailable.' ); $global_settings_payload = isset( $global_settings['error'] ) ? $global_settings : array( 'colors' => isset( $global_settings['color']['palette'] ) ? $global_settings['color']['palette'] : array(), 'gradients' => isset( $global_settings['color']['gradients'] ) ? $global_settings['color']['gradients'] : array(), 'fontSizes' => isset( $global_settings['typography']['fontSizes'] ) ? $global_settings['typography']['fontSizes'] : array(), 'spacingSizes' => isset( $global_settings['spacing']['spacingSizes'] ) ? $global_settings['spacing']['spacingSizes'] : array(), 'aspectRatios' => isset( $global_settings['dimensions']['aspectRatios'] ) ? $global_settings['dimensions']['aspectRatios'] : array(), 'layoutWidths' => array_values( array_filter( array( current_theme_supports( 'align-wide' ) ? 'wide align supported' : '', current_theme_supports( 'align-wide' ) ? 'full align supported' : '', ) ) ), 'layout' => isset( $global_settings['layout'] ) ? $global_settings['layout'] : array(), ); $global_styles_payload = isset( $global_styles['error'] ) ? $global_styles : array( 'color' => isset( $global_styles['color'] ) ? $global_styles['color'] : array(), 'typography' => isset( $global_styles['typography'] ) ? $global_styles['typography'] : array(), 'spacing' => isset( $global_styles['spacing'] ) ? $global_styles['spacing'] : array(), 'layout' => isset( $global_styles['layout'] ) ? $global_styles['layout'] : array(), 'blocks' => isset( $global_styles['blocks'] ) ? $global_styles['blocks'] : array(), ); $global_settings_prompt_text = self::build_ai_prompt_presets_text( $global_settings ); ?>wp_get_global_settings() trimmedwp_get_global_styles() trimmed