'GET', 'callback' => array( $this, 'get_plugin_settings' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ); register_rest_route( 'ai-editor/v1', '/settings/theme-style-context/generate', array( 'methods' => 'POST', 'callback' => array( $this, 'generate_theme_style_context' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ); } /** * Handle the request to get plugin settings. * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|WP_REST_Response */ public function get_plugin_settings( $request ) { // Get the plugin settings. $options = get_option( 'ai_editor_settings' ); // Check if the settings are retrieved successfully. if ( false === $options ) { return new WP_Error( 'settings_not_found', __( 'Set up AI Editor in Settings -> AI Editor.', 'ai-editor' ), array( 'status' => 404 ) ); } $model_key = isset( $options['ai_editor_model'] ) ? $options['ai_editor_model'] : AIEditor_Model_Config::get_recommended_model_key(); $reasoning_effort = AIEditor_Model_Config::normalize_reasoning_effort( isset( $options['ai_editor_reasoning_effort'] ) ? $options['ai_editor_reasoning_effort'] : AIEditor_Model_Config::get_default_reasoning_effort(), AIEditor_Model_Config::get_openai_model_name( $model_key ) ); // Extract the settings. $settings = array( 'ai_editor_model' => isset( $options['ai_editor_model'] ) ? $options['ai_editor_model'] : AIEditor_Model_Config::get_recommended_model_key(), 'ai_editor_model_label' => AIEditor_Model_Config::get_model_label( isset( $options['ai_editor_model'] ) ? $options['ai_editor_model'] : AIEditor_Model_Config::get_recommended_model_key() ), 'ai_editor_recommended_model' => AIEditor_Model_Config::get_recommended_model_key(), 'ai_editor_recommended_model_label' => AIEditor_Model_Config::get_recommended_model_label(), 'ai_editor_reasoning_effort' => $reasoning_effort, 'ai_editor_reasoning_effort_label' => AIEditor_Model_Config::get_reasoning_effort_label( $reasoning_effort ), 'ai_editor_api_key_configured' => ! empty( $options['ai_editor_api_key'] ), 'ai_editor_theme_style_context' => $this->get_valid_theme_style_context( $options ), 'ai_editor_trim_chat_history_enabled' => ! isset( $options['ai_editor_trim_chat_history_enabled'] ) || '1' === (string) $options['ai_editor_trim_chat_history_enabled'], 'ai_editor_trim_chat_history_max_messages' => isset( $options['ai_editor_trim_chat_history_max_messages'] ) ? max( 1, min( 100, absint( $options['ai_editor_trim_chat_history_max_messages'] ) ) ) : 50, ); $settings['ai_editor_model_supported'] = in_array( $settings['ai_editor_model'], $this->get_supported_models(), true ); $settings['ai_editor_model_message'] = $settings['ai_editor_model_supported'] ? '' : __( AIEditor_Model_Config::get_supported_models_message(), 'ai-editor' ); // Return the settings in the response. return new WP_REST_Response( $settings, 200 ); } /** * Generate and persist a short theme style context from the active theme description. * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|WP_REST_Response */ public function generate_theme_style_context( $request ) { $options = get_option( 'ai_editor_settings', array() ); if ( empty( $options['ai_editor_api_key'] ) ) { return new WP_Error( 'api_key_missing', __( 'Enter your OpenAI API key in Settings -> AI Editor.', 'ai-editor' ), array( 'status' => 400 ) ); } $theme_description = trim( sanitize_textarea_field( (string) $request->get_param( 'theme_description' ) ) ); if ( '' === $theme_description ) { $theme_description = $this->get_active_theme_description(); } if ( '' === $theme_description ) { return new WP_Error( 'theme_description_missing', __( 'No active theme description was found to analyze.', 'ai-editor' ), array( 'status' => 400 ) ); } $model_key = isset( $options['ai_editor_model'] ) ? $options['ai_editor_model'] : AIEditor_Model_Config::get_recommended_model_key(); $model = $this->get_openai_model_name( $model_key ); $style_context = $this->generate_theme_style_context_via_openai( $theme_description, (string) $options['ai_editor_api_key'], $model ); if ( is_wp_error( $style_context ) ) { return $style_context; } $active_theme_slug = $this->get_active_theme_slug(); $theme_style_contexts = AIEditor_Settings_Page::get_saved_theme_style_contexts( $options ); $sanitized_style_context = AIEditor_Settings_Page::sanitize_theme_style_context( $style_context ); if ( '' !== $active_theme_slug ) { $theme_style_contexts[ $active_theme_slug ] = $sanitized_style_context; } $options['ai_editor_theme_style_contexts'] = $theme_style_contexts; $options['ai_editor_theme_style_context'] = $sanitized_style_context; $options['ai_editor_theme_style_context_theme_slug'] = $active_theme_slug; update_option( 'ai_editor_settings', $options ); return new WP_REST_Response( array( 'theme_style_context' => $style_context, ), 200 ); } /** * 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' ) ) ) ); } /** * Get the current 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() ); } /** * Return the saved theme style context only when it matches the active theme. * * @param array $options Saved plugin settings. * @return string */ private function get_valid_theme_style_context( $options ) { if ( ! is_array( $options ) ) { return ''; } return AIEditor_Settings_Page::get_saved_theme_style_context_for_slug( $options, $this->get_active_theme_slug() ); } /** * Map stored model keys to OpenAI model names. * * @param string $model_key Stored settings model key. * @return string */ private function get_openai_model_name( $model_key ) { return AIEditor_Model_Config::get_openai_model_name( $model_key ); } /** * Generate a compact theme style summary with OpenAI. * * @param string $theme_description Active theme description. * @param string $api_key OpenAI API key. * @param string $model OpenAI model name. * @return string|WP_Error */ private function generate_theme_style_context_via_openai( $theme_description, $api_key, $model ) { $payload = array( 'model' => $model, 'reasoning' => array( 'effort' => AIEditor_Model_Config::normalize_reasoning_effort( 'none', $model ), ), 'input' => array( array( 'role' => 'system', 'content' => 'You rewrite WordPress theme descriptions into a short design-direction note for another AI. Extract only high-signal guidance about visual style, tone, layout feel, and aesthetic direction. Ignore setup details, technical features, integrations, compatibility, performance claims, admin options, plugin mentions, ecommerce support, and generic marketing filler. Return plain text only, 2-4 concise sentences, no bullets, no labels.', ), array( 'role' => 'user', 'content' => "Theme description:\n" . $theme_description, ), ), ); $response = wp_remote_post( 'https://api.openai.com/v1/responses', array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, ), 'body' => wp_json_encode( $payload ), 'timeout' => 120, ) ); if ( is_wp_error( $response ) ) { return new WP_Error( 'request_failed', $response->get_error_message(), array( 'status' => 500 ) ); } $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { return new WP_Error( 'json_error', __( 'JSON parsing error: ', 'ai-editor' ) . json_last_error_msg(), array( 'status' => 500 ) ); } $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code < 200 || $status_code >= 300 ) { $error_message = isset( $response_data['error']['message'] ) ? $response_data['error']['message'] : __( 'Unknown error.', 'ai-editor' ); return new WP_Error( 'openai_error', $error_message, array( 'status' => $status_code ) ); } $style_context = ''; if ( ! empty( $response_data['output_text'] ) && is_string( $response_data['output_text'] ) ) { $style_context = trim( $response_data['output_text'] ); } elseif ( ! empty( $response_data['output'] ) && is_array( $response_data['output'] ) ) { foreach ( $response_data['output'] as $item ) { if ( empty( $item['type'] ) || 'message' !== $item['type'] || empty( $item['content'] ) || ! is_array( $item['content'] ) ) { continue; } foreach ( $item['content'] as $content_item ) { if ( isset( $content_item['type'], $content_item['text'] ) && 'output_text' === $content_item['type'] ) { $style_context .= trim( $content_item['text'] ) . "\n"; } } } } $style_context = trim( preg_replace( '/\s+/', ' ', $style_context ) ); if ( '' === $style_context ) { return new WP_Error( 'empty_theme_style_context', __( 'OpenAI returned an empty theme style context.', 'ai-editor' ), array( 'status' => 500 ) ); } return $style_context; } /** * Check if the user has permission to access the endpoint. * * @param WP_REST_Request $request Full data about the request. * @return bool */ public function permissions_check( $request ) { return current_user_can( 'manage_options' ); } } // Hook the class to the REST API. add_action( 'rest_api_init', function () { $ai_editor_settings_endpoint = new AIEditor_Settings_Endpoint(); $ai_editor_settings_endpoint->register_routes(); } );