diff --git a/includes/Classifai/Admin/BulkActions.php b/includes/Classifai/Admin/BulkActions.php index 6c6326360..d77572a91 100644 --- a/includes/Classifai/Admin/BulkActions.php +++ b/includes/Classifai/Admin/BulkActions.php @@ -82,7 +82,7 @@ public function register_language_processing_hooks() { continue; } - foreach ( $settings['post_types'] as $key => $post_type ) { + foreach ( $settings['post_types'] as $post_type ) { add_filter( "bulk_actions-edit-$post_type", [ $this, 'register_language_processing_actions' ] ); add_filter( "handle_bulk_actions-edit-$post_type", [ $this, 'language_processing_actions_handler' ], 10, 3 ); @@ -107,6 +107,10 @@ public function register_language_processing_actions( array $bulk_actions ): arr continue; } + if ( ! in_array( get_post_type(), $feature->get_supported_post_types(), true ) ) { + continue; + } + $bulk_actions[ $feature::ID ] = $feature->get_label(); switch ( $feature::ID ) { @@ -153,7 +157,25 @@ function ( $feature ) { foreach ( $post_ids as $post_id ) { switch ( $doaction ) { case Classification::ID: - ( new Classification() )->run( $post_id ); + // Check to see if processing is disabled and overwrite that. + // Since we are manually classifying, we want to force this. + $classification_enabled = get_post_meta( $post_id, '_classifai_process_content', true ); + if ( 'yes' !== $classification_enabled ) { + update_post_meta( $post_id, '_classifai_process_content', 'yes' ); + } + + $classification = new Classification(); + $classify_results = $classification->run( $post_id, 'classify' ); + + // Ensure the processing value is changed back to what it was. + if ( 'yes' !== $classification_enabled ) { + update_post_meta( $post_id, '_classifai_process_content', 'no' ); + } + + if ( ! empty( $classify_results ) && ! is_wp_error( $classify_results ) ) { + $classification->save( $post_id, $classify_results ); + } + $action = $doaction; break; @@ -283,6 +305,17 @@ public function register_language_processing_row_action( array $actions, \WP_Pos continue; } + if ( ! in_array( get_post_type(), $feature->get_supported_post_types(), true ) ) { + continue; + } + + if ( + Classification::ID === $feature::ID && + ! in_array( get_post_status(), $feature->get_supported_post_statuses(), true ) + ) { + continue; + } + switch ( $feature::ID ) { case Classification::ID: $actions[ Classification::ID ] = sprintf( diff --git a/includes/Classifai/Command/ClassifaiCommand.php b/includes/Classifai/Command/ClassifaiCommand.php index 20ec489f6..ad50655b0 100644 --- a/includes/Classifai/Command/ClassifaiCommand.php +++ b/includes/Classifai/Command/ClassifaiCommand.php @@ -61,11 +61,17 @@ public function post( $args = [], $opts = [] ) { $post_ids = $this->get_posts_to_classify( $opts ); } - $total = count( $post_ids ); - $classifier = new PostClassifier(); - $limit = $opts['limit']; - $link = $opts['link']; - $link = filter_var( $link, FILTER_VALIDATE_BOOLEAN ); + $feature = new Classification(); + $provider = $feature->get_feature_provider_instance(); + + if ( Embeddings::ID !== $provider::ID ) { + \WP_CLI::error( 'This command is only available for the IBM Watson Provider' ); + } + + $total = count( $post_ids ); + $limit = $opts['limit']; + $link = $opts['link']; + $link = filter_var( $link, FILTER_VALIDATE_BOOLEAN ); if ( ! empty( $total ) ) { if ( ! empty( $limit ) ) { @@ -83,15 +89,18 @@ public function post( $args = [], $opts = [] ) { $progress_bar->tick(); - if ( $link ) { - $output = $classifier->classify_and_link( $post_id, $opts ); + $results = $feature->run( $post_id, 'classify' ); + + if ( is_wp_error( $results ) ) { + $errors[ $post_id ] = $results; + } - if ( is_wp_error( $output ) ) { - $errors[ $post_id ] = $output; + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + if ( $link ) { + $feature->save( $post_id, $results ); + } else { + $this->print( $results, $post_id ); } - } else { - $output = $classifier->classify( $post_id, $opts ); - $this->print( $output, $post_id ); } } @@ -963,8 +972,8 @@ public function embeddings( $args = [], $opts = [] ) { $embeddings = new Embeddings( false ); $opts = wp_parse_args( $opts, $defaults ); $opts['per_page'] = (int) $opts['per_page'] > 0 ? $opts['per_page'] : 100; - $allowed_post_types = $embeddings->supported_post_types(); - $allowed_post_status = $embeddings->supported_post_statuses(); + $allowed_post_types = $feature->get_supported_post_types(); + $allowed_post_status = $feature->get_supported_post_statuses(); $count = 0; $errors = 0; @@ -1015,9 +1024,13 @@ public function embeddings( $args = [], $opts = [] ) { foreach ( $posts as $post_id ) { if ( ! $dry_run ) { - $result = $feature->run( $post_id ); + $results = $feature->run( $post_id, 'classify' ); - if ( is_wp_error( $result ) ) { + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + $feature->save( $post_id, $results ); + } + + if ( is_wp_error( $results ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s', $post_id ), false ); ++$errors; } @@ -1063,9 +1076,13 @@ public function embeddings( $args = [], $opts = [] ) { } if ( ! $dry_run ) { - $result = $feature->run( $post_id ); + $results = $feature->run( $post_id, 'classify' ); - if ( is_wp_error( $result ) ) { + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + $feature->save( $post_id, $results ); + } + + if ( is_wp_error( $results ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s', $post_id ), false ); ++$errors; } diff --git a/includes/Classifai/Features/Classification.php b/includes/Classifai/Features/Classification.php index f26d161e8..e02449db9 100644 --- a/includes/Classifai/Features/Classification.php +++ b/includes/Classifai/Features/Classification.php @@ -5,9 +5,17 @@ use Classifai\Services\LanguageProcessing; use Classifai\Providers\Watson\NLU; use Classifai\Providers\OpenAI\Embeddings; +use WP_REST_Server; +use WP_REST_Request; +use WP_Error; use function Classifai\get_post_statuses_for_language_settings; use function Classifai\get_post_types_for_language_settings; +use function Classifai\check_term_permissions; +use function Classifai\get_classification_feature_enabled; +use function Classifai\get_classification_feature_taxonomy; +use function Classifai\get_asset_info; +use function Classifai\get_classification_mode; /** * Class Classification @@ -36,21 +44,759 @@ public function __construct() { ]; } + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + $post_types = $this->get_supported_post_types(); + if ( ! empty( $post_types ) ) { + foreach ( $post_types as $post_type ) { + add_action( 'rest_after_insert_' . $post_type, [ $this, 'rest_after_insert' ] ); + } + } + + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'classifai_after_feature_settings_form', [ $this, 'render_previewer' ] ); + add_action( 'rest_api_init', [ $this, 'add_process_content_meta_to_rest_api' ] ); + add_action( 'wp_ajax_classifai_get_post_search_results', array( $this, 'get_post_search_results' ) ); + add_filter( 'default_post_metadata', [ $this, 'default_post_metadata' ], 10, 3 ); + + // Support the Classic Editor. + add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ], 10, 2 ); + add_action( 'save_post', [ $this, 'save_meta_box' ] ); + add_action( 'admin_post_classifai_classify_post', array( $this, 'classifai_classify_post' ) ); + add_action( 'admin_notices', [ $this, 'show_error_if' ] ); + add_filter( 'removable_query_args', [ $this, 'removable_query_args' ] ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + $post_types = $this->get_supported_post_types(); + foreach ( $post_types as $post_type ) { + register_meta( + $post_type, + '_classifai_error', + [ + 'show_in_rest' => true, + 'single' => true, + 'auth_callback' => '__return_true', + ] + ); + } + + register_rest_route( + 'classifai/v1', + 'classify/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => array( + 'id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Post ID to classify.', 'classifai' ), + ), + 'linkTerms' => array( + 'type' => 'boolean', + 'description' => esc_html__( 'Whether to link terms or not.', 'classifai' ), + 'default' => true, + ), + ), + 'permission_callback' => [ $this, 'classify_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to run classification. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function classify_permissions_check( WP_REST_Request $request ) { + $post_id = $request->get_param( 'id' ); + + // Ensure we have a logged in user that can edit the item. + if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { + return false; + } + + $post_type = get_post_type( $post_id ); + $post_type_obj = get_post_type_object( $post_type ); + + // Ensure the post type is allowed in REST endpoints. + if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) { + return false; + } + + // For all enabled features, ensure the user has proper permissions to add/edit terms. + $provider_instance = $this->get_feature_provider_instance(); + if ( empty( $provider_instance->nlu_features ) ) { + return new WP_Error( 'not_enabled', esc_html__( 'Classification not configured correctly for the selected provider.', 'classifai' ) ); + } + + foreach ( $provider_instance->nlu_features as $feature_name => $feature ) { + if ( ! get_classification_feature_enabled( $feature_name ) ) { + continue; + } + + $taxonomy = get_classification_feature_taxonomy( $feature_name ); + $permission = check_term_permissions( $taxonomy ); + + if ( is_wp_error( $permission ) ) { + return $permission; + } + } + + $post_status = get_post_status( $post_id ); + $supported = $this->get_supported_post_types(); + $post_statuses = $this->get_supported_post_statuses(); + + // Check if processing allowed. + if ( + ! in_array( $post_status, $post_statuses, true ) || + ! in_array( $post_type, $supported, true ) || + ! $this->is_feature_enabled() + ) { + return new WP_Error( 'not_enabled', esc_html__( 'Classification not enabled for current item.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/classify' ) === 0 ) { + $results = $this->run( + $request->get_param( 'id' ), + 'classify', + [ + 'link_terms' => $request->get_param( 'linkTerms' ), + ] + ); + + // Save results or return the results that need saved. + if ( ! is_wp_error( $results ) ) { + $results = $this->save( $request->get_param( 'id' ), $results, $request->get_param( 'linkTerms' ) ?? true ); + } + + return rest_ensure_response( + [ + 'terms' => $results, + 'feature_taxonomies' => $this->get_all_feature_taxonomies(), + ] + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Save or return the classification results. + * + * If $link is true, we link the terms to the item. If + * it is false, we just return the terms that need linked + * so they can show in the UI. + * + * @param int $post_id The post ID. + * @param array $results Term results + * @param bool $link Whether to link the terms or not. + * @return array|WP_Error + */ + public function save( int $post_id, array $results, bool $link = true ) { + $provider_instance = $this->get_feature_provider_instance(); + + switch ( $provider_instance::ID ) { + case NLU::ID: + $results = $provider_instance->link( $post_id, $results, $link ); + break; + case Embeddings::ID: + $results = $provider_instance->set_terms( $post_id, $results, $link ); + break; + } + + return $results; + } + + /** + * Run classification after an item has been inserted via REST. + * + * @param \WP_Post $post Post object. + */ + public function rest_after_insert( \WP_Post $post ) { + $supported_post_types = $this->get_supported_post_types(); + $post_statuses = $this->get_supported_post_statuses(); + + // Ensure the post type and status is allowed. + if ( + ! in_array( $post->post_type, $supported_post_types, true ) || + ! in_array( $post->post_status, $post_statuses, true ) + ) { + return; + } + + // Check if processing on save is disabled. + if ( 'no' === get_post_meta( $post->ID, '_classifai_process_content', true ) ) { + return; + } + + $results = $this->run( $post->ID, 'classify' ); + + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + $this->save( $post->ID, $results ); + delete_post_meta( $post->ID, '_classifai_error' ); + } elseif ( is_wp_error( $results ) ) { + update_post_meta( + $post->ID, + '_classifai_error', + wp_json_encode( + [ + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ] + ) + ); + } + } + + /** + * Enqueue the admin scripts. + */ + public function enqueue_admin_assets() { + wp_enqueue_script( + 'classifai-language-processing-script', + CLASSIFAI_PLUGIN_URL . 'dist/language-processing.js', + get_asset_info( 'language-processing', 'dependencies' ), + get_asset_info( 'language-processing', 'version' ), + true + ); + + wp_enqueue_style( + 'classifai-language-processing-style', + CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css', + array(), + get_asset_info( 'language-processing', 'version' ), + 'all' + ); + } + + /** + * Enqueue editor assets. + */ + public function enqueue_editor_assets() { + global $post; + + wp_enqueue_script( + 'classifai-editor', + CLASSIFAI_PLUGIN_URL . 'dist/editor.js', + get_asset_info( 'editor', 'dependencies' ), + get_asset_info( 'editor', 'version' ), + true + ); + + if ( empty( $post ) ) { + return; + } + + wp_enqueue_script( + 'classifai-gutenberg-plugin', + CLASSIFAI_PLUGIN_URL . 'dist/gutenberg-plugin.js', + array_merge( get_asset_info( 'gutenberg-plugin', 'dependencies' ), array( 'lodash' ) ), + get_asset_info( 'gutenberg-plugin', 'version' ), + true + ); + + wp_add_inline_script( + 'classifai-gutenberg-plugin', + sprintf( + 'var classifaiPostData = %s;', + wp_json_encode( + [ + 'NLUEnabled' => $this->is_feature_enabled(), + 'supportedPostTypes' => $this->get_supported_post_types(), + 'supportedPostStatues' => $this->get_supported_post_statuses(), + 'noPermissions' => ! is_user_logged_in() || ! current_user_can( 'edit_post', $post->ID ), + ] + ) + ), + 'before' + ); + } + + /** + * Add `classifai_process_content` to the REST API for view/edit. + */ + public function add_process_content_meta_to_rest_api() { + $supported_post_types = $this->get_supported_post_types(); + + register_rest_field( + $supported_post_types, + 'classifai_process_content', + [ + 'get_callback' => function ( $data ) { + $process_content = get_post_meta( $data['id'], '_classifai_process_content', true ); + return ( 'no' === $process_content ) ? 'no' : 'yes'; + }, + 'update_callback' => function ( $value, $data ) { + $value = ( 'no' === $value ) ? 'no' : 'yes'; + return update_post_meta( $data->ID, '_classifai_process_content', $value ); + }, + 'schema' => [ + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + ], + ] + ); + } + + /** + * Searches and returns posts. + */ + public function get_post_search_results() { + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : false; + + if ( ! ( $nonce && wp_verify_nonce( $nonce, 'classifai-previewer-action' ) ) ) { + wp_send_json_error( esc_html__( 'Failed nonce check.', 'classifai' ) ); + } + + $search_term = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; + $post_types = isset( $_POST['post_types'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_types'] ) ) ) : 'post'; + $post_statuses = isset( $_POST['post_status'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_status'] ) ) ) : 'publish'; + + $posts = get_posts( + array( + 'post_type' => $post_types, + 'post_status' => $post_statuses, + 's' => $search_term, + ) + ); + + wp_send_json_success( $posts ); + } + + /** + * Add metabox to enable/disable language processing. + * + * @param string $post_type Post type. + * @param \WP_Post $post WP_Post object. + */ + public function add_meta_box( string $post_type, $post ) { + $supported_post_types = $this->get_supported_post_types(); + $post_statuses = $this->get_supported_post_statuses(); + $post_status = get_post_status( $post ); + + if ( + in_array( $post_type, $supported_post_types, true ) && + in_array( $post_status, $post_statuses, true ) + ) { + add_meta_box( + 'classifai_language_processing_metabox', + __( 'ClassifAI Language Processing', 'classifai' ), + [ $this, 'render_meta_box' ], + null, + 'side', + 'high', + array( '__back_compat_meta_box' => true ) + ); + } + } + + /** + * Render metabox content. + * + * @param \WP_Post $post WP_Post object. + */ + public function render_meta_box( \WP_Post $post ) { + wp_nonce_field( 'classifai_language_processing_meta_action', 'classifai_language_processing_meta' ); + $process_content = get_post_meta( $post->ID, '_classifai_process_content', true ); + $process_content = ( 'no' === $process_content ) ? 'no' : 'yes'; + ?> + +

+ +

+ + + run( $post_id, 'classify' ); + + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + $this->save( $post_id, $results ); + delete_post_meta( $post_id, '_classifai_error' ); + } elseif ( is_wp_error( $results ) ) { + update_post_meta( + $post_id, + '_classifai_error', + wp_json_encode( + [ + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ] + ) + ); + } + } + } + + /** + * Classify post manually. + * + * Fires when the Classify button is clicked + * in the Classic Editor. + */ + public function classifai_classify_post() { + if ( + empty( $_GET['classifai_classify_post_nonce'] ) || + ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['classifai_classify_post_nonce'] ) ), 'classifai_classify_post_action' ) + ) { + wp_die( esc_html__( 'You don\'t have permission to perform this operation.', 'classifai' ) ); + } + + $post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0; + + if ( ! $post_id ) { + exit(); + } + + // Check to see if processing is disabled and overwrite that. + // Since we are manually classifying, we want to force this. + $enabled = get_post_meta( $post_id, '_classifai_process_content', true ); + if ( 'yes' !== $enabled ) { + update_post_meta( $post_id, '_classifai_process_content', 'yes' ); + } + + $results = $this->run( $post_id, 'classify' ); + + // Ensure the processing value is changed back to what it was. + if ( 'yes' !== $enabled ) { + update_post_meta( $post_id, '_classifai_process_content', 'no' ); + } + + $classified = array(); + + if ( ! empty( $results ) && ! is_wp_error( $results ) ) { + $this->save( $post_id, $results ); + $classified = array( 'classifai_classify' => 1 ); + delete_post_meta( $post_id, '_classifai_error' ); + } elseif ( is_wp_error( $results ) ) { + update_post_meta( + $post_id, + '_classifai_error', + wp_json_encode( + [ + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ] + ) + ); + } + + wp_safe_redirect( esc_url_raw( add_query_arg( $classified, get_edit_post_link( $post_id, 'edit' ) ) ) ); + exit(); + } + + /** + * Outputs an admin notice with the error message if needed. + */ + public function show_error_if() { + global $post; + + if ( empty( $post ) ) { + return; + } + + $post_id = $post->ID; + + if ( empty( $post_id ) ) { + return; + } + + $error = get_post_meta( $post_id, '_classifai_error', true ); + + if ( ! empty( $error ) ) { + delete_post_meta( $post_id, '_classifai_error' ); + $error = (array) json_decode( $error ); + $code = ! empty( $error['code'] ) ? $error['code'] : 500; + $message = ! empty( $error['message'] ) ? $error['message'] : 'Unknown API error'; + + ?> +
+

