// V2 per-item detail page — ported from EvalReportItemPage.tsx import { Page, View, Text } from '@react-pdf/renderer'; import { styles, colors } from '../styles.js'; import type { EvaluationResult, CriterionScore, RubricRuleScore } from './types.js'; interface EvalReportItemPageProps { result: EvaluationResult; itemNumber: number; totalItems: number; } const severityColors: Record = { High: '#3a4389', high: '#3a4389', Medium: '#4752a3', medium: '#4752a3', Low: '#7c85c0', low: '#7c85c0', }; function parseTurnsForExcerpt(text: string, maxTurns: number = 3): Array<{ role: string; content: string }> { const turns: Array<{ role: string; content: string }> = []; const lines = text.split('\n'); let currentRole: string | null = null; let currentContent: string[] = []; for (const line of lines) { if (line.startsWith('User:')) { if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() }); currentRole = 'User'; currentContent = [line.slice(5).trim()]; } else if (line.startsWith('Agent:')) { if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() }); currentRole = 'Agent'; currentContent = [line.slice(6).trim()]; } else if (currentRole) { currentContent.push(line); } } if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() }); return turns.slice(0, maxTurns); } function CriterionScoreRow({ criterion }: { criterion: CriterionScore }) { return ( {criterion.passed ? 'P' : 'F'} {criterion.criterion} {criterion.reasoning && ( {criterion.reasoning.slice(0, 250)}{criterion.reasoning.length > 250 ? '...' : ''} )} {Math.round(criterion.score * 100)}% ); } function RubricScoreRow({ rule }: { rule: RubricRuleScore }) { const sevColor = severityColors[rule.severity] || colors.muted; return ( {rule.passed ? 'P' : 'F'} {rule.rule_name} {rule.severity} {rule.reasoning && ( {rule.reasoning.slice(0, 250)}{rule.reasoning.length > 250 ? '...' : ''} )} {Math.round(rule.score * 100)}% ); } export function EvalReportItemPage({ result, itemNumber, totalItems }: EvalReportItemPageProps) { const itemName = result.item_name || result.input.message || `Test Item ${itemNumber}`; const itemType = result.item_type || 'single_turn'; const criteriaScores = result.criteria_scores || []; const rubricScores = result.rubric_scores || []; const overallPassed = result.passed ?? (result.criteria_passed !== false && result.rubric_passed !== false); const criteriaPassed = criteriaScores.filter(c => c.passed).length; const rubricPassed = rubricScores.filter(r => r.passed).length; const transcript = (itemType === 'scenario' || itemType === 'transcript') ? ((result.output.transcript as string | undefined) || result.output.response || '') : ''; const transcriptTurns = (itemType === 'scenario' || itemType === 'transcript') ? parseTurnsForExcerpt(transcript) : []; return ( {/* Header */} Item {itemNumber} of {totalItems} {itemType === 'single_turn' ? 'SINGLE TURN' : itemType === 'transcript' ? 'TRANSCRIPT' : 'SCENARIO'} {overallPassed ? 'PASSED' : 'FAILED'} {/* Item Name */} Test Item {itemName} {/* Transcript excerpt for scenarios */} {transcriptTurns.length > 0 && ( Conversation Excerpt ({transcriptTurns.length} turn{transcriptTurns.length > 1 ? 's' : ''}) {transcriptTurns.map((turn, i) => ( {turn.role} {turn.content.slice(0, 200)}{turn.content.length > 200 ? '...' : ''} ))} )} {/* Two-column layout for criteria and rubric scores */} {/* Criteria Scores */} {criteriaScores.length > 0 && ( Criteria Scores ({criteriaPassed}/{criteriaScores.length} passed) {criteriaScores.map((criterion, i) => ( ))} )} {/* Rubric Rule Scores */} {rubricScores.length > 0 && ( Rubric Scores ({rubricPassed}/{rubricScores.length} passed) {rubricScores.map((rule) => ( ))} )} {/* Footer */} Indemn Evaluation Report `Page ${pn}`} /> ); }