Changeset 3455295
- Timestamp:
- 02/06/2026 11:18:52 AM (8 weeks ago)
- Location:
- preview-ai/trunk
- Files:
-
- 1 added
- 12 edited
-
admin/class-preview-ai-admin-catalog.php (modified) (10 diffs)
-
admin/class-preview-ai-admin-onboarding.php (modified) (1 diff)
-
admin/class-preview-ai-admin-product.php (modified) (3 diffs)
-
admin/class-preview-ai-admin.php (modified) (4 diffs)
-
admin/css/preview-ai-admin.css (modified) (1 diff)
-
admin/js/preview-ai-admin.js (modified) (1 diff)
-
admin/partials/preview-ai-admin-display.php (modified) (4 diffs)
-
includes/class-preview-ai-api.php (modified) (1 diff)
-
includes/class-preview-ai.php (modified) (1 diff)
-
languages/preview-ai-es_ES.po (added)
-
languages/preview-ai.pot (modified) (3 diffs)
-
preview-ai.php (modified) (2 diffs)
-
readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
preview-ai/trunk/admin/class-preview-ai-admin-catalog.php
r3446676 r3455295 21 21 22 22 /** 23 * Handle AJAX request for Learn MyCatalog feature.23 * Handle AJAX request for Analyze & Enable Catalog feature. 24 24 */ 25 25 public function handle_learn_catalog() { … … 61 61 array( 62 62 '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' ), 64 64 'stats' => array( 65 65 'total' => 0, … … 86 86 'message' => sprintf( 87 87 /* translators: %d: number of products */ 88 __( 'Analy sis scheduled for %d products. Processingin background...', 'preview-ai' ),88 __( 'Analyzing and enabling %d products in background...', 'preview-ai' ), 89 89 $total_products 90 90 ), … … 118 118 'total' => $stats['total'], 119 119 'configured' => $stats['configured'], 120 'needs_review' => $stats['needs_review'], 121 'images_analyzed' => $stats['images_analyzed'], 120 'not_supported' => $stats['not_supported'], 122 121 'analysis_errors' => $analysis_errors, 123 122 'try_product_url' => $try_product_url, 124 123 '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' ), 127 126 $stats['configured'], 128 $stats['needs_review'], 129 $stats['images_analyzed'] 127 $stats['not_supported'] 130 128 ), 131 129 ) … … 145 143 'processed' => 0, 146 144 'configured' => 0, 147 'needs_review' => 0, 148 'images_analyzed' => 0, 145 'not_supported' => 0, 149 146 'analysis_errors' => 0, 150 147 'configured_ids' => array(), … … 186 183 $progress['processed'] += count( $batch ); 187 184 $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']; 190 186 $progress['analysis_errors'] += isset( $result['analysis_errors'] ) ? intval( $result['analysis_errors'] ) : 0; 191 187 $progress['configured_ids'] = array_merge( $progress['configured_ids'], $stats['configured_ids'] ); … … 235 231 236 232 $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']; 239 234 $response['analysis_errors'] = $progress['analysis_errors']; 240 235 $response['try_product_url'] = $try_product_url; 241 236 $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' ), 244 239 $progress['configured'], 245 $progress['needs_review'], 246 $progress['images_analyzed'] 240 $progress['not_supported'] 247 241 ); 248 242 … … 429 423 'total' => 0, 430 424 'configured' => 0, 431 'needs_review' => 0, 432 'images_analyzed' => 0, 425 'not_supported' => 0, 433 426 'configured_ids' => array(), 434 427 ); … … 466 459 } else { 467 460 update_post_meta( $product_id, '_preview_ai_enabled', 'no' ); 468 $stats['n eeds_review']++;461 $stats['not_supported']++; 469 462 } 470 463 } else { 471 464 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']++; 492 466 } 493 467 } … … 496 470 } 497 471 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 }523 472 } 524 473 -
preview-ai/trunk/admin/class-preview-ai-admin-onboarding.php
r3446676 r3455295 28 28 <div id="onboarding-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width 0.5s ease;"></div> 29 29 </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> 31 31 </div> 32 32 -
preview-ai/trunk/admin/class-preview-ai-admin-product.php
r3446676 r3455295 429 429 430 430 /** 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 /** 431 620 * Add Preview AI column to product list. 432 621 */ … … 457 646 printf( 458 647 '<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' ), 460 649 esc_html__( 'Not Analyzed', 'preview-ai' ) 461 650 ); … … 496 685 } 497 686 } 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 } 498 1158 } 499 1159 -
preview-ai/trunk/admin/class-preview-ai-admin.php
r3446676 r3455295 133 133 } 134 134 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 135 151 public function handle_toggle_product() { 136 152 $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(); 137 169 } 138 170 … … 243 275 'elementorSearch' => __( 'Search for "Preview AI" widget', 'preview-ai' ), 244 276 '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' ), 247 279 'closeAndCheck' => __( 'You can close this window and check progress in Preview AI settings.', 'preview-ai' ), 248 280 'closeAndContinue' => __( 'Close & Continue', 'preview-ai' ), … … 250 282 'experienceMagic' => __( 'See how your customers will experience the magic!', 'preview-ai' ), 251 283 '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' ), 254 286 'couldNotAnalyze' => __( 'Could not analyze catalog', 'preview-ai' ), 255 287 'manualConfig' => __( 'You can configure products manually.', 'preview-ai' ), 256 288 'continueToSettings' => __( 'Continue to Settings', 'preview-ai' ), 257 289 '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' ), 259 291 'continue' => __( 'Continue', 'preview-ai' ), 260 292 ), … … 287 319 'activating' => __( 'Activating...', 'preview-ai' ), 288 320 'activated' => __( 'Preview AI activated! Redirecting...', 'preview-ai' ), 289 'analyzing' => __( 'Analyzing your catalog...', 'preview-ai' ),321 'analyzing' => __( 'Analyzing and enabling products...', 'preview-ai' ), 290 322 'catalogStatus' => PREVIEW_AI_Admin::get_catalog_analysis_status()['status'], 291 323 ), -
preview-ai/trunk/admin/css/preview-ai-admin.css
r3446676 r3455295 582 582 } 583 583 584 /* LearnCatalog Section */584 /* Analyze & Enable Catalog Section */ 585 585 .preview-ai-learn-catalog { 586 586 margin-top: 30px; -
preview-ai/trunk/admin/js/preview-ai-admin.js
r3446676 r3455295 72 72 } 73 73 74 // Learn My Catalog functionalwith background processing support.74 // Analyze & Enable Catalog with background processing support. 75 75 var learnBtn = document.getElementById( 'preview_ai_learn_catalog_btn' ); 76 76 var loadingEl = document.getElementById( 'preview_ai_learn_catalog_loading' ); -
preview-ai/trunk/admin/partials/preview-ai-admin-display.php
r3454451 r3455295 308 308 <?php submit_button(); ?> 309 309 310 <!-- Learn MyCatalog Section -->310 <!-- Analyze & Enable Catalog Section --> 311 311 <?php 312 312 $preview_ai_catalog_status = PREVIEW_AI_Admin::get_catalog_analysis_status(); … … 320 320 <div class="preview-ai-learn-catalog"> 321 321 <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' ); ?> 323 323 </h2> 324 324 <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' ); ?> 328 326 </p> 329 327 <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' ); ?> 331 329 </p> 332 330 … … 344 342 345 343 <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' ); ?> 348 346 </button> 349 347 … … 360 358 ); 361 359 } else { 362 esc_html_e( 'Analyzing your catalog...', 'preview-ai' );360 esc_html_e( 'Analyzing and enabling products...', 'preview-ai' ); 363 361 } 364 362 ?> -
preview-ai/trunk/includes/class-preview-ai-api.php
r3446676 r3455295 284 284 285 285 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; 286 314 } 287 315 -
preview-ai/trunk/includes/class-preview-ai.php
r3446676 r3455295 215 215 $this->loader->add_filter( 'manage_edit-product_columns', $plugin_admin, 'add_product_column' ); 216 216 $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' ); 217 231 218 232 // Admin AJAX handlers. -
preview-ai/trunk/languages/preview-ai.pot
r3454904 r3455295 524 524 525 525 #: admin/partials/preview-ai-admin-display.php:322 526 msgid " Learn My Catalog (AI)"526 msgid "Analyze & Enable Catalog" 527 527 msgstr "" 528 528 529 529 #: 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 530 msgid "Automatically detect what type each product is (t-shirts, dresses, pants, accessories…) and enable virtual try-on for all supported products." 531 msgstr "" 532 533 #: admin/partials/preview-ai-admin-display.php:329 534 msgid "Unsupported products will be marked but not modified." 535 msgstr "" 536 537 #: admin/partials/preview-ai-admin-display.php:338 542 538 msgid "Re-verify compatibility" 543 539 msgstr "" 544 540 545 #: admin/partials/preview-ai-admin-display.php:34 7546 msgid "Analyze My Catalog"541 #: admin/partials/preview-ai-admin-display.php:346 542 msgid "Analyze & Enable Products" 547 543 msgstr "" 548 544 … … 663 659 msgstr "" 664 660 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 662 msgid "Analyzing and enabling products..." 663 msgstr "" 664 665 #: admin/class-preview-ai-admin.php:284 666 msgid "Catalog analyzed and enabled!" 667 msgstr "" 668 669 #: admin/class-preview-ai-admin-catalog.php:63 670 msgid "All products have already been analyzed and enabled. No new products to process." 671 msgstr "" 672 673 #: admin/class-preview-ai-admin-catalog.php:89 674 #, php-format 675 msgid "Analyzing and enabling %d products in background..." 676 msgstr "" 677 678 #: admin/class-preview-ai-admin-catalog.php:125 679 #: admin/class-preview-ai-admin-catalog.php:238 680 #, php-format 681 msgid "%1$d products enabled. %2$d not supported." 682 msgstr "" 683 684 #: admin/class-preview-ai-admin-catalog.php:225 685 #, php-format 686 msgid "Processing... %1$d of %2$d products analyzed." 687 msgstr "" 688 689 #: admin/class-preview-ai-admin.php:277 690 msgid "Analyzing and enabling in background" 691 msgstr "" 692 693 #: admin/class-preview-ai-admin.php:278 694 msgid "products are being analyzed and enabled. This may take a few minutes." 695 msgstr "" 696 697 #: admin/class-preview-ai-admin.php:285 698 msgid "products ready for virtual try-on" 699 msgstr "" 700 701 #: admin/class-preview-ai-admin.php:290 702 msgid "You can analyze and enable your catalog later from settings." 672 703 msgstr "" 673 704 … … 759 790 760 791 #: admin/class-preview-ai-admin-product.php:459 761 msgid "Not analyzed yet - run Learn Catalog"792 msgid "Not analyzed yet - run Analyze & Enable from settings" 762 793 msgstr "" 763 794 -
preview-ai/trunk/preview-ai.php
r3454904 r3455295 10 10 * Plugin URI: https://previewai.app/ 11 11 * 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.412 * Version: 1.1.0 13 13 * Author: Preview AI 14 14 * Author URI: https://profiles.wordpress.org/previewai/ … … 27 27 * Current plugin version. 28 28 */ 29 define( 'PREVIEW_AI_VERSION', '1. 0.4' );29 define( 'PREVIEW_AI_VERSION', '1.1.0' ); 30 30 define( 'PREVIEW_AI_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); 31 31 -
preview-ai/trunk/readme.txt
r3454904 r3455295 8 8 WC requires at least: 8.0 9 9 WC tested up to: 10.4 10 Stable tag: 1. 0.410 Stable tag: 1.1.0 11 11 License: GPLv2 or later 12 12 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 15 15 16 16 == Description == 17 18 https://www.youtube.com/watch?v=_IGk4fOwGDs 17 19 18 20 Preview 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 👕✨ … … 155 157 == Changelog == 156 158 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 157 165 = 1.0.4 = 158 166 – Added full internationalization support (i18n) … … 175 183 == Upgrade Notice == 176 184 185 = 1.1.0 = 186 New bulk actions and filtering options for easier catalog management. 187 177 188 = 1.0.0 = 178 189 Initial stable release.
Note: See TracChangeset
for help on using the changeset viewer.