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
This commit is contained in:
ianarawjo 2023-07-07 15:30:56 -04:00 committed by GitHub
parent 9cf9673b27
commit f094fc937b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 82 deletions

View File

@ -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": {

View File

@ -33,7 +33,7 @@ const AreYouSureModal = forwardRef(({title, message, onConfirm}, ref) => {
wrap="wrap"
>
<Button variant='light' color='red' type="submit" w='40%' onClick={close}>Cancel</Button>
<Button variant='filled' color='green' type="submit" w='40%' onClick={confirmAndClose}>Confirm</Button>
<Button variant='filled' color='blue' type="submit" w='40%' onClick={confirmAndClose}>Confirm</Button>
</Flex>
</Modal>
);

View File

@ -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 (<div className="response-var-header">
return (<div className="response-var-header" style={{backgroundColor: header_bg_colors[depth % header_bg_colors.length]}}>
<span className="response-var-name">{key}&nbsp;=&nbsp;</span><span className="response-var-value">"{s}"</span>
</div>);
} 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) => (<div key={val} style={{backgroundColor: color_for_llm(val)}} className='response-llm-header'>{val}</div>))
: ((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 (<div key={'h'+ group_name + '_' + leaf_id}>
{header ?
(<div key={group_name} className="response-group" style={{ backgroundColor: rgroup_color(eatenvars.length) }}>
{header}
<div className="response-boxes-wrapper">
{grouped_resps_divs}
</div>
<ResponseGroup header={header}
responseBoxes={grouped_resps_divs}
responseBoxesWrapperClass="response-boxes-wrapper"
displayStyle="block"
defaultState={defaultOpened} />
</div>)
: <div key={group_name}>{grouped_resps_divs}</div>}
{leftover_resps_divs.length === 0 ? (<></>) : (
@ -303,7 +307,7 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
{leftover_resps_divs}
</div>
)}
</>);
</div>);
};
// Produce DIV elements grouped by selected vars
@ -323,16 +327,20 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
};
return (<div style={{height: '100%'}}>
<MultiSelect ref={multiSelectRef}
onChange={handleMultiSelectValueChange}
className='nodrag nowheel inspect-multiselect'
label={<span style={{marginTop: '0px', fontWeight: 'normal'}}>Group responses by (order matters):</span>}
data={multiSelectVars}
placeholder="Pick vars to group responses, in order of importance"
size={wideFormat ? 'sm' : 'xs'}
value={multiSelectValue}
clearSearchOnChange={true}
clearSearchOnBlur={true} />
{/* <Flex> */}
{/* <NativeSelect label='View as' data={['Hierarchy', 'Table']} mr='8px' w='15%' /> */}
<MultiSelect ref={multiSelectRef}
onChange={handleMultiSelectValueChange}
className='nodrag nowheel inspect-multiselect'
label={<span style={{marginTop: '0px', fontWeight: 'normal'}}>Group responses by (order matters):</span>}
data={multiSelectVars}
placeholder="Pick vars to group responses, in order of importance"
size={wideFormat ? 'sm' : 'xs'}
value={multiSelectValue}
clearSearchOnChange={true}
clearSearchOnBlur={true}
w='100%' />
{/* </Flex> */}
<div className="nowheel nodrag">
{responses}
</div>

View File

@ -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 (<>
<div className="node-header drag-handle">
{icon ? (<>{icon}&nbsp;</>) : <></>}
<AreYouSureModal ref={deleteConfirmModal} title={deleteConfirmProps.title} message={deleteConfirmProps.message} onConfirm={deleteConfirmProps.onConfirm} />
<EditText className="nodrag" name={nodeId ? nodeId + "-label" : "node-label"}
defaultValue={title || 'Node'}
style={{ width: '60%', margin: '0px', padding: '0px', minHeight: '18px' }}

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Handle } from 'react-flow-renderer';
import { Menu, Button, Progress } from '@mantine/core';
import { Menu, Button, Progress, Textarea } from '@mantine/core';
import { v4 as uuid } from 'uuid';
import { IconSearch } from '@tabler/icons-react';
import useStore from './store';
@ -445,6 +445,14 @@ const PromptNode = ({ data, id }) => {
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 (
<div className="prompt-node cfnode">
<NodeLabel title={data.title || 'Prompt Node'}
@ -557,22 +582,18 @@ const PromptNode = ({ data, id }) => {
runButtonTooltip={runTooltip}
/>
<LLMResponseInspectorModal ref={inspectModal} jsonResponses={jsonResponses} prompt={promptText} />
<div className="input-field">
<textarea
rows="4"
cols="40"
defaultValue={data.prompt}
onChange={handleInputChange}
className="nodrag nowheel"
/>
<Handle
type="source"
position="right"
id="prompt"
style={{ top: '50%', background: '#555' }}
/>
</div>
<TemplateHooks vars={templateVars} nodeId={id} startY={138} />
<Textarea ref={setRef}
className="prompt-field-fixed nodrag nowheel"
minRows="4"
defaultValue={data.prompt}
onChange={handleInputChange} />
<Handle
type="source"
position="right"
id="prompt"
style={{ top: '50%', background: '#555' }}
/>
<TemplateHooks vars={templateVars} nodeId={id} startY={hooksY} />
<hr />
<div>
<div style={{marginBottom: '10px', padding: '4px'}}>
@ -609,7 +630,7 @@ const PromptNode = ({ data, id }) => {
]} />)
: <></>}
{ jsonResponses && jsonResponses.length > 0 && status !== 'error' && status !== 'loading' ?
{ jsonResponses && jsonResponses.length > 0 && status !== 'loading' ?
(<div className="eval-inspect-response-footer nodrag" onClick={showResponseInspector} style={{display: 'flex', justifyContent:'center'}}>
<Button color='blue' variant='subtle' w='100%' >Inspect responses&nbsp;<IconSearch size='12pt'/></Button>
</div>) : <></>

View File

@ -588,6 +588,15 @@
line-height: 1.2;
border-color: #999;
}
.prompt-field-fixed .mantine-Textarea-wrapper textarea {
resize: vertical;
overflow-y: auto;
padding: calc(0.5rem / 3);
font-size: 10pt;
font-family: monospace;
line-height: 1.2;
border-color: #999;
}
.add-text-field-btn {
display: flex;