kpfadnis's picture
chore (maintainance): Update depdencies, copyright and minimal support for chat template.
e23b66d
raw
history blame
26.2 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 { useState, useEffect, useMemo } from 'react';
import { isEmpty } from 'lodash';
import {
DataTable,
TableContainer,
Table,
TableToolbar,
TableBatchActions,
TableBatchAction,
TableToolbarContent,
TableToolbarSearch,
TableHead,
TableRow,
TableSelectAll,
TableSelectRow,
TableHeader,
TableBody,
TableCell,
Pagination,
Button,
Tooltip,
} from '@carbon/react';
import { Flag, FlagFilled, DocumentExport, Chat } from '@carbon/icons-react';
import { SimpleBarChart } from '@carbon/charts-react';
import { ScaleTypes } from '@carbon/charts';
import { useTheme } from '@/src/theme';
import { Metric, Model, Task, TaskEvaluation } from '@/src/types';
import { extractMetricDisplayValue } from '@/src/utilities/metrics';
import { truncate } from '@/src/utilities/strings';
import { useDataStore } from '@/src/store';
import { useNotification } from '@/src/components/notification/Notification';
import { exportData } from '@/src/processor';
import classes from './TasksTable.module.scss';
// ===================================================================================
// TYPES
// ===================================================================================
type EvaluationRow = {
id: string;
taskId: string;
};
interface Props {
metrics: Metric[];
evaluations: TaskEvaluation[];
models: Model[];
filters: { [key: string]: string[] };
annotator?: string;
onClick: Function;
}
// ===================================================================================
// COMPUTE FUNCTIONS
// ===================================================================================
/**
* Helper function to compute evaluation table headers and rows
* @param evaluations full set of evaluations
* @param metric metric under consideration
* @returns
*/
function populateTable(
evaluations: TaskEvaluation[],
metrics: Metric[],
models: Model[],
taskInputMap: { [key: string]: any },
filters: { [key: string]: string[] },
annotator?: string,
): [{ key: string; header: string }[], EvaluationRow[]] {
const modelIds = new Set<string>();
const applicableFilters = new Set<string>();
const headers = [
{
key: 'task',
header: 'Task',
},
];
const rows: EvaluationRow[] = [];
// Step 1: Build evaluations map combining different model evaluations
const evaluationsMap = new Map<string, any>();
evaluations.forEach((evaluation) => {
const entry = evaluationsMap.get(evaluation.taskId) || {};
// Step 1.a: Add filter value, if exists in evaluation and not already added
if (isEmpty(entry) && !isEmpty(filters)) {
for (const filter of Object.keys(filters)) {
if (evaluation.hasOwnProperty(filter)) {
entry[filter] = evaluation[filter];
// Add to list of applicable filters
applicableFilters.add(filter);
}
}
}
// Add annotations
entry[`${evaluation.modelId}::value`] = {};
metrics.forEach((metric) => {
if (annotator && evaluation.hasOwnProperty(metric.name)) {
entry[`${evaluation.modelId}::value`][metric.name] =
extractMetricDisplayValue(
evaluation[metric.name][annotator].value,
metric.values,
);
} else if (evaluation.hasOwnProperty(`${metric.name}_agg`)) {
entry[`${evaluation.modelId}::value`][metric.name] =
extractMetricDisplayValue(
evaluation[`${metric.name}_agg`].value,
metric.values,
);
} else {
entry[`${evaluation.modelId}::value`][metric.name] = '-';
}
});
// Step 1.b: Save updated entry into evaluations map
evaluationsMap.set(evaluation.taskId, entry);
// Step 1.c: Add model id to set of model ids
modelIds.add(evaluation.modelId);
});
// Step 2: Update evaluation table header based on filters
applicableFilters.forEach((filter) => {
const header = filter.split('_').join(' ');
headers.push({
key: filter,
header: header.charAt(0).toUpperCase() + header.slice(1).toLowerCase(),
});
});
// Step 3: Update evaluation table headers based on model ids
modelIds.forEach((modelId) => {
headers.push({
key: `${modelId}::value`,
header:
models.find((model) => model.modelId === modelId)?.name || modelId,
});
});
// Step 4: Populate evaluation table rows
evaluationsMap.forEach((record, taskId) => {
rows.push({ id: taskId, task: taskInputMap[taskId] || taskId, ...record });
});
// Step 3: Populate evaluation table rows
return [headers, rows];
}
// ===================================================================================
// RENDER FUNCTIONS
// ===================================================================================
/**
* Build sparkline graph
*/
function sparkline(
annotations: {
[key: string]: { timestamp: number; value: string };
},
metric: Metric,
key: string,
theme?: string,
) {
if (annotations) {
// Initialize distribution with potential values for x-axis
const distribution: { [key: string]: number } = metric.values
? Object.fromEntries(
metric.values.map((entry) => [entry.displayValue, 0]),
)
: {};
// Iterate over annotations to populate distribution (y-axis)
for (const [, entries] of Object.entries(annotations)) {
const displayValue = extractMetricDisplayValue(
entries.value,
metric.values,
);
if (distribution.hasOwnProperty(displayValue)) {
distribution[displayValue] += 1;
} else {
distribution[displayValue] = 1;
}
}
// Transform distribution into data type suitable for SimpleBarChart
const data = Object.entries(distribution).map(([value, count]) => {
return { group: value, value: count };
});
// Identify number of unique used values
const numOfUsedValues = data.filter((entry) => entry.value !== 0).length;
return (
<div key={key}>
<SimpleBarChart
data={data}
options={{
color: {
scale: Object.fromEntries(
data.map((entry) => [
entry.group,
numOfUsedValues === 1
? '#42be65'
: numOfUsedValues >= 3
? '#fa4d56'
: '#f1c21b',
]),
),
},
axes: {
left: {
mapsTo: 'value',
visible: false,
scaleType: ScaleTypes.LINEAR,
},
bottom: {
mapsTo: 'group',
visible: false,
scaleType: ScaleTypes.LABELS,
},
},
grid: {
y: {
enabled: false,
},
x: {
enabled: false,
},
},
legend: {
enabled: false,
},
toolbar: {
enabled: false,
},
height: '24px',
width: '48px',
theme: theme,
}}
></SimpleBarChart>
</div>
);
}
return null;
}
// ===================================================================================
// MAIN FUNCTION
// ===================================================================================
export default function TasksTable({
metrics,
evaluations,
models,
filters,
annotator,
onClick,
}: Props) {
// Step 1: Initialize state and necessary variables
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [visibleRows, setVisibleRows] = useState<EvaluationRow[]>([]);
// Step 2: Run effects
// Step 2.a: Fetch theme
const { theme } = useTheme();
// Step 2.b: Fetch data from data store
const { item, taskMap, updateTask: updateTask } = useDataStore();
// Step 2.c: Notification hook
const { createNotification } = useNotification();
// Step 2.d: Build evaluations map
const evaluationsMap = useMemo(() => {
return Object.fromEntries(
evaluations.map((evaluation) => [
`${evaluation.taskId}:${evaluation.modelId}`,
Object.fromEntries(
metrics.map((metric) => [metric.name, evaluation[metric.name]]),
),
]),
);
}, [evaluations, metrics]);
// Step 2.e: Build tasks map
const taskInputMap = useMemo(() => {
return Object.fromEntries(
evaluations.map((evaluation) => [
`${evaluation.taskId}`,
evaluation.query,
]),
);
}, [evaluations]);
// Step 2.f: Populate table header and rows
var [headers, rows]: [{ key: string; header: string }[], EvaluationRow[]] =
useMemo(
() =>
populateTable(
evaluations,
metrics,
models,
taskInputMap,
filters,
annotator,
),
[evaluations, metrics, models, filters, taskInputMap, annotator],
);
// Step 2.g: Identify visible rows
useEffect(() => {
// Set visible rows
setVisibleRows(() => {
if (rows.length <= pageSize) {
setPage(1);
}
return rows.slice(
(page - 1) * pageSize,
(page - 1) * pageSize + pageSize,
);
});
}, [rows, page, pageSize]);
// Step 3: Render
return (
<>
{headers && rows && (
<div>
<DataTable
rows={visibleRows}
headers={headers}
isSortable
sortRow={(cellA, cellB, { sortDirection, sortStates }) => {
// Step 1: Check if cell values are objects
if (typeof cellA === 'object' && typeof cellB === 'object') {
// Step 1.a: Get first value for each cell object
const valueA = Object.values(cellA)[0];
const valueB = Object.values(cellB)[0];
// Step 1.b: Check if both values are of "string" type
if (typeof valueA === 'string' && typeof valueB === 'string') {
// Step 1.b.i: Check if both values are purely numeric
if (
!isNaN(parseFloat(valueA)) &&
!isNaN(parseFloat(valueB))
) {
if (sortDirection === sortStates.DESC) {
return parseFloat(valueA) - parseFloat(valueB);
}
return parseFloat(valueB) - parseFloat(valueA);
} else {
if (sortDirection === sortStates.DESC) {
return valueA.localeCompare(valueB);
}
return valueB.localeCompare(valueA);
}
}
// Step 1.c: Check if both values are of "number" type
else if (
typeof valueA === 'number' &&
typeof valueB === 'number'
) {
if (sortDirection === sortStates.DESC) {
return valueA - valueB;
}
return valueB - valueA;
}
}
// Step 2: cell values are assumed to be of "string" type
else {
if (sortDirection === sortStates.DESC) {
return cellA.localeCompare(cellB);
}
return cellB.localeCompare(cellA);
}
}}
>
{({
rows,
headers,
getHeaderProps,
getRowProps,
getToolbarProps,
getSelectionProps,
getBatchActionProps,
selectedRows,
getTableProps,
getTableContainerProps,
onInputChange,
selectRow,
}) => {
const batchActionProps = {
...getBatchActionProps({
onSelectAll: () => {
rows.map((row) => {
if (!row.isSelected) {
selectRow(row.id);
}
});
},
}),
};
return (
<TableContainer
className={classes.table}
{...getTableContainerProps()}
>
<TableToolbar {...getToolbarProps()}>
<TableBatchActions {...batchActionProps}>
<TableBatchAction
tabIndex={
batchActionProps.shouldShowBatchActions ? 0 : -1
}
renderIcon={FlagFilled}
onClick={() => {
selectedRows.forEach((entry) =>
updateTask(entry.id, {
flagged: true,
}),
);
}}
>
Flag
</TableBatchAction>
<TableBatchAction
tabIndex={
batchActionProps.shouldShowBatchActions ? 0 : -1
}
renderIcon={Flag}
onClick={() => {
selectedRows.forEach((entry) =>
updateTask(entry.id, {
flagged: false,
}),
);
}}
>
Unflag
</TableBatchAction>
<TableBatchAction
tabIndex={
batchActionProps.shouldShowBatchActions ? 0 : -1
}
renderIcon={DocumentExport}
onClick={() => {
const success = exportData(
item,
selectedRows.map((entry) => taskMap?.get(entry.id)),
);
if (success) {
// Notify user about successfuly export
createNotification({
kind: 'success',
title: 'Export successful.',
subtitle:
'Please look into browser default save location.',
});
} else {
// Notify user about invalid request
createNotification({
kind: 'error',
title: 'Export unsuccessful.',
subtitle: 'Please contact us.',
});
}
}}
>
Export
</TableBatchAction>
</TableBatchActions>
<TableToolbarContent className={classes.toolbar}>
<TableToolbarSearch
className={classes.toolbarSearch}
onChange={onInputChange}
/>
</TableToolbarContent>
</TableToolbar>
<Table {...getTableProps()}>
<TableHead>
<TableRow>
<TableSelectAll {...getSelectionProps()} />
{headers.map((header, index) => (
<TableHeader
key={'header--' + index}
{...getHeaderProps({ header })}
>
{header.header}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableRow
key={'row--' + index}
{...getRowProps({ row })}
>
<TableSelectRow
{...getSelectionProps({
row,
})}
/>
{row.cells.map((cell) =>
cell.info.header === 'task' ? (
<TableCell key={cell.id}>
<div className={classes.taskCell}>
<span
className={classes.link}
onClick={() => {
onClick(row.id);
}}
>
{truncate(cell.value, 80)}
</span>
<div className={classes.taskCellDetails}>
{taskMap?.get(cell.id.split(':task')[0])
?.comments && (
<Tooltip
align={'bottom'}
label={
'Click to view task with comments'
}
>
<Button
id="comments-btn"
className={classes.ViewCommentsBtn}
kind={'ghost'}
onClick={() => {
onClick(row.id);
}}
>
<Chat />
{
taskMap?.get(
cell.id.split(':task')[0],
)?.comments?.length
}
</Button>
</Tooltip>
)}
<Tooltip
align={'bottom-right'}
label={'Click to flag task'}
>
<Button
key={`${cell.value}__flag-btn`}
className={classes.flagTaskBtn}
kind={'ghost'}
onClick={() => {
const taskId =
cell.id.split(':task')[0];
// Step 0: Fetch task to update
const task: Task | undefined =
taskMap?.get(taskId);
// Step 1: Update, if possible
if (task) {
// Step 1.a: Update global copy
updateTask(taskId, {
flagged: !task?.flagged,
});
}
}}
>
{taskMap?.get(cell.id.split(':task')[0])
?.flagged ? (
<FlagFilled />
) : (
<Flag />
)}
</Button>
</Tooltip>
</div>
</div>
</TableCell>
) : (
<TableCell key={cell.id}>
<div className={classes.tableCell}>
<>
{cell.value ? (
typeof cell.value === 'object' ? (
<>
{Array.isArray(cell.value)
? !isEmpty(cell.value)
? cell.value.join(', ')
: '-'
: metrics.map((metric) => {
return (
<>
<div
className={
classes.tableCellValue
}
key={`${cell.id}::${metric.name}`}
>
<div
className={
classes.majorityValue
}
>
{
cell.value[
metric.name
]
}
</div>
{!annotator &&
evaluationsMap[
cell.id.split(
'::value',
1,
)[0]
]
? sparkline(
evaluationsMap[
cell.id.split(
'::value',
1,
)[0]
][metric.name],
metric,
cell.id,
theme,
)
: null}
</div>
</>
);
})}
</>
) : (
<div className={classes.majorityValue}>
{Array.isArray(cell.value)
? !isEmpty(cell.value)
? cell.value.join(', ')
: '-'
: cell.value}
</div>
)
) : (
<div className={classes.majorityValue}>
-
</div>
)}
</>
</div>
</TableCell>
),
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}}
</DataTable>
<Pagination
pageSizes={[10, 25, 50]}
totalItems={rows.length}
onChange={(event: any) => {
// Step 1: Update page size
setPageSize(event.pageSize);
// Step 2: Update page
setPage(event.page);
}}
></Pagination>
</div>
)}
</>
);
}