From 8d71b54a8ff6796de538c2075feca9b0b3cc70ff Mon Sep 17 00:00:00 2001 From: Ian Arawjo Date: Sat, 1 Mar 2025 10:02:12 -0500 Subject: [PATCH] Performance improvements Mantine React Table was rerendering cells even when hidden. --- chainforge/react-server/src/App.tsx | 879 +++++++++--------- chainforge/react-server/src/BaseNode.tsx | 47 +- chainforge/react-server/src/InspectorNode.tsx | 1 + .../react-server/src/LLMResponseInspector.tsx | 70 +- .../src/LLMResponseInspectorDrawer.tsx | 1 + .../src/LLMResponseInspectorModal.tsx | 1 + chainforge/react-server/src/PromptNode.tsx | 108 ++- chainforge/react-server/src/ResponseBoxes.tsx | 11 +- 8 files changed, 611 insertions(+), 507 deletions(-) diff --git a/chainforge/react-server/src/App.tsx b/chainforge/react-server/src/App.tsx index 2cbcbee..e46e8c4 100644 --- a/chainforge/react-server/src/App.tsx +++ b/chainforge/react-server/src/App.tsx @@ -4,6 +4,7 @@ import React, { useRef, useEffect, useContext, + useMemo, } from "react"; import ReactFlow, { Controls, Background, ReactFlowInstance } from "reactflow"; import { @@ -196,6 +197,15 @@ const getSharedFlowURLParam = () => { return undefined; }; +const getWindowSize = () => ({ + width: window.innerWidth, + height: window.innerHeight, +}); +const getWindowCenter = () => { + const { width, height } = getWindowSize(); + return { centerX: width / 2.0, centerY: height / 2.0 }; +}; + const MenuTooltip = ({ label, children, @@ -277,71 +287,35 @@ const App = () => { const [isLoading, setIsLoading] = useState(true); // Helper - const getWindowSize = () => ({ - width: window.innerWidth, - height: window.innerHeight, - }); - const getWindowCenter = () => { - const { width, height } = getWindowSize(); - return { centerX: width / 2.0, centerY: height / 2.0 }; - }; - const getViewportCenter = () => { + const getViewportCenter = useCallback(() => { const { centerX, centerY } = getWindowCenter(); if (rfInstance === null) return { x: centerX, y: centerY }; // Support Zoom const { x, y, zoom } = rfInstance.getViewport(); return { x: -(x / zoom) + centerX / zoom, y: -(y / zoom) + centerY / zoom }; - }; + }, [rfInstance]); - const addNode = ( - id: string, - type?: string, - data?: Dict, - offsetX?: number, - offsetY?: number, - ) => { - const { x, y } = getViewportCenter(); - addNodeToStore({ - id: `${id}-` + Date.now(), - type: type ?? id, - data: data ?? {}, - position: { - x: x - 200 + (offsetX || 0), - y: y - 100 + (offsetY || 0), - }, - }); - }; - - const addTextFieldsNode = () => addNode("textFieldsNode", "textfields"); - const addPromptNode = () => addNode("promptNode", "prompt", { prompt: "" }); - const addChatTurnNode = () => addNode("chatTurn", "chat", { prompt: "" }); - const addSimpleEvalNode = () => addNode("simpleEval", "simpleval"); - const addEvalNode = (progLang: string) => { - let code = ""; - if (progLang === "python") - code = "def evaluate(response):\n return len(response.text)"; - else if (progLang === "javascript") - code = "function evaluate(response) {\n return response.text.length;\n}"; - addNode("evalNode", "evaluator", { language: progLang, code }); - }; - const addVisNode = () => addNode("visNode", "vis", {}); - const addInspectNode = () => addNode("inspectNode", "inspect"); - const addScriptNode = () => addNode("scriptNode", "script"); - const addItemsNode = () => addNode("csvNode", "csv"); - const addTabularDataNode = () => addNode("table"); - const addCommentNode = () => addNode("comment"); - const addLLMEvalNode = () => addNode("llmeval"); - const addMultiEvalNode = () => addNode("multieval"); - const addJoinNode = () => addNode("join"); - const addSplitNode = () => addNode("split"); - const addProcessorNode = (progLang: string) => { - let code = ""; - if (progLang === "python") - code = "def process(response):\n return response.text;"; - else if (progLang === "javascript") - code = "function process(response) {\n return response.text;\n}"; - addNode("process", "processor", { language: progLang, code }); - }; + const addNode = useCallback( + ( + id: string, + type?: string, + data?: Dict, + offsetX?: number, + offsetY?: number, + ) => { + const { x, y } = getViewportCenter(); + addNodeToStore({ + id: `${id}-` + Date.now(), + type: type ?? id, + data: data ?? {}, + position: { + x: x - 200 + (offsetX || 0), + y: y - 100 + (offsetY || 0), + }, + }); + }, + [addNodeToStore], + ); const onClickExamples = () => { if (examplesModal && examplesModal.current) examplesModal.current.trigger(); @@ -350,13 +324,16 @@ const App = () => { if (settingsModal && settingsModal.current) settingsModal.current.trigger(); }; - const handleError = (err: Error | string) => { - const msg = typeof err === "string" ? err : err.message; - setIsLoading(false); - setWaitingForShare(false); - if (showAlert) showAlert(msg); - console.error(msg); - }; + const handleError = useCallback( + (err: Error | string) => { + const msg = typeof err === "string" ? err : err.message; + setIsLoading(false); + setWaitingForShare(false); + if (showAlert) showAlert(msg); + console.error(msg); + }, + [showAlert], + ); /** * SAVING / LOADING, IMPORT / EXPORT (from JSON) @@ -405,6 +382,42 @@ const App = () => { [rfInstance], ); + // Initialize auto-saving + const initAutosaving = useCallback( + (rf_inst: ReactFlowInstance) => { + if (autosavingInterval !== undefined) return; // autosaving interval already set + console.log("Init autosaving"); + + // Autosave the flow to localStorage every minute: + const interv = setInterval(() => { + // Check the visibility of the browser tab --if it's not visible, don't autosave + if (!browserTabIsActive()) return; + + // Start a timer, in case the saving takes a long time + const startTime = Date.now(); + + // Save the flow to localStorage + saveFlow(rf_inst); + + // Check how long the save took + const duration = Date.now() - startTime; + if (duration > 1500) { + // If the operation took longer than 1.5 seconds, that's not good. + // Although this function is called async inside setInterval, + // calls to localStorage block the UI in JavaScript, freezing the screen. + // We smart-disable autosaving here when we detect it's starting the freeze the UI: + console.warn( + "Autosaving disabled. The time required to save to localStorage exceeds 1 second. This can happen when there's a lot of data in your flow. Make sure to export frequently to save your work.", + ); + clearInterval(interv); + setAutosavingInterval(undefined); + } + }, 60000); // 60000 milliseconds = 1 minute + setAutosavingInterval(interv); + }, + [autosavingInterval, saveFlow], + ); + // Triggered when user confirms 'New Flow' button const resetFlow = useCallback(() => { resetLLMColors(); @@ -436,58 +449,64 @@ const App = () => { if (rfInstance) rfInstance.setViewport({ x: 200, y: 80, zoom: 1 }); }, [setNodes, setEdges, resetLLMColors, rfInstance]); - const loadFlow = async (flow?: Dict, rf_inst?: ReactFlowInstance | null) => { - if (flow === undefined) return; - if (rf_inst) { - if (flow.viewport) - rf_inst.setViewport({ - x: flow.viewport.x || 0, - y: flow.viewport.y || 0, - zoom: flow.viewport.zoom || 1, - }); - else rf_inst.setViewport({ x: 0, y: 0, zoom: 1 }); - } - resetLLMColors(); + const loadFlow = useCallback( + async (flow?: Dict, rf_inst?: ReactFlowInstance | null) => { + if (flow === undefined) return; + if (rf_inst) { + if (flow.viewport) + rf_inst.setViewport({ + x: flow.viewport.x || 0, + y: flow.viewport.y || 0, + zoom: flow.viewport.zoom || 1, + }); + else rf_inst.setViewport({ x: 0, y: 0, zoom: 1 }); + } + resetLLMColors(); - // First, clear the ReactFlow state entirely - // NOTE: We need to do this so it forgets any node/edge ids, which might have cross-over in the loaded flow. - setNodes([]); - setEdges([]); + // First, clear the ReactFlow state entirely + // NOTE: We need to do this so it forgets any node/edge ids, which might have cross-over in the loaded flow. + setNodes([]); + setEdges([]); - // After a delay, load in the new state. - setTimeout(() => { - setNodes(flow.nodes || []); - setEdges(flow.edges || []); + // After a delay, load in the new state. + setTimeout(() => { + setNodes(flow.nodes || []); + setEdges(flow.edges || []); - // Save flow that user loaded to autosave cache, in case they refresh the browser - StorageCache.saveToLocalStorage("chainforge-flow", flow); + // Save flow that user loaded to autosave cache, in case they refresh the browser + StorageCache.saveToLocalStorage("chainforge-flow", flow); - // Cancel loading spinner - setIsLoading(false); - }, 10); + // Cancel loading spinner + setIsLoading(false); + }, 10); - // Start auto-saving, if it's not already enabled - if (rf_inst) initAutosaving(rf_inst); - }; + // Start auto-saving, if it's not already enabled + if (rf_inst) initAutosaving(rf_inst); + }, + [resetLLMColors, setNodes, setEdges, initAutosaving], + ); const importGlobalStateFromCache = useCallback(() => { importState(StorageCache.getAllMatching((key) => key.startsWith("r."))); }, [importState]); - const autosavedFlowExists = () => { + const autosavedFlowExists = useCallback(() => { return window.localStorage.getItem("chainforge-flow") !== null; - }; - const loadFlowFromAutosave = async (rf_inst: ReactFlowInstance) => { - const saved_flow = StorageCache.loadFromLocalStorage( - "chainforge-flow", - false, - ) as Dict; - if (saved_flow) { - StorageCache.loadFromLocalStorage("chainforge-state", true); - importGlobalStateFromCache(); - loadFlow(saved_flow, rf_inst); - } - }; + }, []); + const loadFlowFromAutosave = useCallback( + async (rf_inst: ReactFlowInstance) => { + const saved_flow = StorageCache.loadFromLocalStorage( + "chainforge-flow", + false, + ) as Dict; + if (saved_flow) { + StorageCache.loadFromLocalStorage("chainforge-state", true); + importGlobalStateFromCache(); + loadFlow(saved_flow, rf_inst); + } + }, + [importGlobalStateFromCache, loadFlow], + ); // Export / Import (from JSON) const exportFlow = useCallback(() => { @@ -754,107 +773,83 @@ const App = () => { waitingForShare, ]); - // Initialize auto-saving - const initAutosaving = (rf_inst: ReactFlowInstance) => { - if (autosavingInterval !== undefined) return; // autosaving interval already set - console.log("Init autosaving"); - - // Autosave the flow to localStorage every minute: - const interv = setInterval(() => { - // Check the visibility of the browser tab --if it's not visible, don't autosave - if (!browserTabIsActive()) return; - - // Start a timer, in case the saving takes a long time - const startTime = Date.now(); - - // Save the flow to localStorage - saveFlow(rf_inst); - - // Check how long the save took - const duration = Date.now() - startTime; - if (duration > 1500) { - // If the operation took longer than 1.5 seconds, that's not good. - // Although this function is called async inside setInterval, - // calls to localStorage block the UI in JavaScript, freezing the screen. - // We smart-disable autosaving here when we detect it's starting the freeze the UI: - console.warn( - "Autosaving disabled. The time required to save to localStorage exceeds 1 second. This can happen when there's a lot of data in your flow. Make sure to export frequently to save your work.", - ); - clearInterval(interv); - setAutosavingInterval(undefined); - } - }, 60000); // 60000 milliseconds = 1 minute - setAutosavingInterval(interv); - }; - // Run once upon ReactFlow initialization - const onInit = (rf_inst: ReactFlowInstance) => { - setRfInstance(rf_inst); + const onInit = useCallback( + (rf_inst: ReactFlowInstance) => { + setRfInstance(rf_inst); - if (IS_RUNNING_LOCALLY) { - // If we're running locally, try to fetch API keys from Python os.environ variables in the locally running Flask backend: - fetchEnvironAPIKeys() - .then((api_keys) => { - setAPIKeys(api_keys); - }) - .catch((err) => { - // Soft fail - console.warn( - "Warning: Could not fetch API key environment variables from Flask server. Error:", - err.message, - ); - }); - } else { - // Check if there's a shared flow UID in the URL as a GET param - // If so, we need to look it up in the database and attempt to load it: - const shared_flow_uid = getSharedFlowURLParam(); - if (shared_flow_uid !== undefined) { - try { - // The format passed a basic smell test; - // now let's query the server for a flow with that UID: - fetch("/db/get_sharedflow.php", { - method: "POST", - body: shared_flow_uid, + if (IS_RUNNING_LOCALLY) { + // If we're running locally, try to fetch API keys from Python os.environ variables in the locally running Flask backend: + fetchEnvironAPIKeys() + .then((api_keys) => { + setAPIKeys(api_keys); }) - .then((r) => r.text()) - .then((response) => { - if (!response || response.startsWith("Error")) { - // Error encountered during the query; alert the user - // with the error message: - throw new Error(response || "Unknown error"); - } - - // Attempt to parse the response as a compressed flow + import it: - const cforge_json = JSON.parse( - LZString.decompressFromUTF16(response), - ); - importFlowFromJSON(cforge_json, rf_inst); + .catch((err) => { + // Soft fail + console.warn( + "Warning: Could not fetch API key environment variables from Flask server. Error:", + err.message, + ); + }); + } else { + // Check if there's a shared flow UID in the URL as a GET param + // If so, we need to look it up in the database and attempt to load it: + const shared_flow_uid = getSharedFlowURLParam(); + if (shared_flow_uid !== undefined) { + try { + // The format passed a basic smell test; + // now let's query the server for a flow with that UID: + fetch("/db/get_sharedflow.php", { + method: "POST", + body: shared_flow_uid, }) - .catch(handleError); - } catch (err) { - // Soft fail - setIsLoading(false); - console.error(err); + .then((r) => r.text()) + .then((response) => { + if (!response || response.startsWith("Error")) { + // Error encountered during the query; alert the user + // with the error message: + throw new Error(response || "Unknown error"); + } + + // Attempt to parse the response as a compressed flow + import it: + const cforge_json = JSON.parse( + LZString.decompressFromUTF16(response), + ); + importFlowFromJSON(cforge_json, rf_inst); + }) + .catch(handleError); + } catch (err) { + // Soft fail + setIsLoading(false); + console.error(err); + } + + // Since we tried to load from the shared flow ID, don't try to load from autosave + return; } - - // Since we tried to load from the shared flow ID, don't try to load from autosave - return; } - } - // Attempt to load an autosaved flow, if one exists: - if (autosavedFlowExists()) loadFlowFromAutosave(rf_inst); - else { - // Load an interesting default starting flow for new users - importFlowFromJSON(EXAMPLEFLOW_1, rf_inst); + // Attempt to load an autosaved flow, if one exists: + if (autosavedFlowExists()) loadFlowFromAutosave(rf_inst); + else { + // Load an interesting default starting flow for new users + importFlowFromJSON(EXAMPLEFLOW_1, rf_inst); - // Open a welcome pop-up - // openWelcomeModal(); - } + // Open a welcome pop-up + // openWelcomeModal(); + } - // Turn off loading wheel - setIsLoading(false); - }; + // Turn off loading wheel + setIsLoading(false); + }, + [ + setAPIKeys, + handleError, + importFlowFromJSON, + autosavedFlowExists, + loadFlowFromAutosave, + ], + ); useEffect(() => { // Cleanup the autosaving interval upon component unmount: @@ -863,6 +858,273 @@ const App = () => { }; }, []); + const reactFlowUI = useMemo(() => { + return ( +
+
+ { + // Suppress ReactFlow warnings spamming the console. + // console.log(err); + }} + > + + + +
+
+ ); + }, [ + onNodesChange, + onEdgesChange, + onConnect, + nodes, + edges, + onInit, + hideContextMenu, + ]); + + const addNodeMenu = useMemo( + () => ( + + + + + + + Input Data + + addNode("textFieldsNode", "textfields")} + icon={} + > + {" "} + TextFields Node{" "} + + + + addNode("csvNode", "csv")} + icon={} + > + {" "} + Items Node{" "} + + + + addNode("table")} icon={"🗂️"}> + {" "} + Tabular Data Node{" "} + + + + Prompters + + addNode("promptNode", "prompt", { prompt: "" })} + icon={"💬"} + > + {" "} + Prompt Node{" "} + + + + addNode("chatTurn", "chat", { prompt: "" })} + icon={"🗣"} + > + {" "} + Chat Turn Node{" "} + + + + Evaluators + + addNode("simpleEval", "simpleval")} + icon={} + > + {" "} + Simple Evaluator{" "} + + + + + addNode("evalNode", "evaluator", { + language: "javascript", + code: "function evaluate(response) {\n return response.text.length;\n}", + }) + } + icon={} + > + {" "} + JavaScript Evaluator{" "} + + + + + addNode("evalNode", "evaluator", { + language: "python", + code: "def evaluate(response):\n return len(response.text)", + }) + } + icon={} + > + {" "} + Python Evaluator{" "} + + + + addNode("llmeval")} + icon={} + > + {" "} + LLM Scorer{" "} + + + + addNode("multieval")} + icon={} + > + {" "} + Multi-Evaluator{" "} + + + + Visualizers + + addNode("visNode", "vis", {})} + icon={"📊"} + > + {" "} + Vis Node{" "} + + + + addNode("inspectNode", "inspect")} + icon={"🔍"} + > + {" "} + Inspect Node{" "} + + + + Processors + + + addNode("process", "processor", { + language: "javascript", + code: "function process(response) {\n return response.text;\n}", + }) + } + icon={} + > + {" "} + JavaScript Processor{" "} + + + {IS_RUNNING_LOCALLY ? ( + + + addNode("process", "processor", { + language: "python", + code: "def process(response):\n return response.text;", + }) + } + icon={} + > + {" "} + Python Processor{" "} + + + ) : ( + <> + )} + + addNode("join")} + icon={} + > + {" "} + Join Node{" "} + + + + addNode("split")} + icon={} + > + {" "} + Split Node{" "} + + + + Misc + + addNode("comment")} icon={"✏️"}> + {" "} + Comment Node{" "} + + + {IS_RUNNING_LOCALLY ? ( + + addNode("scriptNode", "script")} + icon={} + > + {" "} + Global Python Scripts{" "} + + + ) : ( + <> + )} + + + ), + [addNode], + ); + if (!IS_ACCEPTED_BROWSER) { return ( @@ -925,228 +1187,13 @@ const App = () => { */} -
-
- { - // Suppress ReactFlow warnings spamming the console. - // console.log(err); - }} - > - - - -
-
+ {reactFlowUI}
- - - - - - Input Data - - } - > - {" "} - TextFields Node{" "} - - - - } - > - {" "} - Items Node{" "} - - - - - {" "} - Tabular Data Node{" "} - - - - Prompters - - - {" "} - Prompt Node{" "} - - - - - {" "} - Chat Turn Node{" "} - - - - Evaluators - - } - > - {" "} - Simple Evaluator{" "} - - - - addEvalNode("javascript")} - icon={} - > - {" "} - JavaScript Evaluator{" "} - - - - addEvalNode("python")} - icon={} - > - {" "} - Python Evaluator{" "} - - - - } - > - {" "} - LLM Scorer{" "} - - - - } - > - {" "} - Multi-Evaluator{" "} - - - - Visualizers - - - {" "} - Vis Node{" "} - - - - - {" "} - Inspect Node{" "} - - - - Processors - - addProcessorNode("javascript")} - icon={} - > - {" "} - JavaScript Processor{" "} - - - {IS_RUNNING_LOCALLY ? ( - - addProcessorNode("python")} - icon={} - > - {" "} - Python Processor{" "} - - - ) : ( - <> - )} - - } - > - {" "} - Join Node{" "} - - - - } - > - {" "} - Split Node{" "} - - - - Misc - - - {" "} - Comment Node{" "} - - - {IS_RUNNING_LOCALLY ? ( - - } - > - {" "} - Global Python Scripts{" "} - - - ) : ( - <> - )} - - + {addNodeMenu}
diff --git a/chainforge/react-server/src/LLMResponseInspectorModal.tsx b/chainforge/react-server/src/LLMResponseInspectorModal.tsx index 754efa6..bfc3b3d 100644 --- a/chainforge/react-server/src/LLMResponseInspectorModal.tsx +++ b/chainforge/react-server/src/LLMResponseInspectorModal.tsx @@ -80,6 +80,7 @@ const LLMResponseInspectorModal = forwardRef< }> diff --git a/chainforge/react-server/src/PromptNode.tsx b/chainforge/react-server/src/PromptNode.tsx index c7d28de..35871c4 100644 --- a/chainforge/react-server/src/PromptNode.tsx +++ b/chainforge/react-server/src/PromptNode.tsx @@ -432,7 +432,7 @@ const PromptNode: React.FC = ({ // Debounce refreshing the template hooks so we don't annoy the user // debounce((_value) => refreshTemplateHooks(_value), 500)(value); }, - [promptTextOnLastRun, status, refreshTemplateHooks], + [promptTextOnLastRun, status, refreshTemplateHooks, debounceTimeoutRef], ); // On initialization @@ -543,33 +543,43 @@ const PromptNode: React.FC = ({ }, [id, pullInputData]); // Ask the backend how many responses it needs to collect, given the input data: - const fetchResponseCounts = ( - prompt: string, - vars: Dict, - llms: (StringOrHash | LLMSpec)[], - chat_histories?: - | (ChatHistoryInfo | undefined)[] - | Dict<(ChatHistoryInfo | undefined)[]>, - ) => { - return countQueries( - prompt, - vars, - llms, + const fetchResponseCounts = useCallback( + ( + prompt: string, + vars: Dict, + llms: (StringOrHash | LLMSpec)[], + chat_histories?: + | (ChatHistoryInfo | undefined)[] + | Dict<(ChatHistoryInfo | undefined)[]>, + ) => { + return countQueries( + prompt, + vars, + llms, + numGenerations, + chat_histories, + id, + node_type !== "chat" ? showContToggle && contWithPriorLLMs : undefined, + ).then(function (results) { + return [results.counts, results.total_num_responses] as [ + Dict>, + Dict, + ]; + }); + }, + [ + countQueries, numGenerations, - chat_histories, + showContToggle, + contWithPriorLLMs, id, - node_type !== "chat" ? showContToggle && contWithPriorLLMs : undefined, - ).then(function (results) { - return [results.counts, results.total_num_responses] as [ - Dict>, - Dict, - ]; - }); - }; + node_type, + ], + ); // On hover over the 'info' button, to preview the prompts that will be sent out const [promptPreviews, setPromptPreviews] = useState([]); - const handlePreviewHover = () => { + const handlePreviewHover = useCallback(() => { // Pull input data and prompt try { const pulled_vars = pullInputData(templateVars, id); @@ -590,10 +600,18 @@ const PromptNode: React.FC = ({ console.error(err); setPromptPreviews([]); } - }; + }, [ + pullInputData, + templateVars, + id, + updateShowContToggle, + generatePrompts, + promptText, + pullInputChats, + ]); // On hover over the 'Run' button, request how many responses are required and update the tooltip. Soft fails. - const handleRunHover = () => { + const handleRunHover = useCallback(() => { // Check if the PromptNode is not already waiting for a response... if (status === "loading") { setRunTooltip("Fetching responses..."); @@ -724,9 +742,17 @@ const PromptNode: React.FC = ({ console.error(err); // soft fail setRunTooltip("Could not reach backend server."); }); - }; + }, [ + status, + llmItemsCurrState, + pullInputChats, + contWithPriorLLMs, + pullInputData, + fetchResponseCounts, + promptText, + ]); - const handleRunClick = () => { + const handleRunClick = useCallback(() => { // Go through all template hooks (if any) and check they're connected: const is_fully_connected = templateVars.every((varname) => { // Check that some edge has, as its target, this node and its template hook: @@ -1063,7 +1089,31 @@ Soft failing by replacing undefined with empty strings.`, .then(open_progress_listener) .then(query_llms) .catch(rejected); - }; + }, [ + templateVars, + triggerAlert, + pullInputChats, + pullInputData, + updateShowContToggle, + llmItemsCurrState, + contWithPriorLLMs, + showAlert, + fetchResponseCounts, + numGenerations, + promptText, + apiKeys, + showContToggle, + cancelId, + refreshCancelId, + node_type, + id, + setDataPropsForNode, + llmListContainer, + responsesWillChange, + showDrawer, + pingOutputNodes, + debounceTimeoutRef, + ]); const handleStopClick = useCallback(() => { CancelTracker.add(cancelId); @@ -1081,7 +1131,7 @@ Soft failing by replacing undefined with empty strings.`, setStatus(Status.NONE); setContChatToggleDisabled(false); llmListContainer?.current?.resetLLMItemsProgress(); - }, [cancelId, refreshCancelId]); + }, [cancelId, refreshCancelId, debounceTimeoutRef]); const handleNumGenChange = useCallback( (event: React.ChangeEvent) => { diff --git a/chainforge/react-server/src/ResponseBoxes.tsx b/chainforge/react-server/src/ResponseBoxes.tsx index c0d01a4..2c23eae 100644 --- a/chainforge/react-server/src/ResponseBoxes.tsx +++ b/chainforge/react-server/src/ResponseBoxes.tsx @@ -36,6 +36,7 @@ export const getEvalResultStr = ( } else if (typeof eval_item === "object") { const strs: [JSX.Element | string, string][] = Object.keys(eval_item).map( (key, j) => { + const innerKey = `${key}-${j}`; let val = eval_item[key]; if (typeof val === "number" && val.toString().indexOf(".") > -1) val = val.toFixed(4); // truncate floats to 4 decimal places @@ -43,9 +44,9 @@ export const getEvalResultStr = ( if (onlyString) return [`${key}: ${recurs_str}`, recurs_str]; else return [ -
- {key}: - {recurs_res} +
+ {key}: + {recurs_res}
, recurs_str, ]; @@ -57,7 +58,9 @@ export const getEvalResultStr = ( } else return [ - {strs} + {strs.map((s, i) => ( + s + ))} , joined_strs, ];