kpfadnis commited on
Commit
ae7d3b8
·
unverified ·
1 Parent(s): bac04f0

feat (expression): Experimental support for expression based filtering in model behavior. (#13)

Browse files
next.config.js CHANGED
@@ -22,7 +22,6 @@ const cspMap = {
22
  'base-uri': ["'none'"],
23
  'font-src': ["'self'", 'data:', "'unsafe-inline'"],
24
  'form-action': ["'self'"],
25
- 'frame-ancestors': ["'none'"],
26
  'frame-src': ["'self'"],
27
  'img-src': ["'self'", 'data:', 'blob:', 'www.ibm.com/'],
28
  'media-src': ["'self'", 'blob:', 'www.ibm.com/'],
 
22
  'base-uri': ["'none'"],
23
  'font-src': ["'self'", 'data:', "'unsafe-inline'"],
24
  'form-action': ["'self'"],
 
25
  'frame-src': ["'self'"],
26
  'img-src': ["'self'", 'data:', 'blob:', 'www.ibm.com/'],
27
  'media-src': ["'self'", 'blob:', 'www.ibm.com/'],
src/components/expression-builder/ExpressionBuilder.module.scss ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ *
3
+ * Copyright 2023-2024 InspectorRAGet Team
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ **/
18
+
19
+ @use '@carbon/react/scss/spacing' as *;
20
+ @use '@carbon/colors' as *;
21
+
22
+ .page {
23
+ display: flex;
24
+ flex-direction: column;
25
+ }
26
+
27
+ .actionButtons {
28
+ margin: $spacing-03 0;
29
+ display: flex;
30
+ column-gap: $spacing-03;
31
+ }
32
+
33
+ .containerWarning {
34
+ display: flex;
35
+ column-gap: $spacing-03;
36
+ align-items: center;
37
+ color: var(--cds-support-warning);
38
+ font-size: 14px;
39
+ line-height: 16px;
40
+ }
src/components/expression-builder/ExpressionBuilder.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ *
3
+ * Copyright 2023-2024 InspectorRAGet Team
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ **/
18
+
19
+ 'use client';
20
+
21
+ import { isEmpty } from 'lodash';
22
+ import { useState, useEffect } from 'react';
23
+ import { TextArea, Button } from '@carbon/react';
24
+ import { WarningAlt } from '@carbon/icons-react';
25
+
26
+ import { Model, Metric } from '@/src/types';
27
+ import {
28
+ PLACHOLDER_EXPRESSION_TEXT,
29
+ validate,
30
+ } from '@/src/utilities/expressions';
31
+
32
+ import classes from './ExpressionBuilder.module.scss';
33
+
34
+ // ===================================================================================
35
+ // TYPES
36
+ // ===================================================================================
37
+ interface Props {
38
+ expression?: object;
39
+ models?: Model[];
40
+ metric?: Metric;
41
+ setExpression?: Function;
42
+ }
43
+
44
+ // ===================================================================================
45
+ // MAIN FUNCTION
46
+ // ===================================================================================
47
+ export default function ExpressionBuilder({
48
+ expression,
49
+ models,
50
+ metric,
51
+ setExpression,
52
+ }: Props) {
53
+ // Step 1: Initialize state and necessary variables
54
+ const [updatedExpressionText, setUpdatedExpressionText] = useState<string>(
55
+ expression ? JSON.stringify(expression) : PLACHOLDER_EXPRESSION_TEXT,
56
+ );
57
+ const [errorMessage, setErrorMessage] = useState<string>();
58
+
59
+ // Step 2: Run effects
60
+ // Step 2.a: Validate expression when updated
61
+ useEffect(() => {
62
+ try {
63
+ // Step 2.a: Check JSON validity
64
+ const updatedExpression = JSON.parse(updatedExpressionText);
65
+
66
+ // Step 2.b: Check expression validity
67
+ const errorMessage = validate(
68
+ updatedExpression,
69
+ models?.map((model) => model.modelId),
70
+ );
71
+ if (errorMessage) {
72
+ setErrorMessage(errorMessage);
73
+ } else {
74
+ setErrorMessage(undefined);
75
+ }
76
+ } catch (err) {
77
+ setErrorMessage('Invalid JSON');
78
+ }
79
+ }, [updatedExpressionText]);
80
+
81
+ return (
82
+ <div className={classes.page}>
83
+ <TextArea
84
+ labelText="Expression"
85
+ placeholder={JSON.stringify(expression)}
86
+ value={updatedExpressionText}
87
+ disabled={
88
+ models === undefined ||
89
+ metric === undefined ||
90
+ setExpression === undefined
91
+ }
92
+ invalid={errorMessage !== undefined}
93
+ invalidText={errorMessage}
94
+ onChange={(event) => {
95
+ setUpdatedExpressionText(event.target.value);
96
+ }}
97
+ helperText="Please make sure you select correct model ids and values"
98
+ rows={4}
99
+ id="text-area__expression"
100
+ />
101
+ <div className={classes.actionButtons}>
102
+ <Button
103
+ kind="primary"
104
+ disabled={
105
+ errorMessage !== undefined ||
106
+ models === undefined ||
107
+ metric === undefined ||
108
+ setExpression === undefined
109
+ }
110
+ onClick={() =>
111
+ setExpression
112
+ ? setExpression(JSON.parse(updatedExpressionText))
113
+ : () => {}
114
+ }
115
+ >
116
+ Run
117
+ </Button>
118
+ <Button
119
+ kind="secondary"
120
+ disabled={expression === undefined || isEmpty(expression)}
121
+ onClick={() => {
122
+ // Step 1: Reset updated expression text
123
+ setUpdatedExpressionText('{}');
124
+
125
+ // Step 2: Reset expression
126
+ if (setExpression) {
127
+ setExpression({});
128
+ }
129
+ }}
130
+ >
131
+ Clear
132
+ </Button>
133
+ </div>
134
+
135
+ {models === undefined || metric === undefined ? (
136
+ <div className={classes.containerWarning}>
137
+ <WarningAlt />
138
+ <span>You must select a metric before proceeding.</span>
139
+ </div>
140
+ ) : null}
141
+ </div>
142
+ );
143
+ }
src/components/filters/Filters.module.scss CHANGED
@@ -43,17 +43,22 @@
43
  align-items: center;
44
  }
45
 
46
- .filters {
47
  margin: 0 0 $spacing-03 $spacing-05;
48
- padding: $spacing-05;
49
  display: none;
 
 
 
 
 
 
50
  align-items: baseline;
51
  column-gap: $spacing-09;
52
- box-shadow: 0 0 5px 2px $gray-40;
53
  }
54
 
55
  .visible {
56
  display: flex;
 
57
  animation: fade-in 0.5s;
58
  }
59
 
 
43
  align-items: center;
44
  }
