From f094fc937b7badfb5fb004ba7103c143b02249a9 Mon Sep 17 00:00:00 2001 From: ianarawjo Date: Fri, 7 Jul 2023 15:30:56 -0400 Subject: [PATCH] Collapseable response supergroups; nicer colors for headers of response groups; resizeable Prompt Node text areas (#89) * Resizeable textfield in prompt nodes * Popup when user clicks X to delete node * Collapseable supergroups in response inspector * Nicer hierarchical color scheme for response group headers in response inspectors --- chainforge/react-server/package-lock.json | 54 ++++++--------- .../react-server/src/AreYouSureModal.js | 2 +- .../react-server/src/LLMResponseInspector.js | 50 ++++++++------ .../react-server/src/NodeLabelComponent.js | 25 +++++-- chainforge/react-server/src/PromptNode.js | 69 ++++++++++++------- .../react-server/src/text-fields-node.css | 9 +++ 6 files changed, 127 insertions(+), 82 deletions(-) diff --git a/chainforge/react-server/package-lock.json b/chainforge/react-server/package-lock.json index 8e2498a..3a2c503 100644 --- a/chainforge/react-server/package-lock.json +++ b/chainforge/react-server/package-lock.json @@ -4961,20 +4961,20 @@ } }, "node_modules/@tabler/icons": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.17.0.tgz", - "integrity": "sha512-UeJaylOGNRhQKyDlgZfrQ3UPSGlfVQuXcmCsTYeXioKKepibW6VZ3H36Lo1jvBTBkQD2e9m+k2NxwkztOTXwrA==", + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.24.0.tgz", + "integrity": "sha512-Otv6zrVF3HU54G6FK7OPODcQmKR9KgM6Ppi+ib3gHHB1LZEs2HIdQJYTHP5dGE+yOQWtXS9ZnGmSZDkSFLbkkg==", "funding": { "type": "github", "url": "https://github.com/sponsors/codecalm" } }, "node_modules/@tabler/icons-react": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-2.17.0.tgz", - "integrity": "sha512-kuEW+qNwRqcK5iMl7qTapzX2NiMOwPg4Az01d+IZ1DIMwaZ7iKPJaIor2ihKFLPYrT9D5BZHXB8R5mSkw0FETg==", + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-2.24.0.tgz", + "integrity": "sha512-0pNc+ffp4HZCsozv9aN/hSDiC/RTGozTmf0MCL4U9NIo8yMQh8q3zEfXRNr18IM2InyIBJL95/1J2kzgU2lYeA==", "dependencies": { - "@tabler/icons": "2.17.0", + "@tabler/icons": "2.24.0", "prop-types": "^15.7.2" }, "funding": { @@ -5001,11 +5001,11 @@ } }, "node_modules/@tanstack/react-table": { - "version": "8.9.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.1.tgz", - "integrity": "sha512-yHs2m6lk5J5RNcu2dNtsDGux66wNXZjEgzxos6MRCX8gL+nqxeW3ZglqP6eANN0bGElPnjvqiUHGQvdACOr3Cw==", + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.3.tgz", + "integrity": "sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==", "dependencies": { - "@tanstack/table-core": "8.9.1" + "@tanstack/table-core": "8.9.3" }, "engines": { "node": ">=12" @@ -5035,9 +5035,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.9.1", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.1.tgz", - "integrity": "sha512-2+R83n8vMZND0q3W1lSiF7co9nFbeWbjAErFf27xwbeA9E0wtUu5ZDfgj+TZ6JzdAEQAgfxkk/QNFAKiS8E4MA==", + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.3.tgz", + "integrity": "sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==", "engines": { "node": ">=12" }, @@ -12790,15 +12790,6 @@ "he": "bin/he" } }, - "node_modules/highlight-words": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-1.2.2.tgz", - "integrity": "sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ==", - "engines": { - "node": ">= 16", - "npm": ">= 8" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -16854,14 +16845,13 @@ } }, "node_modules/mantine-react-table": { - "version": "1.0.0-beta.8", - "resolved": "https://registry.npmjs.org/mantine-react-table/-/mantine-react-table-1.0.0-beta.8.tgz", - "integrity": "sha512-um2yN96NXSh7IBYCOxCY4d+zbPiJDiD2u1jAVPOiaKuLysvR9tP2fw2CdlFhfyM2BQD8xjJv+IlTyL602yo2Hw==", + "version": "1.0.0-beta.25", + "resolved": "https://registry.npmjs.org/mantine-react-table/-/mantine-react-table-1.0.0-beta.25.tgz", + "integrity": "sha512-Bu7uyVFusvfho41nNIgPL3iEpTlrwz1V7Y7Oc6cq3sKaB/fa+zkrh2+Rui6motYjEWW4jXqnTlrn42SkVAEv1g==", "dependencies": { "@tanstack/match-sorter-utils": "8.8.4", - "@tanstack/react-table": "8.9.1", - "@tanstack/react-virtual": "3.0.0-beta.54", - "highlight-words": "1.2.2" + "@tanstack/react-table": "8.9.3", + "@tanstack/react-virtual": "3.0.0-beta.54" }, "engines": { "node": ">=14" @@ -16875,9 +16865,9 @@ "@mantine/core": ">=6", "@mantine/dates": ">=6", "@mantine/hooks": ">=6", - "@tabler/icons-react": ">=2", - "react": ">=17.0", - "react-dom": ">=17.0" + "@tabler/icons-react": ">=2.23.0", + "react": ">=18.0", + "react-dom": ">=18.0" } }, "node_modules/map-limit": { diff --git a/chainforge/react-server/src/AreYouSureModal.js b/chainforge/react-server/src/AreYouSureModal.js index 1f6f8b5..535454b 100644 --- a/chainforge/react-server/src/AreYouSureModal.js +++ b/chainforge/react-server/src/AreYouSureModal.js @@ -33,7 +33,7 @@ const AreYouSureModal = forwardRef(({title, message, onConfirm}, ref) => { wrap="wrap" > - + ); diff --git a/chainforge/react-server/src/LLMResponseInspector.js b/chainforge/react-server/src/LLMResponseInspector.js index df2b208..4457de0 100644 --- a/chainforge/react-server/src/LLMResponseInspector.js +++ b/chainforge/react-server/src/LLMResponseInspector.js @@ -5,7 +5,7 @@ * be deployed in multiple locations. */ import React, { useState, useEffect, useRef } from 'react'; -import { Collapse, MultiSelect } from '@mantine/core'; +import { Collapse, Flex, MultiSelect, NativeSelect } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import * as XLSX from 'xlsx'; import useStore from './store'; @@ -127,7 +127,6 @@ const ResponseGroup = ({ header, responseBoxes, responseBoxesWrapperClass, displ }; - const LLMResponseInspector = ({ jsonResponses, wideFormat }) => { const [responses, setResponses] = useState([]); @@ -172,13 +171,14 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => { // Functions to associate a color to each LLM in responses const color_for_llm = (llm) => (getColorForLLMAndSetIfNotFound(llm) + '99'); + const header_bg_colors = ['#e0f4fa', '#c0def9', '#a9c0f9', '#a6b2ea']; const response_box_colors = ['#eee', '#fff', '#eee', '#ddd', '#eee', '#ddd', '#eee']; const rgroup_color = (depth) => response_box_colors[depth % response_box_colors.length]; - const getHeaderBadge = (key, val) => { + const getHeaderBadge = (key, val, depth) => { if (val) { const s = truncStr(val.trim(), 1024); - return (
+ return (
{key} = "{s}"
); } else { @@ -281,21 +281,25 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => { : groupResponsesBy(resps, (r => ((group_name in r.vars) ? r.vars[group_name] : null))); const get_header = (group_name === 'LLM') ? ((key, val) => (
{val}
)) - : ((key, val) => getHeaderBadge(key, val)); + : ((key, val) => getHeaderBadge(key, val, eatenvars.length)); // Now produce nested divs corresponding to the groups const remaining_vars = varnames.slice(1); const updated_eatenvars = eatenvars.concat([group_name]); + const defaultOpened = !first_opened || eatenvars.length === 0 || eatenvars[eatenvars.length-1] === 'LLM'; const grouped_resps_divs = Object.keys(grouped_resps).map(g => groupByVars(grouped_resps[g], remaining_vars, updated_eatenvars, get_header(group_name, g))); const leftover_resps_divs = leftover_resps.length > 0 ? groupByVars(leftover_resps, remaining_vars, updated_eatenvars, get_header(group_name, undefined)) : []; - return (<> + leaf_id += 1; + + return (
{header ? (
- {header} -
- {grouped_resps_divs} -
+
) :
{grouped_resps_divs}
} {leftover_resps_divs.length === 0 ? (<>) : ( @@ -303,7 +307,7 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => { {leftover_resps_divs}
)} - ); +
); }; // Produce DIV elements grouped by selected vars @@ -323,16 +327,20 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => { }; return (
- Group responses by (order matters):} - data={multiSelectVars} - placeholder="Pick vars to group responses, in order of importance" - size={wideFormat ? 'sm' : 'xs'} - value={multiSelectValue} - clearSearchOnChange={true} - clearSearchOnBlur={true} /> + {/* */} + {/* */} + Group responses by (order matters):} + data={multiSelectVars} + placeholder="Pick vars to group responses, in order of importance" + size={wideFormat ? 'sm' : 'xs'} + value={multiSelectValue} + clearSearchOnChange={true} + clearSearchOnBlur={true} + w='100%' /> + {/* */}
{responses}
diff --git a/chainforge/react-server/src/NodeLabelComponent.js b/chainforge/react-server/src/NodeLabelComponent.js index d5cc4e8..e740be2 100644 --- a/chainforge/react-server/src/NodeLabelComponent.js +++ b/chainforge/react-server/src/NodeLabelComponent.js @@ -1,9 +1,11 @@ +import { useRef } from 'react'; import useStore from './store'; import { EditText } from 'react-edit-text'; import 'react-edit-text/dist/index.css'; import StatusIndicator from './StatusIndicatorComponent'; import AlertModal from './AlertModal'; -import { useState, useEffect} from 'react'; +import AreYouSureModal from './AreYouSureModal'; +import { useState, useEffect, useCallback} from 'react'; import { Tooltip } from '@mantine/core'; export default function NodeLabel({ title, nodeId, icon, onEdit, onSave, editable, status, alertModal, customButtons, handleRunClick, handleRunHover, runButtonTooltip }) { @@ -12,6 +14,12 @@ export default function NodeLabel({ title, nodeId, icon, onEdit, onSave, editabl const [runButton, setRunButton] = useState('none'); const removeNode = useStore((state) => state.removeNode); + // For 'delete node' confirmation popup + const deleteConfirmModal = useRef(null); + const [deleteConfirmProps, setDeleteConfirmProps] = useState({ + title: 'Delete node', message: 'Are you sure?', onConfirm: () => {} + }); + const handleNodeLabelChange = (evt) => { const { value } = evt; title = value; @@ -48,13 +56,22 @@ export default function NodeLabel({ title, nodeId, icon, onEdit, onSave, editabl } }, [handleRunClick, runButtonTooltip]); - const handleCloseButtonClick = () => { - removeNode(nodeId); - } + const handleCloseButtonClick = useCallback(() => { + setDeleteConfirmProps({ + title: 'Delete node', + message: 'Are you sure you want to delete this node? This action is irreversible.', + onConfirm: () => removeNode(nodeId), + }); + + // Open the 'are you sure' modal: + if (deleteConfirmModal && deleteConfirmModal.current) + deleteConfirmModal.current.trigger(); + }, [deleteConfirmModal]); return (<>
{icon ? (<>{icon} ) : <>} + { else if (json.responses && json.errors) { FINISHED_QUERY = true; + // Store and log responses (if any) + if (json.responses) { + setJSONResponses(json.responses); + + // Log responses for debugging: + console.log(json.responses); + } + // If there was at least one error collecting a response... const llms_w_errors = Object.keys(json.errors); if (llms_w_errors.length > 0) { @@ -509,12 +517,6 @@ const PromptNode = ({ data, id }) => { setDataPropsForNode(node.id, { refresh: true }); } }); - - // Store responses - setJSONResponses(json.responses); - - // Log responses for debugging: - console.log(json.responses); } else { setStatus('error'); triggerAlert(json.error || 'Unknown error when querying LLM'); @@ -544,6 +546,29 @@ const PromptNode = ({ data, id }) => { if (status !== 'none') { setStatus('none'); } }; + // Dynamically update the textareas and position of the template hooks + const textAreaRef = useRef(null); + const [hooksY, setHooksY] = useState(138); + const setRef = useCallback((elem) => { + // To listen for resize events of the textarea, we need to use a ResizeObserver. + // We initialize the ResizeObserver only once, when the 'ref' is first set, and only on the div wrapping textfields. + // NOTE: This won't work on older browsers, but there's no alternative solution. + if (!textAreaRef.current && elem && window.ResizeObserver) { + let past_hooks_y = 138; + const observer = new ResizeObserver(() => { + if (!textAreaRef || !textAreaRef.current) return; + const new_hooks_y = textAreaRef.current.clientHeight + 68; + if (past_hooks_y !== new_hooks_y) { + setHooksY(new_hooks_y); + past_hooks_y = new_hooks_y; + } + }); + + observer.observe(elem); + textAreaRef.current = elem; + } + }, [textAreaRef]); + return (
{ runButtonTooltip={runTooltip} /> -
-