Spaces:
Paused
Paused
| import * as React from 'react'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import { useTheme } from '@mui/material/styles'; | |
| import Box from '@mui/material/Box'; | |
| import OutlinedInput from '@mui/material/OutlinedInput'; | |
| import InputLabel from '@mui/material/InputLabel'; | |
| import MenuItem from '@mui/material/MenuItem'; | |
| import FormControl from '@mui/material/FormControl'; | |
| import Select from '@mui/material/Select'; | |
| import Chip from '@mui/material/Chip'; | |
| import Button from '@mui/material/Button'; | |
| import Typography from '@mui/material/Typography'; | |
| import './Evaluate.css'; | |
| const MenuProps = { | |
| PaperProps: { | |
| className: 'evaluate-menu', | |
| }, | |
| disableScrollLock: true | |
| }; | |
| function getStyles(name, selectedNames, theme) { | |
| return { | |
| fontWeight: selectedNames.includes(name.toLowerCase()) | |
| ? theme.typography.fontWeightMedium | |
| : theme.typography.fontWeightRegular, | |
| }; | |
| } | |
| export default function MultipleSelectChip({ evaluation }) { | |
| const theme = useTheme(); | |
| const [personName, setPersonName] = React.useState([]); | |
| const [selectedMetrics, setSelectedMetrics] = React.useState([]); | |
| const [evaluationResult, setEvaluationResult] = React.useState(""); | |
| const [isEvaluating, setIsEvaluating] = React.useState(false); | |
| const [localLoading, setLocalLoading] = React.useState(false); | |
| const [noMetricsError, setNoMetricsError] = React.useState(""); | |
| const [metricOptions, setMetricOptions] = React.useState([]); | |
| React.useEffect(() => { | |
| // If 'contents' is undefined in the payload | |
| if (evaluation && evaluation.contents === undefined) { | |
| setMetricOptions([ | |
| "Bias", | |
| "Toxicity", | |
| "Summarization", | |
| "Answer Correctness", | |
| ]); | |
| } else { | |
| // Else, all except "Answer Correctness" | |
| setMetricOptions([ | |
| "Bias", | |
| "Toxicity", | |
| "Summarization", | |
| "Faithfulness", | |
| "Hallucination", | |
| "Answer Relevancy", | |
| "Contextual Relevancy", | |
| "Contextual Recall", | |
| ]); | |
| } | |
| }, [evaluation]); | |
| // Reset the form fields | |
| React.useEffect(() => { | |
| // Reset the form and evaluation result | |
| setPersonName([]); | |
| setSelectedMetrics([]); | |
| setEvaluationResult(""); | |
| setLocalLoading(true); | |
| setNoMetricsError(""); | |
| // Simulate a loading delay | |
| const timer = setTimeout(() => { | |
| setLocalLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| }, [evaluation]); | |
| const handleChange = (event) => { | |
| const { target: { value } } = event; | |
| const metrics = typeof value === 'string' ? value.split(',') : value; | |
| setPersonName(metrics); | |
| setSelectedMetrics(metrics); | |
| setNoMetricsError(""); | |
| }; | |
| const handleDelete = (chipToDelete) => { | |
| setPersonName((chips) => chips.filter((chip) => chip !== chipToDelete)); | |
| }; | |
| // Function to convert a string to title case. | |
| const titleCase = (str) => { | |
| return str | |
| .split(' ') | |
| .map(word => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| }; | |
| const handleEvaluateClick = async () => { | |
| // Clear previous evaluation result immediately. | |
| setEvaluationResult(""); | |
| // Check if no metrics selected | |
| if (selectedMetrics.length === 0) { | |
| setNoMetricsError("No metrics selected"); | |
| return; | |
| } | |
| setNoMetricsError(""); | |
| setIsEvaluating(true); | |
| const payload = { ...evaluation, metrics: selectedMetrics }; | |
| try { | |
| const res = await fetch("/action/evaluate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json(); | |
| throw new Error(`Evaluation Error: ${error.error}`); | |
| } | |
| const data = await res.json(); | |
| if (!data.result) { | |
| throw new Error("No results returned from evaluation"); | |
| } | |
| // Format the JSON into Markdown. | |
| let markdown = "### Result\n\n"; | |
| for (const [metric, details] of Object.entries(data.result)) { | |
| let score = details.score; | |
| if (typeof score === "number") { | |
| const percentage = score * 100; | |
| score = Number.isInteger(percentage) | |
| ? percentage.toFixed(0) + "%" | |
| : percentage.toFixed(2) + "%"; | |
| } | |
| let reason = details.reason; | |
| markdown += `**${titleCase(metric)}:** ${score}\n\n${reason}\n\n`; | |
| } | |
| setEvaluationResult(markdown); | |
| } catch (err) { | |
| // Use the callback to trigger the error block in ChatWindow | |
| if (evaluation.onError && evaluation.blockId) { | |
| evaluation.onError(evaluation.blockId, err.message || "Evaluation failed"); | |
| } | |
| else { | |
| console.error("Evaluation prop is missing or incomplete:", evaluation); | |
| } | |
| } | |
| setIsEvaluating(false); | |
| }; | |
| // Finds the matching display name for a metric. | |
| const getDisplayName = (lowerValue) => { | |
| const found = metricOptions.find(n => n.toLowerCase() === lowerValue); | |
| return found ? found : lowerValue; | |
| }; | |
| return ( | |
| <Box className="evaluate-container"> | |
| {localLoading ? ( | |
| <Box> | |
| <Typography variant="body2">Loading Evaluation...</Typography> | |
| </Box> | |
| ) : ( | |
| <> | |
| <FormControl className="evaluate-form-control"> | |
| <InputLabel id="chip-label">Select Metrics</InputLabel> | |
| <Select | |
| labelId="chip-label" | |
| id="multiple-chip" | |
| multiple | |
| value={personName} | |
| onChange={handleChange} | |
| input={ | |
| <OutlinedInput | |
| id="select-multiple-chip" | |
| label="Select Metrics" | |
| className="evaluate-outlined-input" | |
| /> | |
| } | |
| renderValue={(selected) => ( | |
| <Box className="chip-container"> | |
| {selected.map((value) => ( | |
| <Chip | |
| className="evaluate-chip" | |
| key={value} | |
| label={getDisplayName(value)} | |
| onDelete={() => handleDelete(value)} | |
| onMouseDown={(event) => event.stopPropagation()} | |
| /> | |
| ))} | |
| </Box> | |
| )} | |
| MenuProps={MenuProps} | |
| > | |
| {metricOptions.map((name) => ( | |
| <MenuItem | |
| key={name} | |
| value={name.toLowerCase()} // underlying value is lowercase | |
| style={getStyles(name, personName, theme)} | |
| > | |
| {name} | |
| </MenuItem> | |
| ))} | |
| </Select> | |
| </FormControl> | |
| <Box mt={1}> | |
| <Button | |
| variant="contained" | |
| onClick={handleEvaluateClick} | |
| className="evaluate-button" | |
| > | |
| Evaluate | |
| </Button> | |
| </Box> | |
| {noMetricsError && ( | |
| <Box className="no-metrics-message"> | |
| {noMetricsError} | |
| </Box> | |
| )} | |
| {isEvaluating && ( | |
| <Box mt={1} display="flex" alignItems="center"> | |
| <Box className="custom-spinner" /> | |
| <Box ml={1}>Evaluating...</Box> | |
| </Box> | |
| )} | |
| {evaluationResult && ( | |
| <Box mt={2}> | |
| <ReactMarkdown>{evaluationResult}</ReactMarkdown> | |
| </Box> | |
| )} | |
| </> | |
| )} | |
| </Box> | |
| ); | |
| } |