Refactor: modularize response boxes

This commit is contained in:
Ian Arawjo 2024-02-26 18:13:04 -05:00
parent 5fe3364351
commit b2a96bf9cc
5 changed files with 272 additions and 246 deletions

View File

@ -4,11 +4,10 @@
*/
import React, { useCallback, useMemo, useState, useRef } from "react";
import { Menu } from "@mantine/core";
import { Menu, MenuStylesNames, Styles } from "@mantine/core";
import { IconCopy, IconX } from "@tabler/icons-react";
import AreYouSureModal from "./AreYouSureModal";
import useStore from "./store";
import { Dict } from "./backend/typing";
interface BaseNodeProps {
children: React.ReactNode; // For components, HTML elements, text, etc.
@ -26,10 +25,9 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
const removeNode = useStore((state) => state.removeNode);
const duplicateNode = useStore((state) => state.duplicateNode);
const [contextMenuStyle, setContextMenuStyle] = useState<Dict>({
left: -100,
top: 0,
});
const [contextMenuStyle, setContextMenuStyle] = useState<
Styles<MenuStylesNames>
>({});
const [contextMenuOpened, setContextMenuOpened] = useState(false);
// For 'delete node' confirmation popup

View File

@ -30,6 +30,7 @@ import {
cleanMetavarsFilterFunc,
} from "./backend/utils";
import StorageCache from "./backend/cache";
import { ResponseBox } from "./ResponseBoxes";
const formattingOptions = [
{ value: "\n\n", label: "double newline \\n\\n" },
@ -57,43 +58,19 @@ const DEFAULT_GROUPBY_VAR_ALL = { label: "all text", value: "A" };
const displayJoinedTexts = (textInfos, getColorForLLM) => {
const color_for_llm = (llm) => getColorForLLM(llm) + "99";
return textInfos.map((info, idx) => {
const vars = info.fill_history;
const var_tags =
vars === undefined
? []
: Object.keys(vars).map((varname) => {
const v = truncStr(vars[varname].trim(), 72);
return (
<div key={varname} className="response-var-inline">
<span className="response-var-name">
{varname}&nbsp;=&nbsp;
</span>
<span className="response-var-value">{v}</span>
</div>
);
});
const ps = <pre className="small-response">{info.text || info}</pre>;
return (
<div
<ResponseBox
key={"r" + idx}
className="response-box"
style={{
backgroundColor: info.llm ? color_for_llm(info.llm?.name) : "#ddd",
width: `100%`,
}}
boxColor={info.llm ? color_for_llm(info.llm?.name) : "#ddd"}
width="100%"
vars={info.fill_history ?? {}}
truncLenForVars={72}
llmName={info.llm?.name ?? ""}
>
<div className="response-var-inline-container">{var_tags}</div>
{info.llm === undefined ? (
ps
) : (
<div className="response-item-llm-name-wrapper">
<h1>{info.llm?.name}</h1>
{ps}
</div>
)}
</div>
{ps}
</ResponseBox>
);
});
};

View File

@ -13,7 +13,6 @@ import React, {
Suspense,
} from "react";
import {
Collapse,
MultiSelect,
Table,
NativeSelect,
@ -24,7 +23,7 @@ import {
Tooltip,
TextInput,
} from "@mantine/core";
import { useDisclosure, useToggle } from "@mantine/hooks";
import { useToggle } from "@mantine/hooks";
import {
IconTable,
IconLayoutList,
@ -40,56 +39,17 @@ import {
batchResponsesByUID,
cleanMetavarsFilterFunc,
} from "./backend/utils";
import { getLabelForResponse } from "./ResponseRatingToolbar";
// Lazy load the response toolbars
const ResponseRatingToolbar = lazy(() => import("./ResponseRatingToolbar.js"));
import {
ResponseBox,
ResponseGroup,
genResponseTextsDisplay,
} from "./ResponseBoxes";
// Helper funcs
const countResponsesBy = (responses, keyFunc) => {
const responses_by_key = {};
const unspecified_group = [];
responses.forEach((item, idx) => {
const key = keyFunc(item);
const d = key !== null ? responses_by_key : unspecified_group;
if (key in d) d[key].push(idx);
else d[key] = [idx];
});
return [responses_by_key, unspecified_group];
};
const getLLMName = (resp_obj) =>
typeof resp_obj?.llm === "string" ? resp_obj.llm : resp_obj?.llm?.name;
const escapeRegExp = (txt) => txt.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
const SUCCESS_EVAL_SCORES = new Set(["true", "yes"]);
const FAILURE_EVAL_SCORES = new Set(["false", "no"]);
const getEvalResultStr = (eval_item) => {
if (Array.isArray(eval_item)) {
return "scores: " + eval_item.join(", ");
} else if (typeof eval_item === "object") {
const strs = Object.keys(eval_item).map((key) => {
let val = eval_item[key];
if (typeof val === "number" && val.toString().indexOf(".") > -1)
val = val.toFixed(4); // truncate floats to 4 decimal places
return `${key}: ${val}`;
});
return strs.join(", ");
} else {
const eval_str = eval_item.toString().trim().toLowerCase();
const color = SUCCESS_EVAL_SCORES.has(eval_str)
? "black"
: FAILURE_EVAL_SCORES.has(eval_str)
? "red"
: "black";
return (
<>
<span style={{ color: "gray" }}>{"score: "}</span>
<span style={{ color }}>{eval_str}</span>
</>
);
}
};
function getIndicesOfSubstringMatches(s, substr, caseSensitive) {
const regex = new RegExp(
escapeRegExp(substr),
@ -219,37 +179,6 @@ export const exportToExcel = (jsonResponses, filename) => {
XLSX.writeFile(wb, filename);
};
const ResponseGroup = ({
header,
responseBoxes,
responseBoxesWrapperClass,
displayStyle,
defaultState,
}) => {
const [opened, { toggle }] = useDisclosure(defaultState);
return (
<div>
<div className="response-group-component-header" onClick={toggle}>
{header}
</div>
<Collapse
in={opened}
transitionDuration={500}
transitionTimingFunction="ease-in"
animateOpacity={true}
>
<div
className={responseBoxesWrapperClass}
style={{ display: displayStyle, flexWrap: "wrap" }}
>
{responseBoxes}
</div>
</Collapse>
</div>
);
};
const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
// Responses
const [responses, setResponses] = useState([]);
@ -426,88 +355,27 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
const generateResponseBoxes = (resps, eatenvars, fixed_width) => {
const hide_llm_name = eatenvars.includes("LLM");
return resps.map((res_obj, res_idx) => {
const eval_res_items = res_obj.eval_res ? res_obj.eval_res.items : null;
// Bucket responses that have the same text, and sort by the
// number of same responses so that the top div is the most prevalent response.
let responses = res_obj.responses;
// If user has searched for something, further filter the response texts by only those that contain the search term
if (searchValue.length > 0) {
const respsFilterFunc = (responses) => {
if (searchValue.length === 0) return responses;
const filtered_resps = responses.filter(
search_regex.test.bind(search_regex),
);
numResponsesDisplayed += filtered_resps.length;
if (filterBySearchValue) responses = filtered_resps;
}
if (filterBySearchValue) return filtered_resps;
else return responses;
};
// We need to keep track of the original evaluation result per response str:
const resp_str_to_eval_res = {};
if (eval_res_items)
responses.forEach((r, idx) => {
resp_str_to_eval_res[r] = eval_res_items[idx];
});
// Counts the responses with the same keys
const same_resp_text_counts = countResponsesBy(responses, (r) => r)[0];
const same_resp_keys = Object.keys(same_resp_text_counts).sort(
(key1, key2) =>
same_resp_text_counts[key2].length -
same_resp_text_counts[key1].length,
);
const ps = same_resp_keys.map((r, idx) => {
const origIdxs = same_resp_text_counts[r];
const textToShow = searchValue
? genSpansForHighlightedValue(r, searchValue, caseSensitive)
: r;
return (
<div key={idx}>
<Flex justify="right" gap="xs" align="center">
{!hide_llm_name &&
idx === 0 &&
same_resp_keys.length > 1 &&
wideFormat === true ? (
<h1>{getLLMName(res_obj)}</h1>
) : (
<></>
)}
<Suspense>
<ResponseRatingToolbar
uid={res_obj.uid}
innerIdxs={origIdxs}
wideFormat={wideFormat}
/>
</Suspense>
{!hide_llm_name &&
idx === 0 &&
(same_resp_keys.length === 1 || !wideFormat) ? (
<h1>{getLLMName(res_obj)}</h1>
) : (
<></>
)}
</Flex>
{same_resp_text_counts[r].length > 1 ? (
<span className="num-same-responses">
{same_resp_text_counts[r].length} times
</span>
) : (
<></>
)}
{eval_res_items ? (
<p className="small-response-metrics">
{getEvalResultStr(resp_str_to_eval_res[r])}
</p>
) : (
<></>
)}
{contains_eval_res && onlyShowScores ? (
<pre>{}</pre>
) : (
<div className="small-response">{textToShow}</div>
)}
</div>
);
const innerTextsDisplay = genResponseTextsDisplay({
res_obj: res_obj,
onlyShowScores: contains_eval_res && onlyShowScores,
filterFunc: respsFilterFunc,
customTextDisplay: (txt) =>
searchValue
? genSpansForHighlightedValue(txt, searchValue, caseSensitive)
: txt,
hideLLMName: hide_llm_name,
wideFormat: wideFormat,
});
// At the deepest level, there may still be some vars left over. We want to display these
@ -517,31 +385,18 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
res_obj.vars,
(v) => !eatenvars.includes(v),
);
const var_tags = Object.keys(unused_vars).map((varname) => {
const v = truncStr(unused_vars[varname].trim(), wideFormat ? 72 : 18);
return (
<div key={varname} className="response-var-inline">
<span className="response-var-name">{varname}&nbsp;=&nbsp;</span>
<span className="response-var-value">{v}</span>
</div>
);
});
const llmName = getLLMName(res_obj);
return (
<div
<ResponseBox
key={"r" + res_idx}
className="response-box"
style={{
backgroundColor: color_for_llm(getLLMName(res_obj)),
width: `${fixed_width}%`,
}}
boxColor={color_for_llm(llmName)}
width={`${fixed_width}%`}
vars={unused_vars}
truncLenForVars={wideFormat ? 72 : 18}
llmName={hide_llm_name ? undefined : llmName}
>
<div className="response-var-inline-container">{var_tags}</div>
{hide_llm_name ? (
ps
) : (
<div className="response-item-llm-name-wrapper">{ps}</div>
)}
</div>
{innerTextsDisplay}
</ResponseBox>
);
});
};

View File

@ -0,0 +1,220 @@
import React, { Suspense, useMemo, useState } from "react";
import { Collapse } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { truncStr } from "./backend/utils";
import { getLabelForResponse } from "./ResponseRatingToolbar";
// Lazy load the response toolbars
const ResponseRatingToolbar = lazy(() => import("./ResponseRatingToolbar.js"));
/* HELPER FUNCTIONS */
const SUCCESS_EVAL_SCORES = new Set(["true", "yes"]);
const FAILURE_EVAL_SCORES = new Set(["false", "no"]);
const getEvalResultStr = (eval_item) => {
if (Array.isArray(eval_item)) {
return "scores: " + eval_item.join(", ");
} else if (typeof eval_item === "object") {
const strs = Object.keys(eval_item).map((key) => {
let val = eval_item[key];
if (typeof val === "number" && val.toString().indexOf(".") > -1)
val = val.toFixed(4); // truncate floats to 4 decimal places
return `${key}: ${val}`;
});
return strs.join(", ");
} else {
const eval_str = eval_item.toString().trim().toLowerCase();
const color = SUCCESS_EVAL_SCORES.has(eval_str)
? "black"
: FAILURE_EVAL_SCORES.has(eval_str)
? "red"
: "black";
return (
<>
<span style={{ color: "gray" }}>{"score: "}</span>
<span style={{ color }}>{eval_str}</span>
</>
);
}
};
const countResponsesBy = (responses, keyFunc) => {
const responses_by_key = {};
const unspecified_group = [];
responses.forEach((item) => {
const key = keyFunc(item);
const d = key !== null ? responses_by_key : unspecified_group;
if (key in d) d[key] += 1;
else d[key] = 1;
});
return [responses_by_key, unspecified_group];
};
/**
* A ResponseGroup is used in the Grouped List view to display clickable, collapseable groups of responses.
* These groups may also be ResponseGroups (nested).
*/
export const ResponseGroup = ({
header,
responseBoxes,
responseBoxesWrapperClass,
displayStyle,
defaultState,
}) => {
const [opened, { toggle }] = useDisclosure(defaultState);
return (
<div>
<div className="response-group-component-header" onClick={toggle}>
{header}
</div>
<Collapse
in={opened}
transitionDuration={500}
transitionTimingFunction="ease-in"
animateOpacity={true}
>
<div
className={responseBoxesWrapperClass}
style={{ display: displayStyle, flexWrap: "wrap" }}
>
{responseBoxes}
</div>
</Collapse>
</div>
);
};
/**
* A ResponseBox is the display of an LLM's response(s) for a single prompt.
* It is the colored boxes that appear in the response inspector when you are inspecting responses.
* Note that a ResponseBox could list multiple textual responses if num responses per prompt > 1.
*/
export const ResponseBox = ({
children,
boxColor,
width,
vars,
truncLenForVars,
llmName,
}) => {
const var_tags = useMemo(() => {
return Object.entries(vars).map(([varname, val]) => {
const v = truncStr(val.trim(), truncLenForVars ?? 18);
return (
<div key={varname} className="response-var-inline">
<span className="response-var-name">{varname}&nbsp;=&nbsp;</span>
<span className="response-var-value">{v}</span>
</div>
);
});
}, [vars, truncLenForVars]);
return (
<div
className="response-box"
style={{
backgroundColor: boxColor ?? "white",
width: width ?? "100%",
}}
>
<div className="response-var-inline-container">{var_tags}</div>
{llmName !== undefined ? (
children
) : (
<div className="response-item-llm-name-wrapper">
{children}
</div>
)}
</div>
);
};
/**
* Given a response object, generates the inner divs to put inside a ResponseBox.
* This is the lowest level display for response texts in ChainForge.
*/
export const genResponseTextsDisplay = ({
res_obj,
filterFunc,
customTextDisplay,
onlyShowScores,
hideLLMName,
wideFormat,
}) => {
if (!res_obj) return <></>;
const eval_res_items = res_obj.eval_res ? res_obj.eval_res.items : null;
// Bucket responses that have the same text, and sort by the
// number of same responses so that the top div is the most prevalent response.
let responses = res_obj.responses;
// Perform any post-processing of responses. For instance,
// when searching for a response, we mark up the response texts to spans
// and may filter out some responses, removing them from display.
if (filterFunc) responses = filterFunc(responses);
// Collapse responses with the same texts.
// We need to keep track of the original evaluation result per response str:
const resp_str_to_eval_res = {};
if (eval_res_items)
responses.forEach((r, idx) => {
resp_str_to_eval_res[r] = eval_res_items[idx];
});
const same_resp_text_counts = countResponsesBy(responses, (r) => r)[0];
const same_resp_keys = Object.keys(same_resp_text_counts).sort(
(key1, key2) => same_resp_text_counts[key2] - same_resp_text_counts[key1],
);
return same_resp_keys.map((r, idx) => {
const origIdxs = same_resp_text_counts[r];
const txt = customTextDisplay ? customTextDisplay(r) : r;
return (
<div key={idx}>
<Flex justify="right" gap="xs" align="center">
{!hideLLMName &&
idx === 0 &&
same_resp_keys.length > 1 &&
wideFormat === true ? (
<h1>{getLLMName(res_obj)}</h1>
) : (
<></>
)}
<Suspense>
<ResponseRatingToolbar
uid={res_obj.uid}
innerIdxs={origIdxs}
wideFormat={wideFormat}
/>
</Suspense>
{!hideLLMName &&
idx === 0 &&
(same_resp_keys.length === 1 || !wideFormat) ? (
<h1>{getLLMName(res_obj)}</h1>
) : (
<></>
)}
</Flex>
{same_resp_text_counts[r] > 1 ? (
<span className="num-same-responses">
{same_resp_text_counts[r]} times
</span>
) : (
<></>
)}
{eval_res_items ? (
<p className="small-response-metrics">
{getEvalResultStr(resp_str_to_eval_res[r])}
</p>
) : (
<></>
)}
{onlyShowScores ? (
<pre>{}</pre>
) : (
<div className="small-response">{txt}</div>
)}
</div>
);
});
};

View File

@ -31,6 +31,7 @@ import {
import { fromMarkdown } from "mdast-util-from-markdown";
import StorageCache from "./backend/cache";
import { ResponseBox } from "./ResponseBoxes";
const formattingOptions = [
{ value: "list", label: "- list items" },
@ -102,43 +103,18 @@ export const splitText = (s, format, shouldEscapeBraces) => {
const displaySplitTexts = (textInfos, getColorForLLM) => {
const color_for_llm = (llm) => getColorForLLM(llm) + "99";
return textInfos.map((info, idx) => {
const vars = info.fill_history;
const var_tags =
vars === undefined
? []
: Object.keys(vars).map((varname) => {
const v = truncStr(vars[varname].trim(), 72);
return (
<div key={varname} className="response-var-inline">
<span className="response-var-name">
{varname}&nbsp;=&nbsp;
</span>
<span className="response-var-value">{v}</span>
</div>
);
});
const ps = <pre className="small-response">{info.text ?? info}</pre>;
return (
<div
<ResponseBox
key={"r" + idx}
className="response-box"
style={{
backgroundColor: info.llm ? color_for_llm(info.llm?.name) : "#ddd",
width: `100%`,
}}
boxColor={info.llm ? color_for_llm(info.llm?.name) : "#ddd"}
width="100%"
vars={info.fill_history ?? {}}
truncLenForVars={72}
llmName={info.llm?.name ?? ""}
>
<div className="response-var-inline-container">{var_tags}</div>
{info.llm === undefined ? (
ps
) : (
<div className="response-item-llm-name-wrapper">
<h1>{info.llm?.name}</h1>
{ps}
</div>
)}
</div>
{ps}
</ResponseBox>
);
});
};