provider ) { if ( ! class_exists( 'AILM_OpenAI_Provider' ) ) { require_once dirname( dirname( __DIR__ ) ) . '/modules/ai-core/providers/class-ailm-openai-provider.php'; } $this->provider = new AILM_OpenAI_Provider(); } return $this->provider; } // ------------------------------------------------------------------------- // AJAX: Step 1 — collect graph // ------------------------------------------------------------------------- /** * Builds the full link graph from AILM_Link_Repository and stores it * in a transient for the subsequent compute step. * The JS client calls this first (offset=0), then checks done=true on offset=1. */ public function ajax_collect_batch(): void { check_ajax_referer( 'ailm_orphan_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Permission denied.' ); } $offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0; if ( 0 === $offset ) { delete_transient( self::GRAPH_TRANSIENT ); $graph = AILM_Link_Repository::get_click_enriched_graph(); set_transient( self::GRAPH_TRANSIENT, $graph, HOUR_IN_SECONDS ); wp_send_json_success( [ 'done' => false, 'next_offset' => 1, 'processed' => count( $graph['nodes'] ), ] ); } wp_send_json_success( [ 'done' => true ] ); } // ------------------------------------------------------------------------- // AJAX: Step 2 — dispatch to SaaS and persist results // ------------------------------------------------------------------------- /** * Retrieves the graph from the transient, serializes it, sends it to the * SaaS compute endpoint, and saves the returned scores to postmeta. */ public function ajax_compute(): void { check_ajax_referer( 'ailm_orphan_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Permission denied.' ); } $graph = get_transient( self::GRAPH_TRANSIENT ); if ( ! $graph || empty( $graph['nodes'] ) ) { wp_send_json_error( 'No graph data found. Please collect data first.' ); } // Determine front page ID for Click Depth BFS on the server. $front_page_id = (int) get_option( 'page_on_front' ); if ( ! $front_page_id ) { $front_page_id = (int) get_option( 'page_for_posts' ); } // Convert node keys to strings (JSON requires string keys). $nodes = array_fill_keys( array_map( 'strval', array_keys( $graph['nodes'] ) ), 0 ); // Convert edges: [ from_id => [ to_id => weight ] ] with string keys. $edges = []; foreach ( $graph['edges'] as $from_id => $targets ) { $str_from = (string) $from_id; $edges[ $str_from ] = []; foreach ( $targets as $to_id => $weight ) { $edges[ $str_from ][ (string) $to_id ] = (float) $weight; } } $response = $this->get_provider()->saas_compute_pagerank( $nodes, $edges, $front_page_id ); if ( is_wp_error( $response ) ) { wp_send_json_error( 'SaaS error: ' . $response->get_error_message() ); } if ( empty( $response['scores'] ) || ! is_array( $response['scores'] ) ) { wp_send_json_error( 'Invalid response from SaaS server.' ); } // Persist scores returned by SaaS. foreach ( $response['scores'] as $post_id => $score ) { update_post_meta( (int) $post_id, '_ailm_pagerank', (float) $score ); } delete_transient( self::GRAPH_TRANSIENT ); // Purge dashboard caches so the new scores appear immediately. wp_cache_delete( 'ailm_dashboard_frozen_maps', 'ailm_cache_group' ); wp_cache_delete( 'ailm_dashboard_manual_links', 'ailm_cache_group' ); wp_cache_delete( 'ailm_dashboard_internal_links', 'ailm_cache_group' ); wp_send_json_success( [ 'message' => 'AILM Rank successfully computed.', 'node_count' => $response['node_count'] ?? count( $response['scores'] ), ] ); } }