Plugin Directory

Changeset 3455295


Ignore:
Timestamp:
02/06/2026 11:18:52 AM (8 weeks ago)
Author:
previewai
Message:

Release 1.1.0

Location:
preview-ai/trunk
Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • preview-ai/trunk/admin/class-preview-ai-admin-catalog.php

    r3446676 r3455295  
    2121
    2222    /**
    23      * Handle AJAX request for Learn My Catalog feature.
     23     * Handle AJAX request for Analyze & Enable Catalog feature.
    2424     */
    2525    public function handle_learn_catalog() {
     
    6161                array(
    6262                    'status'  => 'complete',
    63                     'message' => __( 'All products have already been analyzed. No new products to process.', 'preview-ai' ),
     63                    'message' => __( 'All products have already been analyzed and enabled. No new products to process.', 'preview-ai' ),
    6464                    'stats'   => array(
    6565                        'total'      => 0,
     
    8686                'message' => sprintf(
    8787                    /* translators: %d: number of products */
    88                     __( 'Analysis scheduled for %d products. Processing in background...', 'preview-ai' ),
     88                    __( 'Analyzing and enabling %d products in background...', 'preview-ai' ),
    8989                    $total_products
    9090                ),
     
    118118                'total'           => $stats['total'],
    119119                'configured'      => $stats['configured'],
    120                 'needs_review'    => $stats['needs_review'],
    121                 'images_analyzed' => $stats['images_analyzed'],
     120                'not_supported'   => $stats['not_supported'],
    122121                'analysis_errors' => $analysis_errors,
    123122                'try_product_url' => $try_product_url,
    124123                'message'         => sprintf(
    125                     /* translators: 1: number of configured products, 2: number of products needing review, 3: number of images analyzed */
    126                     __( '%1$d products configured. %2$d need review. %3$d images analyzed.', 'preview-ai' ),
     124                    /* translators: 1: number of enabled products, 2: number of not supported products */
     125                    __( '%1$d products enabled. %2$d not supported.', 'preview-ai' ),
    127126                    $stats['configured'],
    128                     $stats['needs_review'],
    129                     $stats['images_analyzed']
     127                    $stats['not_supported']
    130128                ),
    131129            )
     
    145143                'processed'       => 0,
    146144                'configured'      => 0,
    147                 'needs_review'    => 0,
    148                 'images_analyzed' => 0,
     145                'not_supported'   => 0,
    149146                'analysis_errors' => 0,
    150147                'configured_ids'  => array(),
     
    186183            $progress['processed']       += count( $batch );
    187184            $progress['configured']      += $stats['configured'];
    188             $progress['needs_review']    += $stats['needs_review'];
    189             $progress['images_analyzed'] += $stats['images_analyzed'];
     185            $progress['not_supported']   += $stats['not_supported'];
    190186            $progress['analysis_errors'] += isset( $result['analysis_errors'] ) ? intval( $result['analysis_errors'] ) : 0;
    191187            $progress['configured_ids']   = array_merge( $progress['configured_ids'], $stats['configured_ids'] );
     
    235231
    236232            $response['configured']      = $progress['configured'];
    237             $response['needs_review']    = $progress['needs_review'];
    238             $response['images_analyzed'] = $progress['images_analyzed'];
     233            $response['not_supported']   = $progress['not_supported'];
    239234            $response['analysis_errors'] = $progress['analysis_errors'];
    240235            $response['try_product_url'] = $try_product_url;
    241236            $response['message']         = sprintf(
    242                 /* translators: 1: number of configured products, 2: number of products needing review, 3: number of images analyzed */
    243                 __( '%1$d products configured. %2$d need review. %3$d images analyzed.', 'preview-ai' ),
     237                /* translators: 1: number of enabled products, 2: number of not supported products */
     238                __( '%1$d products enabled. %2$d not supported.', 'preview-ai' ),
    244239                $progress['configured'],
    245                 $progress['needs_review'],
    246                 $progress['images_analyzed']
     240                $progress['not_supported']
    247241            );
    248242
     
    429423            'total'           => 0,
    430424            'configured'      => 0,
    431             'needs_review'    => 0,
    432             'images_analyzed' => 0,
     425            'not_supported'   => 0,
    433426            'configured_ids'  => array(),
    434427        );
     
    466459                } else {
    467460                    update_post_meta( $product_id, '_preview_ai_enabled', 'no' );
    468                     $stats['needs_review']++;
     461                    $stats['not_supported']++;
    469462                }
    470463            } else {
    471464                update_post_meta( $product_id, '_preview_ai_enabled', 'no' );
    472                 $stats['needs_review']++;
    473             }
    474 
    475             // Save parent product image analysis.
    476             if ( ! empty( $classification['image_analysis'] ) ) {
    477                 $this->save_image_analysis( $product_id, $classification['image_analysis'] );
    478                 $stats['images_analyzed']++;
    479             }
    480 
    481             // Save variations image analysis.
    482             if ( ! empty( $classification['variations'] ) && is_array( $classification['variations'] ) ) {
    483                 foreach ( $classification['variations'] as $variation_data ) {
    484                     if ( ! empty( $variation_data['variation_id'] ) && ! empty( $variation_data['image_analysis'] ) ) {
    485                         $this->save_image_analysis(
    486                             absint( $variation_data['variation_id'] ),
    487                             $variation_data['image_analysis']
    488                         );
    489                         $stats['images_analyzed']++;
    490                     }
    491                 }
     465                $stats['not_supported']++;
    492466            }
    493467        }
     
    496470    }
    497471
    498     /**
    499      * Save image analysis data to post meta.
    500      *
    501      * @param int   $post_id  Post ID (product or variation).
    502      * @param array $analysis Image analysis data from backend.
    503      */
    504     private function save_image_analysis( $post_id, $analysis ) {
    505         $detected_objects = array();
    506         if ( ! empty( $analysis['detected_objects'] ) && is_array( $analysis['detected_objects'] ) ) {
    507             $detected_objects = array_map( 'sanitize_text_field', $analysis['detected_objects'] );
    508         }
    509 
    510         $image_analysis = array(
    511             'has_model'         => ! empty( $analysis['has_model'] ),
    512             'shot_type'         => sanitize_key( $analysis['shot_type'] ?? 'unknown' ),
    513             'framing'           => sanitize_key( $analysis['framing'] ?? 'unknown' ),
    514             'multiple_garments' => ! empty( $analysis['multiple_garments'] ),
    515             'detected_objects'  => $detected_objects,
    516             'confidence'        => floatval( $analysis['confidence'] ?? 0.0 ),
    517             'image_id'          => absint( $analysis['image_id'] ?? 0 ),
    518             'updated_at'        => sanitize_text_field( $analysis['updated_at'] ?? current_time( 'Y-m-d' ) ),
    519         );
    520 
    521         update_post_meta( $post_id, '_preview_ai_image_analysis', $image_analysis );
    522     }
    523472}
    524473
  • preview-ai/trunk/admin/class-preview-ai-admin-onboarding.php

    r3446676 r3455295  
    2828                        <div id="onboarding-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width 0.5s ease;"></div>
    2929                    </div>
    30                     <p id="onboarding-status" style="margin:16px 0 0;color:#64748b;font-size:14px;"><?php esc_html_e( 'Analyzing your product catalog...', 'preview-ai' ); ?></p>
     30                    <p id="onboarding-status" style="margin:16px 0 0;color:#64748b;font-size:14px;"><?php esc_html_e( 'Analyzing and enabling products...', 'preview-ai' ); ?></p>
    3131                </div>
    3232               
  • preview-ai/trunk/admin/class-preview-ai-admin-product.php

    r3446676 r3455295  
    429429
    430430    /**
     431     * Add Preview AI status filter dropdown to product list.
     432     */
     433    public function add_product_filter_dropdown() {
     434        global $typenow;
     435        if ( 'product' !== $typenow ) {
     436            return;
     437        }
     438
     439        $current = isset( $_GET['preview_ai_status'] ) ? sanitize_key( wp_unslash( $_GET['preview_ai_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     440
     441        $statuses = array(
     442            ''              => __( 'All Preview AI statuses', 'preview-ai' ),
     443            'active'        => __( 'Active', 'preview-ai' ),
     444            'disabled'      => __( 'Disabled', 'preview-ai' ),
     445            'not_analyzed'  => __( 'Not Analyzed', 'preview-ai' ),
     446            'not_supported' => __( 'Not Supported', 'preview-ai' ),
     447        );
     448
     449        echo '<select name="preview_ai_status">';
     450        foreach ( $statuses as $value => $label ) {
     451            printf(
     452                '<option value="%s" %s>%s</option>',
     453                esc_attr( $value ),
     454                selected( $current, $value, false ),
     455                esc_html( $label )
     456            );
     457        }
     458        echo '</select>';
     459    }
     460
     461    /**
     462     * Filter products by Preview AI status in the product list.
     463     *
     464     * @param WP_Query $query The current query.
     465     */
     466    public function filter_products_by_preview_ai( $query ) {
     467        global $typenow, $pagenow;
     468
     469        if ( 'edit.php' !== $pagenow || 'product' !== $typenow || ! $query->is_main_query() ) {
     470            return;
     471        }
     472
     473        if ( ! isset( $_GET['preview_ai_status'] ) || '' === $_GET['preview_ai_status'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     474            return;
     475        }
     476
     477        $status = sanitize_key( wp_unslash( $_GET['preview_ai_status'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     478
     479        $meta_query = $query->get( 'meta_query' );
     480        if ( ! is_array( $meta_query ) ) {
     481            $meta_query = array();
     482        }
     483
     484        $global_enabled = get_option( 'preview_ai_enabled', 0 );
     485
     486        switch ( $status ) {
     487            case 'not_analyzed':
     488                $meta_query[] = array(
     489                    'key'     => '_preview_ai_supported',
     490                    'compare' => 'NOT EXISTS',
     491                );
     492                break;
     493
     494            case 'not_supported':
     495                $meta_query[] = array(
     496                    'key'   => '_preview_ai_supported',
     497                    'value' => 'no',
     498                );
     499                break;
     500
     501            case 'active':
     502                if ( $global_enabled ) {
     503                    // Global ON: active = supported AND not explicitly disabled.
     504                    $meta_query[] = array(
     505                        'key'   => '_preview_ai_supported',
     506                        'value' => 'yes',
     507                    );
     508                    $meta_query[] = array(
     509                        'relation' => 'OR',
     510                        array(
     511                            'key'     => '_preview_ai_enabled',
     512                            'compare' => 'NOT EXISTS',
     513                        ),
     514                        array(
     515                            'key'     => '_preview_ai_enabled',
     516                            'value'   => 'no',
     517                            'compare' => '!=',
     518                        ),
     519                    );
     520                } else {
     521                    // Global OFF: active = supported AND explicitly enabled.
     522                    $meta_query[] = array(
     523                        'key'   => '_preview_ai_supported',
     524                        'value' => 'yes',
     525                    );
     526                    $meta_query[] = array(
     527                        'key'   => '_preview_ai_enabled',
     528                        'value' => 'yes',
     529                    );
     530                }
     531                break;
     532
     533            case 'disabled':
     534                if ( $global_enabled ) {
     535                    // Global ON: disabled = supported AND explicitly disabled.
     536                    $meta_query['relation'] = 'AND';
     537                    $meta_query[]           = array(
     538                        'key'   => '_preview_ai_supported',
     539                        'value' => 'yes',
     540                    );
     541                    $meta_query[]           = array(
     542                        'key'   => '_preview_ai_enabled',
     543                        'value' => 'no',
     544                    );
     545                } else {
     546                    // Global OFF: disabled = supported AND not explicitly enabled.
     547                    $meta_query['relation'] = 'AND';
     548                    $meta_query[]           = array(
     549                        'key'   => '_preview_ai_supported',
     550                        'value' => 'yes',
     551                    );
     552                    $meta_query[]           = array(
     553                        'relation' => 'OR',
     554                        array(
     555                            'key'     => '_preview_ai_enabled',
     556                            'compare' => 'NOT EXISTS',
     557                        ),
     558                        array(
     559                            'key'     => '_preview_ai_enabled',
     560                            'value'   => 'yes',
     561                            'compare' => '!=',
     562                        ),
     563                    );
     564                }
     565                break;
     566        }
     567
     568        $query->set( 'meta_query', $meta_query );
     569    }
     570
     571    /**
     572     * Make Preview AI column sortable.
     573     *
     574     * @param array $columns Sortable columns.
     575     * @return array
     576     */
     577    public function make_column_sortable( $columns ) {
     578        $columns['preview_ai'] = 'preview_ai';
     579        return $columns;
     580    }
     581
     582    /**
     583     * Handle sorting by Preview AI column.
     584     *
     585     * Uses named meta_query clauses with EXISTS/NOT EXISTS so WordPress
     586     * performs a LEFT JOIN, including products without the meta key
     587     * (i.e. "Not Analyzed" products).
     588     *
     589     * @param WP_Query $query The current query.
     590     */
     591    public function sort_by_preview_ai( $query ) {
     592        if ( ! is_admin() || ! $query->is_main_query() ) {
     593            return;
     594        }
     595
     596        if ( 'preview_ai' !== $query->get( 'orderby' ) ) {
     597            return;
     598        }
     599
     600        $meta_query = $query->get( 'meta_query' );
     601        if ( ! is_array( $meta_query ) ) {
     602            $meta_query = array();
     603        }
     604
     605        $meta_query['relation']               = 'OR';
     606        $meta_query['preview_ai_has_status']   = array(
     607            'key'     => '_preview_ai_supported',
     608            'compare' => 'EXISTS',
     609        );
     610        $meta_query['preview_ai_no_status']    = array(
     611            'key'     => '_preview_ai_supported',
     612            'compare' => 'NOT EXISTS',
     613        );
     614
     615        $query->set( 'meta_query', $meta_query );
     616        $query->set( 'orderby', 'preview_ai_has_status' );
     617    }
     618
     619    /**
    431620     * Add Preview AI column to product list.
    432621     */
     
    457646            printf(
    458647                '<span class="preview-ai-col preview-ai-col--pending" title="%s"><span class="dashicons dashicons-clock"></span> %s</span>',
    459                 esc_attr__( 'Not analyzed yet - run Learn Catalog', 'preview-ai' ),
     648                esc_attr__( 'Not analyzed yet - run Analyze & Enable from settings', 'preview-ai' ),
    460649                esc_html__( 'Not Analyzed', 'preview-ai' )
    461650            );
     
    496685        }
    497686    }
     687
     688    // =========================================================================
     689    // Bulk Actions
     690    // =========================================================================
     691
     692    /**
     693     * Option keys for background bulk-activate processing.
     694     */
     695    const BULK_ACTIVATE_STATUS_OPTION   = 'preview_ai_bulk_activate_status';
     696    const BULK_ACTIVATE_PROGRESS_OPTION = 'preview_ai_bulk_activate_progress';
     697    const BULK_ACTIVATE_PENDING_OPTION  = 'preview_ai_bulk_activate_pending';
     698
     699    /**
     700     * Batch size for background bulk-activate processing.
     701     */
     702    const BULK_ACTIVATE_BATCH_SIZE = 50;
     703
     704    /**
     705     * Register Preview AI bulk actions in the product list dropdown.
     706     *
     707     * @param array $actions Existing bulk actions.
     708     * @return array
     709     */
     710    public function register_bulk_actions( $actions ) {
     711        $actions['preview_ai_enable']  = __( 'Enable Preview AI', 'preview-ai' );
     712        $actions['preview_ai_disable'] = __( 'Disable Preview AI', 'preview-ai' );
     713        return $actions;
     714    }
     715
     716    /**
     717     * Handle Preview AI bulk actions.
     718     *
     719     * @param string $redirect_to Current redirect URL.
     720     * @param string $action      The bulk action being processed.
     721     * @param array  $post_ids    Array of selected post IDs.
     722     * @return string Modified redirect URL.
     723     */
     724    public function handle_bulk_actions( $redirect_to, $action, $post_ids ) {
     725        if ( 'preview_ai_disable' === $action ) {
     726            return $this->handle_bulk_disable( $redirect_to, $post_ids );
     727        }
     728
     729        if ( 'preview_ai_enable' === $action ) {
     730            return $this->handle_bulk_enable( $redirect_to, $post_ids );
     731        }
     732
     733        return $redirect_to;
     734    }
     735
     736    /**
     737     * Handle bulk disable: just toggle meta locally (no API call).
     738     *
     739     * @param string $redirect_to Redirect URL.
     740     * @param array  $post_ids    Selected product IDs.
     741     * @return string
     742     */
     743    private function handle_bulk_disable( $redirect_to, $post_ids ) {
     744        foreach ( $post_ids as $post_id ) {
     745            update_post_meta( absint( $post_id ), '_preview_ai_enabled', 'no' );
     746        }
     747
     748        set_transient(
     749            'preview_ai_bulk_result_' . get_current_user_id(),
     750            array(
     751                'action'         => 'disable',
     752                'disabled_count' => count( $post_ids ),
     753            ),
     754            120
     755        );
     756
     757        return $redirect_to;
     758    }
     759
     760    /**
     761     * Handle bulk enable: toggle already-analyzed products locally,
     762     * send unanalyzed products to the backend for classification.
     763     *
     764     * @param string $redirect_to Redirect URL.
     765     * @param array  $post_ids    Selected product IDs.
     766     * @return string
     767     */
     768    private function handle_bulk_enable( $redirect_to, $post_ids ) {
     769        $already_supported = array();
     770        $not_supported     = 0;
     771        $need_analysis     = array();
     772
     773        // Step 1: Categorize products.
     774        foreach ( $post_ids as $post_id ) {
     775            $supported = get_post_meta( $post_id, '_preview_ai_supported', true );
     776
     777            if ( 'yes' === $supported ) {
     778                $already_supported[] = $post_id;
     779            } elseif ( 'no' === $supported ) {
     780                $not_supported++;
     781            } else {
     782                // Not analyzed yet — may need backend classification.
     783                $product = wc_get_product( $post_id );
     784                if ( $product && $product->get_image_id() ) {
     785                    $need_analysis[] = $post_id;
     786                } else {
     787                    $not_supported++;
     788                }
     789            }
     790        }
     791
     792        // Step 2: Enable already-analyzed supported products immediately.
     793        foreach ( $already_supported as $post_id ) {
     794            update_post_meta( $post_id, '_preview_ai_enabled', 'yes' );
     795        }
     796
     797        $enabled_count  = count( $already_supported );
     798        $error_message  = '';
     799        $pending_count  = 0;
     800
     801        // Step 3: Analyze unanalyzed products via backend.
     802        if ( ! empty( $need_analysis ) ) {
     803            // Build data only for the first batch (avoid loading all products at once).
     804            $first_batch_ids = array_splice( $need_analysis, 0, self::BULK_ACTIVATE_BATCH_SIZE );
     805            $first_batch     = $this->build_products_data( $first_batch_ids );
     806
     807            $result = $this->process_activate_batch_sync( $first_batch );
     808
     809            $enabled_count += $result['enabled'];
     810            $not_supported += $result['not_supported'];
     811
     812            if ( ! empty( $result['error_message'] ) ) {
     813                // Backend error (e.g. 405 free tier) — don't schedule remaining.
     814                $error_message = $result['error_message'];
     815            } elseif ( ! empty( $need_analysis ) ) {
     816                // First batch succeeded and there are more products — schedule in background.
     817                // Store only IDs (lightweight); data is built lazily per batch.
     818                $this->schedule_bulk_activate( $need_analysis );
     819                $pending_count = count( $need_analysis );
     820            }
     821        }
     822
     823        set_transient(
     824            'preview_ai_bulk_result_' . get_current_user_id(),
     825            array(
     826                'action'          => 'enable',
     827                'enabled_count'   => $enabled_count,
     828                'not_supported'   => $not_supported,
     829                'pending_count'   => $pending_count,
     830                'error_message'   => $error_message,
     831            ),
     832            120
     833        );
     834
     835        return $redirect_to;
     836    }
     837
     838    /**
     839     * Build product data array for the backend API from product IDs.
     840     *
     841     * @param array $product_ids Array of product IDs.
     842     * @return array Products data formatted for the API.
     843     */
     844    private function build_products_data( $product_ids ) {
     845        $products_data = array();
     846
     847        foreach ( $product_ids as $product_id ) {
     848            $product = wc_get_product( $product_id );
     849            if ( ! $product ) {
     850                continue;
     851            }
     852
     853            $categories     = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'names' ) );
     854            $categories_str = is_array( $categories ) ? implode( ', ', $categories ) : '';
     855            $tags           = wp_get_post_terms( $product_id, 'product_tag', array( 'fields' => 'names' ) );
     856            $tags_str       = is_array( $tags ) ? implode( ', ', $tags ) : '';
     857            $thumbnail_id   = $product->get_image_id();
     858            $thumbnail_url  = $thumbnail_id ? wp_get_attachment_url( $thumbnail_id ) : null;
     859
     860            $product_data = array(
     861                'id'            => $product_id,
     862                'title'         => $product->get_name(),
     863                'categories'    => $categories_str,
     864                'tags'          => $tags_str,
     865                'thumbnail_url' => $thumbnail_url,
     866                'variations'    => array(),
     867            );
     868
     869            // Add variations with different images.
     870            if ( $product->is_type( 'variable' ) ) {
     871                $variation_ids = $product->get_children();
     872                foreach ( $variation_ids as $variation_id ) {
     873                    $variation = wc_get_product( $variation_id );
     874                    if ( ! $variation || ! $variation->is_in_stock() ) {
     875                        continue;
     876                    }
     877
     878                    $var_image_id = $variation->get_image_id();
     879                    if ( $var_image_id && $var_image_id !== $thumbnail_id ) {
     880                        $var_thumbnail_url            = wp_get_attachment_url( $var_image_id );
     881                        $product_data['variations'][] = array(
     882                            'variation_id'  => $variation_id,
     883                            'thumbnail_url' => $var_thumbnail_url,
     884                        );
     885                    }
     886                }
     887            }
     888
     889            $products_data[] = $product_data;
     890        }
     891
     892        return $products_data;
     893    }
     894
     895    /**
     896     * Process a batch of products synchronously via the activate API.
     897     *
     898     * @param array $products_data Products data for the API.
     899     * @return array Results with 'enabled', 'not_supported', 'error_message' keys.
     900     */
     901    private function process_activate_batch_sync( $products_data ) {
     902        $result = array(
     903            'enabled'       => 0,
     904            'not_supported' => 0,
     905            'error_message' => '',
     906        );
     907
     908        $api      = new PREVIEW_AI_Api();
     909        $response = $api->activate_products( $products_data );
     910
     911        if ( is_wp_error( $response ) ) {
     912            $result['error_message'] = $response->get_error_message();
     913            return $result;
     914        }
     915
     916        // Save classifications and enable supported products.
     917        $catalog = new PREVIEW_AI_Admin_Catalog();
     918        $stats   = $catalog->save_catalog_classifications( $response );
     919
     920        // Enable supported products that were just classified.
     921        if ( ! empty( $stats['configured_ids'] ) ) {
     922            foreach ( $stats['configured_ids'] as $product_id ) {
     923                update_post_meta( $product_id, '_preview_ai_enabled', 'yes' );
     924            }
     925        }
     926
     927        $result['enabled']       = $stats['configured'];
     928        $result['not_supported'] = $stats['not_supported'];
     929
     930        return $result;
     931    }
     932
     933    /**
     934     * Schedule remaining products for background activation via Action Scheduler.
     935     *
     936     * Stores only product IDs (lightweight). Product data is built lazily
     937     * in each batch to avoid loading hundreds of products at once.
     938     *
     939     * @param array $product_ids Array of product IDs to process.
     940     */
     941    public function schedule_bulk_activate( $product_ids ) {
     942        update_option( self::BULK_ACTIVATE_PENDING_OPTION, array_map( 'absint', $product_ids ), false );
     943        update_option(
     944            self::BULK_ACTIVATE_PROGRESS_OPTION,
     945            array(
     946                'total'         => count( $product_ids ),
     947                'processed'     => 0,
     948                'enabled'       => 0,
     949                'not_supported' => 0,
     950                'errors'        => 0,
     951            ),
     952            false
     953        );
     954        update_option( self::BULK_ACTIVATE_STATUS_OPTION, 'processing', false );
     955
     956        if ( function_exists( 'as_schedule_single_action' ) ) {
     957            as_schedule_single_action( time() + 2, 'preview_ai_process_bulk_activate_batch' );
     958        } else {
     959            // No Action Scheduler — mark as completed (first batch already processed sync).
     960            update_option( self::BULK_ACTIVATE_STATUS_OPTION, 'completed', false );
     961            delete_option( self::BULK_ACTIVATE_PENDING_OPTION );
     962        }
     963    }
     964
     965    /**
     966     * Process a batch of bulk-activate products in the background.
     967     *
     968     * Invoked by Action Scheduler. Takes BULK_ACTIVATE_BATCH_SIZE product IDs,
     969     * builds product data lazily, classifies them, enables supported ones,
     970     * and schedules the next batch.
     971     */
     972    public function process_bulk_activate_batch() {
     973        $pending_ids = get_option( self::BULK_ACTIVATE_PENDING_OPTION, array() );
     974        $progress    = get_option( self::BULK_ACTIVATE_PROGRESS_OPTION, array() );
     975
     976        if ( empty( $pending_ids ) ) {
     977            update_option( self::BULK_ACTIVATE_STATUS_OPTION, 'completed', false );
     978            delete_option( self::BULK_ACTIVATE_PENDING_OPTION );
     979            return;
     980        }
     981
     982        // Take next batch of IDs and build product data lazily.
     983        $batch_ids = array_splice( $pending_ids, 0, self::BULK_ACTIVATE_BATCH_SIZE );
     984        update_option( self::BULK_ACTIVATE_PENDING_OPTION, $pending_ids, false );
     985
     986        $batch  = $this->build_products_data( $batch_ids );
     987        $result = $this->process_activate_batch_sync( $batch );
     988
     989        $progress['processed']     += count( $batch_ids );
     990        $progress['enabled']       += $result['enabled'];
     991        $progress['not_supported'] += $result['not_supported'];
     992        if ( ! empty( $result['error_message'] ) ) {
     993            $progress['errors']++;
     994        }
     995
     996        update_option( self::BULK_ACTIVATE_PROGRESS_OPTION, $progress, false );
     997
     998        if ( ! empty( $pending_ids ) && empty( $result['error_message'] ) && function_exists( 'as_schedule_single_action' ) ) {
     999            as_schedule_single_action( time() + 2, 'preview_ai_process_bulk_activate_batch' );
     1000        } else {
     1001            update_option( self::BULK_ACTIVATE_STATUS_OPTION, 'completed', false );
     1002            delete_option( self::BULK_ACTIVATE_PENDING_OPTION );
     1003        }
     1004    }
     1005
     1006    /**
     1007     * Show admin notice with bulk action results.
     1008     */
     1009    public function show_bulk_action_notice() {
     1010        $screen = get_current_screen();
     1011        if ( ! $screen || 'edit-product' !== $screen->id ) {
     1012            return;
     1013        }
     1014
     1015        // Show immediate results from transient.
     1016        $transient_key = 'preview_ai_bulk_result_' . get_current_user_id();
     1017        $result        = get_transient( $transient_key );
     1018
     1019        if ( $result ) {
     1020            delete_transient( $transient_key );
     1021            $this->render_bulk_result_notice( $result );
     1022        }
     1023
     1024        // Show background processing status.
     1025        $bg_status = get_option( self::BULK_ACTIVATE_STATUS_OPTION, 'idle' );
     1026
     1027        if ( 'processing' === $bg_status ) {
     1028            $progress = get_option( self::BULK_ACTIVATE_PROGRESS_OPTION, array() );
     1029            printf(
     1030                '<div class="notice notice-info is-dismissible"><p>%s</p></div>',
     1031                sprintf(
     1032                    /* translators: 1: processed count, 2: total count */
     1033                    esc_html__( 'Preview AI: Analyzing products in background... %1$d of %2$d processed.', 'preview-ai' ),
     1034                    intval( $progress['processed'] ?? 0 ),
     1035                    intval( $progress['total'] ?? 0 )
     1036                )
     1037            );
     1038        } elseif ( 'completed' === $bg_status ) {
     1039            $progress = get_option( self::BULK_ACTIVATE_PROGRESS_OPTION, array() );
     1040
     1041            if ( ! empty( $progress ) ) {
     1042                $parts = array();
     1043
     1044                if ( ! empty( $progress['enabled'] ) ) {
     1045                    $parts[] = sprintf(
     1046                        /* translators: %d: number of products enabled */
     1047                        _n( '%d product enabled', '%d products enabled', $progress['enabled'], 'preview-ai' ),
     1048                        $progress['enabled']
     1049                    );
     1050                }
     1051                if ( ! empty( $progress['not_supported'] ) ) {
     1052                    $parts[] = sprintf(
     1053                        /* translators: %d: number of products not supported */
     1054                        _n( '%d not supported', '%d not supported', $progress['not_supported'], 'preview-ai' ),
     1055                        $progress['not_supported']
     1056                    );
     1057                }
     1058                if ( ! empty( $progress['errors'] ) ) {
     1059                    $parts[] = sprintf(
     1060                        /* translators: %d: number of errors */
     1061                        _n( '%d error', '%d errors', $progress['errors'], 'preview-ai' ),
     1062                        $progress['errors']
     1063                    );
     1064                }
     1065
     1066                if ( ! empty( $parts ) ) {
     1067                    printf(
     1068                        '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
     1069                        /* translators: %s: summary of bulk activation results */
     1070                        esc_html( sprintf( __( 'Preview AI bulk activation complete: %s.', 'preview-ai' ), implode( ', ', $parts ) ) )
     1071                    );
     1072                }
     1073            }
     1074
     1075            // Clean up.
     1076            update_option( self::BULK_ACTIVATE_STATUS_OPTION, 'idle', false );
     1077            delete_option( self::BULK_ACTIVATE_PROGRESS_OPTION );
     1078        }
     1079    }
     1080
     1081    /**
     1082     * Render the immediate bulk result admin notice.
     1083     *
     1084     * @param array $result Result data from transient.
     1085     */
     1086    private function render_bulk_result_notice( $result ) {
     1087        if ( 'disable' === $result['action'] ) {
     1088            $count = intval( $result['disabled_count'] ?? 0 );
     1089            printf(
     1090                '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
     1091                sprintf(
     1092                    /* translators: %d: number of products disabled */
     1093                    esc_html( _n(
     1094                        'Preview AI: %d product disabled.',
     1095                        'Preview AI: %d products disabled.',
     1096                        $count,
     1097                        'preview-ai'
     1098                    ) ),
     1099                    $count
     1100                )
     1101            );
     1102            return;
     1103        }
     1104
     1105        // Enable action.
     1106        $parts = array();
     1107
     1108        $enabled = intval( $result['enabled_count'] ?? 0 );
     1109        if ( $enabled > 0 ) {
     1110            $parts[] = sprintf(
     1111                /* translators: %d: number of products enabled */
     1112                _n( '%d product enabled', '%d products enabled', $enabled, 'preview-ai' ),
     1113                $enabled
     1114            );
     1115        }
     1116
     1117        $not_supported = intval( $result['not_supported'] ?? 0 );
     1118        if ( $not_supported > 0 ) {
     1119            $parts[] = sprintf(
     1120                /* translators: %d: number of products not supported */
     1121                _n( '%d not supported', '%d not supported', $not_supported, 'preview-ai' ),
     1122                $not_supported
     1123            );
     1124        }
     1125
     1126        $pending = intval( $result['pending_count'] ?? 0 );
     1127        if ( $pending > 0 ) {
     1128            $parts[] = sprintf(
     1129                /* translators: %d: number of products being analyzed */
     1130                _n( '%d more product being analyzed in background', '%d more products being analyzed in background', $pending, 'preview-ai' ),
     1131                $pending
     1132            );
     1133        }
     1134
     1135        $error_message = $result['error_message'] ?? '';
     1136        $notice_type   = empty( $error_message ) ? 'success' : 'warning';
     1137
     1138        $message = '';
     1139        if ( ! empty( $parts ) ) {
     1140            $message = sprintf( __( 'Preview AI: %s.', 'preview-ai' ), implode( ', ', $parts ) );
     1141        }
     1142
     1143        if ( ! empty( $error_message ) ) {
     1144            if ( ! empty( $message ) ) {
     1145                $message .= ' ';
     1146            }
     1147            $message .= $error_message;
     1148        }
     1149
     1150        if ( ! empty( $message ) ) {
     1151            printf(
     1152                '<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
     1153                esc_attr( $notice_type ),
     1154                esc_html( $message )
     1155            );
     1156        }
     1157    }
    4981158}
    4991159
  • preview-ai/trunk/admin/class-preview-ai-admin.php

    r3446676 r3455295  
    133133    }
    134134
     135    public function add_product_filter_dropdown() {
     136        $this->product->add_product_filter_dropdown();
     137    }
     138
     139    public function filter_products_by_preview_ai( $query ) {
     140        $this->product->filter_products_by_preview_ai( $query );
     141    }
     142
     143    public function make_column_sortable( $columns ) {
     144        return $this->product->make_column_sortable( $columns );
     145    }
     146
     147    public function sort_by_preview_ai( $query ) {
     148        $this->product->sort_by_preview_ai( $query );
     149    }
     150
    135151    public function handle_toggle_product() {
    136152        $this->product->handle_toggle_product();
     153    }
     154
     155    public function register_bulk_actions( $actions ) {
     156        return $this->product->register_bulk_actions( $actions );
     157    }
     158
     159    public function handle_bulk_actions( $redirect_to, $action, $post_ids ) {
     160        return $this->product->handle_bulk_actions( $redirect_to, $action, $post_ids );
     161    }
     162
     163    public function process_bulk_activate_batch() {
     164        $this->product->process_bulk_activate_batch();
     165    }
     166
     167    public function show_bulk_action_notice() {
     168        $this->product->show_bulk_action_notice();
    137169    }
    138170
     
    243275                    'elementorSearch'      => __( 'Search for "Preview AI" widget', 'preview-ai' ),
    244276                    'configureIn'          => __( 'Configure in: Products → Preview AI → Widget tab', 'preview-ai' ),
    245                     'analyzingBackground'  => __( 'Analyzing in background', 'preview-ai' ),
    246                     'productsAnalyzed'     => __( 'products are being analyzed. This may take a few minutes.', 'preview-ai' ),
     277                    'analyzingBackground'  => __( 'Analyzing and enabling in background', 'preview-ai' ),
     278                    'productsAnalyzed'     => __( 'products are being analyzed and enabled. This may take a few minutes.', 'preview-ai' ),
    247279                    'closeAndCheck'        => __( 'You can close this window and check progress in Preview AI settings.', 'preview-ai' ),
    248280                    'closeAndContinue'     => __( 'Close & Continue', 'preview-ai' ),
     
    250282                    'experienceMagic'      => __( 'See how your customers will experience the magic!', 'preview-ai' ),
    251283                    'closeAndConfigure'    => __( 'Close & Configure Products', 'preview-ai' ),
    252                     'catalogConfigured'    => __( 'Catalog configured!', 'preview-ai' ),
    253                     'productsReady'        => __( 'products ready for preview', 'preview-ai' ),
     284                    'catalogConfigured'    => __( 'Catalog analyzed and enabled!', 'preview-ai' ),
     285                    'productsReady'        => __( 'products ready for virtual try-on', 'preview-ai' ),
    254286                    'couldNotAnalyze'      => __( 'Could not analyze catalog', 'preview-ai' ),
    255287                    'manualConfig'         => __( 'You can configure products manually.', 'preview-ai' ),
    256288                    'continueToSettings'   => __( 'Continue to Settings', 'preview-ai' ),
    257289                    'couldNotConnect'      => __( 'Could not connect to server', 'preview-ai' ),
    258                     'analyzeLater'         => __( 'You can analyze your catalog later from settings.', 'preview-ai' ),
     290                    'analyzeLater'         => __( 'You can analyze and enable your catalog later from settings.', 'preview-ai' ),
    259291                    'continue'             => __( 'Continue', 'preview-ai' ),
    260292                ),
     
    287319                    'activating'   => __( 'Activating...', 'preview-ai' ),
    288320                    'activated'    => __( 'Preview AI activated! Redirecting...', 'preview-ai' ),
    289                     'analyzing'    => __( 'Analyzing your catalog...', 'preview-ai' ),
     321                    'analyzing'    => __( 'Analyzing and enabling products...', 'preview-ai' ),
    290322                    'catalogStatus' => PREVIEW_AI_Admin::get_catalog_analysis_status()['status'],
    291323                ),
  • preview-ai/trunk/admin/css/preview-ai-admin.css

    r3446676 r3455295  
    582582}
    583583
    584 /* Learn Catalog Section */
     584/* Analyze & Enable Catalog Section */
    585585.preview-ai-learn-catalog {
    586586    margin-top: 30px;
  • preview-ai/trunk/admin/js/preview-ai-admin.js

    r3446676 r3455295  
    7272        }
    7373
    74         // Learn My Catalog functional with background processing support.
     74        // Analyze & Enable Catalog with background processing support.
    7575        var learnBtn = document.getElementById( 'preview_ai_learn_catalog_btn' );
    7676        var loadingEl = document.getElementById( 'preview_ai_learn_catalog_loading' );
  • preview-ai/trunk/admin/partials/preview-ai-admin-display.php

    r3454451 r3455295  
    308308            <?php submit_button(); ?>
    309309
    310             <!-- Learn My Catalog Section -->
     310            <!-- Analyze & Enable Catalog Section -->
    311311            <?php
    312312            $preview_ai_catalog_status = PREVIEW_AI_Admin::get_catalog_analysis_status();
     
    320320            <div class="preview-ai-learn-catalog">
    321321                <h2 class="preview-ai-catalog-title">
    322                     🧠 <?php esc_html_e( 'Learn My Catalog (AI)', 'preview-ai' ); ?>
     322                    <?php esc_html_e( 'Analyze & Enable Catalog', 'preview-ai' ); ?>
    323323                </h2>
    324324                <p class="preview-ai-catalog-desc">
    325                     <?php esc_html_e( 'Preview AI will automatically detect what type of product each one is (t-shirts, dresses, belts, earrings, fanny packs…).', 'preview-ai' ); ?>
    326                     <br><strong>
    327                     <?php esc_html_e( 'This will analyze your catalog and assign the appropriate product type to each product.', 'preview-ai' ); ?></strong>
     325                    <?php esc_html_e( 'Automatically detect what type each product is (t-shirts, dresses, pants, accessories…) and enable virtual try-on for all supported products.', 'preview-ai' ); ?>
    328326                </p>
    329327                <p class="preview-ai-catalog-note">
    330                     <?php esc_html_e( 'Nothing will be modified in your store. Only recommendations will be assigned.', 'preview-ai' ); ?>
     328                    <?php esc_html_e( 'Unsupported products will be marked but not modified.', 'preview-ai' ); ?>
    331329                </p>
    332330               
     
    344342
    345343                <button type="button" id="preview_ai_learn_catalog_btn" class="button button-primary" <?php echo ( $preview_ai_is_processing || ! $preview_ai_is_compatible ) ? 'disabled' : ''; ?>>
    346                     <span class="dashicons dashicons-welcome-learn-more"></span>
    347                     <?php esc_html_e( 'Analyze My Catalog', 'preview-ai' ); ?>
     344                    <span class="dashicons dashicons-search"></span>
     345                    <?php esc_html_e( 'Analyze & Enable Products', 'preview-ai' ); ?>
    348346                </button>
    349347
     
    360358                            );
    361359                        } else {
    362                             esc_html_e( 'Analyzing your catalog...', 'preview-ai' );
     360                            esc_html_e( 'Analyzing and enabling products...', 'preview-ai' );
    363361                        }
    364362                        ?>
  • preview-ai/trunk/includes/class-preview-ai-api.php

    r3446676 r3455295  
    284284
    285285        return $this->request( 'catalog/preflight', $preflight_data, 30 );
     286    }
     287
     288    /**
     289     * Activate products via bulk action (classify unanalyzed products).
     290     *
     291     * Uses the /catalog/activate endpoint which:
     292     * - Always blocks free tier (405).
     293     * - Has no product count limits for paid tiers.
     294     *
     295     * @param array $products_data Array of products with id, title, categories, tags, thumbnail_url.
     296     * @return array|WP_Error      Response data with classifications or error.
     297     */
     298    public function activate_products( $products_data ) {
     299        PREVIEW_AI_Logger::debug( 'Starting bulk activate', array(
     300            'product_count' => count( $products_data ),
     301        ) );
     302
     303        $result = $this->request( 'catalog/activate', array(
     304            'products' => $products_data,
     305        ), 120 );
     306
     307        if ( ! is_wp_error( $result ) ) {
     308            PREVIEW_AI_Logger::info( 'Bulk activate completed', array(
     309                'total_analyzed' => $result['total_analyzed'] ?? 0,
     310            ) );
     311        }
     312
     313        return $result;
    286314    }
    287315
  • preview-ai/trunk/includes/class-preview-ai.php

    r3446676 r3455295  
    215215        $this->loader->add_filter( 'manage_edit-product_columns', $plugin_admin, 'add_product_column' );
    216216        $this->loader->add_action( 'manage_product_posts_custom_column', $plugin_admin, 'render_product_column', 10, 2 );
     217
     218        // Product list filter and sorting.
     219        $this->loader->add_action( 'restrict_manage_posts', $plugin_admin, 'add_product_filter_dropdown' );
     220        $this->loader->add_action( 'pre_get_posts', $plugin_admin, 'filter_products_by_preview_ai' );
     221        $this->loader->add_filter( 'manage_edit-product_sortable_columns', $plugin_admin, 'make_column_sortable' );
     222        $this->loader->add_action( 'pre_get_posts', $plugin_admin, 'sort_by_preview_ai' );
     223
     224        // Product list bulk actions.
     225        $this->loader->add_filter( 'bulk_actions-edit-product', $plugin_admin, 'register_bulk_actions' );
     226        $this->loader->add_filter( 'handle_bulk_actions-edit-product', $plugin_admin, 'handle_bulk_actions', 10, 3 );
     227        $this->loader->add_action( 'admin_notices', $plugin_admin, 'show_bulk_action_notice' );
     228
     229        // Action Scheduler hook for background bulk-activate processing.
     230        $this->loader->add_action( 'preview_ai_process_bulk_activate_batch', $plugin_admin, 'process_bulk_activate_batch' );
    217231
    218232        // Admin AJAX handlers.
  • preview-ai/trunk/languages/preview-ai.pot

    r3454904 r3455295  
    524524
    525525#: admin/partials/preview-ai-admin-display.php:322
    526 msgid "Learn My Catalog (AI)"
     526msgid "Analyze & Enable Catalog"
    527527msgstr ""
    528528
    529529#: admin/partials/preview-ai-admin-display.php:325
    530 msgid "Preview AI will automatically detect what type of product each one is (t-shirts, dresses, belts, earrings, fanny packs…)."
    531 msgstr ""
    532 
    533 #: admin/partials/preview-ai-admin-display.php:327
    534 msgid "This will analyze your catalog and assign the appropriate product type to each product."
    535 msgstr ""
    536 
    537 #: admin/partials/preview-ai-admin-display.php:330
    538 msgid "Nothing will be modified in your store. Only recommendations will be assigned."
    539 msgstr ""
    540 
    541 #: admin/partials/preview-ai-admin-display.php:339
     530msgid "Automatically detect what type each product is (t-shirts, dresses, pants, accessories…) and enable virtual try-on for all supported products."
     531msgstr ""
     532
     533#: admin/partials/preview-ai-admin-display.php:329
     534msgid "Unsupported products will be marked but not modified."
     535msgstr ""
     536
     537#: admin/partials/preview-ai-admin-display.php:338
    542538msgid "Re-verify compatibility"
    543539msgstr ""
    544540
    545 #: admin/partials/preview-ai-admin-display.php:347
    546 msgid "Analyze My Catalog"
     541#: admin/partials/preview-ai-admin-display.php:346
     542msgid "Analyze & Enable Products"
    547543msgstr ""
    548544
     
    663659msgstr ""
    664660
    665 #: admin/class-preview-ai-admin.php:340
    666 #: admin/class-preview-ai-admin.php:549
    667 msgid "Analyzing your product catalog..."
    668 msgstr ""
    669 
    670 #: admin/class-preview-ai-admin.php:436
    671 msgid "Catalog configured!"
     661#: admin/class-preview-ai-admin-onboarding.php:30
     662msgid "Analyzing and enabling products..."
     663msgstr ""
     664
     665#: admin/class-preview-ai-admin.php:284
     666msgid "Catalog analyzed and enabled!"
     667msgstr ""
     668
     669#: admin/class-preview-ai-admin-catalog.php:63
     670msgid "All products have already been analyzed and enabled. No new products to process."
     671msgstr ""
     672
     673#: admin/class-preview-ai-admin-catalog.php:89
     674#, php-format
     675msgid "Analyzing and enabling %d products in background..."
     676msgstr ""
     677
     678#: admin/class-preview-ai-admin-catalog.php:125
     679#: admin/class-preview-ai-admin-catalog.php:238
     680#, php-format
     681msgid "%1$d products enabled. %2$d not supported."
     682msgstr ""
     683
     684#: admin/class-preview-ai-admin-catalog.php:225
     685#, php-format
     686msgid "Processing... %1$d of %2$d products analyzed."
     687msgstr ""
     688
     689#: admin/class-preview-ai-admin.php:277
     690msgid "Analyzing and enabling in background"
     691msgstr ""
     692
     693#: admin/class-preview-ai-admin.php:278
     694msgid "products are being analyzed and enabled. This may take a few minutes."
     695msgstr ""
     696
     697#: admin/class-preview-ai-admin.php:285
     698msgid "products ready for virtual try-on"
     699msgstr ""
     700
     701#: admin/class-preview-ai-admin.php:290
     702msgid "You can analyze and enable your catalog later from settings."
    672703msgstr ""
    673704
     
    759790
    760791#: admin/class-preview-ai-admin-product.php:459
    761 msgid "Not analyzed yet - run Learn Catalog"
     792msgid "Not analyzed yet - run Analyze & Enable from settings"
    762793msgstr ""
    763794
  • preview-ai/trunk/preview-ai.php

    r3454904 r3455295  
    1010 * Plugin URI:        https://previewai.app/
    1111 * Description:       Preview AI is a plugin that allows your customers to preview your products in real-time using AI image generation.
    12  * Version:           1.0.4
     12 * Version:           1.1.0
    1313 * Author:            Preview AI
    1414 * Author URI:        https://profiles.wordpress.org/previewai/
     
    2727 * Current plugin version.
    2828 */
    29 define( 'PREVIEW_AI_VERSION', '1.0.4' );
     29define( 'PREVIEW_AI_VERSION', '1.1.0' );
    3030define( 'PREVIEW_AI_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
    3131
  • preview-ai/trunk/readme.txt

    r3454904 r3455295  
    88WC requires at least: 8.0
    99WC tested up to: 10.4
    10 Stable tag: 1.0.4
     10Stable tag: 1.1.0
    1111License: GPLv2 or later
    1212License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    1515
    1616== Description ==
     17
     18https://www.youtube.com/watch?v=_IGk4fOwGDs
    1719
    1820Preview AI is an AI-powered Virtual Try-On plugin for WooCommerce that helps fashion stores increase conversion rates and reduce returns by allowing customers to preview how a product may look on them before buying 👕✨
     
    155157== Changelog ==
    156158
     159= 1.1.0 =
     160– Added Bulk Actions to enable or disable Preview AI for multiple products at once.
     161– Added filtering by Preview AI status (Active, Disabled, Not Analyzed, Not Supported) in the product list.
     162– Added sorting capability for the Preview AI column in the product list.
     163– Improved scalability for large catalogs using background processing for bulk activation.
     164
    157165= 1.0.4 =
    158166– Added full internationalization support (i18n)
     
    175183== Upgrade Notice ==
    176184
     185= 1.1.0 =
     186New bulk actions and filtering options for easier catalog management.
     187
    177188= 1.0.0 =
    178189Initial stable release.
Note: See TracChangeset for help on using the changeset viewer.