readiness_score = $readiness_score;
}
/**
* Register the meta box hooks.
*
* @return void
*/
public function register(): void {
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
add_action( 'save_post_product', array( $this, 'on_product_save' ), 20 );
}
/**
* Add the AI Readiness meta box to the product edit screen.
*
* @return void
*/
public function add_meta_box(): void {
add_meta_box(
'aicommerce-readiness',
__( 'AI Readiness Score', 'ai-discoverability-kit' ),
array( $this, 'render' ),
'product',
'side',
'high'
);
add_meta_box(
'aicommerce-identifiers',
__( 'Tyralix AI Discoverability — Product Identifiers', 'ai-discoverability-kit' ),
array( $this, 'render_identifiers' ),
'product',
'normal',
'default'
);
}
/**
* Render the meta box content.
*
* @param \WP_Post $post The post object.
*
* @return void
*/
public function render( \WP_Post $post ): void {
$product = wc_get_product( $post->ID );
if ( ! $product instanceof \WC_Product ) {
printf(
'
%s
',
esc_html__( 'Save the product first to see your AI Readiness Score.', 'ai-discoverability-kit' )
);
return;
}
$cached = $this->readiness_score->get_cached( $product->get_id() );
$score = $cached ?? $this->readiness_score->calculate( $product );
$this->render_score_display( $score );
$this->render_sub_scores( $score );
$this->render_fix_queue( $score );
// Nonce for AJAX refresh.
wp_nonce_field( 'aicommerce_meta_box', 'aicommerce_meta_nonce' );
}
/**
* Render the main score circle.
*
* @param array $score Score data.
*
* @return void
*/
private function render_score_display( array $score ): void {
$overall = $score['overall'] ?? 0;
$grade = $score['grade'] ?? 'F';
$color = $this->grade_color( $grade );
printf(
'',
esc_attr( $color ),
(int) $overall,
esc_html__( 'Grade:', 'ai-discoverability-kit' ),
esc_html( $grade )
);
}
/**
* Render sub-score bars.
*
* @param array $score Score data.
*
* @return void
*/
private function render_sub_scores( array $score ): void {
if ( empty( $score['sub_scores'] ) ) {
return;
}
echo '';
foreach ( $score['sub_scores'] as $sub ) {
$bar_color = $sub['score'] >= 80 ? '#00a32a' : ( $sub['score'] >= 50 ? '#dba617' : '#d63638' );
printf(
'
',
esc_html( $sub['label'] ),
(int) $sub['score'],
esc_attr( $bar_color )
);
}
echo '
';
}
/**
* Render the priority fix queue.
*
* @param array $score Score data.
*
* @return void
*/
private function render_fix_queue( array $score ): void {
if ( empty( $score['fix_queue'] ) ) {
printf(
'✓ %s
',
esc_html__( 'All checks passing!', 'ai-discoverability-kit' )
);
return;
}
$fixes = array_slice( $score['fix_queue'], 0, 5 ); // Show top 5.
printf(
'%s',
esc_html__( 'Priority Fixes:', 'ai-discoverability-kit' )
);
echo '
';
foreach ( $fixes as $fix ) {
printf(
'- %s
',
esc_html( $fix['action'] )
);
}
echo '
';
}
/**
* Render the product identifiers meta box.
*
* @param \WP_Post $post The post object.
*
* @return void
*/
public function render_identifiers( \WP_Post $post ): void {
$product = wc_get_product( $post->ID );
$brand = '';
$gtin = '';
$mpn = '';
if ( $product instanceof \WC_Product ) {
$brand = $product->get_meta( '_aicommerce_brand' );
$gtin = $product->get_meta( '_aicommerce_gtin' );
$mpn = $product->get_meta( '_aicommerce_mpn' );
}
wp_nonce_field( 'aicommerce_identifiers', 'aicommerce_identifiers_nonce' );
printf(
'',
esc_html__( 'Brand', 'ai-discoverability-kit' ),
esc_attr( $brand ),
esc_html__( 'Product brand name. Overrides the default brand from settings.', 'ai-discoverability-kit' ),
esc_html__( 'GTIN / UPC / EAN', 'ai-discoverability-kit' ),
esc_attr( $gtin ),
esc_html__( 'Global Trade Item Number (barcode). Used in Product schema for Google and AI agents.', 'ai-discoverability-kit' ),
esc_html__( 'MPN', 'ai-discoverability-kit' ),
esc_attr( $mpn ),
esc_html__( 'Manufacturer Part Number. Relevant for branded/manufactured products.', 'ai-discoverability-kit' )
);
}
/**
* Recalculate and cache score on product save.
*
* @param int $post_id The post ID.
*
* @return void
*/
public function on_product_save( int $post_id ): void {
// Skip autosaves and revisions.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return;
}
// Save product identifiers if the nonce is valid.
if ( isset( $_POST['aicommerce_identifiers_nonce'] )
&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aicommerce_identifiers_nonce'] ) ), 'aicommerce_identifiers' )
) {
$fields = array(
'aicommerce_brand' => '_aicommerce_brand',
'aicommerce_gtin' => '_aicommerce_gtin',
'aicommerce_mpn' => '_aicommerce_mpn',
);
foreach ( $fields as $post_key => $meta_key ) {
if ( isset( $_POST[ $post_key ] ) ) {
$value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
$product->update_meta_data( $meta_key, $value );
}
}
$product->save_meta_data();
}
$this->readiness_score->calculate_and_cache( $product );
}
/**
* Get the CSS color for a letter grade.
*
* @param string $grade The letter grade.
*
* @return string Hex color code.
*/
private function grade_color( string $grade ): string {
return match ( $grade ) {
'A+', 'A' => '#00a32a',
'B' => '#2271b1',
'C' => '#dba617',
'D' => '#d63638',
default => '#a7aaad',
};
}
}