$data Payload from PRV_Ai_Visibility_Collector.
*
* @return void
*/
public function render( array $data ): void {
$trendline = is_array( $data['trendline'] ?? null ) ? $data['trendline'] : array();
$standings = is_array( $data['standings'] ?? null ) ? $data['standings'] : array();
$last_run_at = isset( $data['last_run_at'] ) && $data['last_run_at'] ? esc_html( (string) $data['last_run_at'] ) : esc_html__( 'Never', 'pr-vision' );
$mtd_cost = isset( $data['mtd_cost_usd'] ) ? (float) $data['mtd_cost_usd'] : 0.0;
$cap = isset( $data['monthly_cap_usd'] ) ? (float) $data['monthly_cap_usd'] : PRV_DEFAULT_MONTHLY_BUDGET_USD;
$this->render_proxy_note();
$this->render_meta_bar( $last_run_at, $mtd_cost, $cap );
$this->render_trendline( $trendline );
$this->render_standings( $standings );
}
/**
* {@inheritDoc}
*/
public function get_title(): string {
return __( 'AI Visibility', 'pr-vision' );
}
/**
* Render the proxy-metric disclaimer note.
*
* @return void
*/
private function render_proxy_note(): void {
echo '
';
echo '' . esc_html__( 'Note:', 'pr-vision' ) . ' ';
echo esc_html__( 'Scores are measured via API probes — a directional proxy, not the consumer ChatGPT/Gemini apps (different retrieval, personalisation, and system prompts).', 'pr-vision' );
echo '
';
}
/**
* Render the run metadata bar (last run, cost, cap).
*
* @param string $last_run_at Formatted last-run timestamp or "Never".
* @param float $mtd_cost Month-to-date spend in USD.
* @param float $cap Monthly cap in USD.
*
* @return void
*/
private function render_meta_bar( string $last_run_at, float $mtd_cost, float $cap ): void {
$pct = $cap > 0 ? min( 100.0, round( $mtd_cost / $cap * 100, 1 ) ) : 0;
echo '';
echo '' . esc_html__( 'Last run:', 'pr-vision' ) . ' ' . esc_html( $last_run_at ) . '';
echo '' . esc_html__( 'MTD cost:', 'pr-vision' ) . ' $' . esc_html( number_format( $mtd_cost, 4 ) ) . ' / $' . esc_html( number_format( $cap, 2 ) ) . ' (' . esc_html( $pct ) . '% of cap)';
echo '
';
}
/**
* Render the Chart.js visibility score trendline.
*
* @param array $trendline Trendline data points.
*
* @return void
*/
private function render_trendline( array $trendline ): void {
echo '' . esc_html__( 'Visibility Score — Trend', 'pr-vision' ) . '
';
if ( empty( $trendline ) ) {
echo '
' . esc_html__( 'No probe runs recorded yet. Click "Run now" to collect the first data point.', 'pr-vision' ) . '
';
return;
}
$labels = array();
$scores = array();
foreach ( $trendline as $point ) {
$labels[] = esc_js( gmdate( 'M j', (int) strtotime( $point['captured_at'] ) ) );
$scores[] = (float) $point['score'];
}
$labels_json = wp_json_encode( $labels );
$scores_json = wp_json_encode( $scores );
echo '';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '';
echo '';
}
/**
* Render the per-peptide current standings table.
*
* @param array $standings Per-peptide standing data.
*
* @return void
*/
private function render_standings( array $standings ): void {
echo '' . esc_html__( 'Current Standings', 'pr-vision' ) . '
';
if ( empty( $standings ) ) {
echo '
' . esc_html__( 'No standings data yet.', 'pr-vision' ) . '
';
return;
}
echo '';
echo '';
echo '| ' . esc_html__( 'Peptide', 'pr-vision' ) . ' | ';
echo '' . esc_html__( 'Cited?', 'pr-vision' ) . ' | ';
echo '' . esc_html__( 'Our Position', 'pr-vision' ) . ' | ';
echo '' . esc_html__( 'Top Competing Domains', 'pr-vision' ) . ' | ';
echo '
';
foreach ( $standings as $row ) {
$cited_icon = $row['cited'] ? '✅' : '❌';
$position = null !== $row['our_position'] ? '#' . (int) $row['our_position'] : '—';
$domains = implode( ', ', array_map( 'esc_html', (array) $row['top_domains'] ) );
echo '';
echo '| ' . esc_html( (string) $row['label'] ) . ' | ';
echo '' . $cited_icon . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- only emoji
echo '' . esc_html( $position ) . ' | ';
echo '' . wp_kses_post( $domains ) . ' | ';
echo '
';
}
echo '
';
}
}