Spaces:
Running
Running
/** | |
* | |
* 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 { isEmpty } from 'lodash'; | |
import DOMPurify from 'dompurify'; | |
import parse from 'html-react-parser'; | |
import { useMemo, useState } from 'react'; | |
import { | |
Tabs, | |
TabList, | |
Tab, | |
TabPanels, | |
TabPanel, | |
Button, | |
ContainedList, | |
ContainedListItem, | |
} from '@carbon/react'; | |
import { TextHighlight } from '@carbon/icons-react'; | |
import { Model, TaskEvaluation, Task, Metric } from '@/src/types'; | |
import { useDataStore } from '@/src/store'; | |
import { truncate, overlaps } from '@/src/utilities/strings'; | |
import { mark } from '@/src/utilities/highlighter'; | |
import AnnotationsTable from '@/src/views/annotations-table/AnnotationsTable'; | |
import TextGenerationTaskCopierModal from '@/src/components/task-copier/TextGenerationTaskCopier'; | |
import classes from './TextGenerationTask.module.scss'; | |
// =================================================================================== | |
// TYPES | |
// =================================================================================== | |
interface Props { | |
task: Task; | |
models: Map<string, Model>; | |
metrics: Metric[]; | |
taskCopierModalOpen: boolean; | |
setTaskCopierModalOpen: Function; | |
updateCommentProvenance: Function; | |
} | |
// =================================================================================== | |
// MAIN FUNCTION | |
// =================================================================================== | |
export default function TextGenerationTask({ | |
task, | |
models, | |
metrics, | |
taskCopierModalOpen, | |
setTaskCopierModalOpen, | |
updateCommentProvenance, | |
}: Props) { | |
// Step 1: Initialize state and necessary variables | |
const [selectedModelIndex, setSelectedModelIndex] = useState<number>(0); | |
const [showOverlap, setShowOverlap] = useState<boolean>(false); | |
// Step 2: Run effects | |
// Step 2.a: Fetch data from data store | |
const { item: data } = useDataStore(); | |
// Step 2.b: Fetch evaluations for the current task | |
const evaluations = useMemo(() => { | |
// Step 2.b.i: Fetch evaluations | |
let taskEvaluations: TaskEvaluation[] | undefined = undefined; | |
if (data) { | |
taskEvaluations = data.evaluations.filter( | |
(evaluation) => evaluation.taskId === task.taskId, | |
); | |
// Step 2.b.ii: Compute input-response overlap and add to evaluation object | |
taskEvaluations.forEach((evaluation) => { | |
if (typeof task.input === 'string') { | |
evaluation.overlaps = overlaps(evaluation.modelResponse, task.input); | |
} else { | |
evaluation.overlaps = []; | |
} | |
}); | |
} | |
return taskEvaluations; | |
}, [task.taskId, task.input, data]); | |
// Step 2.c: Build human & algorithmic metric maps | |
const [hMetrics, aMetrics] = useMemo(() => { | |
const humanMetrics = new Map( | |
metrics | |
?.filter((metric) => metric.author === 'human') | |
.map((metric) => [metric.name, metric]), | |
); | |
const algorithmicMetrics = new Map( | |
metrics | |
?.filter((metric) => metric.author === 'algorithm') | |
.map((metric) => [metric.name, metric]), | |
); | |
return [humanMetrics, algorithmicMetrics]; | |
}, [metrics]); | |
// Step 3: Render | |
return ( | |
<> | |
{models && metrics && task && evaluations && ( | |
<TextGenerationTaskCopierModal | |
open={taskCopierModalOpen} | |
models={Array.from(models.values())} | |
metrics={metrics} | |
task={task} | |
evaluations={evaluations} | |
onClose={() => { | |
setTaskCopierModalOpen(false); | |
}} | |
></TextGenerationTaskCopierModal> | |
)} | |
{task && models && evaluations && ( | |
<> | |
<div className={classes.inputContainer}> | |
{typeof task.input === 'string' ? ( | |
<> | |
<div className={classes.header}> | |
<h4>Input</h4> | |
<div className={classes.headerActions}> | |
<Button | |
id="text-highlight" | |
renderIcon={TextHighlight} | |
kind={'ghost'} | |
hasIconOnly={true} | |
iconDescription={ | |
'Click to highlight text common in input and response' | |
} | |
tooltipAlignment={'end'} | |
tooltipPosition={'bottom'} | |
onClick={() => { | |
setShowOverlap(!showOverlap); | |
}} | |
/> | |
</div> | |
</div> | |
<div className={classes.disclaimers}> | |
{showOverlap && ( | |
<div className={classes.overlapDisclaimer}> | |
<div className={classes.legendCopiedText}>■</div> | |
<span> | |
marks text assumed to be copied from input into | |
model response | |
</span> | |
</div> | |
)} | |
</div> | |
<div | |
className={classes.inputTextContainer} | |
onMouseDown={() => { | |
updateCommentProvenance('input'); | |
}} | |
onMouseUp={() => updateCommentProvenance('input')} | |
> | |
<p> | |
{parse( | |
DOMPurify.sanitize( | |
showOverlap | |
? mark( | |
task.input, | |
evaluations[selectedModelIndex].overlaps, | |
'target', | |
) | |
: task.input, | |
), | |
)} | |
</p> | |
</div> | |
</> | |
) : null} | |
</div> | |
<div className={classes.separator} /> | |
<div className={classes.evaluationsContainer}> | |
<Tabs | |
onChange={(e) => { | |
setSelectedModelIndex(e.selectedIndex); | |
}} | |
> | |
<TabList | |
className={classes.tabList} | |
aria-label="Models tab" | |
contained | |
> | |
{evaluations.map((evaluation) => ( | |
<Tab key={'model-' + evaluation.modelId}> | |
{truncate( | |
models.get(evaluation.modelId)?.name || | |
evaluation.modelId, | |
15, | |
)} | |
</Tab> | |
))} | |
</TabList> | |
<TabPanels> | |
{evaluations.map((evaluation) => ( | |
<TabPanel key={'model-' + evaluation.modelId + '-panel'}> | |
<div className={classes.tabContainer}> | |
<div className={classes.tabContentHeader}> | |
<h5>Model:</h5> | |
<span> | |
{models.get(evaluation.modelId)?.name || | |
evaluation.modelId} | |
</span> | |
</div> | |
<ContainedList | |
size="sm" | |
label="Response" | |
kind="disclosed" | |
> | |
<ContainedListItem> | |
<div | |
className={classes.responseContainer} | |
onMouseDown={() => { | |
updateCommentProvenance( | |
`${evaluation.modelId}::evaluation::response`, | |
); | |
}} | |
onMouseUp={() => | |
updateCommentProvenance( | |
`${evaluation.modelId}::evaluation::response`, | |
) | |
} | |
> | |
{parse( | |
DOMPurify.sanitize( | |
showOverlap | |
? mark( | |
evaluation.modelResponse, | |
evaluation.overlaps, | |
'source', | |
) | |
: evaluation.modelResponse, | |
), | |
)} | |
</div> | |
</ContainedListItem> | |
</ContainedList> | |
{task.targets && !isEmpty(task.targets) ? ( | |
<ContainedList | |
label="Targets" | |
kind="disclosed" | |
size="sm" | |
> | |
{task.targets.map((target, targetIdx) => | |
target.text ? ( | |
<ContainedListItem key={`target--${targetIdx}`}> | |
<span> | |
Target {targetIdx + 1}: {target.text} | |
</span> | |
</ContainedListItem> | |
) : null, | |
)} | |
</ContainedList> | |
) : null} | |
{evaluation.annotations && hMetrics.size ? ( | |
<> | |
<h5>Human Evaluations:</h5> | |
<AnnotationsTable | |
annotations={evaluation.annotations} | |
metrics={[...hMetrics.values()]} | |
></AnnotationsTable> | |
</> | |
) : null} | |
{evaluation.annotations && aMetrics.size ? ( | |
<> | |
<h5>Algorithmic Evaluations:</h5> | |
<AnnotationsTable | |
annotations={evaluation.annotations} | |
metrics={[...aMetrics.values()]} | |
></AnnotationsTable> | |
</> | |
) : null} | |
</div> | |
</TabPanel> | |
))} | |
</TabPanels> | |
</Tabs> | |
</div> | |
</> | |
)} | |
</> | |
); | |
} | |