'; echo '
'; echo ''; echo ''; echo '
'; echo '
'; echo ''; echo '
'; echo ''; // Overlay — pointer-events:none so table stays interactive. echo ''; $this->render_inline_script(); } /** * Render a populated drawer for a specific call (server-side variant). * * Used when building drawer HTML server-side (e.g., for AJAX endpoint). * Security: only prompt_text and response_text shown — never headers. * * Side effects: Outputs HTML. * * @param array $meta Call metadata row. * @param array|null $io I/O record or null (pruned/missing). * * @return void */ public function render_content( array $meta, ?array $io ): void { $id = (int) $meta['id']; $captured = (string) $meta['captured_at']; $peptide = (string) $meta['peptide_slug']; $model = (string) $meta['model']; $intent = (string) $meta['intent_label']; $http_status = (int) ( $meta['http_status'] ?? 200 ); $is_error = $http_status >= 400; $cited = isset( $meta['cited'] ) ? $meta['cited'] : null; $tok_in = isset( $meta['tokens_in'] ) ? (int) $meta['tokens_in'] : null; $tok_out = isset( $meta['tokens_out'] ) ? (int) $meta['tokens_out'] : null; $latency_ms = isset( $meta['latency_ms'] ) ? (int) $meta['latency_ms'] : null; $cost_usd = (float) ( $meta['cost_usd'] ?? 0.0 ); $run_id = (string) ( $meta['run_id'] ?? '' ); $config_ver = isset( $meta['config_version'] ) ? (int) $meta['config_version'] : null; // Determine drawer state. $is_legacy = ( null === $io && ! $is_error ); $is_pruned = false; // Detection: io null + not legacy = checked by caller. $has_io = ( null !== $io ); $vis_row_null = empty( $meta['visibility_row'] ); if ( null === $io && ! $is_error ) { // Could be pruned or legacy (pre-v0.3). Distinguish by config_version. if ( null === $config_ver || $config_ver < 3 ) { $is_legacy = true; } else { $is_pruned = true; $is_legacy = false; } } // Header. echo '
'; echo '' . esc_html( sprintf( /* translators: %d: call ID */ __( 'Call #%d', 'pr-vision' ), $id ) ) . ''; echo '' . esc_html( $captured ) . ' UTC'; echo '
'; echo '
'; echo '' . esc_html( $peptide ) . ''; if ( '' !== $intent ) { echo ' · ' . esc_html( $intent ) . ''; } echo '
'; // Verdict chip. if ( $is_error ) { echo '✕ Error'; } elseif ( null !== $cited ) { if ( (bool) $cited ) { echo '✓ Cited'; } else { echo '– Not yet'; } } // Metadata section. echo '
'; echo '
' . esc_html__( 'Model', 'pr-vision' ) . '
' . esc_html( $model ) . '
'; $tok_str = ( null !== $tok_in && null !== $tok_out ) ? sprintf( '↑ %s in / ↓ %s out', number_format( $tok_in ), number_format( $tok_out ) ) : '—'; echo '
' . esc_html__( 'Tokens', 'pr-vision' ) . '
' . esc_html( $tok_str ) . '
'; echo '
' . esc_html__( 'Latency', 'pr-vision' ) . '
'; echo null !== $latency_ms ? esc_html( number_format( $latency_ms ) ) . 'ms' : '—'; echo '
'; echo '
' . esc_html__( 'Cost', 'pr-vision' ) . '
$' . esc_html( number_format( $cost_usd, 6 ) ) . '
'; if ( '' !== $run_id ) { echo '
' . esc_html__( 'Run ID', 'pr-vision' ) . '
' . esc_html( substr( $run_id, 0, 8 ) ) . '…
'; } if ( null !== $config_ver ) { echo '
' . esc_html__( 'Config', 'pr-vision' ) . '
v' . esc_html( (string) $config_ver ) . '
'; } if ( $is_error ) { echo '
' . esc_html__( 'HTTP Status', 'pr-vision' ) . '
' . esc_html( (string) $http_status ) . '
'; } echo '
'; // Prompt section. echo '
'; echo '
'; echo ''; if ( $has_io && isset( $io['prompt_text'] ) && '' !== $io['prompt_text'] ) { echo ''; } echo '
'; if ( $is_legacy ) { $this->render_not_captured_notice( $captured ); } elseif ( $is_pruned ) { $this->render_pruned_notice( $captured ); } elseif ( $has_io && isset( $io['prompt_text'] ) ) { echo '
' . esc_html( $io['prompt_text'] ) . '
'; } else { echo '

—

'; } echo '
'; // Response section. echo '
'; echo '
'; echo ''; if ( $has_io && isset( $io['response_text'] ) && '' !== $io['response_text'] ) { echo ''; } echo '
'; if ( $is_error ) { echo ''; } elseif ( ! $is_legacy && ! $is_pruned && $has_io && isset( $io['response_text'] ) && '' !== $io['response_text'] ) { echo '
' . esc_html( $io['response_text'] ) . '
'; } elseif ( ! $is_error && ! $is_legacy && ! $is_pruned ) { echo '

—

'; } echo '
'; } /** * Render the "pruned" I/O notice (aged out, not an error). * * Side effects: Outputs HTML. * * @param string $captured_at Timestamp when the call was captured. * * @return void */ private function render_pruned_notice( string $captured_at ): void { echo '
'; echo ''; echo '
'; echo '' . esc_html__( 'Prompt and response pruned', 'pr-vision' ) . '
'; echo esc_html( sprintf( /* translators: 1: retention days, 2: captured date */ __( 'Raw I/O is kept for %1$d days, then auto-deleted. This call was recorded on %2$s UTC.', 'pr-vision' ), PRV_Config::get_io_retention_days(), $captured_at ) ); echo '
'; } /** * Render the "not captured" notice (pre-v0.3 row, never stored). * * Distinct from the pruned state — different copy prevents confusion. * Side effects: Outputs HTML. * * @param string $captured_at Timestamp when the call was captured. * * @return void */ private function render_not_captured_notice( string $captured_at ): void { echo '
'; echo ''; echo '
'; echo '' . esc_html__( 'Not captured for this call', 'pr-vision' ) . '
'; echo esc_html__( 'This call was recorded before the call-inspector feature was added. Prompt and response were never stored — this is not a retention prune.', 'pr-vision' ); echo '
'; } /** * Emit the inline JavaScript for drawer open/close/copy behaviour. * * Security: data is fetched from a server endpoint that enforces the * capture allowlist — no keys or headers can appear in drawer content. * Side effects: Outputs script tag. * * @return void */ private function render_inline_script(): void { PRV_Call_Drawer_Script::render( wp_create_nonce( PRV_Call_Detail_Ajax::NONCE_ACTION ), admin_url( 'admin-ajax.php' ) ); } }