0 renders lime. * Status/verdict are never signalled by colour alone (chip + label). * * Who triggers: PRV_Call_Log_Page::render_page(). * Dependencies: PRV_Costs_Renderer (for subnav helper), PRV_Config. * * @see class-prv-call-log-page.php — Caller, passes data array. * @see class-prv-call-drawer-renderer.php — Drawer renderer (emitted below table). * @see class-prv-costs-renderer.php — render_subnav() shared helper. * @package PrVision */ class PRV_Call_Log_Table_Renderer { /** * Render the full Call Log page including table and drawer. * * Side effects: Outputs HTML. * * @param array $data Keys: rows, total, pages, page, filters. * * @return void */ public function render( array $data ): void { $rows = (array) $data['rows']; $total = (int) $data['total']; $pages = (int) $data['pages']; $page = (int) $data['page']; $filters = (array) $data['filters']; $subnav = new PRV_Costs_Renderer(); echo '
'; $subnav->render_subnav( 'calls' ); echo '
'; echo '

' . esc_html__( 'Call Log', 'pr-vision' ) . '

'; $retention_days = PRV_Config::get_io_retention_days(); echo '

'; echo esc_html( sprintf( /* translators: %d: retention days */ __( 'Prompt + response kept %d days. Cost + metadata kept indefinitely.', 'pr-vision' ), $retention_days ) ); echo '

'; echo '
'; $this->render_filter_bar( $filters ); echo '
'; if ( $total > 0 ) { $from = ( ( $page - 1 ) * PRV_Call_Log_Query::PAGE_SIZE ) + 1; $to = min( $page * PRV_Call_Log_Query::PAGE_SIZE, $total ); echo esc_html( sprintf( /* translators: 1: from, 2: to, 3: total */ __( 'Showing %1$d–%2$d of %3$d calls', 'pr-vision' ), $from, $to, $total ) ); } else { echo esc_html__( 'No calls recorded yet.', 'pr-vision' ); } echo '
'; $this->render_table( $rows ); $this->render_pagination( $page, $pages, $filters ); // Emit drawer (hidden until a row is clicked). $drawer = new PRV_Call_Drawer_Renderer(); $drawer->render_container(); echo '
'; // .wrap } /** * Render the filter bar with 4 select controls and Clear button. * * Side effects: Outputs HTML. * * @param array $filters Active filter values. * * @return void */ private function render_filter_bar( array $filters ): void { $base_url = admin_url( 'admin.php?page=pr-vision-calls' ); echo '
'; echo ''; // Model filter. echo ''; echo ''; // Peptide filter. echo ''; echo ''; // Date range. echo ''; echo ''; echo ''; echo ''; // Status filter. $status_opts = array( '' => __( 'All statuses', 'pr-vision' ), 'cited' => __( 'Cited', 'pr-vision' ), 'not_cited' => __( 'Not cited', 'pr-vision' ), 'error' => __( 'Error', 'pr-vision' ), ); echo ''; echo ''; echo ''; echo '' . esc_html__( 'Clear', 'pr-vision' ) . ''; echo '
'; } /** * Render the calls table. * * Side effects: Outputs HTML. * * @param array> $rows Table rows from query. * * @return void */ private function render_table( array $rows ): void { echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ( empty( $rows ) ) { echo ''; } foreach ( $rows as $row ) { $id = (int) $row['id']; $is_error = ( (int) $row['http_status'] >= 400 ); $cited_null = ( null === $row['cited'] ); $is_cited = ! $cited_null && (bool) $row['cited']; $latency_ms = isset( $row['latency_ms'] ) ? (int) $row['latency_ms'] : null; $lat_warn = ( null !== $latency_ms && $latency_ms > 8000 ) ? ' style="color:#FFB36E"' : ''; $status_chip = $is_error ? '✕ Error' : 'OK'; if ( $is_error ) { $cited_chip = '– Error'; } elseif ( $cited_null ) { $cited_chip = '– —'; } elseif ( $is_cited ) { $cited_chip = '✓ Cited'; } else { $cited_chip = '– Not yet'; } echo ''; echo ''; echo ''; echo ''; echo ''; $tok_in = isset( $row['tokens_in'] ) && null !== $row['tokens_in'] ? (int) $row['tokens_in'] : null; $tok_out = isset( $row['tokens_out'] ) && null !== $row['tokens_out'] ? (int) $row['tokens_out'] : null; $tok_str = ( null !== $tok_in && null !== $tok_out ) ? '↑' . esc_html( number_format( $tok_in ) ) . ' ↓' . esc_html( number_format( $tok_out ) ) : '—'; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- arrows are HTML entities; numbers are escaped above echo ''; echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from safe parts above echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from safe parts above echo ''; } echo '
' . esc_html__( 'Timestamp', 'pr-vision' ) . '' . esc_html__( 'Peptide', 'pr-vision' ) . '' . esc_html__( 'Intent', 'pr-vision' ) . '' . esc_html__( 'Model', 'pr-vision' ) . '' . esc_html__( 'Tokens', 'pr-vision' ) . '' . esc_html__( 'Latency', 'pr-vision' ) . '' . esc_html__( 'Cost', 'pr-vision' ) . '' . esc_html__( 'Cited', 'pr-vision' ) . '' . esc_html__( 'Status', 'pr-vision' ) . '
' . esc_html__( 'No calls recorded yet.', 'pr-vision' ) . '
' . esc_html( (string) $row['captured_at'] ) . '' . esc_html( (string) $row['peptide_slug'] ) . '' . esc_html( (string) $row['intent_label'] ) . '' . esc_html( (string) $row['model'] ) . '' . $tok_str . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static style attr echo null !== $latency_ms ? esc_html( number_format( $latency_ms ) ) . 'ms' : '—'; echo '$' . esc_html( number_format( (float) $row['cost_usd'], 6 ) ) . '' . $cited_chip . '' . $status_chip . '
'; } /** * Render pagination bar. * * Disabled buttons use colour/border, not opacity (WCAG AA). * Side effects: Outputs HTML. * * @param int $page Current page (1-based). * @param int $pages Total pages. * @param array $filters Active filters for link building. * * @return void */ private function render_pagination( int $page, int $pages, array $filters ): void { if ( $pages <= 1 ) { return; } $base = admin_url( 'admin.php' ); $params = array( 'page' => 'pr-vision-calls' ); foreach ( $filters as $k => $v ) { if ( '' !== $v ) { $params[ 'prv_filter_' . ltrim( $k, 'prv_filter_' ) ] = $v; } } echo ''; } }