@indemn/cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.claude-plugin/marketplace.json +28 -0
  2. package/README.md +29 -23
  3. package/dist/cli/agents.d.ts.map +1 -1
  4. package/dist/cli/agents.js +20 -0
  5. package/dist/cli/agents.js.map +1 -1
  6. package/dist/cli/eval.d.ts.map +1 -1
  7. package/dist/cli/eval.js +38 -0
  8. package/dist/cli/eval.js.map +1 -1
  9. package/dist/mcp/tools.d.ts.map +1 -1
  10. package/dist/mcp/tools.js +40 -0
  11. package/dist/mcp/tools.js.map +1 -1
  12. package/dist/report/agent-card/cover.d.ts +20 -0
  13. package/dist/report/agent-card/cover.d.ts.map +1 -0
  14. package/dist/report/agent-card/cover.js +58 -0
  15. package/dist/report/agent-card/cover.js.map +1 -0
  16. package/dist/report/agent-card/document.d.ts +7 -0
  17. package/dist/report/agent-card/document.d.ts.map +1 -0
  18. package/dist/report/agent-card/document.js +144 -0
  19. package/dist/report/agent-card/document.js.map +1 -0
  20. package/dist/report/agent-card/types.d.ts +77 -0
  21. package/dist/report/agent-card/types.d.ts.map +1 -0
  22. package/dist/report/agent-card/types.js +3 -0
  23. package/dist/report/agent-card/types.js.map +1 -0
  24. package/dist/report/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  25. package/dist/report/assets/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  26. package/dist/report/assets/assets/fonts/Barlow-Bold.ttf +0 -0
  27. package/dist/report/assets/assets/fonts/Barlow-Medium.ttf +0 -0
  28. package/dist/report/assets/assets/fonts/Barlow-Regular.ttf +0 -0
  29. package/dist/report/assets/assets/fonts/Barlow-SemiBold.ttf +0 -0
  30. package/dist/report/assets/fonts/Barlow-Bold.ttf +0 -0
  31. package/dist/report/assets/fonts/Barlow-Medium.ttf +0 -0
  32. package/dist/report/assets/fonts/Barlow-Regular.ttf +0 -0
  33. package/dist/report/assets/fonts/Barlow-SemiBold.ttf +0 -0
  34. package/dist/report/eval-report/cover.d.ts +16 -0
  35. package/dist/report/eval-report/cover.d.ts.map +1 -0
  36. package/dist/report/eval-report/cover.js +8 -0
  37. package/dist/report/eval-report/cover.js.map +1 -0
  38. package/dist/report/eval-report/document.d.ts +19 -0
  39. package/dist/report/eval-report/document.d.ts.map +1 -0
  40. package/dist/report/eval-report/document.js +32 -0
  41. package/dist/report/eval-report/document.js.map +1 -0
  42. package/dist/report/eval-report/example-page.d.ts +11 -0
  43. package/dist/report/eval-report/example-page.d.ts.map +1 -0
  44. package/dist/report/eval-report/example-page.js +18 -0
  45. package/dist/report/eval-report/example-page.js.map +1 -0
  46. package/dist/report/eval-report/item-page.d.ts +9 -0
  47. package/dist/report/eval-report/item-page.d.ts.map +1 -0
  48. package/dist/report/eval-report/item-page.js +72 -0
  49. package/dist/report/eval-report/item-page.js.map +1 -0
  50. package/dist/report/eval-report/matrix-page.d.ts +8 -0
  51. package/dist/report/eval-report/matrix-page.d.ts.map +1 -0
  52. package/dist/report/eval-report/matrix-page.js +31 -0
  53. package/dist/report/eval-report/matrix-page.js.map +1 -0
  54. package/dist/report/eval-report/types.d.ts +69 -0
  55. package/dist/report/eval-report/types.d.ts.map +1 -0
  56. package/dist/report/eval-report/types.js +7 -0
  57. package/dist/report/eval-report/types.js.map +1 -0
  58. package/dist/report/eval-report/v2-cover.d.ts +9 -0
  59. package/dist/report/eval-report/v2-cover.d.ts.map +1 -0
  60. package/dist/report/eval-report/v2-cover.js +32 -0
  61. package/dist/report/eval-report/v2-cover.js.map +1 -0
  62. package/dist/report/export.d.ts +17 -0
  63. package/dist/report/export.d.ts.map +1 -0
  64. package/dist/report/export.js +327 -0
  65. package/dist/report/export.js.map +1 -0
  66. package/dist/report/generate.d.ts +7 -0
  67. package/dist/report/generate.d.ts.map +1 -0
  68. package/dist/report/generate.js +30 -0
  69. package/dist/report/generate.js.map +1 -0
  70. package/dist/report/scoring.d.ts +90 -0
  71. package/dist/report/scoring.d.ts.map +1 -0
  72. package/dist/report/scoring.js +174 -0
  73. package/dist/report/scoring.js.map +1 -0
  74. package/dist/report/styles.d.ts +111 -0
  75. package/dist/report/styles.d.ts.map +1 -0
  76. package/dist/report/styles.js +134 -0
  77. package/dist/report/styles.js.map +1 -0
  78. package/docs/plans/2026-03-13-cli-exports.md +819 -0
  79. package/package.json +14 -4
  80. package/skills/eval-analysis/SKILL.md +178 -0
  81. package/skills/eval-analysis/references/data-shapes.md +279 -0
  82. package/src/cli/agents.ts +22 -0
  83. package/src/cli/eval.ts +42 -0
  84. package/src/mcp/tools.ts +50 -0
  85. package/src/report/agent-card/cover.tsx +232 -0
  86. package/src/report/agent-card/document.tsx +582 -0
  87. package/src/report/agent-card/types.ts +86 -0
  88. package/src/report/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  89. package/src/report/assets/fonts/Barlow-Bold.ttf +0 -0
  90. package/src/report/assets/fonts/Barlow-Medium.ttf +0 -0
  91. package/src/report/assets/fonts/Barlow-Regular.ttf +0 -0
  92. package/src/report/assets/fonts/Barlow-SemiBold.ttf +0 -0
  93. package/src/report/eval-report/cover.tsx +69 -0
  94. package/src/report/eval-report/document.tsx +105 -0
  95. package/src/report/eval-report/example-page.tsx +121 -0
  96. package/src/report/eval-report/item-page.tsx +211 -0
  97. package/src/report/eval-report/matrix-page.tsx +124 -0
  98. package/src/report/eval-report/types.ts +77 -0
  99. package/src/report/eval-report/v2-cover.tsx +102 -0
  100. package/src/report/export.ts +394 -0
  101. package/src/report/generate.tsx +37 -0
  102. package/src/report/scoring.ts +281 -0
  103. package/src/report/styles.ts +138 -0
  104. package/tsconfig.json +2 -1