45
 
46
+ .container {
47
  margin: 0 0 $spacing-03 $spacing-05;
 
48
  display: none;
49
+ box-shadow: 0 0 5px 2px $gray-40;
50
+ }
51
+
52
+ .filters {
53
+ padding: $spacing-05;
54
+ display: flex;
55
  align-items: baseline;
56
  column-gap: $spacing-09;
 
57
  }
58
 
59
  .visible {
60
  display: flex;
61
+ flex-direction: column;
62
  animation: fade-in 0.5s;
63
  }
64
 
src/components/filters/Filters.tsx CHANGED
@@ -22,10 +22,22 @@ import { isEmpty, omit } from 'lodash';
22
  import cx from 'classnames';
23
  import { useState, useEffect } from 'react';
24
 
25
- import { FilterableMultiSelect, Tag, Tooltip, Button } from '@carbon/react';
 
 
 
 
 
 
 
 
 
 
26
  import { ChevronUp, ChevronDown, Filter } from '@carbon/icons-react';
 
27
 
28
  import classes from './Filters.module.scss';
 
29
 
30
  // ===================================================================================
31
  // TYPES
@@ -35,6 +47,10 @@ interface Props {
35
  filters: { [key: string]: string[] };
36
  selectedFilters: { [key: string]: string[] };
37
  setSelectedFilters: Function;
 
 
 
 
38
  }
39
 
40
  // ===================================================================================
@@ -45,6 +61,10 @@ export default function Filters({
45
  filters,
46
  selectedFilters,
47
  setSelectedFilters,
 
 
 
 
48
  }: Props) {
49
  // Step 1: Initialize state and necessary variables
50
  const [showFilters, setShowFilters] = useState<boolean>(true);
@@ -52,7 +72,7 @@ export default function Filters({
52
  // Step 2: Run effects
53
  // Step 2.a: If no filters are found, set show filters to false
54
  useEffect(() => {
55
- if (filters === undefined) {
56
  setShowFilters(false);
57
  }
58
  }, [filters]);
@@ -90,48 +110,123 @@ export default function Filters({
90
  </Button>
91
  </Tooltip>
92
  )}
93
- <div className={cx(classes.filters, showFilters && classes.visible)}>
94
- {showFilters &&
95
- filters &&
96
- Object.entries(filters).map(([filterType, values]) => {
97
- return (
98
- <div
99
- key={`${keyPrefix}-filter` + filterType + '-selector'}
100
- className={classes.filterSelector}
101
- >
102
- <FilterableMultiSelect
103
- id={`${keyPrefix}-filter` + filterType + '-selector'}
104
- titleText={filterType}
105
- items={values}
106
- itemToString={(item) => String(item)}
107
- onChange={(event) => {
108
- setSelectedFilters((prevState) =>
109
- isEmpty(event.selectedItems)
110
- ? omit(prevState, filterType)
111
- : {
112
- ...prevState,
113
- [filterType]: event.selectedItems,
114
- },
115
- );
116
- }}
117
- ></FilterableMultiSelect>
118
- {Object.keys(selectedFilters).includes(filterType) ? (
119
- <div>
120
- {selectedFilters[filterType].map((value) => {
121
  return (
122
- <Tag
123
- type={'cool-gray'}
124
- key={`${keyPrefix}-filter-value` + value}
125
  >
126
- {value}
127
- </Tag>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  );
129
  })}
130
  </div>
131
- ) : null}
132
- </div>
133
- );
134
- })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  </div>
136
  </>
137
  );
 
22
  import cx from 'classnames';
23
  import { useState, useEffect } from 'react';
24
 
25
+ import {
26
+ FilterableMultiSelect,
27
+ Tag,
28
+ Tooltip,
29
+ Button,
30
+ Tabs,
31
+ TabList,
32
+ Tab,
33
+ TabPanels,
34
+ TabPanel,
35
+ } from '@carbon/react';
36
  import { ChevronUp, ChevronDown, Filter } from '@carbon/icons-react';
37
+ import ExpressionBuilder from '@/src/components/expression-builder/ExpressionBuilder';
38
 
39
  import classes from './Filters.module.scss';
40
+ import { Metric, Model } from '@/src/types';
41
 
42
  // ===================================================================================
43
  // TYPES
 
47
  filters: { [key: string]: string[] };
48
  selectedFilters: { [key: string]: string[] };
49
  setSelectedFilters: Function;
50
+ models?: Model[];
51
+ metric?: Metric;
52
+ expression?: object;
53
+ setExpression?: Function;
54
  }
55
 
56
  // ===================================================================================
 
61
  filters,
62
  selectedFilters,
63
  setSelectedFilters,
64
+ models,
65
+ metric,
66
+ expression,
67
+ setExpression,
68
  }: Props) {
69
  // Step 1: Initialize state and necessary variables
70
  const [showFilters, setShowFilters] = useState<boolean>(true);
 
72
  // Step 2: Run effects
73
  // Step 2.a: If no filters are found, set show filters to false
74
  useEffect(() => {
75
+ if (filters === undefined && setExpression === undefined) {
76
  setShowFilters(false);
77
  }
78
  }, [filters]);
 
110
  </Button>
111
  </Tooltip>
112
  )}
