kpfadnis's picture
chore (maintainance): Update depdencies, copyright and minimal support for chat template.
e23b66d
raw
history blame
33.6 kB
/**
*
* Copyright 2023-2025 InspectorRAGet Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**/
'use client';
import { countBy, isEmpty } from 'lodash';
import cx from 'classnames';
import { useState, useMemo, useEffect, useRef } from 'react';
import { Tile, Button, Slider } from '@carbon/react';
import { WarningAlt } from '@carbon/icons-react';
import { ScatterChart } from '@carbon/charts-react';
import { useTheme } from '@/src/theme';
import { Model, Metric, TaskEvaluation } from '@/src/types';
import {
castToNumber,
AgreementLevels,
extractMetricDisplayName,
} from '@/src/utilities/metrics';
import { calculateFisherRandomization } from '@/src/utilities/significance';
import { areObjectsIntersecting } from '@/src/utilities/objects';
import { hash } from '@/src/utilities/strings';
import Filters from '@/src/components/filters/Filters';
import TasksTable from '@/src/views/tasks-table/TasksTable';
import ModelSelector from '@/src/components/selectors/ModelSelector';
import MetricSelector from '@/src/components/selectors/MetricSelector';
import { getModelColorPalette } from '@/src/utilities/colors';
import '@carbon/charts-react/styles.css';
import classes from './ModelComparator.module.scss';
// ===================================================================================
// TYPES
// ===================================================================================
type StatisticalInformation = {
p: number;
distributionA: number[];
meanA: number;
distributionB: number[];
meanB: number;
taskIds?: string[];
};
interface Props {
evaluationsPerMetric: { [key: string]: TaskEvaluation[] };
models: Model[];
metrics: Metric[];
filters: { [key: string]: string[] };
onTaskSelection: Function;
}
// ===================================================================================
// COMPUTE FUNCTIONS
// ===================================================================================
/**
* Build an array containing evaluations only for selected models for each task.
*
* Eligbility criteria:
*
* 1. Must have evaluations for both selected models
*
* 2. Each evaluation must have an agreement value for selected metric
*
*
* @param evaluations evaluations for all task
* @param modelA selected model
* @param modelB selected model
* @param metric selected metric
* @returns
*/
function extractEvaluationsPerTask(
evaluations: TaskEvaluation[],
modelA: Model,
modelB: Model,
metric: string,
selectedFilters: { [key: string]: string[] },
selectedMetricRange?: number[],
) {
// Step 1: Initiaze necessary variable
const modelEvaluationsPerTask: { [key: string]: TaskEvaluation[] } = {};
// Step 2: Add to model evaluations for a task, if evaluation meets eligbility criteria
evaluations.forEach((evaluation) => {
if (
(evaluation.modelId === modelA.modelId ||
evaluation.modelId === modelB.modelId) &&
evaluation[`${metric}_agg`].level !== AgreementLevels.NO_AGREEMENT &&
(!isEmpty(selectedFilters)
? areObjectsIntersecting(selectedFilters, evaluation)
: true)
) {
const modelEvaluationsForTask =
modelEvaluationsPerTask[evaluation.taskId];
if (modelEvaluationsForTask) {
modelEvaluationsForTask.push(evaluation);
} else {
modelEvaluationsPerTask[evaluation.taskId] = [evaluation];
}
}
});
// Step 3: Retain only those task which has evaluations for both models
// and one or more models have aggregate value in the selected range
return Object.values(modelEvaluationsPerTask).filter(
(entry) =>
entry.length == 2 &&
(selectedMetricRange
? (entry[0][`${metric}_agg`].value >= selectedMetricRange[0] &&
entry[0][`${metric}_agg`].value <= selectedMetricRange[1]) ||
(entry[1][`${metric}_agg`].value >= selectedMetricRange[0] &&
entry[1][`${metric}_agg`].value <= selectedMetricRange[1])
: true),
);
}
/**
* Run statistical significance test based on Fisher randomization method.
* @param evaluationsPerMetric evaluations per metric
* @param metrics metrics
* @param modelA selected model
* @param modelB selected model
* @param selectedMetric If `undefined`, run for all metrics in `evaluationsPerMetric` object
* @returns
*/
function runStatisticalSignificanceTest(
evaluationsPerMetric: { [key: string]: TaskEvaluation[] },
metrics: Metric[],
modelA: Model,
modelB: Model,
selectedMetric: Metric | undefined,
selectedFilters: { [key: string]: string[] },
selectedMetricRange?: number[],
) {
// Step 1: Initialize necessary variables
const evaluationsPerMetricPerTask: { [key: string]: TaskEvaluation[][] } = {};
// Step 2: Retain evaluations for tasks where both models have agreement value
if (selectedMetric) {
const evaluationsPerTask = extractEvaluationsPerTask(
evaluationsPerMetric[selectedMetric.name],
modelA,
modelB,
selectedMetric.name,
selectedFilters,
selectedMetricRange,
);
if (evaluationsPerTask.length !== 0) {
evaluationsPerMetricPerTask[selectedMetric.name] = evaluationsPerTask;
}
} else {
Object.keys(evaluationsPerMetric).forEach((metric) => {
const evaluationsPerTask = extractEvaluationsPerTask(
evaluationsPerMetric[metric],
modelA,
modelB,
metric,
selectedFilters,
selectedMetricRange,
);
if (evaluationsPerTask.length !== 0) {
evaluationsPerMetricPerTask[metric] = evaluationsPerTask;
}
});
}
// Step 3: Compute model value distribution for every metric
const distributionA: { [key: string]: number[] } = {};
const distributionB: { [key: string]: number[] } = {};
const taskIds: { [key: string]: string[] } = {};
Object.keys(evaluationsPerMetricPerTask).forEach((metric) => {
const metricValues = metrics.find((entry) => entry.name === metric)?.values;
taskIds[metric] = evaluationsPerMetricPerTask[metric].map(
(entry) => entry[0].taskId,
);
distributionA[metric] = evaluationsPerMetricPerTask[metric].map((entry) =>
castToNumber(
entry[0].modelId === modelA.modelId
? entry[0][`${metric}_agg`].value
: entry[1][`${metric}_agg`].value,
metricValues,
),
);
distributionB[metric] = evaluationsPerMetricPerTask[metric].map((entry) =>
castToNumber(
entry[1].modelId === modelB.modelId
? entry[1][`${metric}_agg`].value
: entry[0][`${metric}_agg`].value,
metricValues,
),
);
});
// Step 3: Compute p value and means for every metric by comparing distributions
const information: { [key: string]: StatisticalInformation } = {};
Object.keys(evaluationsPerMetricPerTask).forEach((metric) => {
const [p, meanA, meanB] = calculateFisherRandomization(
distributionA[metric],
distributionB[metric],
);
information[metric] = {
p: p,
distributionA: distributionA[metric],
meanA: meanA,
distributionB: distributionB[metric],
meanB: meanB,
taskIds: taskIds[metric],
};
});
return information;
}
// ===================================================================================
// RENDER FUNCTIONS
// ===================================================================================
function prepareScatterPlotData(
modelA: string,
distributionA: number[],
modelB: string,
distributionB: number[],
taskIds?: string[],
) {
if (distributionA.length !== distributionB.length) {
return [];
}
// Step 2: Collate model wise predictions per task
const distributions: { values: number[]; taskId: string }[] = [];
distributionA.forEach((valueA, index) => {
distributions.push({
taskId: taskIds ? taskIds[index] : `${index}`,
values: [valueA, distributionB[index]],
});
});
// Step 3: Primary sort based on model A's value
distributions.sort((a, b) => a.values[0] - b.values[0]);
// Step 4: Scondary sort based on Model B's value
distributions.sort((a, b) => a.values[1] - b.values[1]);
// Step 5: Prepare chart data
const chartData: { [key: string]: string | number }[] = [];
distributions.forEach((entry, idx) => {
// Model A record
chartData.push({
group: modelA,
key: idx,
value: entry.values[0],
...(taskIds && { taskId: entry.taskId }),
});
// Model B record
chartData.push({
group: modelB,
key: idx,
value: entry.values[1],
...(taskIds && { taskId: entry.taskId }),
});
});
return chartData;
}
function renderResult(
statisticalInformationPerMetric: { [key: string]: StatisticalInformation },
metric: Metric,
modelA: Model,
modelB: Model,
numEvaluations: number,
modelColors: { [key: string]: string },
modelOrder: string[],
theme?: string,
) {
if (statisticalInformationPerMetric.hasOwnProperty(metric.name)) {
return (
<div
key={'statisticalInformation-metric-' + metric.name}
className={classes.performanceInformation}
>
<h5>
<strong>{extractMetricDisplayName(metric)}</strong>
</h5>
<Tile className={classes.tile}>
<div className={classes.tileContent}>
<span className={classes.tileContentInformation}>p-value</span>
<span
className={classes.tileContentValue}
suppressHydrationWarning={true}
>
{statisticalInformationPerMetric[metric.name]['p'].toFixed(4)}
</span>
<span
className={classes.tileContentDecision}
suppressHydrationWarning={true}
>
{statisticalInformationPerMetric[metric.name]['p'] <= 0.05
? 'Significant'
: 'Not significant'}
</span>
</div>
</Tile>
<ScatterChart
data={prepareScatterPlotData(
modelA.name,
statisticalInformationPerMetric[metric.name].distributionA,
modelB.name,
statisticalInformationPerMetric[metric.name].distributionB,
statisticalInformationPerMetric[metric.name].taskIds,
)}
options={{
axes: {
left: {
mapsTo: 'value',
...(metric.type === 'numerical' &&
typeof metric.minValue === 'number' &&
typeof metric.maxValue === 'number' && {
domain: [metric.minValue, metric.maxValue],
}),
...(metric.type === 'categorical' &&
typeof metric.minValue !== 'number' &&
typeof metric.maxValue !== 'number' && {
domain: [
castToNumber(metric.minValue?.value || 0, metric.values),
castToNumber(metric.maxValue?.value || 4, metric.values),
],
}),
title: extractMetricDisplayName(metric),
},
bottom: {
mapsTo: 'key',
ticks: {
values: [],
},
title: `Tasks (${
statisticalInformationPerMetric[metric.name].distributionA
.length
}/${numEvaluations})`,
},
},
width: '500px',
height: '500px',
toolbar: {
enabled: false,
},
color: {
scale: modelColors,
},
legend: {
order: modelOrder,
},
theme: theme,
}}
></ScatterChart>
</div>
);
} else {
return null;
}
}
// ===================================================================================
// MAIN FUNCTION
// ===================================================================================
export default function ModelComparator({
evaluationsPerMetric,
models,
metrics,
filters,
onTaskSelection,
}: Props) {
// Step 1: Initialize state and necessary variables
const [WindowWidth, setWindowWidth] = useState<number>(
global?.window && window.innerWidth,
);
const [modelA, setModelA] = useState<Model>(models[0]);
const [modelB, setModelB] = useState<Model>(models[1]);
const [selectedMetric, setSelectedMetric] = useState<Metric | undefined>(
undefined,
);
const [selectedFilters, setSelectedFilters] = useState<{
[key: string]: string[];
}>({});
const [statisticalInformationPerMetric, setStatisticalInformationPerMetric] =
useState<{ [key: string]: StatisticalInformation } | undefined>(undefined);
const [modelColors, modelOrder] = getModelColorPalette(models);
const [selectedMetricRange, setSelectedMetricRange] = useState<number[]>();
const chartRef = useRef(null);
// Step 2: Run effects
// Step 2.a: Window resizing
useEffect(() => {
const handleWindowResize = () => {
setWindowWidth(window.innerWidth);
};
// Step: Add event listener
window.addEventListener('resize', handleWindowResize);
// Step: Cleanup to remove event listener
return () => {
window.removeEventListener('resize', handleWindowResize);
};
}, []);
// Step 2.a: Fetch theme
const { theme } = useTheme();
//Step 2.c: Bucket human and algoritmic metrics
const [humanMetrics, algorithmMetrics] = useMemo(() => {
const hMetrics: Metric[] = [];
const aMetrics: Metric[] = [];
Object.values(metrics).forEach((metric) => {
if (metric.author === 'human') {
hMetrics.push(metric);
} else if (metric.author === 'algorithm') {
aMetrics.push(metric);
}
});
return [hMetrics, aMetrics];
}, [metrics]);
// Step 2.d: Reset selected metric range, only applicable for numerical metrics
useEffect(() => {
if (
selectedMetric &&
selectedMetric.type === 'numerical' &&
selectedMetric.range
) {
setSelectedMetricRange([
selectedMetric.range[0],
selectedMetric.range[1],
]);
} else setSelectedMetricRange(undefined);
}, [selectedMetric]);
// Step 2.e: Identify visible evaluations
const filteredEvaluations = useMemo(() => {
if (selectedMetric) {
// Step 1: Identify evaluations for selected models
const evaluationsForSelectedModels = evaluationsPerMetric[
selectedMetric.name
].filter(
(evaluation) =>
(evaluation.modelId === modelA.modelId ||
evaluation.modelId === modelB.modelId) &&
(!isEmpty(selectedFilters)
? areObjectsIntersecting(selectedFilters, evaluation)
: true),
);
// Step 2: Collate evaluation per task id
const evaluationsPerTask: { [key: string]: { [key: string]: number } } =
{};
evaluationsForSelectedModels.forEach((evaluation) => {
const entry = evaluationsPerTask[evaluation.taskId];
if (entry) {
entry[evaluation.modelId] =
evaluation[`${selectedMetric.name}_agg`].value;
} else {
evaluationsPerTask[evaluation.taskId] = {
[evaluation.modelId]:
evaluation[`${selectedMetric.name}_agg`].value,
};
}
});
// Step 3: Only select evaluation tasks where models aggregate values differe
// and one or more models have aggregate value in the selected range
const visibleEvaluationTaskIds = Object.keys(evaluationsPerTask).filter(
(taskId) =>
Object.keys(countBy(Object.values(evaluationsPerTask[taskId])))
.length > 1 &&
(selectedMetricRange
? (Object.values(evaluationsPerTask[taskId])[0] >=
selectedMetricRange[0] &&
Object.values(evaluationsPerTask[taskId])[0] <=
selectedMetricRange[1]) ||
(Object.values(evaluationsPerTask[taskId])[1] >=
selectedMetricRange[0] &&
Object.values(evaluationsPerTask[taskId])[1] <=
selectedMetricRange[1])
: true),
);
// Step 4: Return evaluations for selected evaluation tasks where models aggregate values differe
return evaluationsForSelectedModels.filter((evaluation) =>
visibleEvaluationTaskIds.includes(evaluation.taskId),
);
}
return [];
}, [
evaluationsPerMetric,
selectedMetric,
modelA,
modelB,
selectedMetricRange,
]);
// Step 2.f: Reset statistical information, if either of model changes or filters are changed
useEffect(() => {
setStatisticalInformationPerMetric(undefined);
}, [modelA, modelB, selectedFilters]);
// Step 2.g: Recalculate statistical information, if metric changes
useEffect(() => {
if (
!selectedMetric &&
statisticalInformationPerMetric &&
Object.keys(statisticalInformationPerMetric).length == 1
) {
setStatisticalInformationPerMetric(
runStatisticalSignificanceTest(
evaluationsPerMetric,
metrics,
modelA,
modelB,
selectedMetric,
selectedFilters,
selectedMetricRange,
),
);
} else if (
selectedMetric &&
selectedMetricRange &&
statisticalInformationPerMetric &&
statisticalInformationPerMetric.hasOwnProperty(selectedMetric.name)
) {
setStatisticalInformationPerMetric(
runStatisticalSignificanceTest(
evaluationsPerMetric,
metrics,
modelA,
modelB,
selectedMetric,
selectedFilters,
selectedMetricRange,
),
);
}
}, [selectedMetric, selectedMetricRange]);
// Step 2.h: Compute computation complexity
const complexity = useMemo(() => {
let size = 0;
if (selectedMetric) {
size = evaluationsPerMetric[selectedMetric.name].length / models.length;
} else {
size = Object.values(evaluationsPerMetric)
.map((evaluations) => evaluations.length / models.length)
.reduce((a, b) => a + b, 0);
}
if (size > 1000) {
return 'high';
}
return 'low';
}, [evaluationsPerMetric, selectedMetric]);
// Step 2.i: Add chart event
useEffect(() => {
// Step 2.i.*: Local copy of reference
let ref = null;
// Step 2.i.**: Update reference and add event
if (chartRef && chartRef.current) {
ref = chartRef.current;
//@ts-ignore
ref.chart.services.events.addEventListener(
'scatter-click',
({ detail }) => {
onTaskSelection(detail.datum.taskId);
},
);
}
// Step 2.i.***: Cleanup function
return () => {
if (ref) {
//@ts-ignore
ref.chart.services.events.removeEventListener(
'scatter-click',
({ detail }) => {
onTaskSelection(detail.datum.taskId);
},
);
}
};
}, [chartRef, selectedMetric, statisticalInformationPerMetric]);
// Step 3: Render
return (
<div className={classes.page}>
<div className={classes.selectors}>
<div className={classes.modelSelector}>
<ModelSelector
id={'modelA-selector-excluding-model-' + modelB.modelId}
key={'modelA-selector-excluding-model-' + modelB.modelId}
models={models}
defaultValue={modelA}
onSelect={(modelId: string) => {
const selectedModel = models.find(
(model) => model.modelId === modelId,
);
if (selectedModel) {
setModelA(selectedModel);
}
}}
disabledModels={[modelB]}
/>
</div>
<div className={classes.modelSelector}>
<ModelSelector
id={'modelB-selector-excluding-model-' + modelA.modelId}
key={'modelB-selector-excluding-model-' + modelA.modelId}
models={models}
defaultValue={modelB}
onSelect={(modelId: string) => {
const selectedModel = models.find(
(model) => model.modelId === modelId,
);
if (selectedModel) {
setModelB(selectedModel);
}
}}
disabledModels={[modelA]}
/>
</div>
<div className={classes.metricSelector}>
<MetricSelector
metrics={metrics}
onSelect={(metric: Metric | undefined) => {
setSelectedMetric(metric);
}}
warn={!selectedMetric}
warnText={'You must select a single metric to view tasks. '}
/>
</div>
{selectedMetric &&
selectedMetric.type === 'numerical' &&
selectedMetric.range ? (
<div>
<Slider
ariaLabelInput="Lower bound"
unstable_ariaLabelInputUpper="Upper bound"
labelText={`Choose range`}
value={
selectedMetricRange
? selectedMetricRange[0]
: selectedMetric.range[0]
}
unstable_valueUpper={
selectedMetricRange
? selectedMetricRange[1]
: selectedMetric.range[1]
}
min={selectedMetric.range[0]}
max={selectedMetric.range[1]}
step={
selectedMetric.range.length === 3 ? selectedMetric.range[2] : 1
}
onChange={({
value,
valueUpper,
}: {
value: number;
valueUpper?: number;
}) => {
setSelectedMetricRange((prev) => [
value,
valueUpper
? valueUpper
: prev
? prev[1]
: selectedMetric.range
? selectedMetric.range[2]
: 100,
]);
}}
/>
</div>
) : null}
<div className={classes.calculateBtn}>
<Button
onClick={() => {
// Run statistical significance calculations
setStatisticalInformationPerMetric(
runStatisticalSignificanceTest(
evaluationsPerMetric,
metrics,
modelA,
modelB,
selectedMetric,
selectedFilters,
selectedMetricRange,
),
);
}}
>
Calculate
</Button>
</div>
</div>
{!isEmpty(filters) ? (
<Filters
keyPrefix="ModelComparator"
filters={filters}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
) : null}
{statisticalInformationPerMetric ? (
<div className={classes.row}>
<div className={classes.hypothesisContainer}>
<span className={classes.hypothesisStatement}>
H<sub>0</sub>: {modelA.name} and {modelB.name} scores are derived
from the same distribution.
</span>
<span className={classes.hypothesisValidityCondition}>
<span>{'Reject the null hypothesis if p < 0.05'}</span>
</span>
</div>
{!selectedMetric && humanMetrics.length ? (
<div className={classes.row}>
<h4>Human Evaluations</h4>
<div
className={cx(
humanMetrics.length > 3
? classes.graphsGrid
: classes.graphsFlex,
)}
>
{humanMetrics.map((metric) =>
renderResult(
statisticalInformationPerMetric,
metric,
modelA,
modelB,
evaluationsPerMetric[metric.name].length / models.length,
modelColors,
modelOrder,
theme,
),
)}
</div>
</div>
) : null}
{!selectedMetric && algorithmMetrics.length ? (
<div className={classes.row}>
<h4>Algorithmic Evaluations</h4>
<div
className={cx(
algorithmMetrics.length > 3
? classes.graphsGrid
: classes.graphsFlex,
)}
>
{algorithmMetrics.map((metric) =>
renderResult(
statisticalInformationPerMetric,
metric,
modelA,
modelB,
evaluationsPerMetric[metric.name].length / models.length,
modelColors,
modelOrder,
theme,
),
)}
</div>
</div>
) : null}
{selectedMetric &&
statisticalInformationPerMetric.hasOwnProperty(
selectedMetric.name,
) ? (
<div className={classes.row}>
<div
key={`statisticalInformation-metric-${selectedMetric.name}--${hash(JSON.stringify(statisticalInformationPerMetric[selectedMetric.name]))}`}
className={classes.performanceInformation}
>
<h5>
<strong>{extractMetricDisplayName(selectedMetric)}</strong>
</h5>
<Tile className={classes.tile}>
<div className={classes.tileContent}>
<span className={classes.tileContentInformation}>
p-value
</span>
<span
className={classes.tileContentValue}
suppressHydrationWarning={true}
>
{statisticalInformationPerMetric[selectedMetric.name][
'p'
].toFixed(4)}
</span>
<span
className={classes.tileContentDecision}
suppressHydrationWarning={true}
>
{statisticalInformationPerMetric[selectedMetric.name][
'p'
] <= 0.05
? 'Significant'
: 'Not significant'}
</span>
</div>
</Tile>
<ScatterChart
ref={chartRef}
data={prepareScatterPlotData(
modelA.name,
statisticalInformationPerMetric[selectedMetric.name]
.distributionA,
modelB.name,
statisticalInformationPerMetric[selectedMetric.name]
.distributionB,
statisticalInformationPerMetric[selectedMetric.name]
.taskIds,
)}
options={{
axes: {
left: {
mapsTo: 'value',
...(selectedMetric.type === 'numerical' &&
typeof selectedMetric.minValue === 'number' &&
typeof selectedMetric.maxValue === 'number' && {
domain: [
selectedMetric.minValue,
selectedMetric.maxValue,
],
}),
...(selectedMetric.type === 'categorical' &&
typeof selectedMetric.minValue !== 'number' &&
typeof selectedMetric.maxValue !== 'number' && {
domain: [
castToNumber(
selectedMetric.minValue?.value || 0,
selectedMetric.values,
),
castToNumber(
selectedMetric.maxValue?.value || 4,
selectedMetric.values,
),
],
}),
title: extractMetricDisplayName(selectedMetric),
},
bottom: {
mapsTo: 'key',
ticks: {
values: [],
},
title: `Tasks (${
statisticalInformationPerMetric[selectedMetric.name]
.distributionA.length
}/${
evaluationsPerMetric[selectedMetric.name].length /
models.length
})`,
},
},
width: `${Math.round(WindowWidth * 0.8)}px`,
height: '500px',
toolbar: {
enabled: false,
},
color: {
scale: modelColors,
},
legend: {
order: modelOrder,
},
theme: theme,
}}
></ScatterChart>
</div>
</div>
) : (
<>
<div className={classes.tasksContainerNotification}>
<span
className={classes.tasksContainerNotificationText}
>{`Press calculate to measure statistical significance ${selectedMetric ? 'for' : 'across'} "${selectedMetric ? extractMetricDisplayName(selectedMetric) : 'all'}" metric${selectedMetric ? '' : 's'}`}</span>
<span
className={classes.tasksContainerNotificationText}
>{`for "${modelA.name}" and "${modelB.name}" models.`}</span>
{complexity === 'high' ? (
<div className={classes.tasksContainerWarning}>
<WarningAlt
height={'24px'}
width={'24px'}
className={classes.tasksContainerWarningIcon}
/>
<span className={classes.tasksContainerWarningText}>
It might take few minutes to build this view.
</span>
</div>
) : null}
</div>
</>
)}
</div>
) : (
<>
<div className={classes.tasksContainerNotification}>
<span
className={classes.tasksContainerNotificationText}
>{`Press calculate to measure statistical significance ${selectedMetric ? 'for' : 'across'} "${selectedMetric ? extractMetricDisplayName(selectedMetric) : 'all'}" metric${selectedMetric ? '' : 's'}`}</span>
<span
className={classes.tasksContainerNotificationText}
>{`for "${modelA.name}" and "${modelB.name}" models.`}</span>
{complexity === 'high' ? (
<div className={classes.tasksContainerWarning}>
<WarningAlt
height={'24px'}
width={'24px'}
className={classes.tasksContainerWarningIcon}
/>
<span className={classes.tasksContainerWarningText}>
It might take few minutes to build this view.
</span>
</div>
) : null}
</div>
</>
)}
{selectedMetric &&
statisticalInformationPerMetric &&
statisticalInformationPerMetric.hasOwnProperty(selectedMetric.name) && (
<div className={classes.row}>
<h4>
Tasks{selectedMetric && filteredEvaluations && <sup>*</sup>}
</h4>
{filteredEvaluations ? (
<>
<TasksTable
metrics={[selectedMetric]}
evaluations={filteredEvaluations}
models={[modelA, modelB]}
filters={filters}
onClick={onTaskSelection}
/>
<span className={classes.tasksTableWarning}>
<sup>*</sup> Only tasks with different model aggregate scores
are shown in the above table.
</span>
</>
) : null}
</div>
)}
</div>
);
}