kpfadnis's picture
feat (chat task): Minor enhancements to chat task view.
3939dda
raw
history blame
17.6 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 { 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}>&#9632;</div>
<span>
&nbsp;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>
</>
)}
</>
);
}