Generated at {dataset.createdAt} by Microsoft.Extensions.AI.Evaluation.Reporting version {dataset.generatorVersion}
+ >
+ )
+}
+
+export default App
diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts
new file mode 100644
index 00000000000..20a3df81b21
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts
@@ -0,0 +1,80 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+type Dataset = {
+ scenarioRunResults: ScenarioRunResult[];
+ generatorVersion?: string;
+ createdAt?: string;
+};
+
+type ScenarioRunResult = {
+ scenarioName: string;
+ iterationName: string;
+ executionName: string;
+ creationTime?: string;
+ messages: ChatMessage[];
+ modelResponse: ChatMessage;
+ evaluationResult: EvaluationResult;
+};
+
+type ChatMessage = {
+ authorName?: string;
+ role: string;
+ contents: AIContent[]
+};
+
+type AIContent = {
+ $type: string;
+};
+
+// TODO: Model other types of AIContent such as function calls, function call results, images, audio etc.
+type TextContent = AIContent & {
+ $type: "text";
+ text: string;
+};
+
+type EvaluationResult = {
+ metrics: {
+ [K: string]: MetricWithNoValue | NumericMetric | BooleanMetric | StringMetric;
+ };
+};
+
+type EvaluationDiagnostic = {
+ severity: "informational" | "warning" | "error";
+ message: string;
+};
+
+type EvaluationRating = "unknown" | "inconclusive" | "exceptional" | "good" | "average" | "poor" | "unacceptable";
+
+type EvaluationMetricInterpretation = {
+ rating: EvaluationRating;
+ reason?: string;
+ failed: boolean;
+};
+
+type BaseEvaluationMetric = {
+ $type: string;
+ name: string;
+ interpretation?: EvaluationMetricInterpretation;
+ diagnostics: EvaluationDiagnostic[];
+};
+
+type MetricWithNoValue = BaseEvaluationMetric & {
+ $type: "none";
+ value: undefined;
+};
+
+type NumericMetric = BaseEvaluationMetric & {
+ $type: "numeric";
+ value?: number;
+};
+
+type BooleanMetric = BaseEvaluationMetric & {
+ $type: "boolean";
+ value?: boolean;
+};
+
+type StringMetric = BaseEvaluationMetric & {
+ $type: "string";
+ value?: string;
+};
diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx
new file mode 100644
index 00000000000..504674bcab4
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx
@@ -0,0 +1,169 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import { makeStyles, mergeClasses, tokens, Tooltip } from "@fluentui/react-components";
+import { DismissCircle16Regular, ErrorCircleRegular, Info16Regular, InfoRegular, Warning16Regular, WarningRegular } from "@fluentui/react-icons";
+
+const useCardListStyles = makeStyles({
+ metricCardList: { display: 'flex', gap: '1rem', flexWrap: 'wrap' },
+});
+
+export const MetricCardList = ({ scenario }: { scenario: ScenarioRunResult }) => {
+ const classes = useCardListStyles();
+ return (
+
+ {Object.values(scenario.evaluationResult.metrics).map((metric, index) => (
+
+ ))}
+
+ );
+};
+
+const useCardStyles = makeStyles({
+ card: {
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem',
+ padding: '.75rem', border: '1px solid #e0e0e0', borderRadius: '4px',
+ minWidth: '8rem'
+ },
+ metricText: { fontSize: '1rem', fontWeight: 'normal' },
+ valueText: { fontSize: '1.5rem', fontWeight: 'bold' },
+ scoreFgDefault: { color: tokens.colorNeutralStrokeAccessible },
+ scoreFg0: { color: tokens.colorStatusDangerForeground1 },
+ scoreFg1: { color: tokens.colorStatusDangerForeground2 },
+ scoreFg2: { color: tokens.colorStatusDangerForeground2 },
+ scoreFg3: { color: tokens.colorStatusWarningForeground2 },
+ scoreFg4: { color: tokens.colorStatusSuccessForeground2 },
+ scoreFg5: { color: tokens.colorStatusSuccessForeground2 },
+ scoreBgDefault: { backgroundColor: tokens.colorNeutralCardBackground },
+ scoreBg0: { backgroundColor: tokens.colorStatusDangerBackground1 },
+ scoreBg1: { backgroundColor: tokens.colorStatusDangerBackground2 },
+ scoreBg2: { backgroundColor: tokens.colorStatusDangerBackground2 },
+ scoreBg3: { backgroundColor: tokens.colorStatusWarningBackground2 },
+ scoreBg4: { backgroundColor: tokens.colorStatusSuccessBackground2 },
+ scoreBg5: { backgroundColor: tokens.colorStatusSuccessBackground2 },
+});
+
+const useCardColors = (interpretation?: EvaluationMetricInterpretation) => {
+ const classes = useCardStyles();
+ let fg = classes.scoreFgDefault;
+ let bg = classes.scoreBgDefault;
+ if (interpretation?.rating) {
+ switch (interpretation.rating) {
+ case "unknown":
+ case "inconclusive":
+ fg = classes.scoreFg0;
+ bg = classes.scoreBg0;
+ break;
+ case "exceptional":
+ fg = classes.scoreFg5;
+ bg = classes.scoreBg5;
+ break;
+ case "good":
+ fg = classes.scoreFg4;
+ bg = classes.scoreBg4;
+ break;
+ case "average":
+ fg = classes.scoreFg3;
+ bg = classes.scoreBg3;
+ break;
+ case "poor":
+ fg = classes.scoreFg2;
+ bg = classes.scoreBg2;
+ break;
+ case "unacceptable":
+ fg = classes.scoreFg1;
+ bg = classes.scoreBg1;
+ break;
+ }
+ }
+ return { fg, bg };
+};
+
+type MetricType = StringMetric | NumericMetric | BooleanMetric | MetricWithNoValue;
+
+export const MetricCard = ({ metric }: { metric: MetricType }) => {
+
+ let renderValue: (metric: MetricType) => React.ReactNode;
+ switch (metric.$type) {
+ case "string":
+ renderValue = (metric: MetricType) => <>{metric?.value ?? "??"}>;
+ break;
+ case "boolean":
+ renderValue = (metric: MetricType) => <>{
+ !metric || metric.value === undefined || metric.value === null ?
+ '??' :
+ metric.value ? 'Pass' : 'Fail'}>;
+ break;
+ case "numeric":
+ renderValue = (metric: MetricType) => <>{metric?.value ?? "??"}>;
+ break;
+ case "none":
+ renderValue = () => <>None>;
+ break;
+ default:
+ throw new Error(`Unknown metric type: ${metric["$type"]}`);
+ }
+
+ const classes = useCardStyles();
+ const { fg, bg } = useCardColors(metric.interpretation);
+ const hasReason = metric.interpretation?.reason != null;
+ const hasInformationalMessages = metric.diagnostics.some((d: EvaluationDiagnostic) => d.severity == "informational");
+ const hasWarningMessages = metric.diagnostics.some((d: EvaluationDiagnostic) => d.severity == "warning");
+ const hasErrorMessages = metric.diagnostics.some((d: EvaluationDiagnostic) => d.severity == "error");
+ const supportsHover = hasReason || hasInformationalMessages || hasWarningMessages || hasErrorMessages;
+ const card =
+ (
+ {reason &&
+ {failed ?
+
{reason}
:
+
{reason}
+ }
+
}
+ {hasErrorMessages &&
+ {errorMessages.map((message: string, index: number) =>
+
{message}
)}
+
}
+ {hasWarningMessages &&
+ {warningMessages.map((message: string, index: number) =>
+
{message}
)}
+
}
+ {hasInformationalMessages &&
+ {informationalMessages.map((message: string, index: number) =>
+
{message}
)}
+
}
+
);
+};
\ No newline at end of file
diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/PassFailBar.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/PassFailBar.tsx
new file mode 100644
index 00000000000..f0e60046643
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/PassFailBar.tsx
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import { makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ passFail: {
+ display: 'inline-flex',
+ flexDirection: 'row',
+ },
+ pass: {
+ borderTopRightRadius: '2px',
+ borderBottomRightRadius: '2px',
+ backgroundColor: tokens.colorStatusSuccessBackground3,
+ border: `1px solid`,
+ borderLeft: 'none',
+ },
+ fail: {
+ borderTopLeftRadius: '2px',
+ borderBottomLeftRadius: '2px',
+ backgroundColor: tokens.colorStatusDangerBackground3,
+ border: `1px solid`,
+ borderRight: 'none',
+ },
+ allPass: {
+ borderRadius: '2px',
+ backgroundColor: tokens.colorStatusSuccessBackground3,
+ border: `1px solid`,
+ },
+ allFail: {
+ borderRadius: '2px',
+ backgroundColor: tokens.colorStatusDangerBackground3,
+ border: `1px solid`,
+ }
+});
+
+export const PassFailBar = ({ pass, total, width, height }: { pass: number, total: number, width?: string, height?: string }) => {
+ const classes = useStyles();
+ const passPct = total > 0 ? (pass / total) * 100 : 0;
+ const failPct = 100 - passPct;
+ width = width || '3rem';
+
+ if (pass === 0) {
+ return