mirror of
https://github.com/ianarawjo/ChainForge.git
synced 2025-03-14 08:16:37 +00:00
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:
parent
9cf9673b27
commit
f094fc937b
54
chainforge/react-server/package-lock.json
generated
54
chainforge/react-server/package-lock.json
generated
@ -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": {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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} = </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>
|
||||
|
@ -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} </>) : <></>}
|
||||
<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' }}
|
||||
|
69
chainforge/react-server/src/PromptNode.js
vendored
69
chainforge/react-server/src/PromptNode.js
vendored
@ -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 <IconSearch size='12pt'/></Button>
|
||||
</div>) : <></>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user