113
+ <div className={cx(classes.container, showFilters && classes.visible)}>
114
+ {showFilters ? (
115
+ filters && expression ? (
116
+ <Tabs>
117
+ <TabList aria-label="additional filters" contained fullWidth>
118
+ <Tab>Static</Tab>
119
+ <Tab>
120
+ Expression <Tag type="green">Experimental</Tag>
121
+ </Tab>
122
+ </TabList>
123
+ <TabPanels>
124
+ <TabPanel>
125
+ <div className={classes.filters}>
126
+ {Object.entries(filters).map(([filterType, values]) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  return (
128
+ <div
129
+ key={`${keyPrefix}-filter` + filterType + '-selector'}
130
+ className={classes.filterSelector}
131
  >
132
+ <FilterableMultiSelect
133
+ id={
134
+ `${keyPrefix}-filter` + filterType + '-selector'
135
+ }
136
+ titleText={filterType}
137
+ items={values}
138
+ itemToString={(item) => String(item)}
139
+ onChange={(event) => {
140
+ setSelectedFilters((prevState) =>
141
+ isEmpty(event.selectedItems)
142
+ ? omit(prevState, filterType)
143
+ : {
144
+ ...prevState,
145
+ [filterType]: event.selectedItems,
146
+ },
147
+ );
148
+ }}
149
+ ></FilterableMultiSelect>
150
+ {Object.keys(selectedFilters).includes(filterType) ? (
151
+ <div>
152
+ {selectedFilters[filterType].map((value) => {
153
+ return (
154
+ <Tag
155
+ type={'cool-gray'}
156
+ key={`${keyPrefix}-filter-value` + value}
157
+ >
158
+ {value}
159
+ </Tag>
160
+ );
161
+ })}
162
+ </div>
163
+ ) : null}
164
+ </div>
165
  );
166
  })}
167
  </div>
168
+ </TabPanel>
169
+ <TabPanel>
170
+ <ExpressionBuilder
171
+ expression={expression}
172
+ models={models}
173
+ metric={metric}
174
+ setExpression={setExpression}
175
+ ></ExpressionBuilder>
176
+ </TabPanel>
177
+ </TabPanels>
178
+ </Tabs>
179
+ ) : filters ? (
180
+ <div className={classes.filters}>
181
+ {Object.entries(filters).map(([filterType, values]) => {
182
+ return (
183
+ <div
184
+ key={`${keyPrefix}-filter` + filterType + '-selector'}
185
+ className={classes.filterSelector}
186
+ >
187
+ <FilterableMultiSelect
188
+ id={`${keyPrefix}-filter` + filterType + '-selector'}
189
+ titleText={filterType}
190
+ items={values}
191
+ itemToString={(item) => String(item)}
192
+ onChange={(event) => {
193
+ setSelectedFilters((prevState) =>
194
+ isEmpty(event.selectedItems)
195
+ ? omit(prevState, filterType)
196
+ : {
197
+ ...prevState,
198
+ [filterType]: event.selectedItems,
199
+ },
200
+ );
201
+ }}
202
+ ></FilterableMultiSelect>
203
+ {Object.keys(selectedFilters).includes(filterType) ? (
204
+ <div>
205
+ {selectedFilters[filterType].map((value) => {
206
+ return (
207
+ <Tag
208
+ type={'cool-gray'}
209
+ key={`${keyPrefix}-filter-value` + value}
210
+ >
211
+ {value}
212
+ </Tag>
213
+ );
214
+ })}
215
+ </div>
216
+ ) : null}
217
+ </div>
218
+ );
219
+ })}
220
+ </div>
221
+ ) : expression ? (
222
+ <ExpressionBuilder
223
+ expression={expression}
224
+ models={models}
225
+ metric={metric}
226
+ setExpression={setExpression}
227
+ ></ExpressionBuilder>
228
+ ) : null
229
+ ) : null}
230
  </div>
231
  </>
232
  );
