diff --git a/src/renderer/components/Experiment/DynamicPluginForm.tsx b/src/renderer/components/Experiment/DynamicPluginForm.tsx index 9e9733b5598792a966d4e52e5a9f23463a8f8b5d..a2455bad467f401268d0bbbddc49b007178be276 100644 --- a/src/renderer/components/Experiment/DynamicPluginForm.tsx +++ b/src/renderer/components/Experiment/DynamicPluginForm.tsx @@ -22,10 +22,11 @@ import { Slider, Stack, Option, - Autocomplete + Autocomplete, } from '@mui/joy'; import { useMemo } from 'react'; import ModelProviderWidget from 'renderer/components/Experiment/Widgets/ModelProviderWidget'; +import CustomEvaluationWidget from './Widgets/CustomEvaluationWidget'; import { RegistryWidgetsType, @@ -421,7 +422,13 @@ function CustomAutocompleteWidget<T = any, S extends StrictRJSFSchema = RJSFSche // Determine default value. const defaultValue = _multiple ? [] : ''; // Use the provided value or fallback to default. - const currentValue = value !== undefined ? value : defaultValue; + let currentValue = value !== undefined ? value : defaultValue; + + // Check if currentValue is an array, if a string, convert it to an array. + const isString = typeof currentValue === 'string'; + if (isString) { + currentValue = currentValue.split(','); + } // Map enumOptions into objects with label and value. const processedOptionsValues = enumOptions.map((opt) => @@ -492,6 +499,7 @@ const widgets: RegistryWidgetsType = { RangeWidget: CustomRange, SelectWidget: CustomSelectSimple, AutoCompleteWidget: CustomAutocompleteWidget, + EvaluationWidget: CustomEvaluationWidget, ModelProviderWidget: ModelProviderWidget }; diff --git a/src/renderer/components/Experiment/Eval/EvalTasksTable.tsx b/src/renderer/components/Experiment/Eval/EvalTasksTable.tsx index 20b8861dad8e6dd94844c63727342dcf5e4ba8ef..68220d9b06ec180e2d05df2968563d10695d82df 100644 --- a/src/renderer/components/Experiment/Eval/EvalTasksTable.tsx +++ b/src/renderer/components/Experiment/Eval/EvalTasksTable.tsx @@ -20,7 +20,20 @@ function formatTemplateConfig(script_parameters): ReactElement { // Remove the author/full path from the model name for cleanliness // const short_model_name = c.model_name.split('/').pop(); // Set main_task as either or the metric name from the script parameters - const main_task = script_parameters.tasks + const main_task = (() => { + if (script_parameters.tasks) { + try { + const tasksArray = JSON.parse(script_parameters.tasks); + if (Array.isArray(tasksArray)) { + return tasksArray.map((task) => task.name).join(', '); + } + } catch (error) { + // Invalid JSON; fall back to the original value + } + return script_parameters.tasks; + } + return script_parameters.tasks; + })(); const dataset_name = script_parameters.dataset_name ? script_parameters.dataset_name : 'N/A'; diff --git a/src/renderer/components/Experiment/Eval/ViewCSVModal.tsx b/src/renderer/components/Experiment/Eval/ViewCSVModal.tsx index 40e8b81517f5b8fe12403a7e7405fd4e3eb35962..e933f1c9f63d27f72699b673da332baefce2f65e 100644 --- a/src/renderer/components/Experiment/Eval/ViewCSVModal.tsx +++ b/src/renderer/components/Experiment/Eval/ViewCSVModal.tsx @@ -75,7 +75,8 @@ function formatEvalData(data) { } function formatArrayOfScores(scores) { - const formattedScores = scores.map((score) => { + const scoresArray = Array.isArray(scores) ? scores : [scores]; + const formattedScores = scoresArray.map((score) => { const metricName = Object.keys(score)[0]; const value = Object.values(score)[0]; diff --git a/src/renderer/components/Experiment/Widgets/CustomEvaluationWidget.tsx b/src/renderer/components/Experiment/Widgets/CustomEvaluationWidget.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2ffc64ca24df8005cc5147e27d3729e072a4eb70 --- /dev/null +++ b/src/renderer/components/Experiment/Widgets/CustomEvaluationWidget.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { WidgetProps } from '@rjsf/core'; +import { Button, Input, Select, Option } from '@mui/joy'; + +type EvaluationField = { + name: string; + expression: string; + return_type: string; +}; + +const parseValue = (val: any): EvaluationField[] => { + if (Array.isArray(val)) { + if (val.every(item => typeof item === "string")) { + // If every element is a string: join them and parse the result. + try { + const joined = val.join(','); + const parsed = JSON.parse(joined); + return Array.isArray(parsed) ? parsed : []; + } catch (err) { + console.error("Error parsing evaluation widget value:", err); + return []; + } + } else { + // If not all elements are strings, assume it's already an array of EvaluationField. + return val; + } + } else if (typeof val === "string") { + try { + return JSON.parse(val); + } catch (err) { + console.error("Error parsing evaluation widget value string:", err); + return []; + } + } + return []; +}; + +const CustomEvaluationWidget = (props: WidgetProps<any>) => { + const { id, value, onChange, disabled, readonly } = props; + + // Directly derive evaluation metrics from the value prop. + const evalMetrics: EvaluationField[] = React.useMemo(() => parseValue(value), [value]); + + const handleAddField = () => { + const updatedMetrics = [ + ...evalMetrics, + { name: '', expression: '', return_type: 'boolean' } + ]; + onChange(updatedMetrics); + }; + + const handleFieldChange = ( + index: number, + field: keyof EvaluationField, + newValue: string + ) => { + const updated = evalMetrics.map((evaluation, i) => + i === index ? { ...evaluation, [field]: newValue } : evaluation + ); + onChange(updated); + }; + + const handleRemoveField = (index: number) => { + const updated = evalMetrics.filter((_, i) => i !== index); + onChange(updated); + }; + + return ( + <div id={id}> + {evalMetrics.map((evaluation, index) => ( + <div + key={index} + style={{ + marginBottom: '1rem', + border: '1px solid #ccc', + padding: '0.5rem' + }} + > + <Input + placeholder="Evaluation Name" + value={evaluation.name} + onChange={(e) => + handleFieldChange(index, 'name', e.target.value) + } + disabled={disabled || readonly} + style={{ marginBottom: '0.5rem' }} + /> + <textarea + placeholder="Regular Expression/String" + value={evaluation.expression} + onChange={(e) => + handleFieldChange(index, 'expression', e.target.value) + } + disabled={disabled || readonly} + style={{ marginBottom: '0.5rem' }} + /> + <Select + placeholder="Output Type" + value={evaluation.return_type} + onChange={(e, newValue) => + handleFieldChange(index, 'return_type', newValue as string) + } + disabled={disabled || readonly} + style={{ marginBottom: '0.5rem' }} + > + <Option value="boolean">Boolean</Option> + <Option value="number">Number</Option> + <Option value="contains">Contains</Option> + <Option value="isequal">IsEqual</Option> + </Select> + <Button + onClick={() => handleRemoveField(index)} + disabled={disabled || readonly} + size="sm" + variant="outlined" + > + Remove Field + </Button> + </div> + ))} + <Button + onClick={handleAddField} + disabled={disabled || readonly} + variant="solid" + > + Add Field + </Button> + {/* Hidden input to capture the JSON result on form submission */} + <input type="hidden" id={id} name={id} value={JSON.stringify(evalMetrics)} /> + </div> + ); +}; + +export default CustomEvaluationWidget;