@@ -0,0 +1,69 @@
1
+ // V1 eval report cover — ported from EvalReportCover.tsx
2
+ import { Page, View, Text, Image } from '@react-pdf/renderer';
3
+ import { styles, colors, logoPath } from '../styles.js';
4
+
5
+ interface EvalReportCoverProps {
6
+ customerName: string;
7
+ reportDate: string;
8
+ highlights: {
9
+ totalQuestions: number;
10
+ totalRules: number;
11
+ overallScore: number;
12
+ highSeverityScore: number;
13
+ mediumSeverityScore: number;
14
+ highCount: number;
15
+ mediumCount: number;
16
+ };
17
+ }
18
+
19
+ export function EvalReportCover({ customerName, reportDate, highlights }: EvalReportCoverProps) {
20
+ return (
21
+ <Page size="A4" style={[styles.page, { justifyContent: 'center' }]}>
22
+ {/* Logo */}
23
+ <View style={{ alignItems: 'center', marginBottom: 48 }}>
24
+ <Image src={logoPath} style={{ width: 200, height: 46 }} />
25
+ </View>
26
+
27
+ {/* Title */}
28
+ <View style={{ alignItems: 'center', marginBottom: 48 }}>
29
+ <Text style={{ fontSize: 28, fontWeight: 700, color: colors.eggplant, marginBottom: 8 }}>
30
+ Evaluation Report
31
+ </Text>
32
+ <Text style={{ fontSize: 16, fontWeight: 500, color: colors.muted }}>
33
+ {customerName}
34
+ </Text>
35
+ <Text style={{ fontSize: 14, fontWeight: 400, color: colors.muted, marginTop: 4 }}>
36
+ {reportDate}
37
+ </Text>
38
+ </View>
39
+
40
+ {/* Key Highlights */}
41
+ <View style={[styles.card, { marginHorizontal: 40 }]}>
42
+ <Text style={[styles.sectionTitle, { textAlign: 'center', marginBottom: 16 }]}>
43
+ Key Highlights
44
+ </Text>
45
+
46
+ <View style={{ gap: 8 }}>
47
+ <Text style={styles.bodyText}>
48
+ {'\u2022'} {highlights.totalQuestions} questions evaluated against {highlights.totalRules} rules
49
+ </Text>
50
+ <Text style={styles.bodyText}>
51
+ {'\u2022'} Overall weighted score: <Text style={{ fontWeight: 600 }}>{Math.round(highlights.overallScore * 100)}%</Text>
52
+ </Text>
53
+ <Text style={styles.bodyText}>
54
+ {'\u2022'} High severity compliance ({highlights.highCount} rules): <Text style={{ fontWeight: 600 }}>{Math.round(highlights.highSeverityScore * 100)}%</Text>
55
+ </Text>
56
+ <Text style={styles.bodyText}>
57
+ {'\u2022'} Medium severity compliance ({highlights.mediumCount} rules): <Text style={{ fontWeight: 600 }}>{Math.round(highlights.mediumSeverityScore * 100)}%</Text>
58
+ </Text>
59
+ </View>
60
+ </View>
61
+
62
+ {/* Footer */}
63
+ <View style={[styles.footer, { position: 'absolute', bottom: 40 }]}>
64
+ <Text style={styles.caption}>Powered by Indemn Evals</Text>
65
+ <Text style={styles.caption}>Page 1</Text>
66
+ </View>
67
+ </Page>
68
+ );
69
+ }
@@ -0,0 +1,105 @@
1
+ // Eval report document — ported from EvalReportDocument.tsx
2
+ import { Document } from '@react-pdf/renderer';
3
+ import { EvalReportCover } from './cover.js';
4
+ import { EvalReportMatrixPage } from './matrix-page.js';
5
+ import { EvalReportExamplePage } from './example-page.js';
6
+ import { EvalReportV2Cover } from './v2-cover.js';
7
+ import { EvalReportItemPage } from './item-page.js';
8
+ import { calculateWeightedScores } from '../scoring.js';
9
+ import type { MatrixData, WeightConfig, EvaluationResult } from './types.js';
10
+
11
+ // V1 report data (matrix-based)
12
+ export interface EvalReportDataV1 {
13
+ customerName: string;
14
+ reportDate: string;
15
+ matrixData: MatrixData;
16
+ weights: WeightConfig;
17
+ }
18
+
19
+ // V2 report data (per-item results)
20
+ export interface EvalReportDataV2 {
21
+ customerName: string;
22
+ reportDate: string;
23
+ v2Results: EvaluationResult[];
24
+ }
25
+
26
+ // Union type
27
+ export type EvalReportData = EvalReportDataV1 | EvalReportDataV2;
28
+
29
+ function isV2ReportData(data: EvalReportData): data is EvalReportDataV2 {
30
+ return 'v2Results' in data && Array.isArray(data.v2Results);
31
+ }
32
+
33
+ interface EvalReportDocumentProps {
34
+ data: EvalReportData;
35
+ }
36
+
37
+ export function EvalReportDocument({ data }: EvalReportDocumentProps) {
38
+ if (isV2ReportData(data)) {
39
+ return (
40
+ <Document
41
+ title={`Indemn Evaluation Report - ${data.customerName}`}
42
+ author="Indemn Evals"
43
+ subject={`Evaluation report for ${data.reportDate}`}
44
+ >
45
+ <EvalReportV2Cover
46
+ customerName={data.customerName}
47
+ reportDate={data.reportDate}
48
+ results={data.v2Results}
49
+ />
50
+ {data.v2Results.map((result, index) => (
51
+ <EvalReportItemPage
52
+ key={result.result_id}
53
+ result={result}
54
+ itemNumber={index + 1}
55
+ totalItems={data.v2Results.length}
56
+ />
57
+ ))}
58
+ </Document>
59
+ );
60
+ }
61
+
62
+ // V1 report
63
+ const weightedScores = calculateWeightedScores(data.matrixData, data.weights);
64
+
65
+ const highlights = {
66
+ totalQuestions: data.matrixData.rows.length,
67
+ totalRules: data.matrixData.evaluators.length,
68
+ overallScore: weightedScores.overall,
69
+ highSeverityScore: weightedScores.bySeverity.High.score,
70
+ mediumSeverityScore: weightedScores.bySeverity.Medium.score,
71
+ highCount: weightedScores.bySeverity.High.count,
72
+ mediumCount: weightedScores.bySeverity.Medium.count,
73
+ };
74
+
75
+ const matrixPageNumber = 2;
76
+ const exampleStartPage = 3;
77
+
78
+ return (
79
+ <Document
80
+ title={`Indemn Evaluation Report - ${data.customerName}`}
81
+ author="Indemn Evals"
82
+ subject={`Evaluation report for ${data.reportDate}`}
83
+ >
84
+ <EvalReportCover
85
+ customerName={data.customerName}
86
+ reportDate={data.reportDate}
87
+ highlights={highlights}
88
+ />
89
+ <EvalReportMatrixPage
90
+ matrixData={data.matrixData}
91
+ pageNumber={matrixPageNumber}
92
+ />
93
+ {data.matrixData.rows.map((row, index) => (
94
+ <EvalReportExamplePage
95
+ key={row.resultId}
96
+ exampleNumber={index + 1}
97
+ totalExamples={data.matrixData.rows.length}
98
+ pageNumber={exampleStartPage + index}
99
+ row={row}
100
+ evaluators={data.matrixData.evaluators}
101
+ />
102
+ ))}
103
+ </Document>
104
+ );
105
+ }
@@ -0,0 +1,121 @@
1
+ // V1 example detail page — ported from EvalReportExamplePage.tsx
2
+ import { Page, View, Text } from '@react-pdf/renderer';
3
+ import { colors } from '../styles.js';
4
+ import { SCORE_THRESHOLDS, getScoreHexColor } from '../scoring.js';
5
+ import type { MatrixRow, EvaluatorInfo } from './types.js';
6
+
7
+ interface EvalReportExamplePageProps {
8
+ exampleNumber: number;
9
+ totalExamples: number;
10
+ pageNumber: number;
11
+ row: MatrixRow;
12
+ evaluators: EvaluatorInfo[];
13
+ }
14
+
15
+ export function EvalReportExamplePage({
16
+ exampleNumber,
17
+ totalExamples,
18
+ pageNumber: _pageNumber,
19
+ row,
20
+ evaluators,
21
+ }: EvalReportExamplePageProps) {
22
+ void _pageNumber;
23
+ const passedCount = Object.values(row.cells).filter(c => c.passed).length;
24
+ const totalCount = Object.keys(row.cells).length;
25
+ const passRate = Math.round(row.avgScore * 100);
26
+
27
+ const failed = evaluators.filter(e => row.cells[e.key] && !row.cells[e.key].passed);
28
+ const passed = evaluators.filter(e => row.cells[e.key] && row.cells[e.key].passed);
29
+
30
+ return (
31
+ <Page size="A4" orientation="landscape" style={{ fontFamily: 'Barlow', fontSize: 7, color: colors.eggplant, backgroundColor: colors.white, padding: 20 }} wrap>
32
+ {/* Header Row */}
33
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, paddingBottom: 6, borderBottomWidth: 1, borderBottomColor: colors.border }} fixed>
34
+ <Text style={{ fontSize: 11, fontWeight: 600, color: colors.iris }}>
35
+ Question {exampleNumber} of {totalExamples}
36
+ </Text>
37
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
38
+ <Text style={{ fontSize: 8, color: colors.muted }}>
39
+ {passedCount}/{totalCount} passed
40
+ </Text>
41
+ <View style={{ backgroundColor: passRate >= SCORE_THRESHOLDS.SUCCESS * 100 ? '#dcfce7' : passRate >= SCORE_THRESHOLDS.WARNING * 100 ? '#fef9c3' : '#fee2e2', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 3 }}>
42
+ <Text style={{ fontSize: 12, fontWeight: 700, color: getScoreHexColor(passRate / 100) }}>
43
+ {passRate}%
44
+ </Text>
45
+ </View>
46
+ </View>
47
+ </View>
48
+
49
+ {/* FULL Question */}
50
+ <View style={{ marginBottom: 8 }} wrap={false}>
51
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>Question</Text>
52
+ <View style={{ backgroundColor: colors.background, padding: 6, borderRadius: 3 }}>
53
+ <Text style={{ fontSize: 6, color: colors.eggplant, lineHeight: 1.4 }}>{row.question}</Text>
54
+ </View>
55
+ </View>
56
+
57
+ {/* FULL Agent Response */}
58
+ <View style={{ marginBottom: 8 }}>
59
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>Agent Response</Text>
60
+ <View style={{ backgroundColor: colors.background, padding: 6, borderRadius: 3 }}>
61
+ <Text style={{ fontSize: 5.5, color: colors.eggplant, lineHeight: 1.4 }}>{row.response}</Text>
62
+ </View>
63
+ </View>
64
+
65
+ {/* Evaluation Results - Two columns side by side */}
66
+ <View style={{ flexDirection: 'row', gap: 12 }}>
67
+ {/* Failed Rules */}
68
+ <View style={{ flex: 1 }}>
69
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.error, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>
70
+ Failed ({failed.length})
71
+ </Text>
72
+ <View style={{ backgroundColor: '#fef2f2', padding: 6, borderRadius: 3 }}>
73
+ {failed.length === 0 ? (
74
+ <Text style={{ fontSize: 6, color: colors.success }}>All rules passed!</Text>
75
+ ) : (
76
+ failed.map((evaluator, idx) => {
77
+ const cell = row.cells[evaluator.key];
78
+ return (
79
+ <View key={evaluator.key} style={{ marginBottom: idx < failed.length - 1 ? 6 : 0 }}>
80
+ <Text style={{ fontSize: 6, fontWeight: 600, color: colors.error, marginBottom: 1 }}>
81
+ x {evaluator.name} [{evaluator.severity}]
82
+ </Text>
83
+ {cell?.comment && (
84
+ <Text style={{ fontSize: 5, color: colors.muted, lineHeight: 1.3 }}>
85
+ {cell.comment}
86
+ </Text>
87
+ )}
88
+ </View>
89
+ );
90
+ })
91
+ )}
92
+ </View>
93
+ </View>
94
+
95
+ {/* Passed Rules */}
96
+ <View style={{ flex: 1 }}>
97
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.success, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>
98
+ Passed ({passed.length})
99
+ </Text>
100
+ <View style={{ backgroundColor: '#f0fdf4', padding: 6, borderRadius: 3 }}>
101
+ {passed.length === 0 ? (
102
+ <Text style={{ fontSize: 6, color: colors.error }}>No rules passed</Text>
103
+ ) : (
104
+ passed.map((evaluator) => (
105
+ <Text key={evaluator.key} style={{ fontSize: 5.5, color: colors.success, marginBottom: 2 }}>
106
+ + {evaluator.name}
107
+ </Text>
108
+ ))
109
+ )}
110
+ </View>
111
+ </View>
112
+ </View>
113
+
114
+ {/* Footer */}
115
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingTop: 6, borderTopWidth: 0.5, borderTopColor: colors.border, marginTop: 'auto' }} fixed>
116
+ <Text style={{ fontSize: 6, color: colors.muted }}>Indemn Evaluation Report</Text>
117
+ <Text style={{ fontSize: 6, color: colors.muted }} render={({ pageNumber: pn }) => `Page ${pn}`} />
118
+ </View>
119
+ </Page>
120
+ );
121
+ }
@@ -0,0 +1,211 @@
1
+ // V2 per-item detail page — ported from EvalReportItemPage.tsx
2
+ import { Page, View, Text } from '@react-pdf/renderer';
3
+ import { styles, colors } from '../styles.js';
4
+ import type { EvaluationResult, CriterionScore, RubricRuleScore } from './types.js';
5
+
6
+ interface EvalReportItemPageProps {
7
+ result: EvaluationResult;
8
+ itemNumber: number;
9
+ totalItems: number;
10
+ }
11
+
12
+ const severityColors: Record<string, string> = {
13
+ High: '#3a4389',
14
+ high: '#3a4389',
15
+ Medium: '#4752a3',
16
+ medium: '#4752a3',
17
+ Low: '#7c85c0',
18
+ low: '#7c85c0',
19
+ };
20
+
21
+ function parseTurnsForExcerpt(text: string, maxTurns: number = 3): Array<{ role: string; content: string }> {
22
+ const turns: Array<{ role: string; content: string }> = [];
23
+ const lines = text.split('\n');
24
+ let currentRole: string | null = null;
25
+ let currentContent: string[] = [];
26
+
27
+ for (const line of lines) {
28
+ if (line.startsWith('User:')) {
29
+ if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() });
30
+ currentRole = 'User';
31
+ currentContent = [line.slice(5).trim()];
32
+ } else if (line.startsWith('Agent:')) {
33
+ if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() });
34
+ currentRole = 'Agent';
35
+ currentContent = [line.slice(6).trim()];
36
+ } else if (currentRole) {
37
+ currentContent.push(line);
38
+ }
39
+ }
40
+ if (currentRole) turns.push({ role: currentRole, content: currentContent.join('\n').trim() });
41
+ return turns.slice(0, maxTurns);
42
+ }
43
+
44
+ function CriterionScoreRow({ criterion }: { criterion: CriterionScore }) {
45
+ return (
46
+ <View style={{ flexDirection: 'row', marginBottom: 4, paddingVertical: 3, paddingHorizontal: 6, backgroundColor: criterion.passed ? '#f0fdf4' : '#fef2f2', borderRadius: 3 }}>
47
+ <Text style={{ fontSize: 7, fontWeight: 700, color: criterion.passed ? colors.success : colors.error, width: 12 }}>
48
+ {criterion.passed ? 'P' : 'F'}
49
+ </Text>
50
+ <View style={{ flex: 1 }}>
51
+ <Text style={{ fontSize: 6, fontWeight: 600, color: colors.eggplant, marginBottom: 1 }}>
52
+ {criterion.criterion}
53
+ </Text>
54
+ {criterion.reasoning && (
55
+ <Text style={{ fontSize: 5.5, color: colors.muted, lineHeight: 1.3 }}>
56
+ {criterion.reasoning.slice(0, 250)}{criterion.reasoning.length > 250 ? '...' : ''}
57
+ </Text>
58
+ )}
59
+ </View>
60
+ <Text style={{ fontSize: 6, fontWeight: 600, color: criterion.passed ? colors.success : colors.error, width: 30, textAlign: 'right' }}>
61
+ {Math.round(criterion.score * 100)}%
62
+ </Text>
63
+ </View>
64
+ );
65
+ }
66
+
67
+ function RubricScoreRow({ rule }: { rule: RubricRuleScore }) {
68
+ const sevColor = severityColors[rule.severity] || colors.muted;
69
+ return (
70
+ <View style={{ flexDirection: 'row', marginBottom: 4, paddingVertical: 3, paddingHorizontal: 6, backgroundColor: rule.passed ? '#f0fdf4' : '#fef2f2', borderRadius: 3 }}>
71
+ <Text style={{ fontSize: 7, fontWeight: 700, color: rule.passed ? colors.success : colors.error, width: 12 }}>
72
+ {rule.passed ? 'P' : 'F'}
73
+ </Text>
74
+ <View style={{ flex: 1 }}>
75
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 1 }}>
76
+ <Text style={{ fontSize: 6, fontWeight: 600, color: colors.eggplant }}>
77
+ {rule.rule_name}
78
+ </Text>
79
+ <View style={{ backgroundColor: `${sevColor}20`, paddingHorizontal: 4, paddingVertical: 1, borderRadius: 2 }}>
80
+ <Text style={{ fontSize: 5, fontWeight: 600, color: sevColor, textTransform: 'uppercase' }}>
81
+ {rule.severity}
82
+ </Text>
83
+ </View>
84
+ </View>
85
+ {rule.reasoning && (
86
+ <Text style={{ fontSize: 5.5, color: colors.muted, lineHeight: 1.3 }}>
87
+ {rule.reasoning.slice(0, 250)}{rule.reasoning.length > 250 ? '...' : ''}
88
+ </Text>
89
+ )}
90
+ </View>
91
+ <Text style={{ fontSize: 6, fontWeight: 600, color: rule.passed ? colors.success : colors.error, width: 30, textAlign: 'right' }}>
92
+ {Math.round(rule.score * 100)}%
93
+ </Text>
94
+ </View>
95
+ );
96
+ }
97
+
98
+ export function EvalReportItemPage({ result, itemNumber, totalItems }: EvalReportItemPageProps) {
99
+ const itemName = result.item_name || result.input.message || `Test Item ${itemNumber}`;
100
+ const itemType = result.item_type || 'single_turn';
101
+ const criteriaScores = result.criteria_scores || [];
102
+ const rubricScores = result.rubric_scores || [];
103
+ const overallPassed = result.passed ?? (result.criteria_passed !== false && result.rubric_passed !== false);
104
+
105
+ const criteriaPassed = criteriaScores.filter(c => c.passed).length;
106
+ const rubricPassed = rubricScores.filter(r => r.passed).length;
107
+
108
+ const transcript = (itemType === 'scenario' || itemType === 'transcript')
109
+ ? ((result.output.transcript as string | undefined) || result.output.response || '')
110
+ : '';
111
+ const transcriptTurns = (itemType === 'scenario' || itemType === 'transcript') ? parseTurnsForExcerpt(transcript) : [];
112
+
113
+ return (
114
+ <Page size="A4" style={[styles.page, { padding: 24 }]} wrap>
115
+ {/* Header */}
116
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10, paddingBottom: 6, borderBottomWidth: 1, borderBottomColor: colors.border }} fixed>
117
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
118
+ <Text style={{ fontSize: 11, fontWeight: 600, color: colors.iris }}>
119
+ Item {itemNumber} of {totalItems}
120
+ </Text>
121
+ <View style={{
122
+ backgroundColor: itemType === 'scenario' ? `${colors.iris}20`
123
+ : itemType === 'transcript' ? '#10b98120'
124
+ : `${colors.muted}20`,
125
+ paddingHorizontal: 6,
126
+ paddingVertical: 2,
127
+ borderRadius: 3,
128
+ }}>
129
+ <Text style={{ fontSize: 7, fontWeight: 600, color: itemType === 'scenario' ? colors.iris : itemType === 'transcript' ? '#059669' : colors.muted }}>
130
+ {itemType === 'single_turn' ? 'SINGLE TURN' : itemType === 'transcript' ? 'TRANSCRIPT' : 'SCENARIO'}
131
+ </Text>
132
+ </View>
133
+ </View>
134
+ <View style={{
135
+ backgroundColor: overallPassed ? '#dcfce7' : '#fee2e2',
136
+ paddingHorizontal: 8,
137
+ paddingVertical: 3,
138
+ borderRadius: 3,
139
+ }}>
140
+ <Text style={{ fontSize: 10, fontWeight: 700, color: overallPassed ? colors.success : colors.error }}>
141
+ {overallPassed ? 'PASSED' : 'FAILED'}
142
+ </Text>
143
+ </View>
144
+ </View>
145
+
146
+ {/* Item Name */}
147
+ <View style={{ marginBottom: 8 }} wrap={false}>
148
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>
149
+ Test Item
150
+ </Text>
151
+ <View style={{ backgroundColor: colors.background, padding: 6, borderRadius: 3 }}>
152
+ <Text style={{ fontSize: 7, color: colors.eggplant, lineHeight: 1.4 }}>{itemName}</Text>
153
+ </View>
154
+ </View>
155
+
156
+ {/* Transcript excerpt for scenarios */}
157
+ {transcriptTurns.length > 0 && (
158
+ <View style={{ marginBottom: 8 }}>
159
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 3, textTransform: 'uppercase', letterSpacing: 0.5 }}>
160
+ Conversation Excerpt ({transcriptTurns.length} turn{transcriptTurns.length > 1 ? 's' : ''})
161
+ </Text>
162
+ <View style={{ backgroundColor: colors.background, padding: 6, borderRadius: 3 }}>
163
+ {transcriptTurns.map((turn, i) => (
164
+ <View key={i} style={{ marginBottom: i < transcriptTurns.length - 1 ? 4 : 0 }}>
165
+ <Text style={{ fontSize: 5.5, fontWeight: 700, color: turn.role === 'User' ? colors.muted : colors.iris, marginBottom: 1 }}>
166
+ {turn.role}
167
+ </Text>
168
+ <Text style={{ fontSize: 5.5, color: colors.eggplant, lineHeight: 1.3 }}>
169
+ {turn.content.slice(0, 200)}{turn.content.length > 200 ? '...' : ''}
170
+ </Text>
171
+ </View>
172
+ ))}
173
+ </View>
174
+ </View>
175
+ )}
176
+
177
+ {/* Two-column layout for criteria and rubric scores */}
178
+ <View style={{ flexDirection: 'row', gap: 10 }}>
179
+ {/* Criteria Scores */}
180
+ {criteriaScores.length > 0 && (
181
+ <View style={{ flex: 1 }}>
182
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 4, textTransform: 'uppercase', letterSpacing: 0.5 }}>
183
+ Criteria Scores ({criteriaPassed}/{criteriaScores.length} passed)
184
+ </Text>
185
+ {criteriaScores.map((criterion, i) => (
186
+ <CriterionScoreRow key={i} criterion={criterion} />
187
+ ))}
188
+ </View>
189
+ )}
190
+
191
+ {/* Rubric Rule Scores */}
192
+ {rubricScores.length > 0 && (
193
+ <View style={{ flex: 1 }}>
194
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant, marginBottom: 4, textTransform: 'uppercase', letterSpacing: 0.5 }}>
195
+ Rubric Scores ({rubricPassed}/{rubricScores.length} passed)
196
+ </Text>
197
+ {rubricScores.map((rule) => (
198
+ <RubricScoreRow key={rule.rule_id} rule={rule} />
199
+ ))}
200
+ </View>
201
+ )}
202
+ </View>
203
+
204
+ {/* Footer */}
205
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingTop: 6, borderTopWidth: 0.5, borderTopColor: colors.border, marginTop: 'auto' }} fixed>
206
+ <Text style={{ fontSize: 6, color: colors.muted }}>Indemn Evaluation Report</Text>
207
+ <Text style={{ fontSize: 6, color: colors.muted }} render={({ pageNumber: pn }) => `Page ${pn}`} />
208
+ </View>
209
+ </Page>
210
+ );
211
+ }
@@ -0,0 +1,124 @@
1
+ // V1 evaluation matrix page — ported from EvalReportMatrixPage.tsx
2
+ import { Page, View, Text } from '@react-pdf/renderer';
3
+ import { styles, colors } from '../styles.js';
4
+ import type { MatrixData } from './types.js';
5
+
6
+ interface EvalReportMatrixPageProps {
7
+ matrixData: MatrixData;
8
+ pageNumber: number;
9
+ }
10
+
11
+ export function EvalReportMatrixPage({ matrixData, pageNumber }: EvalReportMatrixPageProps) {
12
+ const { rows, evaluators, columnAverages } = matrixData;
13
+
14
+ const numCol = 25;
15
+ const questionCol = 160;
16
+ const evalCol = 28;
17
+ const scoreCol = 35;
18
+
19
+ return (
20
+ <Page size="A4" orientation="landscape" style={[styles.page, { padding: 20 }]}>
21
+ {/* Header */}
22
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10, paddingBottom: 6, borderBottomWidth: 1, borderBottomColor: colors.border }}>
23
+ <Text style={{ fontSize: 14, fontWeight: 600, color: colors.iris }}>Evaluation Matrix</Text>
24
+ <Text style={{ fontSize: 8, color: colors.muted }}>Page {pageNumber}</Text>
25
+ </View>
26
+
27
+ {/* Matrix Table */}
28
+ <View style={{ flex: 1 }}>
29
+ {/* Table Header */}
30
+ <View style={{ flexDirection: 'row', borderBottomWidth: 1.5, borderBottomColor: colors.eggplant, paddingBottom: 4, marginBottom: 2 }}>
31
+ <View style={{ width: numCol }}>
32
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant }}>#</Text>
33
+ </View>
34
+ <View style={{ width: questionCol }}>
35
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant }}>Question</Text>
36
+ </View>
37
+ {evaluators.map((ev) => (
38
+ <View key={ev.key} style={{ width: evalCol, alignItems: 'center' }}>
39
+ <Text style={{ fontSize: 5, fontWeight: 700, color: ev.severity === 'High' ? '#3a4389' : colors.iris }}>
40
+ {ev.name.split(':')[0]}
41
+ </Text>
42
+ </View>
43
+ ))}
44
+ <View style={{ width: scoreCol, alignItems: 'center' }}>
45
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant }}>Score</Text>
46
+ </View>
47
+ </View>
48
+
49
+ {/* Table Rows */}
50
+ {rows.map((row, index) => (
51
+ <View
52
+ key={row.resultId}
53
+ style={{
54
+ flexDirection: 'row',
55
+ alignItems: 'center',
56
+ paddingVertical: 2,
57
+ backgroundColor: index % 2 === 0 ? colors.white : '#f8f8fc',
58
+ }}
59
+ >
60
+ <View style={{ width: numCol }}>
61
+ <Text style={{ fontSize: 5, color: colors.muted }}>{index + 1}</Text>
62
+ </View>
63
+ <View style={{ width: questionCol }}>
64
+ <Text style={{ fontSize: 5, color: colors.eggplant }}>
65
+ {row.question.slice(0, 45)}{row.question.length > 45 ? '...' : ''}
66
+ </Text>
67
+ </View>
68
+ {evaluators.map((ev) => {
69
+ const cell = row.cells[ev.key];
70
+ const hasCellData = cell !== undefined && cell !== null;
71
+ const passed = hasCellData && cell.passed;
72
+ return (
73
+ <View key={ev.key} style={{ width: evalCol, alignItems: 'center', backgroundColor: hasCellData ? (passed ? '#dcfce7' : '#fee2e2') : 'transparent' }}>
74
+ <Text style={{ fontSize: 6, fontWeight: 700, color: hasCellData ? (passed ? colors.success : colors.error) : colors.muted }}>
75
+ {hasCellData ? (passed ? 'P' : 'F') : '-'}
76
+ </Text>
77
+ </View>
78
+ );
79
+ })}
80
+ <View style={{ width: scoreCol, alignItems: 'center' }}>
81
+ <Text style={{ fontSize: 5, fontWeight: 600, color: colors.eggplant }}>
82
+ {Math.round(row.avgScore * 100)}%
83
+ </Text>
84
+ </View>
85
+ </View>
86
+ ))}
87
+
88
+ {/* Column Averages Row */}
89
+ <View style={{
90
+ flexDirection: 'row',
91
+ alignItems: 'center',
92
+ paddingVertical: 4,
93
+ borderTopWidth: 1.5,
94
+ borderTopColor: colors.eggplant,
95
+ marginTop: 2,
96
+ backgroundColor: colors.background,
97
+ }}>
98
+ <View style={{ width: numCol }} />
99
+ <View style={{ width: questionCol }}>
100
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.eggplant }}>Pass Rate</Text>
101
+ </View>
102
+ {evaluators.map((ev) => (
103
+ <View key={ev.key} style={{ width: evalCol, alignItems: 'center' }}>
104
+ <Text style={{ fontSize: 5, fontWeight: 700, color: colors.eggplant }}>
105
+ {Math.round((columnAverages[ev.key] || 0) * 100)}%
106
+ </Text>
107
+ </View>
108
+ ))}
109
+ <View style={{ width: scoreCol, alignItems: 'center' }}>
110
+ <Text style={{ fontSize: 6, fontWeight: 700, color: colors.iris }}>
111
+ {Math.round(matrixData.overallAverage * 100)}%
112
+ </Text>
113
+ </View>
114
+ </View>
115
+ </View>
116
+
117
+ {/* Footer */}
118
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingTop: 8, borderTopWidth: 0.5, borderTopColor: colors.border, marginTop: 8 }}>
119
+ <Text style={{ fontSize: 7, color: colors.muted }}>Indemn Evaluation Report</Text>
120
+ <Text style={{ fontSize: 7, color: colors.muted }}>Page {pageNumber}</Text>
121
+ </View>
122
+ </Page>
123
+ );
124
+ }