src/utilities/expressions.ts ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ *
3
+ * Copyright 2023-2024 InspectorRAGet Team
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ **/
18
+
19
+ import { isEmpty, intersectionWith, unionWith, isEqual } from 'lodash';
20
+ import { Metric, TaskEvaluation } from '@/src/types';
21
+ import { castToNumber } from '@/src/utilities/metrics';
22
+
23
+ // ===================================================================================
24
+ // CONSTANTS
25
+ // ===================================================================================
26
+ export const PLACHOLDER_EXPRESSION_TEXT = '{}';
27
+ export enum EXPRESSION_OPERATORS {
28
+ // Logical operators
29
+ AND = '$and',
30
+ OR = '$or',
31
+
32
+ // Comparison operators
33
+ EQ = '$eq',
34
+ NEQ = '$neq',
35
+ GT = '$gt',
36
+ GTE = '$gte',
37
+ LT = '$lt',
38
+ LTE = '$lte',
39
+ }
40
+
41
+ export function validate(
42
+ expression: object,
43
+ modelIds?: string[],
44
+ values?: (string | number)[],
45
+ parent?: string,
46
+ ): string | null {
47
+ // Step 1: Identify all the keys
48
+ const keys = Object.keys(expression);
49
+
50
+ // Step 2: In case of operator presence
51
+ const operators = keys.filter((key) => key.startsWith('$'));
52
+ if (operators.length > 1) {
53
+ return `More than one operator [${operators.join(', ')}] on the same level in the expression`;
54
+ }
55
+
56
+ if (operators.length === 1 && keys.length > 1) {
57
+ return `Additional keys on the same level in the expression`;
58
+ }
59
+
60
+ if (operators.length === 1) {
61
+ const operator = operators[0];
62
+
63
+ // Logical operator condition
64
+ if (
65
+ operator === EXPRESSION_OPERATORS.AND ||
66
+ operator === EXPRESSION_OPERATORS.OR
67
+ ) {
68
+ if (parent && modelIds && modelIds.includes(parent)) {
69
+ return `Logical operator ("${operator}") must not preceed with model ID`;
70
+ }
71
+
72
+ if (
73
+ !Array.isArray(expression[operator]) ||
74
+ expression[operator].some((value) => typeof value !== 'object')
75
+ ) {
76
+ return `Logical operator ("${operator}") must follow with array of expressions`;
77
+ }
78
+
79
+ if (
80
+ isEmpty(expression[operator]) ||
81
+ expression[operator].some((entry) => isEmpty(entry))
82
+ ) {
83
+ return `Logical operator ("${operator}") cannot have empty expression value`;
84
+ }
85
+
86
+ for (let index = 0; index < expression[operator].length; index++) {
87
+ const nestedErrorMessage = validate(
88
+ expression[operator][index],
89
+ modelIds,
90
+ );
91
+ if (nestedErrorMessage) {
92
+ return nestedErrorMessage;
93
+ }
94
+ }
95
+ }
96
+ // Comparison operators condition
97
+ else if (
98
+ operator === EXPRESSION_OPERATORS.EQ ||
99
+ operator === EXPRESSION_OPERATORS.NEQ ||
100
+ operator === EXPRESSION_OPERATORS.LT ||
101
+ operator === EXPRESSION_OPERATORS.LTE ||
102
+ operator === EXPRESSION_OPERATORS.GT ||
103
+ operator === EXPRESSION_OPERATORS.GTE
104
+ ) {
105
+ if (parent === undefined || parent.startsWith('$')) {
106
+ return `Comparison operator ("${operator}") must preceed with model ID`;
107
+ }
108
+ if (
109
+ typeof expression[operator] !== 'string' &&
110
+ typeof expression[operator] !== 'number'
111
+ ) {
112
+ return `Comparison operator ("${operator}") must follow primitive data types ("string" or "number")`;
113
+ }
114
+ }
115
+ } else {
116
+ // Step 3: In case of operator less expression
117
+ for (let idx = 0; idx < keys.length; idx++) {
118
+ // Step 3.a: If model IDs are provided, make sure key is one of those model IDs
119
+ if (modelIds && !modelIds.includes(keys[idx])) {
120
+ return `Model ("${keys[idx]}") does not exists. Please use one for the following models: ${modelIds.join(', ')}`;
121
+ }
122
+
123
+ const value = expression[keys[idx]];
124
+ if (
125
+ typeof value !== 'object' &&
126
+ typeof value !== 'string' &&
127
+ typeof value !== 'number'
128
+ ) {
129
+ return `Model ("${keys[idx]}") must follow either expression or primitive data types ("string" or "number")`;
130
+ }
131
+
132
+ if (typeof value === 'object') {
133
+ const nestedErrorMessage = validate(
134
+ expression[keys[idx]],
135
+ modelIds,
136
+ values,
137
+ keys[idx],
138
+ );
139
+ if (nestedErrorMessage) {
140
+ return nestedErrorMessage;
141
+ }
142
+ } else {
143
+ if (values && !values.includes(value)) {
144
+ return `"${value}" is not a valid value option. Please use one of the following: ${values.join(', ')}`;
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ export function evaluate(
154
+ evaluationsPerTaskPerModel: {
155
+ [key: string]: { [key: string]: TaskEvaluation };
156
+ },
157
+ expression: object,
158
+ metric: Metric,
159
+ annotator?: string,
160
+ ): TaskEvaluation[] {
161
+ // Step 1: Initialize necessary variables
162
+ const eligibleEvaluations: TaskEvaluation[] = [];
163
+
164
+ // Step 2: Identify all the keys
165
+ const keys = Object.keys(expression);
166
+
167
+ // Step 3: In case of operator presence
168
+ const operators = keys.filter((key) => key.startsWith('$'));
169
+ if (operators.length === 1) {
170
+ const operator = operators[0];
171
+
172
+ // Step 3.a: In case of a logical operator
173
+ if (
174
+ operator === EXPRESSION_OPERATORS.AND ||
175
+ operator === EXPRESSION_OPERATORS.OR
176
+ ) {
177
+ // Step 3.a.i: Initialize necessary variables
178
+ const results: TaskEvaluation[][] = [];
179
+
180
+ // Step 3.a.ii: Identify evaluations meeting nested expression
181
+ expression[operator].forEach((condition) => {
182
+ results.push(
183
+ evaluate(evaluationsPerTaskPerModel, condition, metric, annotator),
184
+ );
185
+ });
186
+
187
+ // Step 3.a.iii: Apply intersection ('$and') or union ('$or') logic based on the logical operator
188
+ if (operator === EXPRESSION_OPERATORS.AND) {
189
+ return intersectionWith(...results, isEqual);
190
+ } else {
191
+ return unionWith(...results, isEqual);
192
+ }
193
+ }
194
+ } else {
195
+ // Step 3: In case of expression without logical operators
196
+ // Step 3.a: Iterate over evaluations for each task
197
+ Object.values(evaluationsPerTaskPerModel).forEach((evaluationPerModel) => {
198
+ // Step 3.a.i: Initialize necessary variables
199
+ let satisfy: boolean = true;
200
+
201
+ // Step 3.a.ii: Iterate over conditions for each model in the expression
202
+ for (let idx = 0; idx < keys.length; idx++) {
203
+ // Step 3.a.ii.*: Check if evaluation exists
204
+ if (!evaluationPerModel.hasOwnProperty(keys[idx])) {
205
+ satisfy = false;
206
+ break;
207
+ }
208
+
209
+ // Step 3.a.ii.**: Fetch evaluation, value and expected value condition
210
+ const evaluation = evaluationPerModel[keys[idx]];
211
+
212
+ // Step 3.a.ii.***: Calculate value
213
+ /**
214
+ * annotator specific value if annotator is specified
215
+ * OR
216
+ * aggregate value
217
+ */
218
+ let value: string | number;
219
+ if (annotator) {
220
+ if (!evaluation[metric.name].hasOwnProperty(annotator)) {
221
+ satisfy = false;
222
+ break;
223
+ }
224
+ value = castToNumber(
225
+ evaluation[metric.name][annotator].value,
226
+ metric.values,
227
+ );
228
+ } else {
229
+ value = castToNumber(
230
+ evaluation[`${metric.name}_agg`].value,
231
+ metric.values,
232
+ );
233
+ }
234
+
235
+ // Step 3.a.ii.*****: Extract expection from expression
236
+ const expectation = expression[keys[idx]];
237
+
238
+ // Step 3.a.ii.******: In case of expectation is an expression
239
+ if (typeof expectation === 'object') {
240
+ // Extract comparison operator from expectation expression
241
+ const operator = Object.keys(expectation).filter((key) =>
242
+ key.startsWith('$'),
243
+ )[0];
244
+
245
+ // If comparison operator is "$gt" OR "$gte"
246
+ if (
247
+ (operator === EXPRESSION_OPERATORS.GTE ||
248
+ operator === EXPRESSION_OPERATORS.GT) &&
249
+ (isNaN(value) ||
250
+ value <
251
+ castToNumber(
252
+ expectation[operator],
253
+ metric.values,
254
+ typeof expectation[operator] === 'string'
255
+ ? 'displayValue'
256
+ : 'value',
257
+ ))
258
+ ) {
259
+ satisfy = false;
260
+ break;
261
+ }
262
+
263
+ // If comparison operator is "$lt" OR "$lte"
264
+ if (
265
+ (operator === EXPRESSION_OPERATORS.LTE ||
266
+ operator === EXPRESSION_OPERATORS.LT) &&
267
+ (isNaN(value) ||
268
+ value >
269
+ castToNumber(
270
+ expectation[operator],
271
+ metric.values,
272
+ typeof expectation[operator] === 'string'
273
+ ? 'displayValue'
274
+ : 'value',
275
+ ))
276
+ ) {
277
+ satisfy = false;
278
+ break;
279
+ }
280
+
281
+ // If comparison operator is "$eq"
282
+ if (
283
+ operator === EXPRESSION_OPERATORS.EQ &&
284
+ (isNaN(value) ||
285
+ value !==
286
+ castToNumber(
287
+ expectation[operator],
288
+ metric.values,
289
+ typeof expectation[operator] === 'string'
290
+ ? 'displayValue'
291
+ : 'value',
292
+ ))
293
+ ) {
294
+ satisfy = false;
295
+ break;
296
+ }
297
+
298
+ // If comparison operator is "$neq"
299
+ if (
300
+ operator === EXPRESSION_OPERATORS.NEQ &&
301
+ (isNaN(value) ||
302
+ value ===
303
+ castToNumber(
304
+ expectation[operator],
305
+ metric.values,
306
+ typeof expectation[operator] === 'string'
307
+ ? 'displayValue'
308
+ : 'value',
309
+ ))
310
+ ) {
311
+ satisfy = false;
312
+ break;
313
+ }
314
+ } else {
315
+ // 3.a.ii.******: In case of expectation is a primitive type ("string"/"number") value
316
+ if (
317
+ isNaN(value) ||
318
+ value !==
319
+ castToNumber(
320
+ expectation,
321
+ metric.values,
322
+ typeof expectation === 'string' ? 'displayValue' : 'value',
323
+ )
324
+ ) {
325
+ satisfy = false;
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ // Step 3.a.iii: If satisfy expression requirments, add all evaluations for the current task to eligible evaluations list
332
+ if (satisfy) {
333
+ eligibleEvaluations.push(...Object.values(evaluationPerModel));
334
+ }
335
+ });
336
+ }
337
+
338
+ // Step 4: Return
339
+ return eligibleEvaluations;
340
+ }
src/utilities/metrics.ts CHANGED
@@ -80,13 +80,16 @@ export function extractMetricDisplayName(metric: Metric): string {
80
  export function castToNumber(
81
  value: string | number,
82
  references?: MetricValue[],
 
83
  ): number {
84
  // If value is of type "string"
85
  if (typeof value === 'string') {
86
  // Step 1: Check if references are provided to convert "string" value to "numeric" value
87
  if (references) {
88
  // Step 1.a: Find appropriate reference by comparing "string" values
89
- const reference = references.find((entry) => entry.value === value);
 
 
90
 
91
  // Step 1.b: If numeric value exists in reference, then return it
92
  if (
@@ -96,7 +99,7 @@ export function castToNumber(
96
  ) {
97
  return reference.numericValue;
98
  } else {
99
- return parseInt(value);
100
  }
101
  }
102
  // Step 2: Cast to int, if references are absent
 
80
  export function castToNumber(
81
  value: string | number,
82
  references?: MetricValue[],
83
+ key?: 'value' | 'displayValue',
84
  ): number {
85
  // If value is of type "string"
86
  if (typeof value === 'string') {
87
  // Step 1: Check if references are provided to convert "string" value to "numeric" value
88
  if (references) {
89
  // Step 1.a: Find appropriate reference by comparing "string" values
90
+ const reference = references.find((entry) =>
91
+ key ? entry[key] === value : entry.value === value,
92
+ );
93
 
94
  // Step 1.b: If numeric value exists in reference, then return it
95
  if (
 
99
  ) {
100
  return reference.numericValue;
101
  } else {
102
+ return parseFloat(value);
103
  }
104
  }
105
  // Step 2: Cast to int, if references are absent
src/views/model-behavior/ModelBehavior.tsx CHANGED
@@ -18,7 +18,7 @@
18
 
19
  'use client';
20
 
21
- import { isEmpty, omit } from 'lodash';
22
  import Link from 'next/link';
23
  import cx from 'classnames';
24
  import { useState, useMemo, useEffect } from 'react';
@@ -56,6 +56,7 @@ import {
56
  } from '@/src/utilities/metrics';
57
  import { areObjectsIntersecting } from '@/src/utilities/objects';
58
  import { getModelColorPalette } from '@/src/utilities/colors';
 
59
  import TasksTable from '@/src/views/tasks-table/TasksTable';
60
  import MetricSelector from '@/src/components/selectors/MetricSelector';
61
  import Filters from '@/src/components/filters/Filters';
@@ -158,14 +159,17 @@ function process(
158
  selectedAllowedValues: string[],
159
  selectedAnnotator: string | undefined,
160
  filters: { [key: string]: string[] },
 
161
  ): [(record & { [key: string]: string | number })[], TaskEvaluation[]] {
 
162
  const models = selectedModels.reduce(
163
  (obj, item) => ((obj[item.modelId] = item), obj),
164
  {},
165
  );
166
  const records: (record & { [key: string]: string | number })[] = [];
167
- const filteredEvaluations: TaskEvaluation[] = [];
168
- // apply filters
 
169
  const filteredEvaluationsPerMetric: { [key: string]: TaskEvaluation[] } = {};
170
  for (const [metric, evals] of Object.entries(evaluationsPerMetric)) {
171
  filteredEvaluationsPerMetric[metric] = !isEmpty(filters)
@@ -173,66 +177,115 @@ function process(
173
  : evals;
174
  }
175
 
 
176
  if (selectedMetric) {
177
- filteredEvaluationsPerMetric[selectedMetric.name].forEach((evaluation) => {
178
- // If individual annotator is selected
179
- if (selectedAnnotator) {
180
- /**
181
- * Evaluation's model id fall within selected models
182
- * OR
183
- * Evaluation's selected metric's value fall within allowed values
184
- */
185
- if (
186
- evaluation.modelId in models &&
187
- evaluation[selectedMetric.name].hasOwnProperty(selectedAnnotator) &&
188
- (!selectedAllowedValues.length ||
189
- selectedAllowedValues.includes(
190
- evaluation[selectedMetric.name][selectedAnnotator].value,
191
- ))
192
- ) {
193
- // Create and add record
194
- records.push({
195
- taskId: evaluation.taskId,
196
- modelName: models[evaluation.modelId].name,
197
- [`${selectedMetric.name}_value`]:
198
- evaluation[selectedMetric.name][selectedAnnotator].value,
199
- });
200
-
201
- // Add evaluation
202
- filteredEvaluations.push(evaluation);
203
- }
204
- } else {
205
- if (
206
- evaluation.modelId in models &&
207
- selectedAgreementLevels
208
- .map((level) => level.value)
209
- .includes(evaluation[`${selectedMetric.name}_agg`].level) &&
210
- (!selectedAllowedValues.length ||
211
- selectedAllowedValues.includes(
212
- evaluation[`${selectedMetric.name}_agg`].value,
213
- ))
214
- ) {
215
- // Create and add record
216
- records.push({
217
- taskId: evaluation.taskId,
218
- modelName: models[evaluation.modelId].name,
219
- [`${selectedMetric.name}_value`]:
220
- evaluation[`${selectedMetric.name}_agg`].value,
221
- [`${selectedMetric.name}_aggLevel`]:
222
- evaluation[`${selectedMetric.name}_agg`].level,
223
- });
224
-
225
- // Add evaluation
226
- filteredEvaluations.push(evaluation);
227
- }
228
- }
229
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  } else {
 
231
  for (const [metric, evaluations] of Object.entries(
232
  filteredEvaluationsPerMetric,
233
  )) {
234
  evaluations.forEach((evaluation) => {
235
- // If invidiual annotator is selected
236
  if (selectedAnnotator) {
237
  /**
238
  * Evaluation's model id fall within selected models
@@ -254,6 +307,7 @@ function process(
254
  });
255
  }
256
  } else {
 
257
  if (
258
  evaluation.modelId in models &&
259
  selectedAgreementLevels
@@ -274,7 +328,7 @@ function process(
274
  }
275
  }
276
 
277
- return [records, filteredEvaluations];
278
  }
279
 
280
  // ===================================================================================
@@ -318,6 +372,13 @@ export default function ModelBehavior({
318
  [key: string]: string[];
319
  }>({});
320
  const [modelColors, modelOrder] = getModelColorPalette(models);
 
 
 
 
 
 
 
321
 
322
  // Step 2: Run effects
323
  // Step 2.a: Adjust graph width & heigh based on window size
@@ -358,7 +419,12 @@ export default function ModelBehavior({
358
  return annotatorsSet;
359
  }, [evaluationsPerMetric, metrics]);
360
 
361
- // Step 2.d: Configure available majority values, if metric is selected
 
 
 
 
 
362
  const availableAllowedValues = useMemo(() => {
363
  if (selectedMetric && selectedMetric.type === 'categorical') {
364
  if (selectedAnnotator) {
@@ -408,30 +474,20 @@ export default function ModelBehavior({
408
  selectedAgreementLevels,
409
  ]);
410
 
411
- // Step 2.e: Update selected values list
412
  useEffect(() => {
413
  setSelectedAllowedValues(availableAllowedValues);
414
  }, [availableAllowedValues]);
415
 
416
- // Step 2.f: Calculate graph data and prepare visible evaluations list
417
  /**
418
  * Adjust graph records based on selected agreement levels, models and annotator
419
  * visibleEvaluations : [{taskId: <>, modelId: <>, [metric]_score: <>}]
420
  * NOTE: * [metric]_score field avialable metrics (all OR single)
421
  * * score field could be either majority score or individual annotator's score (based on selected annotator)
422
  */
423
- const [graphRecords, visibleEvaluations] = useMemo(
424
- () =>
425
- process(
426
- evaluationsPerMetric,
427
- selectedAgreementLevels,
428
- selectedModels,
429
- selectedMetric,
430
- selectedAllowedValues,
431
- selectedAnnotator,
432
- selectedFilters,
433
- ),
434
- [
435
  evaluationsPerMetric,
436
  selectedAgreementLevels,
437
  selectedModels,
@@ -439,10 +495,24 @@ export default function ModelBehavior({
439
  selectedAllowedValues,
440
  selectedAnnotator,
441
  selectedFilters,
442
- ],
443
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
- // Step 2.g: Calculate visible tasks per metric
446
  const visibleTasksPerMetric = useMemo(() => {
447
  const data = {};
448
  metrics.forEach((metric) => {
@@ -458,7 +528,7 @@ export default function ModelBehavior({
458
  return data;
459
  }, [graphRecords, metrics]);
460
 
461
- // Step 2.h: Buckets human and algoritmic metrics into individual buckets
462
  const [humanMetrics, algorithmMetrics] = useMemo(() => {
463
  const hMetrics: Metric[] = [];
464
  const aMetrics: Metric[] = [];
@@ -648,6 +718,10 @@ export default function ModelBehavior({
648
  filters={filters}
649
  selectedFilters={selectedFilters}
650
  setSelectedFilters={setSelectedFilters}
 
 
 
 
651
  />
652
  ) : null}
653
 
 
18
 
19
  'use client';
20
 
21
+ import { isEmpty } from 'lodash';
22
  import Link from 'next/link';
23
  import cx from 'classnames';
24
  import { useState, useMemo, useEffect } from 'react';
 
56
  } from '@/src/utilities/metrics';
57
  import { areObjectsIntersecting } from '@/src/utilities/objects';
58
  import { getModelColorPalette } from '@/src/utilities/colors';
59
+ import { evaluate } from '@/src/utilities/expressions';
60
  import TasksTable from '@/src/views/tasks-table/TasksTable';
61
  import MetricSelector from '@/src/components/selectors/MetricSelector';
62
  import Filters from '@/src/components/filters/Filters';
 
159
  selectedAllowedValues: string[],
160
  selectedAnnotator: string | undefined,
161
  filters: { [key: string]: string[] },
162
+ expression?: object,
163
  ): [(record & { [key: string]: string | number })[], TaskEvaluation[]] {
164
+ // Step 1: Initialize necessary variables
165
  const models = selectedModels.reduce(
166
  (obj, item) => ((obj[item.modelId] = item), obj),
167
  {},
168
  );
169
  const records: (record & { [key: string]: string | number })[] = [];
170
+ const visibleEvaluations: TaskEvaluation[] = [];
171
+
172
+ // Step 2: If filters are specified
173
  const filteredEvaluationsPerMetric: { [key: string]: TaskEvaluation[] } = {};
174
  for (const [metric, evals] of Object.entries(evaluationsPerMetric)) {
175
  filteredEvaluationsPerMetric[metric] = !isEmpty(filters)
 
177
  : evals;
178
  }
179
 
180
+ // Step 3: If a metric is selected
181
  if (selectedMetric) {
182
+ // Step 3.a: If an expression is specified
183
+ if (expression && !isEmpty(expression)) {
184
+ // Step 3.a.ii: Build an object containing evaluations per model for every task
185
+ const evaluationsPerTaskPerModel: {
186
+ [key: string]: { [key: string]: TaskEvaluation };
187
+ } = {};
188
+ filteredEvaluationsPerMetric[selectedMetric.name].forEach(
189
+ (evaluation) => {
190
+ if (evaluationsPerTaskPerModel.hasOwnProperty(evaluation.taskId)) {
191
+ evaluationsPerTaskPerModel[evaluation.taskId][evaluation.modelId] =
192
+ evaluation;
193
+ } else {
194
+ evaluationsPerTaskPerModel[evaluation.taskId] = {
195
+ [evaluation.modelId]: evaluation,
196
+ };
197
+ }
198
+ },
199
+ );
200
+
201
+ // Step 3.a.iii: Find evaluations meeting expression criteria
202
+ evaluate(
203
+ evaluationsPerTaskPerModel,
204
+ expression,
205
+ selectedMetric,
206
+ selectedAnnotator,
207
+ ).forEach((evaluation) => {
208
+ // Step 3.a.iii.*: Create and add record
209
+ records.push({
210
+ taskId: evaluation.taskId,
211
+ modelName: models[evaluation.modelId].name,
212
+ [`${selectedMetric.name}_value`]:
213
+ evaluation[`${selectedMetric.name}_agg`].value,
214
+ [`${selectedMetric.name}_aggLevel`]:
215
+ evaluation[`${selectedMetric.name}_agg`].level,
216
+ });
217
+
218
+ // Step 3.a.iii.**: Add evaluation
219
+ visibleEvaluations.push(evaluation);
220
+ });
221
+ } else {
222
+ // Step 3.b: Filter evaluations for the selected metric
223
+ filteredEvaluationsPerMetric[selectedMetric.name].forEach(
224
+ (evaluation) => {
225
+ // Step 3.b.i: If individual annotator is selected, verify against annotator's value
226
+ if (selectedAnnotator) {
227
+ /**
228
+ * Evaluation's model id fall within selected models
229
+ * OR
230
+ * Evaluation's selected metric's value fall within allowed values
231
+ */
232
+ if (
233
+ evaluation.modelId in models &&
234
+ evaluation[selectedMetric.name].hasOwnProperty(
235
+ selectedAnnotator,
236
+ ) &&
237
+ (!selectedAllowedValues.length ||
238
+ selectedAllowedValues.includes(
239
+ evaluation[selectedMetric.name][selectedAnnotator].value,
240
+ ))
241
+ ) {
242
+ // Step 3.b.i.*: Create and add record
243
+ records.push({
244
+ taskId: evaluation.taskId,
245
+ modelName: models[evaluation.modelId].name,
246
+ [`${selectedMetric.name}_value`]:
247
+ evaluation[selectedMetric.name][selectedAnnotator].value,
248
+ });
249
+
250
+ // Step 3.b.i.**: Add evaluation
251
+ visibleEvaluations.push(evaluation);
252
+ }
253
+ } else {
254
+ // Step 3.b.ii: Verify against aggregate value
255
+ if (
256
+ evaluation.modelId in models &&
257
+ selectedAgreementLevels
258
+ .map((level) => level.value)
259
+ .includes(evaluation[`${selectedMetric.name}_agg`].level) &&
260
+ (!selectedAllowedValues.length ||
261
+ selectedAllowedValues.includes(
262
+ evaluation[`${selectedMetric.name}_agg`].value,
263
+ ))
264
+ ) {
265
+ // Step 3.b.ii.*: Create and add record
266
+ records.push({
267
+ taskId: evaluation.taskId,
268
+ modelName: models[evaluation.modelId].name,
269
+ [`${selectedMetric.name}_value`]:
270
+ evaluation[`${selectedMetric.name}_agg`].value,
271
+ [`${selectedMetric.name}_aggLevel`]:
272
+ evaluation[`${selectedMetric.name}_agg`].level,
273
+ });
274
+
275
+ // Step 3.b.ii.**: Add evaluation
276
+ visibleEvaluations.push(evaluation);
277
+ }
278
+ }
279
+ },
280
+ );
281
+ }
282
  } else {
283
+ // Step 3: For every metric
284
  for (const [metric, evaluations] of Object.entries(
285
  filteredEvaluationsPerMetric,
286
  )) {
287
  evaluations.forEach((evaluation) => {
288
+ // Step 3.a: If invidiual annotator is selected, verify against annotator's value
289
  if (selectedAnnotator) {
290
  /**
291
  * Evaluation's model id fall within selected models
 
307
  });
308
  }
309
  } else {
310
+ // Step 3.a: Verify against aggregate value
311
  if (
312
  evaluation.modelId in models &&
313
  selectedAgreementLevels
 
328
  }
329
  }
330
 
331
+ return [records, visibleEvaluations];
332
  }
333
 
334
  // ===================================================================================
 
372
  [key: string]: string[];
373
  }>({});
374
  const [modelColors, modelOrder] = getModelColorPalette(models);
375
+ const [expression, setExpression] = useState<object>({});
376
+ const [graphRecords, setGraphRecords] = useState<
377
+ (record & { [key: string]: string | number })[]
378
+ >([]);
379
+ const [visibleEvaluations, setVisibleEvaluations] = useState<
380
+ TaskEvaluation[]
381
+ >([]);
382
 
383
  // Step 2: Run effects
384
  // Step 2.a: Adjust graph width & heigh based on window size
 
419
  return annotatorsSet;
420
  }, [evaluationsPerMetric, metrics]);
421
 
422
+ // Step 2.d: Reset expression, if selected metric changes
423
+ useEffect(() => {
424
+ setExpression({});
425
+ }, [selectedMetric]);
426
+
427
+ // Step 2.e: Configure available majority values, if metric is selected
428
  const availableAllowedValues = useMemo(() => {
429
  if (selectedMetric && selectedMetric.type === 'categorical') {
430
  if (selectedAnnotator) {
 
474
  selectedAgreementLevels,
475
  ]);
476
 
477
+ // Step 2.f: Update selected values list
478
  useEffect(() => {
479
  setSelectedAllowedValues(availableAllowedValues);
480
  }, [availableAllowedValues]);
481
 
482
+ // Step 2.g: Calculate graph data and prepare visible evaluations list
483
  /**
484
  * Adjust graph records based on selected agreement levels, models and annotator
485
  * visibleEvaluations : [{taskId: <>, modelId: <>, [metric]_score: <>}]
486
  * NOTE: * [metric]_score field avialable metrics (all OR single)
487
  * * score field could be either majority score or individual annotator's score (based on selected annotator)
488
  */
489
+ useEffect(() => {
490
+ const [records, evaluations] = process(
 
 
 
 
 
 
 
 
 
 
491
  evaluationsPerMetric,
492
  selectedAgreementLevels,
493
  selectedModels,
 
495
  selectedAllowedValues,
496
  selectedAnnotator,
497
  selectedFilters,
498
+ expression,
499
+ );
500
+
501
+ // Set graph records and visible evaluations
502
+ setGraphRecords(records);
503
+ setVisibleEvaluations(evaluations);
504
+ }, [
505
+ evaluationsPerMetric,
506
+ selectedAgreementLevels,
507
+ selectedModels,
508
+ selectedMetric,
509
+ selectedAllowedValues,
510
+ selectedAnnotator,
511
+ selectedFilters,
512
+ expression,
513
+ ]);
514
 
515
+ // Step 2.h: Calculate visible tasks per metric
516
  const visibleTasksPerMetric = useMemo(() => {
517
  const data = {};
518
  metrics.forEach((metric) => {
 
528
  return data;
529
  }, [graphRecords, metrics]);
530
 
531
+ // Step 2.i: Buckets human and algoritmic metrics into individual buckets
532
  const [humanMetrics, algorithmMetrics] = useMemo(() => {
533
  const hMetrics: Metric[] = [];
534
  const aMetrics: Metric[] = [];
 
718
  filters={filters}
719
  selectedFilters={selectedFilters}
720
  setSelectedFilters={setSelectedFilters}
721
+ models={selectedModels}
722
+ metric={selectedMetric}
723
+ expression={expression}
724
+ setExpression={setExpression}
725
  />
726
  ) : null}
727
 
src/views/tasks-table/TasksTable.tsx CHANGED
@@ -343,7 +343,58 @@ export default function TasksTable({
343
  <>
344
  {headers && rows && (
345
  <div>
346
- <DataTable rows={visibleRows} headers={headers} isSortable>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  {({
348
  rows,
349
  headers,
 
343
  <>
344
  {headers && rows && (
345
  <div>
346
+ <DataTable
347
+ rows={visibleRows}
348
+ headers={headers}
349
+ isSortable
350
+ sortRow={(cellA, cellB, { sortDirection, sortStates }) => {
351
+ // Step 1: Check if cell values are objects
352
+ if (typeof cellA === 'object' && typeof cellB === 'object') {
353
+ // Step 1.a: Get first value for each cell object
354
+ const valueA = Object.values(cellA)[0];
355
+ const valueB = Object.values(cellB)[0];
356
+
357
+ // Step 1.b: Check if both values are of "string" type
358
+ if (typeof valueA === 'string' && typeof valueB === 'string') {
359
+ // Step 1.b.i: Check if both values are purely numeric
360
+ if (
361
+ !isNaN(parseFloat(valueA)) &&
362
+ !isNaN(parseFloat(valueB))
363
+ ) {
364
+ if (sortDirection === sortStates.DESC) {
365
+ return parseFloat(valueA) - parseFloat(valueB);
366
+ }
367
+ return parseFloat(valueB) - parseFloat(valueA);
368
+ } else {
369
+ if (sortDirection === sortStates.DESC) {
370
+ return valueA.localeCompare(valueB);
371
+ }
372
+
373
+ return valueB.localeCompare(valueA);
374
+ }
375
+ }
376
+ // Step 1.c: Check if both values are of "number" type
377
+ else if (
378
+ typeof valueA === 'number' &&
379
+ typeof valueB === 'number'
380
+ ) {
381
+ if (sortDirection === sortStates.DESC) {
382
+ return valueA - valueB;
383
+ }
384
+
385
+ return valueB - valueA;
386
+ }
387
+ }
388
+ // Step 2: cell values are assumed to be of "string" type
389
+ else {
390
+ if (sortDirection === sortStates.DESC) {
391
+ return cellA.localeCompare(cellB);
392
+ }
393
+
394
+ return cellB.localeCompare(cellA);
395
+ }
396
+ }}
397
+ >
398
  {({
399
  rows,
400
  headers,