+ +

+

+ + - + +

+
+ labels->singular_name; + } + ?> + +
+

+ +

+
+ + is_feature_enabled() ) { + return; + } + + $provider_instance = $this->get_feature_provider_instance(); + $nlu_features = array(); + $supported_post_statuses = $this->get_supported_post_statuses(); + $supported_post_types = $this->get_supported_post_types(); + + $posts_to_preview = get_posts( + array( + 'post_type' => $supported_post_types, + 'post_status' => $supported_post_statuses, + 'posts_per_page' => 10, + ) + ); + + if ( ! empty( $provider_instance->nlu_features ) ) { + $nlu_features = $provider_instance->nlu_features; + } + ?> + +
+

+ +
+ + + + + +
+ +
+ $feature ) : + if ( ! get_classification_feature_enabled( $feature_slug ) ) { + continue; + } + ?> + +
+
+
+ + +
+
+ + get_settings(); - $post_statuses = get_post_statuses_for_language_settings(); + $settings = $this->get_settings(); + $provider_instance = $this->get_feature_provider_instance(); + $nlu_features = array(); + $post_statuses = get_post_statuses_for_language_settings(); + $post_types = get_post_types_for_language_settings(); + $post_type_options = array(); + + if ( ! empty( $provider_instance->nlu_features ) ) { + $nlu_features = $provider_instance->nlu_features; + } + + foreach ( $post_types as $post_type ) { + $post_type_options[ $post_type->name ] = $post_type->label; + } + + add_settings_field( + 'classification_mode', + esc_html__( 'Classification mode', 'classifai' ), + [ $this, 'render_radio_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'classification_mode', + 'default_value' => $settings['classification_mode'], + 'options' => array( + 'manual_review' => __( 'Manual review', 'classifai' ), + 'automatic_classification' => __( 'Automatic classification', 'classifai' ), + ), + ] + ); + + $method_options = array( + 'recommended_terms' => __( 'Recommend terms even if they do not exist on the site', 'classifai' ), + 'existing_terms' => __( 'Only recommend terms that already exist on the site', 'classifai' ), + ); + + // Embeddings only supports existing terms. + if ( isset( $settings['provider'] ) && Embeddings::ID === $settings['provider'] ) { + unset( $method_options['recommended_terms'] ); + $settings['classification_method'] = 'existing_terms'; + } + + add_settings_field( + 'classification_method', + esc_html__( 'Classification method', 'classifai' ), + [ $this, 'render_radio_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'classification_method', + 'default_value' => $settings['classification_method'], + 'options' => $method_options, + ] + ); + + foreach ( $nlu_features as $classify_by => $labels ) { + add_settings_field( + $classify_by, + esc_html( $labels['feature'] ), + [ $this, 'render_nlu_feature_settings' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'feature' => $classify_by, + 'labels' => $labels, + 'default_value' => $settings[ $classify_by ], + 'post_types' => $settings['post_types'], + ] + ); + } add_settings_field( 'post_statuses', @@ -66,13 +812,6 @@ public function add_custom_settings_fields() { ] ); - $post_types = get_post_types_for_language_settings(); - $post_type_options = array(); - - foreach ( $post_types as $post_type ) { - $post_type_options[ $post_type->name ] = $post_type->label; - } - add_settings_field( 'post_types', esc_html__( 'Post types', 'classifai' ), @@ -95,13 +834,15 @@ public function add_custom_settings_fields() { */ public function get_feature_default_settings(): array { return [ - 'post_statuses' => [ + 'post_statuses' => [ 'publish' => 1, ], - 'post_types' => [ + 'post_types' => [ 'post' => 1, ], - 'provider' => NLU::ID, + 'classification_mode' => 'manual_review', + 'classification_method' => 'recommended_terms', + 'provider' => NLU::ID, ]; } @@ -112,46 +853,180 @@ public function get_feature_default_settings(): array { * @return array */ public function sanitize_default_feature_settings( array $new_settings ): array { - $settings = $this->get_settings(); + $settings = $this->get_settings(); + $provider_instance = $this->get_feature_provider_instance(); + + $new_settings['classification_mode'] = sanitize_text_field( $new_settings['classification_mode'] ?? $settings['classification_mode'] ); + + $new_settings['classification_method'] = sanitize_text_field( $new_settings['classification_method'] ?? $settings['classification_method'] ); + + // Embeddings only supports existing terms. + if ( isset( $new_settings['provider'] ) && Embeddings::ID === $new_settings['provider'] ) { + $new_settings['classification_method'] = 'existing_terms'; + } $new_settings['post_statuses'] = isset( $new_settings['post_statuses'] ) ? array_map( 'sanitize_text_field', $new_settings['post_statuses'] ) : $settings['post_statuses']; - $new_settings['post_types'] = isset( $new_settings['post_types'] ) ? array_map( 'sanitize_text_field', $new_settings['post_types'] ) : $settings['post_types']; + + $new_settings['post_types'] = isset( $new_settings['post_types'] ) ? array_map( 'sanitize_text_field', $new_settings['post_types'] ) : $settings['post_types']; + + if ( ! empty( $provider_instance->nlu_features ) ) { + foreach ( array_keys( $provider_instance->nlu_features ) as $feature_name ) { + $new_settings[ $feature_name ] = absint( $new_settings[ $feature_name ] ?? $settings[ $feature_name ] ); + $new_settings[ "{$feature_name}_threshold" ] = absint( $new_settings[ "{$feature_name}_threshold" ] ?? $settings[ "{$feature_name}_threshold" ] ); + $new_settings[ "{$feature_name}_taxonomy" ] = sanitize_text_field( $new_settings[ "{$feature_name}_taxonomy" ] ?? $settings[ "{$feature_name}_taxonomy" ] ); + } + } return $new_settings; } /** - * Runs the feature. + * Get all feature taxonomies. * - * @param mixed ...$args Arguments required by the feature depending on the provider selected. - * @return mixed + * @return array */ - public function run( ...$args ) { - $settings = $this->get_settings(); - $provider_id = $settings['provider'] ?? NLU::ID; - $provider_instance = $this->get_feature_provider_instance( $provider_id ); - $result = ''; - - if ( NLU::ID === $provider_instance::ID ) { - /** @var NLU $provider_instance */ - $result = call_user_func_array( - [ $provider_instance, 'classify' ], - [ ...$args ] - ); - } elseif ( Embeddings::ID === $provider_instance::ID ) { - /** @var Embeddings $provider_instance */ - $result = call_user_func_array( - [ $provider_instance, 'generate_embeddings_for_post' ], - [ ...$args ] - ); + public function get_all_feature_taxonomies(): array { + $feature_taxonomies = []; + $provider_instance = $this->get_feature_provider_instance(); + + if ( empty( $provider_instance->nlu_features ) ) { + return $feature_taxonomies; } - return apply_filters( - 'classifai_' . static::ID . '_run', - $result, - $provider_instance, - $args, - $this - ); + foreach ( array_keys( $provider_instance->nlu_features ) as $feature_name ) { + if ( ! get_classification_feature_enabled( $feature_name ) ) { + continue; + } + + $taxonomy = get_classification_feature_taxonomy( $feature_name ); + $permission = check_term_permissions( $taxonomy ); + + if ( is_wp_error( $permission ) ) { + continue; + } + + if ( 'post_tag' === $taxonomy ) { + $taxonomy = 'tags'; + } + + if ( 'category' === $taxonomy ) { + $taxonomy = 'categories'; + } + + $feature_taxonomies[] = $taxonomy; + } + + return $feature_taxonomies; + } + + /** + * Render the NLU features settings. + * + * @param array $args Settings for the inputs + */ + public function render_nlu_feature_settings( array $args ) { + $feature = $args['feature']; + $labels = $args['labels']; + + $taxonomies = $this->get_supported_taxonomies( $args['post_types'] ); + $features = $this->get_settings(); + $taxonomy = isset( $features[ "{$feature}_taxonomy" ] ) ? $features[ "{$feature}_taxonomy" ] : $labels['taxonomy_default']; + + // Enable classification type + $feature_args = [ + 'label_for' => $feature, + 'input_type' => 'checkbox', + ]; + + $threshold_args = [ + 'label_for' => "{$feature}_threshold", + 'input_type' => 'number', + 'default_value' => $labels['threshold_default'], + ]; + ?> + + + + + +

+ render_input( $feature_args ); ?> + +

+ +

+
+ render_input( $threshold_args ); ?> +

+ + +

+
+ +

+ + $enabled ) { + if ( ! empty( $enabled ) ) { + $supported_post_types[] = $post_type; + } + } + } + + $taxonomies = get_taxonomies( [], 'objects' ); + $taxonomies = array_filter( $taxonomies, 'is_taxonomy_viewable' ); + $supported = []; + + foreach ( $taxonomies as $taxonomy ) { + // Remove this taxonomy if it doesn't support at least one of our post types. + if ( + ( + ! empty( $supported_post_types ) && + empty( array_intersect( $supported_post_types, $taxonomy->object_type ) ) + ) || + 'post_format' === $taxonomy->name + ) { + continue; + } + + $supported[ $taxonomy->name ] = $taxonomy->labels->singular_name; + } + + /** + * Filter taxonomies shown in settings. + * + * @since 3.0.0 + * @hook classifai_feature_classification_setting_taxonomies + * + * @param {array} $supported Array of supported taxonomies. + * @param {object} $this Current instance of the class. + * + * @return {array} Array of taxonomies. + */ + return apply_filters( 'classifai_' . static::ID . '_setting_taxonomies', $supported, $this ); } } diff --git a/includes/Classifai/Features/Feature.php b/includes/Classifai/Features/Feature.php index 74b3d76da..013390a72 100644 --- a/includes/Classifai/Features/Feature.php +++ b/includes/Classifai/Features/Feature.php @@ -192,6 +192,7 @@ protected function get_default_settings(): array { * @hook classifai_{feature}_get_default_settings * * @param {array} $defaults Default feature settings. + * @param {object} $this Feature instance. * * @return {array} Filtered default feature settings. */ @@ -201,7 +202,8 @@ protected function get_default_settings(): array { $shared_defaults, $feature_settings, $provider_settings - ) + ), + $this ); } @@ -991,6 +993,70 @@ public function is_enabled(): bool { return apply_filters( 'classifai_' . static::ID . '_is_enabled', $is_enabled, $settings ); } + /** + * The list of post types that are supported. + * + * @return array + */ + public function get_supported_post_types(): array { + $settings = $this->get_settings(); + $post_types = []; + + if ( isset( $settings['post_types'] ) && is_array( $settings['post_types'] ) ) { + foreach ( $settings['post_types'] as $post_type => $enabled ) { + if ( ! empty( $enabled ) ) { + $post_types[] = $post_type; + } + } + } + + /** + * Filter post types supported for a feature. + * + * @since 3.0.0 + * @hook classifai_{feature}_post_types + * + * @param {array} $post_types Array of post types to be classified. + * + * @return {array} Array of post types. + */ + $post_types = apply_filters( 'classifai_' . static::ID . '_post_types', $post_types ); + + return $post_types; + } + + /** + * The list of post statuses that are supported. + * + * @return array + */ + public function get_supported_post_statuses(): array { + $settings = $this->get_settings(); + $post_statuses = []; + + if ( ! empty( $settings ) && isset( $settings['post_statuses'] ) ) { + foreach ( $settings['post_statuses'] as $post_status => $enabled ) { + if ( ! empty( $enabled ) ) { + $post_statuses[] = $post_status; + } + } + } + + /** + * Filter post statuses supported for a feature. + * + * @since 3.0.0 + * @hook classifai_{feature}_post_statuses + * + * @param {array} $post_types Array of post statuses to be classified. + * + * @return {array} Array of post statuses. + */ + $post_statuses = apply_filters( 'classifai_' . static::ID . '_post_statuses', $post_statuses ); + + return $post_statuses; + } + /** * Returns array of instances of provider classes registered for the service. * @@ -1003,7 +1069,7 @@ protected function get_provider_instances( array $services ): array { $provider_instances = []; foreach ( $services as $provider_class ) { - $provider_instances[] = new $provider_class( $this ); + $provider_instances[] = new $provider_class(); } return $provider_instances; diff --git a/includes/Classifai/Features/TextToSpeech.php b/includes/Classifai/Features/TextToSpeech.php index d0fdcf779..8deda3892 100644 --- a/includes/Classifai/Features/TextToSpeech.php +++ b/includes/Classifai/Features/TextToSpeech.php @@ -717,24 +717,6 @@ protected function get_post_types_select_options(): array { return $options; } - /** - * The list of post types that TTS supports. - * - * @return array Supported Post Types. - */ - public function get_supported_post_types(): array { - $selected = $this->get_settings( 'post_types' ); - $post_types = []; - - foreach ( $selected as $post_type => $enabled ) { - if ( ! empty( $enabled ) ) { - $post_types[] = $post_type; - } - } - - return $post_types; - } - /** * Returns the default settings for the feature. * diff --git a/includes/Classifai/Helpers.php b/includes/Classifai/Helpers.php index 9e16003d3..d490ec960 100644 --- a/includes/Classifai/Helpers.php +++ b/includes/Classifai/Helpers.php @@ -6,6 +6,7 @@ use Classifai\Providers\Provider; use Classifai\Admin\UserProfile; use Classifai\Providers\Watson\NLU; +use Classifai\Providers\OpenAI\Embeddings; use Classifai\Services\Service; use Classifai\Services\ServicesManager; use WP_Error; @@ -577,3 +578,87 @@ function ( $prompt ) { function sanitize_number_of_responses_field( string $key, array $new_settings, array $settings ): int { return absint( $new_settings[ $key ] ?? $settings[ $key ] ?? '' ); } + +/** + * Returns a bool based on whether the specified classification feature is enabled. + * + * @param string $classify_by Feature to check. + * @return bool + */ +function get_classification_feature_enabled( string $classify_by ): bool { + $settings = ( new Classification() )->get_settings(); + + return filter_var( + $settings[ $classify_by ], + FILTER_VALIDATE_BOOLEAN + ); +} + +/** + * Returns the Taxonomy for the specified NLU feature. + * + * Returns defaults in config.php if options have not been configured. + * + * @param string $classify_by NLU feature name. + * @return string + */ +function get_classification_feature_taxonomy( string $classify_by = '' ): string { + $taxonomy = ''; + $settings = ( new Classification() )->get_settings(); + + if ( ! empty( $settings[ $classify_by . '_taxonomy' ] ) ) { + $taxonomy = $settings[ $classify_by . '_taxonomy' ]; + } + + if ( Embeddings::ID === $settings['provider'] ) { + $taxonomy = $classify_by; + } + + if ( empty( $taxonomy ) ) { + $constant = 'WATSON_' . strtoupper( $classify_by ) . '_TAXONOMY'; + + if ( defined( $constant ) ) { + $taxonomy = constant( $constant ); + } + } + + /** + * Filter the Taxonomy for the specified NLU feature. + * + * @since 3.0.0 + * @hook classifai_feature_classification_taxonomy_for_feature + * + * @param {string} $taxonomy The slug of the taxonomy to use. + * @param {string} $classify_by The NLU feature this taxonomy is for. + * + * @return {string} The filtered taxonomy slug. + */ + return apply_filters( 'classifai_feature_classification_taxonomy_for_feature', $taxonomy, $classify_by ); +} + +/** + * Get Classification mode. + * + * @since 2.5.0 + * + * @return string + */ +function get_classification_mode(): string { + $feature = new Classification(); + $settings = $feature->get_settings(); + $value = $settings['classification_mode'] ?? ''; + + if ( $feature->is_feature_enabled() ) { + if ( empty( $value ) ) { + // existing users + // default: automatic_classification + return 'automatic_classification'; + } + } else { + // new users + // default: manual_review + return 'manual_review'; + } + + return $value; +} diff --git a/includes/Classifai/Providers/OpenAI/Embeddings.php b/includes/Classifai/Providers/OpenAI/Embeddings.php index 318c15457..43b650e9b 100644 --- a/includes/Classifai/Providers/OpenAI/Embeddings.php +++ b/includes/Classifai/Providers/OpenAI/Embeddings.php @@ -11,12 +11,9 @@ use Classifai\Providers\OpenAI\EmbeddingCalculations; use Classifai\Normalizer; use Classifai\Features\Classification; +use Classifai\Features\Feature; use WP_Error; -use function Classifai\get_asset_info; -use function Classifai\Providers\Watson\get_supported_post_statuses; -use function Classifai\Providers\Watson\get_supported_post_types; - class Embeddings extends Provider { use \Classifai\Providers\OpenAI\OpenAI; @@ -44,6 +41,13 @@ class Embeddings extends Provider { */ protected $max_tokens = 8191; + /** + * NLU features that are supported by this provider. + * + * @var array + */ + public $nlu_features = []; + /** * OpenAI Embeddings constructor. * @@ -51,6 +55,24 @@ class Embeddings extends Provider { */ public function __construct( $feature_instance = null ) { $this->feature_instance = $feature_instance; + + if ( + $this->feature_instance && + method_exists( $this->feature_instance, 'get_supported_taxonomies' ) + ) { + $settings = get_option( $this->feature_instance->get_option_name(), [] ); + $post_types = isset( $settings['post_types'] ) ? $settings['post_types'] : [ 'post' => 1 ]; + + foreach ( $this->feature_instance->get_supported_taxonomies( $post_types ) as $tax => $label ) { + $this->nlu_features[ $tax ] = [ + 'feature' => $label, + 'threshold' => __( 'Threshold (%)', 'classifai' ), + 'threshold_default' => 75, + 'taxonomy' => __( 'Taxonomy', 'classifai' ), + 'taxonomy_default' => $tax, + ]; + } + } } /** @@ -87,93 +109,7 @@ public function render_provider_fields() { ] ); - add_settings_field( - static::ID . '_number_of_terms', - esc_html__( 'Number of terms', 'classifai' ), - [ $this->feature_instance, 'render_input' ], - $this->feature_instance->get_option_name(), - $this->feature_instance->get_option_name() . '_section', - [ - 'option_index' => static::ID, - 'label_for' => 'number_of_terms', - 'input_type' => 'number', - 'min' => 1, - 'step' => 1, - 'default_values' => $settings['number_of_terms'], - 'description' => esc_html__( 'Maximum number of terms that will get auto-assigned.', 'classifai' ), - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. - ] - ); - - add_settings_field( - static::ID . '_taxonomies', - esc_html__( 'Taxonomies', 'classifai' ), - [ $this, 'render_checkbox_group' ], - $this->feature_instance->get_option_name(), - $this->feature_instance->get_option_name() . '_section', - [ - 'option_index' => static::ID, - 'label_for' => 'taxonomies', - 'options' => $this->get_taxonomies_for_settings(), - 'default_values' => $settings['taxonomies'], - 'description' => __( 'Choose which taxonomies will be used for classification.', 'classifai' ), - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. - ] - ); - do_action( 'classifai_' . static::ID . '_render_provider_fields', $this ); - add_action( 'classifai_after_feature_settings_form', [ $this, 'render_previewer' ] ); - } - - /** - * Renders the previewer window for the feature. - * - * @param string $active_feature The active feature. - */ - public function render_previewer( string $active_feature ) { - $feature = new Classification(); - $provider = $feature->get_feature_provider_instance(); - - if ( - self::ID !== $provider::ID || - $feature::ID !== $active_feature || - ! $feature->is_feature_enabled() - ) { - return; - } - ?> - -
- $supported_post_types, - 'post_status' => $supported_post_statuses, - 'posts_per_page' => 10, - ) - ); - ?> - -

-
- - - -
-
-
-
- - '', - 'number_of_terms' => 1, - 'authenticated' => false, + 'api_key' => '', + 'authenticated' => false, ]; - switch ( $this->feature_instance::ID ) { - case Classification::ID: - return array_merge( - $common_settings, - [ - 'taxonomies' => [ - 'category', - ], - ] - ); - } - return $common_settings; } @@ -209,78 +132,49 @@ public function get_default_provider_settings(): array { * This only fires if can_register returns true. */ public function register() { - $feature = new Classification(); + add_filter( 'classifai_feature_classification_get_default_settings', [ $this, 'modify_default_feature_settings' ], 10, 2 ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + $feature = new Classification(); - if ( ! $feature->is_feature_enabled() || $feature->get_feature_provider_instance()::ID !== static::ID ) { + if ( + ! $feature->is_feature_enabled() || + $feature->get_feature_provider_instance()::ID !== static::ID + ) { return; } - add_action( 'wp_insert_post', [ $this, 'generate_embeddings_for_post' ] ); add_action( 'created_term', [ $this, 'generate_embeddings_for_term' ] ); add_action( 'edited_terms', [ $this, 'generate_embeddings_for_term' ] ); - add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ], 9 ); - add_filter( 'rest_api_init', [ $this, 'add_process_content_meta_to_rest_api' ] ); - add_action( 'add_meta_boxes', [ $this, 'add_metabox' ] ); - add_action( 'save_post', [ $this, 'save_metabox' ] ); add_action( 'wp_ajax_get_post_classifier_embeddings_preview_data', array( $this, 'get_post_classifier_embeddings_preview_data' ) ); } /** - * Enqueue the admin scripts. + * Modify the default settings for the classification feature. + * + * @param array $settings Current settings. + * @param Feature $feature_instance The feature instance. + * @return array */ - public function enqueue_admin_assets() { - wp_enqueue_script( - 'classifai-language-processing-script', - CLASSIFAI_PLUGIN_URL . 'dist/language-processing.js', - get_asset_info( 'language-processing', 'dependencies' ), - get_asset_info( 'language-processing', 'version' ), - true - ); + public function modify_default_feature_settings( array $settings, $feature_instance ): array { + remove_filter( 'classifai_feature_classification_get_default_settings', [ $this, 'modify_default_feature_settings' ], 10, 2 ); - wp_enqueue_style( - 'classifai-language-processing-style', - CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css', - array(), - get_asset_info( 'language-processing', 'version' ), - 'all' - ); - } + if ( $feature_instance->get_settings( 'provider' ) !== static::ID ) { + return $settings; + } - /** - * Enqueue editor assets. - */ - public function enqueue_editor_assets() { - global $post; + add_filter( 'classifai_feature_classification_get_default_settings', [ $this, 'modify_default_feature_settings' ], 10, 2 ); - if ( empty( $post ) ) { - return; - } + $defaults = []; - wp_enqueue_script( - 'classifai-gutenberg-plugin', - CLASSIFAI_PLUGIN_URL . 'dist/gutenberg-plugin.js', - array_merge( get_asset_info( 'gutenberg-plugin', 'dependencies' ), array( 'lodash' ) ), - get_asset_info( 'gutenberg-plugin', 'version' ), - true - ); + foreach ( array_keys( $feature_instance->get_supported_taxonomies() ) as $tax ) { + $enabled = 'category' === $tax ? true : false; - wp_add_inline_script( - 'classifai-gutenberg-plugin', - sprintf( - 'var classifaiEmbeddingData = %s;', - wp_json_encode( - [ - 'enabled' => true, - 'supportedPostTypes' => $this->supported_post_types(), - 'supportedPostStatues' => $this->supported_post_statuses(), - 'noPermissions' => ! is_user_logged_in() || ! current_user_can( 'edit_post', $post->ID ), - ] - ) - ), - 'before' - ); + $defaults[ $tax ] = $enabled; + $defaults[ $tax . '_threshold' ] = 75; + $defaults[ $tax . '_taxonomy' ] = $tax; + } + + return array_merge( $settings, $defaults ); } /** @@ -296,15 +190,11 @@ public function sanitize_settings( array $new_settings ): array { $new_settings[ static::ID ]['api_key'] = $api_key_settings[ static::ID ]['api_key']; $new_settings[ static::ID ]['authenticated'] = $api_key_settings[ static::ID ]['authenticated']; - if ( $this->feature_instance instanceof Classification ) { - // Sanitize the taxonomy checkboxes. - $taxonomies = $this->get_taxonomies_for_settings(); - foreach ( $taxonomies as $taxonomy_key => $taxonomy_value ) { - if ( isset( $new_settings[ static::ID ]['taxonomies'][ $taxonomy_key ] ) && '0' !== $new_settings[ static::ID ]['taxonomies'][ $taxonomy_key ] ) { - $new_settings[ static::ID ]['taxonomies'][ $taxonomy_key ] = sanitize_text_field( $new_settings[ static::ID ]['taxonomies'][ $taxonomy_key ] ?? $settings[ static::ID ]['taxonomies'][ $taxonomy_key ] ); - $this->trigger_taxonomy_update( $taxonomy_key ); - } else { - $new_settings[ static::ID ]['taxonomies'][ $taxonomy_key ] = '0'; + // Trigger embedding generation for all terms in enabled taxonomies if the feature is on. + if ( isset( $new_settings['status'] ) && 1 === (int) $new_settings['status'] ) { + foreach ( array_keys( $this->nlu_features ) as $feature_name ) { + if ( isset( $new_settings[ $feature_name ] ) && 1 === (int) $new_settings[ $feature_name ] ) { + $this->trigger_taxonomy_update( $feature_name ); } } } @@ -312,25 +202,6 @@ public function sanitize_settings( array $new_settings ): array { return $new_settings; } - /** - * The list of supported post types. - * - * @return array - */ - public function supported_post_types(): array { - /** - * Filter post types supported for embeddings. - * - * @since 2.2.0 - * @hook classifai_post_types - * - * @param {array} $post_types Array of post types to be classified. - * - * @return {array} Array of post types. - */ - return apply_filters( 'classifai_openai_embeddings_post_types', $this->get_supported_post_types( new Classification() ) ); - } - /** * Get the threshold for the similarity calculation. * @@ -344,7 +215,7 @@ public function get_threshold( string $taxonomy = '' ): float { $threshold = 1; if ( ! empty( $taxonomy ) ) { - $threshold = isset( $settings['taxonomies'][ $taxonomy . '_threshold' ] ) ? $settings['taxonomies'][ $taxonomy . '_threshold' ] : 75; + $threshold = isset( $settings[ $taxonomy . '_threshold' ] ) ? $settings[ $taxonomy . '_threshold' ] : 75; } // Convert $threshold (%) to decimal. @@ -364,44 +235,6 @@ public function get_threshold( string $taxonomy = '' ): float { return apply_filters( 'classifai_threshold', $threshold, $taxonomy ); } - /** - * The list of supported post statuses. - * - * @return array - */ - public function supported_post_statuses(): array { - /** - * Filter post statuses supported for embeddings. - * - * @since 2.2.0 - * @hook classifai_openai_embeddings_post_statuses - * - * @param {array} $post_types Array of post statuses to be classified. - * - * @return {array} Array of post statuses. - */ - return apply_filters( 'classifai_openai_embeddings_post_statuses', $this->get_supported_post_statuses( new Classification() ) ); - } - - /** - * The list of supported taxonomies. - * - * @return array - */ - public function supported_taxonomies(): array { - /** - * Filter taxonomies supported for embeddings. - * - * @since 2.2.0 - * @hook classifai_openai_embeddings_taxonomies - * - * @param {array} $taxonomies Array of taxonomies to be classified. - * - * @return {array} Array of taxonomies. - */ - return apply_filters( 'classifai_openai_embeddings_taxonomies', $this->get_supported_taxonomies( new Classification() ) ); - } - /** * Get the data to preview terms. * @@ -418,7 +251,13 @@ public function get_post_classifier_embeddings_preview_data(): array { $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT ); - $embeddings_terms = $this->generate_embeddings_for_post( $post_id, true ); + $embeddings = $this->generate_embeddings( $post_id, 'post' ); + $embeddings_terms = []; + + // Add terms to this item based on embedding data. + if ( $embeddings && ! is_wp_error( $embeddings ) ) { + $embeddings_terms = $this->get_terms( $embeddings ); + } return wp_send_json_success( $embeddings_terms ); } @@ -426,50 +265,47 @@ public function get_post_classifier_embeddings_preview_data(): array { /** * Trigger embedding generation for content being saved. * - * @param int $post_id ID of post being saved. - * @param bool $dryrun Whether to run the process or just return the data. + * @param int $post_id ID of post being saved. * @return array|WP_Error */ - public function generate_embeddings_for_post( int $post_id, bool $dryrun = false ) { + public function generate_embeddings_for_post( int $post_id ) { // Don't run on autosaves. if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { - return; + return new WP_Error( 'invalid', esc_html__( 'Classification will not work during an autosave.', 'classifai' ) ); } // Ensure the user has permissions to edit. if ( ! current_user_can( 'edit_post', $post_id ) && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) { - return; + return new WP_Error( 'invalid', esc_html__( 'User does not have permission to classify this item.', 'classifai' ) ); } - $post = get_post( $post_id ); - - // Only run on supported post types and statuses. - if ( - ! $dryrun - && ( - ! in_array( $post->post_type, $this->supported_post_types(), true ) || - ! in_array( $post->post_status, $this->supported_post_statuses(), true ) - ) - ) { - return; - } - - // Don't run if turned off for this particular post. - if ( 'no' === get_post_meta( $post_id, '_classifai_process_content', true ) && ! $dryrun ) { - return; + /** + * Filter whether ClassifAI should classify a post. + * + * Default is true, return false to skip classifying a post. + * + * @since 1.2.0 + * @hook classifai_should_classify_post + * + * @param {bool} $should_classify Whether the post should be classified. Default `true`, return `false` to skip + * classification for this post. + * @param {int} $post_id The ID of the post to be considered for classification. + * + * @return {bool} Whether the post should be classified. + */ + $should_classify = apply_filters( 'classifai_should_classify_post', true, $post_id ); + if ( ! $should_classify ) { + return new WP_Error( 'invalid', esc_html__( 'Classification is disabled for this item.', 'classifai' ) ); } $embeddings = $this->generate_embeddings( $post_id, 'post' ); // Add terms to this item based on embedding data. if ( $embeddings && ! is_wp_error( $embeddings ) ) { - if ( $dryrun ) { - return $this->get_terms( $embeddings ); - } else { - update_post_meta( $post_id, 'classifai_openai_embeddings', array_map( 'sanitize_text_field', $embeddings ) ); - return $this->set_terms( $post_id, $embeddings ); - } + update_post_meta( $post_id, 'classifai_openai_embeddings', array_map( 'sanitize_text_field', $embeddings ) ); } + + return $embeddings; } /** @@ -477,8 +313,10 @@ public function generate_embeddings_for_post( int $post_id, bool $dryrun = false * * @param int $post_id ID of post to set terms on. * @param array $embedding Embedding data. + * @param bool $link Whether to link the terms or not. + * @return array|WP_Error */ - private function set_terms( int $post_id = 0, array $embedding = [] ) { + public function set_terms( int $post_id = 0, array $embedding = [], bool $link = true ) { if ( ! $post_id || ! get_post( $post_id ) ) { return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to set terms.', 'classifai' ) ); } @@ -487,26 +325,40 @@ private function set_terms( int $post_id = 0, array $embedding = [] ) { return new WP_Error( 'data_required', esc_html__( 'Valid embedding data is required to set terms.', 'classifai' ) ); } - $settings = ( new Classification() )->get_settings(); - $number_to_add = $settings['number_of_terms'] ?? 1; $embedding_similarity = $this->get_embeddings_similarity( $embedding ); if ( empty( $embedding_similarity ) ) { - return; + return new WP_Error( 'invalid', esc_html__( 'No matching terms found.', 'classifai' ) ); } - // Set terms based on similarity. + $return = []; + + /** + * If $link is true, immediately link all the terms + * to the item. + * + * If it is false, build an array of term data that + * can be used to display the terms in the UI. + */ foreach ( $embedding_similarity as $tax => $terms ) { - // Sort embeddings from lowest to highest. - asort( $terms ); + if ( $link ) { + wp_set_object_terms( $post_id, array_map( 'absint', array_keys( $terms ) ), $tax, false ); + } else { + $terms_to_link = []; - // Only add the number of terms specified in settings. - if ( count( $terms ) > $number_to_add ) { - $terms = array_slice( $terms, 0, $number_to_add, true ); - } + foreach ( array_keys( $terms ) as $term_id ) { + $term = get_term( $term_id ); + + if ( $term && ! is_wp_error( $term ) ) { + $terms_to_link[ $term->name ] = $term_id; + } + } - wp_set_object_terms( $post_id, array_map( 'absint', array_keys( $terms ) ), $tax, false ); + $return[ $tax ] = $terms_to_link; + } } + + return empty( $return ) ? $embedding_similarity : $return; } /** @@ -515,20 +367,18 @@ private function set_terms( int $post_id = 0, array $embedding = [] ) { * @param array $embedding Embedding data. * @return array|WP_Error */ - private function get_terms( array $embedding = [] ) { + public function get_terms( array $embedding = [] ) { if ( empty( $embedding ) ) { return new WP_Error( 'data_required', esc_html__( 'Valid embedding data is required to get terms.', 'classifai' ) ); } - $settings = ( new Classification() )->get_settings(); - $number_to_add = $settings[ static::ID ]['number_of_terms'] ?? 1; $embedding_similarity = $this->get_embeddings_similarity( $embedding, false ); if ( empty( $embedding_similarity ) ) { - return; + return new WP_Error( 'invalid', esc_html__( 'No matching terms found.', 'classifai' ) ); } - // Set terms based on similarity. + // Sort terms based on similarity. $index = 0; $result = []; @@ -547,11 +397,6 @@ private function get_terms( array $embedding = [] ) { $term_added = 0; foreach ( $terms as $term_id => $similarity ) { - // Stop if we have added the number of terms specified in settings. - if ( $number_to_add <= $term_added ) { - break; - } - // Convert $similarity to percentage. $similarity = round( ( 1 - $similarity ), 10 ); @@ -562,11 +407,6 @@ private function get_terms( array $embedding = [] ) { ++$term_added; } - // Only add the number of terms specified in settings. - if ( count( $terms ) > $number_to_add ) { - $terms = array_slice( $terms, 0, $number_to_add, true ); - } - ++$index; } @@ -583,22 +423,40 @@ private function get_terms( array $embedding = [] ) { * @return array */ private function get_embeddings_similarity( array $embedding, bool $consider_threshold = true ): array { + $feature = new Classification(); $embedding_similarity = []; - $taxonomies = $this->supported_taxonomies(); + $taxonomies = $feature->get_all_feature_taxonomies(); $calculations = new EmbeddingCalculations(); foreach ( $taxonomies as $tax ) { + $exclude = []; + if ( is_numeric( $tax ) ) { continue; } + if ( 'tags' === $tax ) { + $tax = 'post_tag'; + } + + if ( 'categories' === $tax ) { + $tax = 'category'; + + // Exclude the uncategorized term. + $uncat_term = get_term_by( 'name', 'Uncategorized', 'category' ); + if ( $uncat_term ) { + $exclude = [ $uncat_term->term_id ]; + } + } + $terms = get_terms( [ 'taxonomy' => $tax, 'hide_empty' => false, 'fields' => 'ids', 'meta_key' => 'classifai_openai_embeddings', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - // 'number' => 500, TODO: see if we need a limit here. + 'number' => 500, + 'exclude' => $exclude, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude ] ); @@ -642,7 +500,7 @@ private function trigger_taxonomy_update( string $taxonomy = '' ) { 'fields' => 'ids', 'meta_key' => 'classifai_openai_embeddings', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_compare' => 'NOT EXISTS', - // 'number' => 500, TODO: see if we need a limit here. + 'number' => 500, ] ); @@ -673,7 +531,16 @@ public function generate_embeddings_for_term( int $term_id ) { return; } - $taxonomies = $this->supported_taxonomies(); + $feature = new Classification(); + $taxonomies = $feature->get_all_feature_taxonomies(); + + if ( in_array( 'tags', $taxonomies, true ) ) { + $taxonomies[] = 'post_tag'; + } + + if ( in_array( 'categories', $taxonomies, true ) ) { + $taxonomies[] = 'category'; + } // Ensure this term is part of a taxonomy we support. if ( ! in_array( $term->taxonomy, $taxonomies, true ) ) { @@ -823,208 +690,29 @@ public function get_content( int $id = 0, string $type = 'post' ): string { } /** - * Add `classifai_process_content` to the REST API for view/edit. - */ - public function add_process_content_meta_to_rest_api() { - $supported_post_types = $this->supported_post_types( new Classification() ); - - register_rest_field( - $supported_post_types, - 'classifai_process_content', - [ - 'get_callback' => function ( $data ) { - $process_content = get_post_meta( $data['id'], '_classifai_process_content', true ); - return ( 'no' === $process_content ) ? 'no' : 'yes'; - }, - 'update_callback' => function ( $value, $data ) { - $value = ( 'no' === $value ) ? 'no' : 'yes'; - return update_post_meta( $data->ID, '_classifai_process_content', $value ); - }, - 'schema' => [ - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - ], - ] - ); - } - - /** - * Add metabox. - * - * @param string $post_type Post type name. - */ - public function add_metabox( string $post_type ) { - if ( ! in_array( $post_type, $this->get_supported_post_types( new Classification() ), true ) ) { - return; - } - - \add_meta_box( - 'classifai_language_processing_metabox', - __( 'ClassifAI Language Processing', 'classifai' ), - array( $this, 'render_metabox' ), - null, - 'side', - 'default', - [ - '__back_compat_meta_box' => true, - ] - ); - } - - /** - * Render metabox. - * - * @param \WP_Post $post A WordPress post instance. - */ - public function render_metabox( \WP_Post $post ) { - - $classifai_process_content = get_post_meta( $post->ID, '_classifai_process_content', true ); - $checked = 'no' === $classifai_process_content ? '' : 'checked="checked"'; - - // Add nonce. - wp_nonce_field( 'classifai_language_processing_meta_action', 'classifai_language_processing_meta' ); - wp_nonce_field( 'classifai_embeddings_save_posts', '_nonce' ); - ?> -
-

- -

-
- feature_instance->get_settings( static::ID ); - $options = $args['options'] ?? []; - $option_index = $args['option_index']; - - if ( ! is_array( $options ) ) { - return; + public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) { + if ( ! $post_id || ! get_post( $post_id ) ) { + return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to run classification.', 'classifai' ) ); } - // Iterate through all of our options. - foreach ( $options as $option_value => $option_label ) { - $value = ''; - $default_key = array_search( $option_value, $args['default_values'], true ); - $option_value_theshold = $option_value . '_threshold'; + $route_to_call = strtolower( $route_to_call ); + $return = ''; - // Get saved value, if any. - if ( isset( $setting_index[ $args['label_for'] ] ) ) { - $value = $setting_index[ $args['label_for'] ][ $option_value ] ?? ''; - $threshold_value = $setting_index[ $args['label_for'] ][ $option_value_theshold ] ?? ''; - } - - // If no saved value, check if we have a default value. - if ( empty( $value ) && '0' !== $value && isset( $args['default_values'][ $default_key ] ) ) { - $value = $args['default_values'][ $default_key ]; - } - - // Render checkbox. - printf( - '

- -

', - esc_attr( $this->feature_instance->get_option_name() ), - esc_attr( $option_index ), - esc_attr( $args['label_for'] ?? '' ), - esc_attr( $option_value ), - checked( $value, $option_value, false ), - esc_html( $option_label ) - ); - - // Render Threshold field. - if ( 'taxonomies' === $args['label_for'] ) { - $this->render_threshold_field( $args, $option_value_theshold, $threshold_value ); - } + // Handle all of our routes. + switch ( $route_to_call ) { + case 'classify': + $return = $this->generate_embeddings_for_post( $post_id ); + break; } - // Render description, if any. - if ( ! empty( $args['description'] ) ) { - printf( - '%s', - esc_html( $args['description'] ) - ); - } - } - - /** - * Render a threshold field. - * - * @since 2.5.0 - * - * @param array $args The args passed to add_settings_field - * @param string $option_value The option value. - * @param string $value The value. - */ - public function render_threshold_field( array $args, string $option_value, string $value ) { - printf( - '

- -
- -

', - esc_attr( $this->feature_instance->get_option_name() ), - esc_attr( $args['option_index'] ), - esc_attr( $args['label_for'] ?? '' ), - esc_attr( $option_value ), - esc_html__( 'Threshold (%)', 'classifai' ), - $value ? esc_attr( $value ) : 75 - ); + return $return; } /** @@ -1033,21 +721,16 @@ public function render_threshold_field( array $args, string $option_value, strin * @return array */ public function get_debug_information(): array { - $settings = $this->feature_instance->get_settings(); - $provider_settings = $settings[ static::ID ]; - $debug_info = []; + $settings = $this->feature_instance->get_settings(); + $debug_info = []; if ( $this->feature_instance instanceof Classification ) { - $debug_info[ __( 'Number of terms', 'classifai' ) ] = $provider_settings['number_of_terms'] ?? 1; - $debug_info[ __( 'Taxonomy (category)', 'classifai' ) ] = $provider_settings['taxonomies']['category'] ? __( 'Enabled', 'classifai' ) : __( 'Disabled', 'classifai' ); - $debug_info[ __( 'Taxonomy (category threshold)', 'classifai' ) ] = $provider_settings['taxonomies']['category_threshold']; - $debug_info[ __( 'Taxonomy (tag)', 'classifai' ) ] = $provider_settings['taxonomies']['post_tag'] ? __( 'Enabled', 'classifai' ) : __( 'Disabled', 'classifai' ); - $debug_info[ __( 'Taxonomy (tag threshold)', 'classifai' ) ] = $provider_settings['taxonomies']['post_tag_threshold']; - $debug_info[ __( 'Taxonomy (format)', 'classifai' ) ] = $provider_settings['taxonomies']['post_format'] ? __( 'Enabled', 'classifai' ) : __( 'Disabled', 'classifai' ); - $debug_info[ __( 'Taxonomy (format threshold)', 'classifai' ) ] = $provider_settings['taxonomies']['post_format_threshold']; - $debug_info[ __( 'Taxonomy (image tag)', 'classifai' ) ] = $provider_settings['taxonomies']['classifai-image-tags'] ? __( 'Enabled', 'classifai' ) : __( 'Disabled', 'classifai' ); - $debug_info[ __( 'Taxonomy (image tag threshold)', 'classifai' ) ] = $provider_settings['taxonomies']['classifai-image-tags_threshold']; - $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_embeddings_latest_response' ) ); + foreach ( array_keys( $this->feature_instance->get_supported_taxonomies() ) as $tax ) { + $debug_info[ "Taxonomy ($tax)" ] = Feature::get_debug_value_text( $settings[ $tax ], 1 ); + $debug_info[ "Taxonomy ($tax threshold)" ] = Feature::get_debug_value_text( $settings[ $tax . '_threshold' ], 1 ); + } + + $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_embeddings_latest_response' ) ); } return apply_filters( diff --git a/includes/Classifai/Providers/OpenAI/OpenAI.php b/includes/Classifai/Providers/OpenAI/OpenAI.php index 26d5dd104..992e621e5 100644 --- a/includes/Classifai/Providers/OpenAI/OpenAI.php +++ b/includes/Classifai/Providers/OpenAI/OpenAI.php @@ -156,48 +156,6 @@ public function get_taxonomies_for_settings(): array { return apply_filters( 'classifai_openai_settings_taxonomies', $supported, $this ); } - /** - * The list of supported post types. - * - * @param \Classifai\Features\Feature $feature Feature to check. - * @return array - */ - public function get_supported_post_types( \Classifai\Features\Feature $feature ): array { - $settings = $feature->get_settings(); - $post_types = []; - - if ( ! empty( $settings ) && isset( $settings['post_types'] ) ) { - foreach ( $settings['post_types'] as $post_type => $enabled ) { - if ( ! empty( $enabled ) ) { - $post_types[] = $post_type; - } - } - } - - return $post_types; - } - - /** - * The list of supported post statuses. - * - * @param \Classifai\Features\Feature $feature Feature to check - * @return array - */ - public function get_supported_post_statuses( \Classifai\Features\Feature $feature ): array { - $settings = $feature->get_settings(); - $post_statuses = []; - - if ( ! empty( $settings ) && isset( $settings['post_statuses'] ) ) { - foreach ( $settings['post_statuses'] as $post_status => $enabled ) { - if ( ! empty( $enabled ) ) { - $post_statuses[] = $post_status; - } - } - } - - return $post_statuses; - } - /** * The list of supported taxonomies. * diff --git a/includes/Classifai/Providers/Watson/Helpers.php b/includes/Classifai/Providers/Watson/Helpers.php index 075f2afc6..ed3542e69 100644 --- a/includes/Classifai/Providers/Watson/Helpers.php +++ b/includes/Classifai/Providers/Watson/Helpers.php @@ -78,120 +78,11 @@ function get_password(): string { * @return string */ function get_classification_method(): string { - $settings = ( new Classification() )->get_settings( NLU::ID ); + $settings = ( new Classification() )->get_settings(); return $settings['classification_method'] ?? ''; } -/** - * Get Classification mode. - * - * @since 2.5.0 - * - * @return string - */ -function get_classification_mode(): string { - $feature = new Classification(); - $settings = $feature->get_settings( NLU::ID ); - $value = $settings['classification_mode'] ?? ''; - - if ( $feature->is_feature_enabled() ) { - if ( empty( $value ) ) { - // existing users - // default: automatic_classification - return 'automatic_classification'; - } - } else { - // new users - // default: manual_review - return 'manual_review'; - } - - return $value; -} - -/** - * The list of post types that support classification. - * - * Defaults to 'post'. - * - * @return array - */ -function get_supported_post_types(): array { - $feature = new Classification(); - $settings = $feature->get_settings(); - $post_types = []; - - foreach ( $settings['post_types'] as $post_type => $enabled ) { - if ( ! empty( $enabled ) ) { - $post_types[] = $post_type; - } - } - - /** - * Filter post types supported for classification. - * - * @since 1.0.0 - * @hook classifai_post_types - * - * @param {array} $post_types Array of post types to be classified. - * - * @return {array} Array of post types. - */ - $post_types = apply_filters( 'classifai_post_types', $post_types ); - - return $post_types; -} - -/** - * The list of post statuses that support classification. - * - * Defaults to 'publish'. - * - * @return array - */ -function get_supported_post_statuses(): array { - $feature = new Classification(); - $settings = $feature->get_settings(); - $post_statuses = []; - - foreach ( $settings['post_statuses'] as $post_status => $enabled ) { - if ( ! empty( $enabled ) ) { - $post_statuses[] = $post_status; - } - } - - /** - * Filter post statuses supported for classification. - * - * @since 1.7.1 - * @hook classifai_post_statuses - * - * @param {array} $post_types Array of post statuses to be classified. - * - * @return {array} Array of post statuses. - */ - $post_statuses = apply_filters( 'classifai_post_statuses', $post_statuses ); - - return $post_statuses; -} - -/** - * Returns a bool based on whether the specified feature is enabled - * - * @param string $classify_by category,keyword,entity,concept - * @return bool - */ -function get_feature_enabled( string $classify_by ): bool { - $feature = new Classification(); - $settings = $feature->get_settings( NLU::ID ); - - return filter_var( - $settings[ $classify_by ], - FILTER_VALIDATE_BOOLEAN - ); -} - /** * Returns the feature threshold based on current configuration. Lookup * order is. @@ -206,7 +97,7 @@ function get_feature_enabled( string $classify_by ): bool { */ function get_feature_threshold( string $feature ): float { $classification_feature = new Classification(); - $settings = $classification_feature->get_settings( NLU::ID ); + $settings = $classification_feature->get_settings(); $threshold = 0; if ( ! empty( $settings ) && ! empty( $settings[ $feature . '_threshold' ] ) ) { @@ -240,43 +131,3 @@ function get_feature_threshold( string $feature ): float { */ return apply_filters( 'classifai_feature_threshold', $threshold, $feature ); } - -/** - * Returns the Taxonomy for the specified NLU feature. - * - * Returns defaults in config.php if options have not been configured. - * - * @param string $classify_by NLU feature name - * @return string Taxonomy mapped to the feature - */ -function get_feature_taxonomy( string $classify_by = '' ): string { - $taxonomy = 0; - - $feature = new Classification(); - $settings = $feature->get_settings( NLU::ID ); - - if ( ! empty( $settings[ $classify_by . '_taxonomy' ] ) ) { - $taxonomy = $settings[ $classify_by . '_taxonomy' ]; - } - - if ( empty( $taxonomy ) ) { - $constant = 'WATSON_' . strtoupper( $classify_by ) . '_TAXONOMY'; - - if ( defined( $constant ) ) { - $taxonomy = constant( $constant ); - } - } - - /** - * Filter the Taxonomy for the specified NLU feature. - * - * @since 1.1.0 - * @hook classifai_taxonomy_for_feature - * - * @param {string} $taxonomy The slug of the taxonomy to use. - * @param {string} $classify_by The NLU feature this taxonomy is for. - * - * @return {string} The filtered taxonomy slug. - */ - return apply_filters( 'classifai_taxonomy_for_feature', $taxonomy, $classify_by ); -} diff --git a/includes/Classifai/Providers/Watson/Linker.php b/includes/Classifai/Providers/Watson/Linker.php index 533361eda..30bcdbf26 100644 --- a/includes/Classifai/Providers/Watson/Linker.php +++ b/includes/Classifai/Providers/Watson/Linker.php @@ -2,6 +2,8 @@ namespace Classifai\Providers\Watson; +use function Classifai\get_classification_feature_taxonomy; + /** * Linker connects Watson classification results with Taxonomy Terms. * @@ -92,7 +94,7 @@ public function link( int $post_id, array $output, array $options = [], bool $li */ public function link_categories( int $post_id, array $categories, bool $link_categories = true ) { $terms_to_link = []; - $taxonomy = get_feature_taxonomy( 'category' ); + $taxonomy = get_classification_feature_taxonomy( 'category' ); $classify_existing_terms = 'existing_terms' === get_classification_method(); foreach ( $categories as $category ) { @@ -160,7 +162,7 @@ public function link_categories( int $post_id, array $categories, bool $link_cat */ public function link_keywords( int $post_id, array $keywords, bool $link_keywords = true ) { $terms_to_link = []; - $taxonomy = get_feature_taxonomy( 'keyword' ); + $taxonomy = get_classification_feature_taxonomy( 'keyword' ); $classify_existing_terms = 'existing_terms' === get_classification_method(); foreach ( $keywords as $keyword ) { @@ -219,7 +221,7 @@ public function link_keywords( int $post_id, array $keywords, bool $link_keyword */ public function link_concepts( int $post_id, array $concepts, bool $link_concepts = true ) { $terms_to_link = []; - $taxonomy = get_feature_taxonomy( 'concept' ); + $taxonomy = get_classification_feature_taxonomy( 'concept' ); $classify_existing_terms = 'existing_terms' === get_classification_method(); foreach ( $concepts as $concept ) { @@ -285,7 +287,7 @@ public function link_concepts( int $post_id, array $concepts, bool $link_concept */ public function link_entities( int $post_id, array $entities, bool $link_entities = true ) { $terms_to_link = []; - $taxonomy = get_feature_taxonomy( 'entity' ); + $taxonomy = get_classification_feature_taxonomy( 'entity' ); $classify_existing_terms = 'existing_terms' === get_classification_method(); foreach ( $entities as $entity ) { diff --git a/includes/Classifai/Providers/Watson/NLU.php b/includes/Classifai/Providers/Watson/NLU.php index 1a69db4eb..251c2337f 100644 --- a/includes/Classifai/Providers/Watson/NLU.php +++ b/includes/Classifai/Providers/Watson/NLU.php @@ -11,11 +11,8 @@ use Classifai\Features\Feature; use Classifai\Providers\Watson\PostClassifier; use WP_Error; -use WP_REST_Request; -use WP_REST_Server; -use function Classifai\get_asset_info; -use function Classifai\check_term_permissions; +use function Classifai\get_classification_feature_taxonomy; class NLU extends Provider { @@ -27,14 +24,11 @@ class NLU extends Provider { public $taxonomy_factory; /** - * @var $save_post_handler SavePostHandler Triggers a classification with Watson - */ - public $save_post_handler; - - /** - * @var $nlu_features array The list of NLU features + * NLU features that are supported by this provider + * + * @var array */ - protected $nlu_features = []; + public $nlu_features = []; /** * Watson NLU constructor. @@ -42,6 +36,8 @@ class NLU extends Provider { * @param \Classifai\Features\Feature $feature Feature instance (Optional, only required in admin). */ public function __construct( $feature = null ) { + $this->feature_instance = $feature; + $this->nlu_features = [ 'category' => [ 'feature' => __( 'Category', 'classifai' ), @@ -72,10 +68,6 @@ public function __construct( $feature = null ) { 'taxonomy_default' => WATSON_CONCEPT_TAXONOMY, ], ]; - - $this->feature_instance = $feature; - - add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); } /** @@ -165,144 +157,45 @@ function ( $args = [] ) { ] ); - add_settings_field( - static::ID . '_classification_mode', - esc_html__( 'Classification mode', 'classifai' ), - [ $this->feature_instance, 'render_radio_group' ], - $this->feature_instance->get_option_name(), - $this->feature_instance->get_option_name() . '_section', - [ - 'option_index' => static::ID, - 'label_for' => 'classification_mode', - 'default_value' => $settings['classification_mode'], - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. - 'options' => array( - 'manual_review' => __( 'Manual review', 'classifai' ), - 'automatic_classification' => __( 'Automatic classification', 'classifai' ), - ), - ] - ); - - add_settings_field( - static::ID . '_classification_method', - esc_html__( 'Classification method', 'classifai' ), - [ $this->feature_instance, 'render_radio_group' ], - $this->feature_instance->get_option_name(), - $this->feature_instance->get_option_name() . '_section', - [ - 'option_index' => static::ID, - 'label_for' => 'classification_method', - 'default_value' => $settings['classification_method'], - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. - 'options' => array( - 'recommended_terms' => __( 'Recommend terms even if they do not exist on the site', 'classifai' ), - 'existing_terms' => __( 'Only recommend terms that already exist on the site', 'classifai' ), - ), - ] - ); - - foreach ( $this->nlu_features as $classify_by => $labels ) { - add_settings_field( - static::ID . '_' . $classify_by, - esc_html( $labels['feature'] ), - [ $this, 'render_nlu_feature_settings' ], - $this->feature_instance->get_option_name(), - $this->feature_instance->get_option_name() . '_section', - [ - 'option_index' => static::ID, - 'feature' => $classify_by, - 'labels' => $labels, - 'default_value' => $settings[ $classify_by ], - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. - ] - ); - } - do_action( 'classifai_' . static::ID . '_render_provider_fields', $this ); - add_action( 'classifai_after_feature_settings_form', [ $this, 'render_previewer' ] ); } /** - * Renders the previewer window for the feature. + * Modify the default settings for the classification feature. * - * @param string $active_feature The active feature. + * @param array $settings Current settings. + * @param Feature $feature_instance The feature instance. + * @return array */ - public function render_previewer( string $active_feature ) { - $feature = new Classification(); - $provider = $feature->get_feature_provider_instance(); + public function modify_default_feature_settings( array $settings, $feature_instance ): array { + remove_filter( 'classifai_feature_classification_get_default_settings', [ $this, 'modify_default_feature_settings' ], 10, 2 ); - if ( - self::ID !== $provider::ID || - $feature::ID !== $active_feature || - ! $feature->is_feature_enabled() - ) { - return; + if ( $feature_instance->get_settings( 'provider' ) !== static::ID ) { + return $settings; } - ?> - -
- $supported_post_types, - 'post_status' => $supported_post_statuses, - 'posts_per_page' => 10, - ) - ); - $features = array( - 'category' => array( - 'name' => esc_html__( 'Category', 'classifai' ), - 'enabled' => get_feature_enabled( 'category' ), - 'plural' => 'categories', - ), - 'keyword' => array( - 'name' => esc_html__( 'Keyword', 'classifai' ), - 'enabled' => get_feature_enabled( 'keyword' ), - 'plural' => 'keywords', - ), - 'entity' => array( - 'name' => esc_html__( 'Entity', 'classifai' ), - 'enabled' => get_feature_enabled( 'entity' ), - 'plural' => 'entities', - ), - 'concept' => array( - 'name' => esc_html__( 'Concept', 'classifai' ), - 'enabled' => get_feature_enabled( 'concept' ), - 'plural' => 'concepts', - ), - ); - ?> - -

-
- - - -
-
- $feature ) : - ?> -
-
-
- -
-
- - true, + 'category_threshold' => WATSON_CATEGORY_THRESHOLD, + 'category_taxonomy' => WATSON_CATEGORY_TAXONOMY, + + 'keyword' => true, + 'keyword_threshold' => WATSON_KEYWORD_THRESHOLD, + 'keyword_taxonomy' => WATSON_KEYWORD_TAXONOMY, + + 'concept' => false, + 'concept_threshold' => WATSON_CONCEPT_THRESHOLD, + 'concept_taxonomy' => WATSON_CONCEPT_TAXONOMY, + + 'entity' => false, + 'entity_threshold' => WATSON_ENTITY_THRESHOLD, + 'entity_taxonomy' => WATSON_ENTITY_TAXONOMY, + ] + ); } /** @@ -312,38 +205,12 @@ public function render_previewer( string $active_feature ) { */ public function get_default_provider_settings(): array { $common_settings = [ - 'endpoint_url' => '', - 'apikey' => '', - 'username' => '', - 'password' => '', - 'classification_mode' => 'manual_review', - 'classification_method' => 'recommended_terms', + 'endpoint_url' => '', + 'apikey' => '', + 'username' => '', + 'password' => '', ]; - switch ( $this->feature_instance::ID ) { - case Classification::ID: - return array_merge( - $common_settings, - [ - 'category' => true, - 'category_threshold' => WATSON_CATEGORY_THRESHOLD, - 'category_taxonomy' => WATSON_CATEGORY_TAXONOMY, - - 'keyword' => true, - 'keyword_threshold' => WATSON_KEYWORD_THRESHOLD, - 'keyword_taxonomy' => WATSON_KEYWORD_TAXONOMY, - - 'concept' => false, - 'concept_threshold' => WATSON_CONCEPT_THRESHOLD, - 'concept_taxonomy' => WATSON_CONCEPT_TAXONOMY, - - 'entity' => false, - 'entity_threshold' => WATSON_ENTITY_THRESHOLD, - 'entity_taxonomy' => WATSON_ENTITY_TAXONOMY, - ] - ); - } - return $common_settings; } @@ -351,85 +218,124 @@ public function get_default_provider_settings(): array { * Register what we need for the plugin. */ public function register() { - $feature = new Classification(); + add_filter( 'classifai_feature_classification_get_default_settings', [ $this, 'modify_default_feature_settings' ], 10, 2 ); - if ( $feature->is_feature_enabled() && $feature->get_feature_provider_instance()::ID === static::ID ) { - add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); - - // Add classifai meta box to classic editor. - add_action( 'add_meta_boxes', [ $this, 'add_classifai_meta_box' ], 10, 2 ); - add_action( 'save_post', [ $this, 'classifai_save_post_metadata' ], 5 ); + $feature = new Classification(); - add_filter( 'rest_api_init', [ $this, 'add_process_content_meta_to_rest_api' ] ); + if ( + $feature->is_feature_enabled() && + $feature->get_feature_provider_instance()::ID === static::ID + ) { $this->taxonomy_factory = new TaxonomyFactory(); $this->taxonomy_factory->build_all(); - $this->save_post_handler = new SavePostHandler(); - $this->save_post_handler->register(); - - new PreviewClassifierData(); + add_action( 'wp_ajax_get_post_classifier_preview_data', array( $this, 'get_post_classifier_preview_data' ) ); } } /** - * Enqueue the editor scripts. + * Returns classifier data for previewing. */ - public function enqueue_editor_assets() { - global $post; - wp_enqueue_script( - 'classifai-editor', - CLASSIFAI_PLUGIN_URL . 'dist/editor.js', - get_asset_info( 'editor', 'dependencies' ), - get_asset_info( 'editor', 'version' ), - true - ); + public function get_post_classifier_preview_data() { + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : false; - if ( empty( $post ) ) { - return; + if ( ! $nonce || ! wp_verify_nonce( $nonce, 'classifai-previewer-action' ) ) { + wp_send_json_error( esc_html__( 'Failed nonce check.', 'classifai' ) ); } - wp_enqueue_script( - 'classifai-gutenberg-plugin', - CLASSIFAI_PLUGIN_URL . 'dist/gutenberg-plugin.js', - array_merge( get_asset_info( 'gutenberg-plugin', 'dependencies' ), array( 'lodash' ) ), - get_asset_info( 'gutenberg-plugin', 'dependencies' ), - get_asset_info( 'gutenberg-plugin', 'version' ), - true - ); + $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT ); + $classifier = new Classifier(); + $normalizer = new \Classifai\Normalizer(); - wp_localize_script( - 'classifai-gutenberg-plugin', - 'classifaiPostData', - [ - 'NLUEnabled' => ( new Classification() )->is_feature_enabled(), - 'supportedPostTypes' => get_supported_post_types(), - 'supportedPostStatues' => get_supported_post_statuses(), - 'noPermissions' => ! is_user_logged_in() || ! current_user_can( 'edit_post', $post->ID ), - ] - ); + $text_to_classify = $normalizer->normalize( $post_id ); + $body = $classifier->get_body( $text_to_classify ); + $request_options['body'] = $body; + $request = $classifier->get_request(); + + $classified_data = $request->post( $classifier->get_endpoint(), $request_options ); + $classified_data = $this->filter_classify_preview_data( $classified_data ); + + wp_send_json_success( $classified_data ); } /** - * Enqueue the admin scripts. + * Filter classifier preview based on the feature settings. + * + * @param array $classified_data The classified data. + * @return array */ - public function enqueue_admin_assets() { - wp_enqueue_script( - 'classifai-language-processing-script', - CLASSIFAI_PLUGIN_URL . 'dist/language-processing.js', - get_asset_info( 'language-processing', 'dependencies' ), - get_asset_info( 'language-processing', 'version' ), - true - ); + public function filter_classify_preview_data( array $classified_data ): array { + if ( is_wp_error( $classified_data ) ) { + return $classified_data; + } - wp_enqueue_style( - 'classifai-language-processing-style', - CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css', - array(), - get_asset_info( 'language-processing', 'version' ), - 'all' - ); + $classify_existing_terms = 'existing_terms' === get_classification_method(); + if ( ! $classify_existing_terms ) { + return $classified_data; + } + + $features = [ + 'category' => 'categories', + 'concept' => 'concepts', + 'entity' => 'entities', + 'keyword' => 'keywords', + ]; + foreach ( $features as $key => $feature ) { + $taxonomy = get_classification_feature_taxonomy( $key ); + if ( ! $taxonomy ) { + continue; + } + + if ( ! isset( $classified_data[ $feature ] ) || empty( $classified_data[ $feature ] ) ) { + continue; + } + + // Handle categories feature. + if ( 'categories' === $feature ) { + $classified_data[ $feature ] = array_filter( + $classified_data[ $feature ], + function ( $item ) use ( $taxonomy ) { + $keep = false; + $parts = explode( '/', $item['label'] ); + $parts = array_filter( $parts ); + if ( ! empty( $parts ) ) { + foreach ( $parts as $part ) { + $term = get_term_by( 'name', $part, $taxonomy ); + if ( ! empty( $term ) ) { + $keep = true; + break; + } + } + } + return $keep; + } + ); + // Reset array keys. + $classified_data[ $feature ] = array_values( $classified_data[ $feature ] ); + continue; + } + + $classified_data[ $feature ] = array_filter( + $classified_data[ $feature ], + function ( $item ) use ( $taxonomy, $key ) { + $name = $item['text']; + if ( 'keyword' === $key ) { + $name = preg_replace( '#^[a-z]+ ([A-Z].*)$#', '$1', $name ); + } elseif ( 'entity' === $key ) { + if ( ! empty( $item['disambiguation'] ) && ! empty( $item['disambiguation']['name'] ) ) { + $name = $item['disambiguation']['name']; + } + } + $term = get_term_by( 'name', $name, $taxonomy ); + return ! empty( $term ); + } + ); + // Reset array keys. + $classified_data[ $feature ] = array_values( $classified_data[ $feature ] ); + } + + return $classified_data; } /** @@ -448,80 +354,6 @@ protected function use_username_password(): bool { return 'apikey' === $settings['username']; } - /** - * Render the NLU features settings. - * - * @param array $args Settings for the inputs - */ - public function render_nlu_feature_settings( array $args ) { - $feature = $args['feature']; - $labels = $args['labels']; - $option_index = $args['option_index']; - - $taxonomies = $this->get_supported_taxonomies(); - $features = $this->feature_instance->get_settings( static::ID ); - $taxonomy = isset( $features[ "{$feature}_taxonomy" ] ) ? $features[ "{$feature}_taxonomy" ] : $labels['taxonomy_default']; - - // Enable classification type - $feature_args = [ - 'label_for' => $feature, - 'option_index' => $option_index, - 'input_type' => 'checkbox', - ]; - - $threshold_args = [ - 'label_for' => "{$feature}_threshold", - 'option_index' => $option_index, - 'input_type' => 'number', - 'default_value' => $labels['threshold_default'], - ]; - ?> - -
- - -

- feature_instance->render_input( $feature_args ); ?> - -

- -

-
- feature_instance->render_input( $threshold_args ); ?> -

- -

-
- -

- name ] = $taxonomy->labels->singular_name; - } - - return $supported; - } - /** * Helper to ensure the authentication works. * @@ -590,302 +422,15 @@ public function sanitize_settings( array $new_settings ): array { $new_settings[ static::ID ]['authenticated'] = true; } - $new_settings[ static::ID ]['classification_mode'] = sanitize_text_field( $new_settings[ static::ID ]['classification_mode'] ?? $settings[ static::ID ]['classification_mode'] ); - $new_settings[ static::ID ]['classification_method'] = sanitize_text_field( $new_settings[ static::ID ]['classification_method'] ?? $settings[ static::ID ]['classification_method'] ); - $new_settings[ static::ID ]['endpoint_url'] = esc_url_raw( $new_settings[ static::ID ]['endpoint_url'] ?? $settings[ static::ID ]['endpoint_url'] ); $new_settings[ static::ID ]['username'] = sanitize_text_field( $new_settings[ static::ID ]['username'] ?? $settings[ static::ID ]['username'] ); $new_settings[ static::ID ]['password'] = sanitize_text_field( $new_settings[ static::ID ]['password'] ?? $settings[ static::ID ]['password'] ); - $new_settings[ static::ID ]['category'] = absint( $new_settings[ static::ID ]['category'] ?? $settings[ static::ID ]['category'] ); - $new_settings[ static::ID ]['category_threshold'] = absint( $new_settings[ static::ID ]['category_threshold'] ?? $settings[ static::ID ]['category_threshold'] ); - $new_settings[ static::ID ]['category_taxonomy'] = sanitize_text_field( $new_settings[ static::ID ]['category_taxonomy'] ?? $settings[ static::ID ]['category_taxonomy'] ); - - $new_settings[ static::ID ]['keyword'] = absint( $new_settings[ static::ID ]['keyword'] ?? $settings[ static::ID ]['keyword'] ); - $new_settings[ static::ID ]['keyword_threshold'] = absint( $new_settings[ static::ID ]['keyword_threshold'] ?? $settings[ static::ID ]['keyword_threshold'] ); - $new_settings[ static::ID ]['keyword_taxonomy'] = sanitize_text_field( $new_settings[ static::ID ]['keyword_taxonomy'] ?? $settings[ static::ID ]['keyword_taxonomy'] ); - - $new_settings[ static::ID ]['entity'] = absint( $new_settings[ static::ID ]['entity'] ?? $settings[ static::ID ]['entity'] ); - $new_settings[ static::ID ]['entity_threshold'] = absint( $new_settings[ static::ID ]['entity_threshold'] ?? $settings[ static::ID ]['entity_threshold'] ); - $new_settings[ static::ID ]['entity_taxonomy'] = sanitize_text_field( $new_settings[ static::ID ]['entity_taxonomy'] ?? $settings[ static::ID ]['entity_taxonomy'] ); - - $new_settings[ static::ID ]['concept'] = absint( $new_settings[ static::ID ]['concept'] ?? $settings[ static::ID ]['concept'] ); - $new_settings[ static::ID ]['concept_threshold'] = absint( $new_settings[ static::ID ]['concept_threshold'] ?? $settings[ static::ID ]['concept_threshold'] ); - $new_settings[ static::ID ]['concept_taxonomy'] = sanitize_text_field( $new_settings[ static::ID ]['concept_taxonomy'] ?? $settings[ static::ID ]['concept_taxonomy'] ); - return $new_settings; } - /** - * Format the result of most recent request. - * - * @param array|WP_Error $data Response data to format. - * @return string - */ - protected function get_formatted_latest_response( $data ): string { - if ( ! $data ) { - return __( 'N/A', 'classifai' ); - } - - if ( is_wp_error( $data ) ) { - return $data->get_error_message(); - } - - $formatted_data = array_intersect_key( - $data, - [ - 'usage' => 1, - 'language' => 1, - ] - ); - - foreach ( array_diff_key( $data, $formatted_data ) as $key => $value ) { - $formatted_data[ $key ] = count( $value ); - } - - return preg_replace( '/,"/', ', "', wp_json_encode( $formatted_data ) ); - } - - /** - * Add metabox to enable/disable language processing on post/post types. - * - * @since 1.8.0 - * - * @param string $post_type Post Type. - * @param \WP_Post $post WP_Post object. - */ - public function add_classifai_meta_box( string $post_type, \WP_Post $post ) { - $supported_post_types = get_supported_post_types(); - $post_statuses = get_supported_post_statuses(); - $post_status = get_post_status( $post ); - if ( in_array( $post_type, $supported_post_types, true ) && in_array( $post_status, $post_statuses, true ) ) { - add_meta_box( - 'classifai_language_processing_metabox', - __( 'ClassifAI Language Processing', 'classifai' ), - [ $this, 'render_classifai_meta_box' ], - null, - 'side', - 'low', - array( '__back_compat_meta_box' => true ) - ); - } - } - - /** - * Render metabox content. - * - * @since 1.8.0 - * - * @param \WP_Post $post WP_Post object. - */ - public function render_classifai_meta_box( \WP_Post $post ) { - wp_nonce_field( 'classifai_language_processing_meta_action', 'classifai_language_processing_meta' ); - $classifai_process_content = get_post_meta( $post->ID, '_classifai_process_content', true ); - $classifai_process_content = ( 'no' === $classifai_process_content ) ? 'no' : 'yes'; - - $post_type = get_post_type_object( get_post_type( $post ) ); - $post_type_label = esc_html__( 'Post', 'classifai' ); - if ( $post_type ) { - $post_type_label = $post_type->labels->singular_name; - } - ?> -

- -

- - function ( $data ) { - $process_content = get_post_meta( $data['id'], '_classifai_process_content', true ); - return ( 'no' === $process_content ) ? 'no' : 'yes'; - }, - 'update_callback' => function ( $value, $data ) { - $value = ( 'no' === $value ) ? 'no' : 'yes'; - return update_post_meta( $data->ID, '_classifai_process_content', $value ); - }, - 'schema' => [ - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - ], - ) - ); - } - - /** - * Returns whether the provider is configured or not. - * - * For backwards compat, we've maintained the use of the - * `classifai_configured` option. We default to looking for - * the `authenticated` setting though. - * - * @return bool - */ - public function is_configured(): bool { - $is_configured = parent::is_configured(); - - if ( ! $is_configured ) { - $is_configured = (bool) get_option( 'classifai_configured', false ); - } - - return $is_configured; - } - - /** - * Register REST endpoints. - */ - public function register_endpoints() { - $post_types = get_supported_post_types(); - foreach ( $post_types as $post_type ) { - register_meta( - $post_type, - '_classifai_error', - [ - 'show_in_rest' => true, - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - } - - register_rest_route( - 'classifai/v1', - 'generate-tags/(?P\d+)', - [ - 'methods' => WP_REST_Server::READABLE, - 'callback' => [ $this, 'generate_post_tags' ], - 'args' => array( - 'id' => array( - 'required' => true, - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'description' => esc_html__( 'Post ID to generate tags.', 'classifai' ), - ), - 'linkTerms' => array( - 'type' => 'boolean', - 'description' => esc_html__( 'Whether to link terms or not.', 'classifai' ), - 'default' => true, - ), - ), - 'permission_callback' => [ $this, 'generate_post_tags_permissions_check' ], - ] - ); - } - - /** - * Handle request to generate tags for given post ID. - * - * @param WP_REST_Request $request The full request object. - * @return array|bool|string|WP_Error - */ - public function generate_post_tags( WP_REST_Request $request ) { - $post_id = $request->get_param( 'id' ); - $link_terms = $request->get_param( 'linkTerms' ); - - if ( empty( $post_id ) ) { - return new WP_Error( 'post_id_required', esc_html__( 'Post ID is required to classify post.', 'classifai' ) ); - } - - $result = $this->rest_endpoint_callback( - $post_id, - 'classify', - [ - 'link_terms' => $link_terms, - ] - ); - - return rest_ensure_response( - array( - 'terms' => $result, - 'feature_taxonomies' => $this->get_all_feature_taxonomies(), - ) - ); - } - - /** - * Get all feature taxonomies. - * - * @since 2.5.0 - * - * @return array|WP_Error - */ - public function get_all_feature_taxonomies() { - // Get all feature taxonomies. - $feature_taxonomies = []; - foreach ( [ 'category', 'keyword', 'concept', 'entity' ] as $feature ) { - if ( get_feature_enabled( $feature ) ) { - $taxonomy = get_feature_taxonomy( $feature ); - $permission = check_term_permissions( $taxonomy ); - - if ( is_wp_error( $permission ) ) { - return $permission; - } - - if ( 'post_tag' === $taxonomy ) { - $taxonomy = 'tags'; - } - if ( 'category' === $taxonomy ) { - $taxonomy = 'categories'; - } - $feature_taxonomies[] = $taxonomy; - } - } - - return $feature_taxonomies; - } - /** * Common entry point for all REST endpoints for this provider. - * This is called by the Service. * * @param int $post_id The Post Id we're processing. * @param string $route_to_call The route we are processing. @@ -893,120 +438,32 @@ public function get_all_feature_taxonomies() { * @return string|WP_Error */ public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) { - $route_to_call = strtolower( $route_to_call ); - if ( ! $post_id || ! get_post( $post_id ) ) { - return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to generate an excerpt.', 'classifai' ) ); + return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to run classification.', 'classifai' ) ); } - $return = ''; + $route_to_call = strtolower( $route_to_call ); + $return = ''; // Handle all of our routes. switch ( $route_to_call ) { case 'classify': - $return = ( new Classification() )->run( $post_id, $args['link_terms'] ?? true ); + $return = $this->classify( $post_id ); break; } return $return; } - /** - * Handle request to generate tags for given post ID. - * - * @param int $post_id The Post Id we're processing. - * @return mixed - */ - public function classify_post( int $post_id ) { - try { - if ( empty( $post_id ) ) { - return new WP_Error( 'post_id_required', esc_html__( 'Post ID is required to classify post.', 'classifai' ) ); - } - - $taxonomy_terms = []; - $features = [ 'category', 'keyword', 'concept', 'entity' ]; - - // Process post content. - $result = $this->classify( $post_id ); - - if ( is_wp_error( $result ) ) { - return $result; - } - - foreach ( $features as $feature ) { - $taxonomy = get_feature_taxonomy( $feature ); - $terms = wp_get_object_terms( $post_id, $taxonomy ); - if ( ! is_wp_error( $terms ) ) { - foreach ( $terms as $term ) { - $taxonomy_terms[ $taxonomy ][] = $term->term_id; - } - } - } - - // Return taxonomy terms. - return rest_ensure_response( [ 'terms' => $taxonomy_terms ] ); - } catch ( \Exception $e ) { - return new WP_Error( 'request_failed', $e->getMessage() ); - } - } - - /** - * Check if a given request has access to generate tags - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|bool - */ - public function generate_post_tags_permissions_check( WP_REST_Request $request ) { - $post_id = $request->get_param( 'id' ); - - // Ensure we have a logged in user that can edit the item. - if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { - return false; - } - - $post_type = get_post_type( $post_id ); - $post_type_obj = get_post_type_object( $post_type ); - - // Ensure the post type is allowed in REST endpoints. - if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) { - return false; - } - - // For all enabled features, ensure the user has proper permissions to add/edit terms. - foreach ( [ 'category', 'keyword', 'concept', 'entity' ] as $feature ) { - if ( ! get_feature_enabled( $feature ) ) { - continue; - } - - $taxonomy = get_feature_taxonomy( $feature ); - $permission = check_term_permissions( $taxonomy ); - - if ( is_wp_error( $permission ) ) { - return $permission; - } - } - - $post_status = get_post_status( $post_id ); - $supported = get_supported_post_types(); - $post_statuses = get_supported_post_statuses(); - - // Check if processing allowed. - if ( ! in_array( $post_status, $post_statuses, true ) || ! in_array( $post_type, $supported, true ) || ! ( new Classification() )->is_feature_enabled() ) { - return new WP_Error( 'not_enabled', esc_html__( 'Language Processing not enabled for current post.', 'classifai' ) ); - } - - return true; - } - /** * Classifies the post specified with the PostClassifier object. - * Existing terms relationships are removed before classification. * - * @param int $post_id the post to classify & link - * @param bool $link_terms Whether to link the terms to the post. - * @return array|bool + * Existing terms relationships are removed during classification. + * + * @param int $post_id the post to classify & link + * @return array|WP_Error */ - public function classify( int $post_id, bool $link_terms = true ) { + public function classify( int $post_id ) { /** * Filter whether ClassifAI should classify a post. * @@ -1021,50 +478,66 @@ public function classify( int $post_id, bool $link_terms = true ) { * * @return {bool} Whether the post should be classified. */ - $classifai_should_classify_post = apply_filters( 'classifai_should_classify_post', true, $post_id ); - if ( ! $classifai_should_classify_post ) { - return false; + $should_classify = apply_filters( 'classifai_should_classify_post', true, $post_id ); + if ( ! $should_classify ) { + return new WP_Error( 'invalid', esc_html__( 'Classification is disabled for this item.', 'classifai' ) ); } $classifier = new PostClassifier(); - if ( $link_terms ) { - if ( get_feature_enabled( 'category' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'category' ) ); - } + $output = $classifier->classify( $post_id ); - if ( get_feature_enabled( 'keyword' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'keyword' ) ); - } + return $output; + } - if ( get_feature_enabled( 'concept' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'concept' ) ); - } + /** + * Links the Watson NLU response output to taxonomy terms. + * + * @param int $post_id The post ID. + * @param array $terms The classification results from Watson NLU. + * @param bool $link Whether to link the terms or not. + * @return array|WP_Error + */ + public function link( int $post_id, array $terms, bool $link = true ) { + if ( empty( $terms ) ) { + return new WP_Error( 'invalid', esc_html__( 'No terms to link.', 'classifai' ) ); + } - if ( get_feature_enabled( 'entity' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'entity' ) ); - } + $classifier = new PostClassifier(); + + $output = $classifier->link( $post_id, $terms, [], $link ); + + return $output; + } + + /** + * Format the result of most recent request. + * + * @param array|WP_Error $data Response data to format. + * @return string + */ + protected function get_formatted_latest_response( $data ): string { + if ( ! $data ) { + return __( 'N/A', 'classifai' ); } - $output = $classifier->classify_and_link( $post_id, [], $link_terms ); - - if ( is_wp_error( $output ) ) { - update_post_meta( - $post_id, - '_classifai_error', - wp_json_encode( - [ - 'code' => $output->get_error_code(), - 'message' => $output->get_error_message(), - ] - ) - ); - } else { - // If there is no error, clear any existing error states. - delete_post_meta( $post_id, '_classifai_error' ); + if ( is_wp_error( $data ) ) { + return $data->get_error_message(); } - return $output; + $formatted_data = array_intersect_key( + $data, + [ + 'usage' => 1, + 'language' => 1, + ] + ); + + foreach ( array_diff_key( $data, $formatted_data ) as $key => $value ) { + $formatted_data[ $key ] = count( $value ); + } + + return preg_replace( '/,"/', ', "', wp_json_encode( $formatted_data ) ); } /** @@ -1073,26 +546,15 @@ public function classify( int $post_id, bool $link_terms = true ) { * @return array */ public function get_debug_information(): array { - $settings = $this->feature_instance->get_settings(); - $provider_settings = $settings[ static::ID ]; - $debug_info = []; + $settings = $this->feature_instance->get_settings(); + $debug_info = []; if ( $this->feature_instance instanceof Classification ) { - $debug_info[ __( 'Category (status)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['category'], 1 ); - $debug_info[ __( 'Category (threshold)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['category_threshold'], 1 ); - $debug_info[ __( 'Category (taxonomy)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['category_taxonomy'], 1 ); - - $debug_info[ __( 'Keyword (status)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['keyword'], 1 ); - $debug_info[ __( 'Keyword (threshold)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['keyword_threshold'], 1 ); - $debug_info[ __( 'Keyword (taxonomy)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['keyword_taxonomy'], 1 ); - - $debug_info[ __( 'Entity (status)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['entity'], 1 ); - $debug_info[ __( 'Entity (threshold)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['entity_threshold'], 1 ); - $debug_info[ __( 'Entity (taxonomy)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['entity_taxonomy'], 1 ); - - $debug_info[ __( 'Concept (status)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['concept'], 1 ); - $debug_info[ __( 'Concept (threshold)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['concept_threshold'], 1 ); - $debug_info[ __( 'Concept (taxonomy)', 'classifai' ) ] = Feature::get_debug_value_text( $provider_settings['concept_taxonomy'], 1 ); + foreach ( $this->nlu_features as $slug => $feature ) { + $debug_info[ $feature['feature'] . ' (status)' ] = Feature::get_debug_value_text( $settings[ $slug ], 1 ); + $debug_info[ $feature['feature'] . ' (threshold)' ] = Feature::get_debug_value_text( $settings[ $slug . '_threshold' ], 1 ); + $debug_info[ $feature['feature'] . ' (taxonomy)' ] = Feature::get_debug_value_text( $settings[ $slug . '_taxonomy' ], 1 ); + } $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_watson_nlu_latest_response' ) ); } diff --git a/includes/Classifai/Providers/Watson/PostClassifier.php b/includes/Classifai/Providers/Watson/PostClassifier.php index eb001e3d2..d2dfb4189 100644 --- a/includes/Classifai/Providers/Watson/PostClassifier.php +++ b/includes/Classifai/Providers/Watson/PostClassifier.php @@ -47,7 +47,7 @@ class PostClassifier { * * @param int $post_id The post to classify * @param array $opts The classification options - * @return array|bool|\WP_Error + * @return array|\WP_Error */ public function classify( int $post_id, array $opts = [] ) { $classifier = $this->get_classifier(); @@ -62,7 +62,7 @@ public function classify( int $post_id, array $opts = [] ) { if ( ! empty( $text_to_classify ) ) { return $classifier->classify( $text_to_classify, $opts ); } else { - return false; + return new \WP_Error( 'invalid', esc_html__( 'No text found.', 'classifai' ) ); } } @@ -167,7 +167,7 @@ public function get_classifier(): Classifier { */ public function get_features(): array { $classification = new Classification(); - $settings = $classification->get_settings( NLU::ID ); + $settings = $classification->get_settings(); $features = []; if ( $settings['category'] ) { diff --git a/includes/Classifai/Providers/Watson/PreviewClassifierData.php b/includes/Classifai/Providers/Watson/PreviewClassifierData.php deleted file mode 100644 index fbbf7e4c0..000000000 --- a/includes/Classifai/Providers/Watson/PreviewClassifierData.php +++ /dev/null @@ -1,145 +0,0 @@ -normalize( $post_id ); - $body = $classifier->get_body( $text_to_classify ); - $request_options['body'] = $body; - $request = $classifier->get_request(); - - $classified_data = $request->post( $classifier->get_endpoint(), $request_options ); - $classified_data = $this->filter_classify_preview_data( $classified_data ); - - wp_send_json_success( $classified_data ); - } - - /** - * Searches and returns posts. - */ - public function get_post_search_results() { - $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : false; - - if ( ! ( $nonce && wp_verify_nonce( $nonce, 'classifai-previewer-nonce' ) ) ) { - wp_send_json_error( esc_html__( 'Failed nonce check.', 'classifai' ) ); - } - - $search_term = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; - $post_types = isset( $_POST['post_types'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_types'] ) ) ) : 'post'; - $post_statuses = isset( $_POST['post_status'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_status'] ) ) ) : 'publish'; - - $posts = get_posts( - array( - 'post_type' => $post_types, - 'post_status' => $post_statuses, - 's' => $search_term, - ) - ); - - wp_send_json_success( $posts ); - } - - /** - * Filter classifier preview based on the feature settings. - * - * @param array $classified_data The classified data. - * @return array - */ - public function filter_classify_preview_data( array $classified_data ): array { - if ( is_wp_error( $classified_data ) ) { - return $classified_data; - } - - $classify_existing_terms = 'existing_terms' === get_classification_method(); - if ( ! $classify_existing_terms ) { - return $classified_data; - } - - $features = [ - 'category' => 'categories', - 'concept' => 'concepts', - 'entity' => 'entities', - 'keyword' => 'keywords', - ]; - foreach ( $features as $key => $feature ) { - $taxonomy = get_feature_taxonomy( $key ); - if ( ! $taxonomy ) { - continue; - } - - if ( ! isset( $classified_data[ $feature ] ) || empty( $classified_data[ $feature ] ) ) { - continue; - } - - // Handle categories feature. - if ( 'categories' === $feature ) { - $classified_data[ $feature ] = array_filter( - $classified_data[ $feature ], - function ( $item ) use ( $taxonomy ) { - $keep = false; - $parts = explode( '/', $item['label'] ); - $parts = array_filter( $parts ); - if ( ! empty( $parts ) ) { - foreach ( $parts as $part ) { - $term = get_term_by( 'name', $part, $taxonomy ); - if ( ! empty( $term ) ) { - $keep = true; - break; - } - } - } - return $keep; - } - ); - // Reset array keys. - $classified_data[ $feature ] = array_values( $classified_data[ $feature ] ); - continue; - } - - $classified_data[ $feature ] = array_filter( - $classified_data[ $feature ], - function ( $item ) use ( $taxonomy, $key ) { - $name = $item['text']; - if ( 'keyword' === $key ) { - $name = preg_replace( '#^[a-z]+ ([A-Z].*)$#', '$1', $name ); - } elseif ( 'entity' === $key ) { - if ( ! empty( $item['disambiguation'] ) && ! empty( $item['disambiguation']['name'] ) ) { - $name = $item['disambiguation']['name']; - } - } - $term = get_term_by( 'name', $name, $taxonomy ); - return ! empty( $term ); - } - ); - // Reset array keys. - $classified_data[ $feature ] = array_values( $classified_data[ $feature ] ); - } - - return $classified_data; - } -} diff --git a/includes/Classifai/Providers/Watson/SavePostHandler.php b/includes/Classifai/Providers/Watson/SavePostHandler.php deleted file mode 100644 index 8be5373da..000000000 --- a/includes/Classifai/Providers/Watson/SavePostHandler.php +++ /dev/null @@ -1,299 +0,0 @@ -classify( $post_id ); - } - } - - /** - * Classifies the post specified with the PostClassifier object. - * Existing terms relationships are removed before classification. - * - * @param int $post_id the post to classify & link. - * @param bool $link_terms Whether to link the terms to the post. - * @return object|bool - */ - public function classify( int $post_id, bool $link_terms = true ) { - /** - * Filter whether ClassifAI should classify a post. - * - * Default is true, return false to skip classifying a post. - * - * @since 1.2.0 - * @hook classifai_should_classify_post - * - * @param {bool} $should_classify Whether the post should be classified. Default `true`, return `false` to skip - * classification for this post. - * @param {int} $post_id The ID of the post to be considered for classification. - * - * @return {bool} Whether the post should be classified. - */ - $classifai_should_classify_post = apply_filters( 'classifai_should_classify_post', true, $post_id ); - if ( ! $classifai_should_classify_post ) { - return false; - } - - $classifier = $this->get_classifier(); - - if ( $link_terms ) { - if ( get_feature_enabled( 'category' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'category' ) ); - } - - if ( get_feature_enabled( 'keyword' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'keyword' ) ); - } - - if ( get_feature_enabled( 'concept' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'concept' ) ); - } - - if ( get_feature_enabled( 'entity' ) ) { - wp_delete_object_term_relationships( $post_id, get_feature_taxonomy( 'entity' ) ); - } - } - - $output = $classifier->classify_and_link( $post_id, [], $link_terms ); - - if ( is_wp_error( $output ) ) { - update_post_meta( - $post_id, - '_classifai_error', - wp_json_encode( - [ - 'code' => $output->get_error_code(), - 'message' => $output->get_error_message(), - ] - ) - ); - } else { - // If there is no error, clear any existing error states. - delete_post_meta( $post_id, '_classifai_error' ); - } - - return $output; - } - - /** - * Lazy initializes the Post Classifier object. - * - * @return PostClassifier - */ - public function get_classifier(): PostClassifier { - if ( is_null( $this->classifier ) ) { - $this->classifier = new PostClassifier(); - } - - return $this->classifier; - } - - /** - * Outputs an Admin Notice with the error message if NLU - * classification had failed earlier. - */ - public function show_error_if() { - global $post; - - if ( empty( $post ) ) { - return; - } - - $post_id = $post->ID; - - if ( empty( $post_id ) ) { - return; - } - - $error = get_post_meta( $post_id, '_classifai_error', true ); - - if ( ! empty( $error ) ) { - delete_post_meta( $post_id, '_classifai_error' ); - $error = (array) json_decode( $error ); - $code = ! empty( $error['code'] ) ? $error['code'] : 500; - $message = ! empty( $error['message'] ) ? $error['message'] : 'Unknown NLU API error'; - - ?> -
-

- -

-

- - - - -

-
- labels->singular_name; - } - ?> -
-

- -

-
- classify( $post_id ); - $classified = array(); - if ( ! is_wp_error( $result ) ) { - $classified = array( 'classifai_classify' => 1 ); - } - wp_safe_redirect( esc_url_raw( add_query_arg( $classified, get_edit_post_link( $post_id, 'edit' ) ) ) ); - exit(); - } - } else { - wp_die( esc_html__( 'You don\'t have permission to perform this operation.', 'classifai' ) ); - } - } - - /** - * Add "classifai_classify" in list of query variable names to remove. - * - * @param [] $removable_query_args An array of query variable names to remove from a URL. - * @return [] - */ - public function classifai_removable_query_args( array $removable_query_args ): array { - $removable_query_args[] = 'classifai_classify'; - return $removable_query_args; - } -} diff --git a/includes/Classifai/Taxonomy/CategoryTaxonomy.php b/includes/Classifai/Taxonomy/CategoryTaxonomy.php index e805ad882..653f006e8 100644 --- a/includes/Classifai/Taxonomy/CategoryTaxonomy.php +++ b/includes/Classifai/Taxonomy/CategoryTaxonomy.php @@ -2,8 +2,8 @@ namespace Classifai\Taxonomy; -use function Classifai\Providers\Watson\get_feature_enabled; -use function Classifai\Providers\Watson\get_feature_taxonomy; +use function Classifai\get_classification_feature_enabled; +use function Classifai\get_classification_feature_taxonomy; /** * The Classifai Category Taxonomy. @@ -52,7 +52,7 @@ public function get_plural_label(): string { * @return bool */ public function get_visibility(): bool { - return get_feature_enabled( 'category' ) && - get_feature_taxonomy( 'category' ) === $this->get_name(); + return get_classification_feature_enabled( 'category' ) && + get_classification_feature_taxonomy( 'category' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/ConceptTaxonomy.php b/includes/Classifai/Taxonomy/ConceptTaxonomy.php index fe6d4003a..d913f9926 100644 --- a/includes/Classifai/Taxonomy/ConceptTaxonomy.php +++ b/includes/Classifai/Taxonomy/ConceptTaxonomy.php @@ -2,8 +2,8 @@ namespace Classifai\Taxonomy; -use function Classifai\Providers\Watson\get_feature_enabled; -use function Classifai\Providers\Watson\get_feature_taxonomy; +use function Classifai\get_classification_feature_enabled; +use function Classifai\get_classification_feature_taxonomy; /** * The Classifai Concept Taxonomy. @@ -52,7 +52,7 @@ public function get_plural_label(): string { * @return bool */ public function get_visibility(): bool { - return get_feature_enabled( 'concept' ) && - get_feature_taxonomy( 'concept' ) === $this->get_name(); + return get_classification_feature_enabled( 'concept' ) && + get_classification_feature_taxonomy( 'concept' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/EntityTaxonomy.php b/includes/Classifai/Taxonomy/EntityTaxonomy.php index 526924fd1..a687826c7 100644 --- a/includes/Classifai/Taxonomy/EntityTaxonomy.php +++ b/includes/Classifai/Taxonomy/EntityTaxonomy.php @@ -2,8 +2,8 @@ namespace Classifai\Taxonomy; -use function Classifai\Providers\Watson\get_feature_enabled; -use function Classifai\Providers\Watson\get_feature_taxonomy; +use function Classifai\get_classification_feature_enabled; +use function Classifai\get_classification_feature_taxonomy; /** * The ClassifAI Entity Taxonomy. @@ -52,7 +52,7 @@ public function get_plural_label(): string { * @return bool */ public function get_visibility(): bool { - return get_feature_enabled( 'entity' ) && - get_feature_taxonomy( 'entity' ) === $this->get_name(); + return get_classification_feature_enabled( 'entity' ) && + get_classification_feature_taxonomy( 'entity' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/KeywordTaxonomy.php b/includes/Classifai/Taxonomy/KeywordTaxonomy.php index 29cc272b6..fb418f2ac 100644 --- a/includes/Classifai/Taxonomy/KeywordTaxonomy.php +++ b/includes/Classifai/Taxonomy/KeywordTaxonomy.php @@ -2,8 +2,8 @@ namespace Classifai\Taxonomy; -use function Classifai\Providers\Watson\get_feature_enabled; -use function Classifai\Providers\Watson\get_feature_taxonomy; +use function Classifai\get_classification_feature_enabled; +use function Classifai\get_classification_feature_taxonomy; /** * The ClassifAI Keyword Taxonomy. @@ -52,7 +52,7 @@ public function get_plural_label(): string { * @return bool */ public function get_visibility(): bool { - return get_feature_enabled( 'keyword' ) && - get_feature_taxonomy( 'keyword' ) === $this->get_name(); + return get_classification_feature_enabled( 'keyword' ) && + get_classification_feature_taxonomy( 'keyword' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/TaxonomyFactory.php b/includes/Classifai/Taxonomy/TaxonomyFactory.php index 344ca2fda..0858b585a 100644 --- a/includes/Classifai/Taxonomy/TaxonomyFactory.php +++ b/includes/Classifai/Taxonomy/TaxonomyFactory.php @@ -2,7 +2,7 @@ namespace Classifai\Taxonomy; -use function Classifai\Providers\Watson\get_supported_post_types; +use Classifai\Features\Classification; /** * TaxonomyFactory builds the Taxonomy taxonomy class instances. Instances @@ -47,7 +47,7 @@ class TaxonomyFactory { * frontend and backend to get these taxonomies. */ public function build_all() { - $supported_post_types = get_supported_post_types(); + $supported_post_types = ( new Classification() )->get_supported_post_types(); foreach ( $this->get_supported_taxonomies() as $taxonomy ) { $this->build_if( $taxonomy, $supported_post_types ); diff --git a/src/js/gutenberg-plugin.js b/src/js/gutenberg-plugin.js index 818c2f372..593449d86 100644 --- a/src/js/gutenberg-plugin.js +++ b/src/js/gutenberg-plugin.js @@ -18,8 +18,7 @@ import TaxonomyControls from './taxonomy-controls'; import PrePubClassifyPost from './gutenberg-plugins/pre-publish-classify-post'; import { DisableFeatureButton } from './components'; -const { classifaiEmbeddingData, classifaiPostData, classifaiTTSEnabled } = - window; +const { classifaiPostData, classifaiTTSEnabled } = window; /** * Create the ClassifAI icon @@ -29,10 +28,11 @@ const ClassifAIIcon = () => ( ); /** - * ClassifAIToggle Component. + * ClassificationToggle Component. * + * Used to toggle the classification process on or off. */ -const ClassifAIToggle = () => { +const ClassificationToggle = () => { // Use the datastore to retrieve all the meta for this post. const processContent = useSelect( ( select ) => select( 'core/editor' ).getEditedPostAttribute( @@ -56,9 +56,11 @@ const ClassifAIToggle = () => { }; /** - * Classify Post Button + * Classify button. + * + * Used to manually classify the content. */ -const ClassifAIGenerateTagsButton = () => { +const ClassificationButton = () => { const processContent = useSelect( ( select ) => select( 'core/editor' ).getEditedPostAttribute( 'classifai_process_content' @@ -286,7 +288,7 @@ const ClassifAIGenerateTagsButton = () => {
{ isOpen && ( { onClick={ ( e ) => { handleClick( { button: e.target, - endpoint: '/classifai/v1/generate-tags/', + endpoint: '/classifai/v1/classify/', callback: buttonClickCallBack, callbackArgs: { openPopup: true, @@ -333,7 +335,7 @@ const ClassifAIGenerateTagsButton = () => { onClick={ ( e ) => { handleClick( { button: e.target, - endpoint: '/classifai/v1/generate-tags/', + endpoint: '/classifai/v1/classify/', callback: buttonClickCallBack, buttonText, linkTerms: false, @@ -585,31 +587,18 @@ const ClassifAIPlugin = () => { const isNLULanguageProcessingEnabled = classifaiPostData && classifaiPostData.NLUEnabled; - const isEmbeddingProcessingEnabled = - classifaiEmbeddingData && classifaiEmbeddingData.enabled; - // Ensure we are on a supported post type, checking settings from all features. const isNLUPostTypeSupported = classifaiPostData && classifaiPostData.supportedPostTypes && classifaiPostData.supportedPostTypes.includes( postType ); - const isEmbeddingPostTypeSupported = - classifaiEmbeddingData && - classifaiEmbeddingData.supportedPostTypes && - classifaiEmbeddingData.supportedPostTypes.includes( postType ); - // Ensure we are on a supported post status, checking settings from all features. const isNLUPostStatusSupported = classifaiPostData && classifaiPostData.supportedPostStatues && classifaiPostData.supportedPostStatues.includes( postStatus ); - const isEmbeddingPostStatusSupported = - classifaiEmbeddingData && - classifaiEmbeddingData.supportedPostStatues && - classifaiEmbeddingData.supportedPostStatues.includes( postStatus ); - // Ensure the user has permissions to use the feature. const userHasNLUPermissions = classifaiPostData && @@ -618,25 +607,12 @@ const ClassifAIPlugin = () => { 1 === parseInt( classifaiPostData.noPermissions ) ); - const userHasEmbeddingPermissions = - classifaiEmbeddingData && - ! ( - classifaiEmbeddingData.noPermissions && - 1 === parseInt( classifaiEmbeddingData.noPermissions ) - ); - const nluPermissionCheck = userHasNLUPermissions && isNLULanguageProcessingEnabled && isNLUPostTypeSupported && isNLUPostStatusSupported; - const embeddingsPermissionCheck = - userHasEmbeddingPermissions && - isEmbeddingProcessingEnabled && - isEmbeddingPostTypeSupported && - isEmbeddingPostStatusSupported; - return ( { className="classifai-panel" > <> - { ( nluPermissionCheck || embeddingsPermissionCheck ) && ( + { nluPermissionCheck && ( <> - - { nluPermissionCheck && ( - - ) } + + { nluPermissionCheck && } ) } { classifaiTTSEnabled && } diff --git a/src/js/language-processing.js b/src/js/language-processing.js index 5be79de71..47e22bcda 100644 --- a/src/js/language-processing.js +++ b/src/js/language-processing.js @@ -446,7 +446,7 @@ import '../scss/language-processing.scss'; document.addEventListener( 'DOMContentLoaded', function () { // Display "Classify Post" button only when "Process content on update" is unchecked (Classic Editor). const classifaiNLUCheckbox = document.getElementById( - '_classifai_process_content' + 'classifai-process-content' ); if ( classifaiNLUCheckbox ) { classifaiNLUCheckbox.addEventListener( 'change', function () { diff --git a/tests/Classifai/HelpersTest.php b/tests/Classifai/HelpersTest.php index 9e161cb77..e8927c3be 100644 --- a/tests/Classifai/HelpersTest.php +++ b/tests/Classifai/HelpersTest.php @@ -2,17 +2,27 @@ namespace Classifai; +use Classifai\Features\Classification; + use function Classifai\Providers\Watson\get_username; use function Classifai\Providers\Watson\get_password; -use function Classifai\Providers\Watson\get_supported_post_types; use function Classifai\Providers\Watson\get_feature_threshold; -use function Classifai\Providers\Watson\get_feature_taxonomy; +use function Classifai\get_classification_feature_taxonomy; /** * @group helpers */ class HelpersTest extends \WP_UnitTestCase { + /** + * Provides a Feature instance. + * + * @return Classification + */ + public function get_feature_class() : Classification { + return new Classification(); + } + /** * Set up method. */ @@ -43,7 +53,7 @@ function test_it_has_a_plugin_instance() { } function test_it_has_default_supported_post_types() { - $actual = get_supported_post_types(); + $actual = $this->get_feature_class()->get_supported_post_types(); $this->assertEquals( ['post'], $actual ); } @@ -51,16 +61,16 @@ function test_it_can_lookup_supported_post_types_from_option() { $this->markTestSkipped(); update_option( 'classifai_settings', [ 'post_types' => [ 'post' => 1, 'page' => 1 ] ] ); - $actual = get_supported_post_types(); + $actual = $this->get_feature_class()->get_supported_post_types(); $this->assertEquals( [ 'post', 'page' ], $actual ); } function test_it_can_override_supported_post_types_with_filter() { - add_filter( 'classifai_post_types', function() { + add_filter( 'classifai_feature_classification_post_types', function() { return [ 'page' ]; } ); - $actual = get_supported_post_types(); + $actual = $this->get_feature_class()->get_supported_post_types(); $this->assertEquals( [ 'page' ], $actual ); } @@ -121,7 +131,7 @@ function test_it_has_default_feature_taxonomies() { ]; foreach ( $expected as $feature => $taxonomy ) { - $actual = get_feature_taxonomy( $feature ); + $actual = get_classification_feature_taxonomy( $feature ); $this->assertEquals( $taxonomy, $actual ); } } @@ -152,7 +162,7 @@ function test_it_knows_configured_feature_taxonomies() { ]; foreach ( $expected as $feature => $taxonomy ) { - $actual = get_feature_taxonomy( $feature ); + $actual = get_classification_feature_taxonomy( $feature ); $this->assertEquals( $taxonomy, $actual ); } } diff --git a/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js b/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js index b145c400e..37dd3e480 100644 --- a/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js +++ b/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js @@ -26,43 +26,12 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { cy.get( '#classifai_feature_classification_post_statuses_publish' ).check(); - cy.get( - '#classifai_feature_classification_openai_embeddings_taxonomies_category' - ).check(); - cy.get( - '#classifai_feature_classification_openai_embeddings_taxonomies_category_threshold' - ) - .clear() - .type( 100 ); // "Test" requires 80% confidence. At 81%, it does not apply. - cy.get( '#number_of_terms' ).clear().type( 1 ); + cy.get( '#category' ).check(); + cy.get( '#category_threshold' ).clear().type( 100 ); // "Test" requires 80% confidence. At 81%, it does not apply. cy.get( '#submit' ).click(); } ); - it( 'Can see the preview on the settings page', () => { - cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' - ); - - cy.get( '#submit' ).click(); - - // Click the Preview button. - const closePanelSelector = '#get-classifier-preview-data-btn'; - cy.get( closePanelSelector ).click(); - - // Check the term is received and visible. - cy.get( '.tax-row--Category' ).should( 'exist' ); - } ); - it( 'Can create category and post and category will get auto-assigned', () => { - // Remove custom taxonomies so those don't interfere with the test. - cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' - ); - cy.get( - '#classifai_feature_classification_openai_embeddings_taxonomies_category' - ).uncheck(); - cy.get( '#submit' ).click(); - // Create test term. cy.deleteAllTerms( 'category' ); cy.createTerm( 'Test', 'category' ); @@ -109,6 +78,21 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { } ); } ); + it( 'Can see the preview on the settings page', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' + ); + + cy.get( '#submit' ).click(); + + // Click the Preview button. + const closePanelSelector = '#get-classifier-preview-data-btn'; + cy.get( closePanelSelector ).click(); + + // Check the term is received and visible. + cy.get( '.tax-row--Category' ).should( 'exist' ); + } ); + it( 'Can create category and post and category will not get auto-assigned if feature turned off', () => { cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' @@ -175,10 +159,7 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { cy.get( '#classifai_feature_classification_post_statuses_publish' ).check(); - cy.get( - '#classifai_feature_classification_openai_embeddings_taxonomies_category' - ).check(); - cy.get( '#number_of_terms' ).clear().type( 1 ); + cy.get( '#category' ).check(); cy.get( '#submit' ).click(); cy.createClassicPost( {