'; echo '

' . esc_html__( 'LinkCortex', 'linkcortex' ) . '

'; echo '
  1. ' . esc_html__( 'Scan', 'linkcortex' ) . ' ' . esc_html__( 'AI reads your pages and suggests links.', 'linkcortex' ) . '
  2. ' . esc_html__( 'Review & Apply', 'linkcortex' ) . ' ' . esc_html__( 'Accept suggestions to instantly write links to your pages.', 'linkcortex' ) . '
  3. ' . esc_html__( 'Undo anytime', 'linkcortex' ) . ' ' . esc_html__( 'One-click undo from the Applied list.', 'linkcortex' ) . '
'; if ( $is_admin ) { Onboarding::render( $kpis ); } $budget = $kpis['budget']; if ( $budget['reached'] ) { echo '

' . esc_html__( 'The estimated monthly AI budget is used up. AI scans are paused until you raise the cap or a new month starts. Everything else keeps working.', 'linkcortex' ) . '

'; } elseif ( $budget['alert'] ) { /* translators: %s: percent. */ echo '

' . esc_html( sprintf( __( 'You have used %s%% of this month\'s estimated AI budget.', 'linkcortex' ), number_format_i18n( (int) round( $budget['percent'] ) ) ) ) . '

'; } self::render_kpis( $kpis ); $status = Availability::cached(); echo '
'; if ( $is_admin && Availability::is_available() ) { echo ''; echo '

' . esc_html__( 'You will see the estimated cost and can cancel before anything is sent to AI. Pages that have not changed since the last scan are skipped for free.', 'linkcortex' ) . '

'; } elseif ( $is_admin ) { echo '

' . esc_html__( 'AI is not ready yet.', 'linkcortex' ) . ' ' . esc_html( $status ? $status['message'] : __( 'Check the AI connection to start scanning.', 'linkcortex' ) ) . '

'; echo '

' . esc_html__( 'Check AI connection', 'linkcortex' ) . ' '; echo '

'; echo '

' . esc_html__( 'Your page index, link map and orphan report work without AI.', 'linkcortex' ) . '

'; } else { echo '

' . esc_html__( 'An administrator runs scans. You can review suggestions for your own pages.', 'linkcortex' ) . ' ' . esc_html__( 'Review suggestions', 'linkcortex' ) . '

