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, WarningAlt } from '@carbon/icons-react'; | |
import { | |
Model, | |
StringMatchObject, | |
TaskEvaluation, | |
RetrievedDocument, | |
Task, | |
Metric, | |
RetrievedDocumentAnnotation, | |
} from '@/src/types'; | |
import { useDataStore } from '@/src/store'; | |
import { truncate, overlaps } from '@/src/utilities/strings'; | |
import { mark } from '@/src/utilities/highlighter'; | |
import DocumentPanel from '@/src/views/document/DocumentPanel'; | |
import AnnotationsTable from '@/src/views/annotations-table/AnnotationsTable'; | |
import RAGTaskCopierModal from '@/src/components/task-copier/RAGTaskCopier'; | |
import classes from './RAGTask.module.scss'; | |
// =================================================================================== | |
// TYPES | |
// =================================================================================== | |
interface Props { | |
task: Task; | |
models: Map<string, Model>; | |
metrics: Metric[]; | |
taskCopierModalOpen: boolean; | |
setTaskCopierModalOpen: Function; | |
updateCommentProvenance: Function; | |
} | |
// =================================================================================== | |
// MAIN FUNCTION | |
// =================================================================================== | |
export default function RAGTask({ | |
task, | |
models, | |
metrics, | |
taskCopierModalOpen, | |
setTaskCopierModalOpen, | |
updateCommentProvenance, | |
}: Props) { | |
// Step 1: Initialize state and necessary variables | |
const [selectedEvaluationIndex, setSelectedEvaluationIndex] = | |
useState<number>(0); | |
const [showOverlap, setShowOverlap] = useState<boolean>(false); | |
const [activeDocumentIndex, setActiveDocumentIndex] = useState<number>(0); | |
// Step 2: Run effects | |
// Step 2.a: Fetch data from data store | |
const { item: data } = useDataStore(); | |
// Step 2.b: Fetch documents and evaluations | |
const [documentsPerEvaluation, evaluations] = useMemo(() => { | |
// Step 2.b.i: Initialize necessary variables | |
const contextsPerEvaluation: RetrievedDocument[][] = []; | |
// 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.i.*: Identify context document for each evaluation and compute context-response overlap and add to evaluation object | |
taskEvaluations.forEach((evaluation) => { | |
const contextDocuments: RetrievedDocument[] = []; | |
const contexts = evaluation.contexts | |
? evaluation.contexts | |
: task.contexts | |
? task.contexts | |
: []; | |
if (!isEmpty(contexts)) { | |
contexts.forEach((context, contextIdx) => { | |
if (data.documents) { | |
const referenceDocument = data.documents.find( | |
(document) => document.documentId === context.documentId, | |
); | |
if (referenceDocument) { | |
// Step 2.b.i.*: Fetch context relevant annotations, if present | |
if ( | |
task?.annotations && | |
task.annotations.hasOwnProperty('context_relevance') | |
) { | |
const documentAnnotation: RetrievedDocumentAnnotation = { | |
text: 'Relevant', | |
authors: [], | |
color: 'green', | |
}; | |
for (const [annotator, annotations] of Object.entries( | |
task.annotations.context_relevance, | |
)) { | |
if ( | |
Array.isArray(annotations) && | |
annotations.includes(contextIdx) | |
) { | |
documentAnnotation.authors.push(annotator); | |
} | |
} | |
if (!isEmpty(documentAnnotation.authors)) { | |
referenceDocument.annotations = [documentAnnotation]; | |
} | |
} | |
contextDocuments.push(referenceDocument); | |
} else { | |
contextDocuments.push({ | |
documentId: context.documentId, | |
text: 'Missing document text', | |
}); | |
} | |
} else { | |
contextDocuments.push({ | |
documentId: context.documentId, | |
text: 'Missing document text', | |
}); | |
} | |
}); | |
} | |
// Compute context-response overlap and add to evaluation object | |
const textOverlaps: StringMatchObject[][] = []; | |
contextDocuments.forEach((contextDocument) => { | |
textOverlaps.push( | |
overlaps(evaluation.modelResponse, contextDocument.text), | |
); | |
}); | |
evaluation.overlaps = textOverlaps; | |
// Add context documents | |
contextsPerEvaluation.push(contextDocuments); | |
}); | |
} | |
return [contextsPerEvaluation, taskEvaluations]; | |
}, [task.taskId, task.contexts, 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 && ( | |
<RAGTaskCopierModal | |
open={taskCopierModalOpen} | |
models={Array.from(models.values())} | |
metrics={metrics} | |
task={task} | |
evaluations={evaluations} | |
documents={documentsPerEvaluation[selectedEvaluationIndex]} | |
onClose={() => { | |
setTaskCopierModalOpen(false); | |
}} | |
></RAGTaskCopierModal> | |
)} | |
{task && models && evaluations && ( | |
<> | |
<div className={classes.inputContainer}> | |
<div | |
className={ | |
Array.isArray(task.input) && task.input.length > 1 | |
? classes.conversationContainer | |
: classes.questionContainer | |
} | |
> | |
{typeof task.input === 'string' ? ( | |
<> | |
<div className={classes.header}> | |
<h4>Question</h4> | |
</div> | |
<div | |
className={classes.questionTextContainer} | |
onMouseDown={() => { | |
updateCommentProvenance('input'); | |
}} | |
onMouseUp={() => updateCommentProvenance('input')} | |
> | |
{task.input} | |
</div> | |
</> | |
) : Array.isArray(task.input) ? ( | |
<> | |
<div className={classes.header}> | |
<h4>Conversation</h4> | |
</div> | |
<div | |
className={classes.conversationUtteranceContainer} | |
onMouseDown={() => { | |
updateCommentProvenance('input'); | |
}} | |
onMouseUp={() => updateCommentProvenance('input')} | |
> | |
{task.input.map((utterance, idx) => ( | |
<span key={'uttereance-' + idx}> | |
<strong> | |
{utterance.speaker.charAt(0).toUpperCase() + | |
utterance.speaker.slice(1).toLowerCase()} | |
</strong> | |
: {utterance.text} | |
</span> | |
))} | |
</div> | |
</> | |
) : typeof task.input === 'string' ? ( | |
<> | |
<div className={classes.header}> | |
<h4>Input</h4> | |
</div> | |
<div | |
className={classes.inputTextContainer} | |
onMouseDown={() => { | |
updateCommentProvenance('input'); | |
}} | |
onMouseUp={() => updateCommentProvenance('input')} | |
> | |
{task.input} | |
</div> | |
</> | |
) : null} | |
</div> | |
{documentsPerEvaluation && ( | |
<div className={classes.contextContainer}> | |
<div className={classes.header}> | |
<h4>Contexts</h4> | |
<div className={classes.headerActions}> | |
<Button | |
id="text-highlight" | |
renderIcon={TextHighlight} | |
kind={'ghost'} | |
hasIconOnly={true} | |
iconDescription={ | |
'Click to highlight text common in context and response' | |
} | |
tooltipAlignment={'end'} | |
tooltipPosition={'bottom'} | |
onClick={() => { | |
setShowOverlap(!showOverlap); | |
}} | |
/> | |
</div> | |
</div> | |
<div className={classes.contextDisclaimers}> | |
{showOverlap && ( | |
<div className={classes.contextOverlapDisclaimer}> | |
<div className={classes.legendCopiedText}>■</div> | |
<span> | |
marks text assumed to be copied from context into | |
model response | |
</span> | |
</div> | |
)} | |
</div> | |
{isEmpty(documentsPerEvaluation[selectedEvaluationIndex]) ? ( | |
<div className={classes.contextUnavailableWarning}> | |
<WarningAlt size={24} /> No context is available | |
</div> | |
) : ( | |
<DocumentPanel | |
key={`evaluation--${selectedEvaluationIndex}__documents`} | |
documents={documentsPerEvaluation[ | |
selectedEvaluationIndex | |
].map((document, documentIdx) => { | |
return { | |
documentId: document.documentId, | |
text: showOverlap | |
? mark( | |
document.text, | |
evaluations[selectedEvaluationIndex].overlaps[ | |
documentIdx | |
], | |
'target', | |
) | |
: document.text, | |
...(document.title && { title: document.title }), | |
...(document.url && { url: document.url }), | |
...(document.annotations && { | |
annotations: document.annotations, | |
}), | |
}; | |
})} | |
onMouseDown={(provenance: string) => { | |
updateCommentProvenance(provenance); | |
}} | |
onMouseUp={(provenance: string) => | |
updateCommentProvenance(provenance) | |
} | |
notify={(documentIndex: number) => { | |
setActiveDocumentIndex(documentIndex); | |
}} | |
/> | |
)} | |
</div> | |
)} | |
</div> | |
<div className={classes.separator} /> | |
<div className={classes.evaluationsContainer}> | |
<Tabs | |
onChange={(e) => { | |
setSelectedEvaluationIndex(e.selectedIndex); | |
setActiveDocumentIndex(0); | |
}} | |
> | |
<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, evaluationIdx) => ( | |
<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 && | |
evaluationIdx === selectedEvaluationIndex | |
? mark( | |
evaluation.modelResponse, | |
evaluation.overlaps[activeDocumentIndex], | |
'source', | |
) | |
: evaluation.modelResponse, | |
), | |
)} | |
</div> | |
</ContainedListItem> | |
</ContainedList> | |
{task.targets && !isEmpty(task.targets) ? ( | |
<ContainedList | |
label="Targets" | |
kind="disclosed" | |
size="sm" | |
> | |
{task.targets.length > 1 ? ( | |
task.targets.map((target, targetIdx) => | |
target.text ? ( | |
<ContainedListItem key={`target--${targetIdx}`}> | |
<span> | |
Target {targetIdx + 1}: {target.text} | |
</span> | |
</ContainedListItem> | |
) : null, | |
) | |
) : ( | |
<ContainedListItem key={`target--0`}> | |
<span>{task.targets[0].text}</span> | |
</ContainedListItem> | |
)} | |
</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> | |
</> | |
)} | |
</> | |
); | |
} | |