'; } Keywords_Page::progress_box( $is_admin ? Queue::active_run( array( 'scan', 'regenerate', 'keywords', 'index' ) ) : null ); echo '
'; self::render_quick_wins(); echo ''; } /** * KPI numbers. * * @return array */ public static function kpis() { global $wpdb; $t = Activator::tables(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own tables, live numbers. $indexed = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $t['index'] ) ); $pending = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status IN ('pending','accepted')", $t['suggestions'] ) ); $applied = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE reverted_at IS NULL AND applied_at >= %s', $t['applied'], gmdate( 'Y-m-01 00:00:00' ) ) ); $scans = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE type IN ('scan','regenerate')", $t['runs'] ) ); // phpcs:enable return array( 'indexed' => $indexed, 'orphans' => LinkGraph::orphan_count(), 'pending' => $pending, 'applied' => $applied, 'scans' => $scans, 'budget' => Budget::status(), ); } /** * KPI cards. * * @param array $k KPIs. * @return void */ private static function render_kpis( array $k ) { $b = $k['budget']; $spend = $b['cap'] > 0 /* translators: 1: spent, 2: cap. */ ? sprintf( __( '%1$s of %2$s', 'linkcortex' ), Cost_Panel::money( $b['spent'] ), Cost_Panel::money( $b['cap'] ) ) : Cost_Panel::money( $b['spent'] ); $cards = array( array( __( 'Pages indexed', 'linkcortex' ), number_format_i18n( $k['indexed'] ), Menu::url( 'keywords' ), current_user_can( 'edit_others_posts' ) ), array( __( 'Orphan pages', 'linkcortex' ), number_format_i18n( $k['orphans'] ), Menu::url( 'keywords', array( 'view' => 'orphan' ) ), current_user_can( 'edit_others_posts' ), __( 'No other page links to them.', 'linkcortex' ) ), array( __( 'Suggestions to review', 'linkcortex' ), number_format_i18n( $k['pending'] ), Menu::url( 'suggestions' ), true ), array( __( 'Links applied this month', 'linkcortex' ), number_format_i18n( $k['applied'] ), Menu::url( 'suggestions', array( 'status' => 'applied' ) ), true ), array( __( 'Estimated AI spend this month', 'linkcortex' ), $spend, Menu::url( 'settings', array( 'tab' => 'ai' ) ), current_user_can( 'manage_options' ) ), ); echo ''; } /** * Top 20 suggestions by score × the target's inbound-link deficit. * * @return array[] */ public static function quick_wins() { global $wpdb; $t = Activator::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table. $avg = (float) $wpdb->get_var( $wpdb->prepare( 'SELECT AVG(inbound_count) FROM %i', $t['index'] ) ); if ( current_user_can( 'edit_others_posts' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table + posts table. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, tgt.title AS target_title, tgt.inbound_count AS target_inbound, s.score * GREATEST(1, %f + 1 - tgt.inbound_count) AS win FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE s.status = 'pending' ORDER BY win DESC, s.score DESC LIMIT %d", $avg, $t['suggestions'], $t['index'], $t['index'], $wpdb->posts, 20 ), ARRAY_A ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table + posts table. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, tgt.title AS target_title, tgt.inbound_count AS target_inbound, s.score * GREATEST(1, %f + 1 - tgt.inbound_count) AS win FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE s.status = 'pending' AND p.post_author = %d ORDER BY win DESC, s.score DESC LIMIT %d", $avg, $t['suggestions'], $t['index'], $t['index'], $wpdb->posts, (int) get_current_user_id(), 20 ), ARRAY_A ); } return is_array( $rows ) ? $rows : array(); } /** * Quick wins table. * * @return void */ private static function render_quick_wins() { $rows = self::quick_wins(); echo '

' . esc_html__( 'Quick wins', 'linkcortex' ) . '

'; echo '

' . esc_html__( 'The most useful links to add first: strong suggestions that point to pages with few or no links yet.', 'linkcortex' ) . '

'; if ( ! $rows ) { echo '

' . esc_html__( 'No suggestions to review yet. Run a scan to get link ideas.', 'linkcortex' ) . '

'; return; } echo ''; foreach ( array( __( 'Page to edit', 'linkcortex' ), __( 'Link text', 'linkcortex' ), __( 'Links to', 'linkcortex' ), __( 'Relevance', 'linkcortex' ), __( 'Action', 'linkcortex' ) ) as $h ) { echo ''; } echo ''; foreach ( $rows as $r ) { $ctx = Store::split_snippet( (string) $r['context_snippet'] ); $target = esc_html( (string) $r['target_title'] ); if ( 0 === (int) $r['target_inbound'] ) { $target .= ' ' . esc_html__( 'Orphan page', 'linkcortex' ) . ''; } printf( '', esc_html( (string) $r['source_title'] ), esc_html( '' !== $ctx['match'] ? $ctx['match'] : (string) $r['anchor_text'] ), wp_kses_post( $target ), esc_html( (string) (int) $r['score'] ), esc_url( Menu::url( 'suggestions', array( 'source' => (int) $r['source_post_id'], 'min_score' => 0, ) ) ), esc_html__( 'Review', 'linkcortex' ), /* translators: %s: page title. */ esc_html( sprintf( __( 'suggestions for %s', 'linkcortex' ), (string) $r['source_title'] ) ) ); }//end foreach echo '
' . esc_html( $h ) . '
%1$s%2$s%3$s%4$s%6$s %7$s
'; } /** * Hide the onboarding checklist for this user. * * @return void */ public static function dismiss_onboarding() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'linkcortex' ) ); } check_admin_referer( 'ailsug_dismiss_onboarding' ); update_user_meta( get_current_user_id(), 'ailsug_dismissed_onboarding', 1 ); wp_safe_redirect( Menu::url() ); exit; } }