This commit is contained in:
Ian Arawjo 2023-12-06 21:41:03 -05:00
commit 2e0ff1d317
19 changed files with 205 additions and 97 deletions

View File

@ -1,15 +1,15 @@
{
"files": {
"main.css": "/static/css/main.8665fcca.css",
"main.js": "/static/js/main.df1a9b08.js",
"main.css": "/static/css/main.ff59165b.css",
"main.js": "/static/js/main.54b090ad.js",
"static/js/787.4c72bb55.chunk.js": "/static/js/787.4c72bb55.chunk.js",
"index.html": "/index.html",
"main.8665fcca.css.map": "/static/css/main.8665fcca.css.map",
"main.df1a9b08.js.map": "/static/js/main.df1a9b08.js.map",
"main.ff59165b.css.map": "/static/css/main.ff59165b.css.map",
"main.54b090ad.js.map": "/static/js/main.54b090ad.js.map",
"787.4c72bb55.chunk.js.map": "/static/js/787.4c72bb55.chunk.js.map"
},
"entrypoints": [
"static/css/main.8665fcca.css",
"static/js/main.df1a9b08.js"
"static/css/main.ff59165b.css",
"static/js/main.54b090ad.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script async src="https://www.googletagmanager.com/gtag/js?id=G-RN3FDBLMCR"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-RN3FDBLMCR")</script><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A visual programming environment for prompt engineering"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>ChainForge</title><script defer="defer" src="/static/js/main.df1a9b08.js"></script><link href="/static/css/main.8665fcca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script async src="https://www.googletagmanager.com/gtag/js?id=G-RN3FDBLMCR"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-RN3FDBLMCR")</script><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A visual programming environment for prompt engineering"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>ChainForge</title><script defer="defer" src="/static/js/main.54b090ad.js"></script><link href="/static/css/main.ff59165b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ import fetch_from_backend from './fetch_from_backend';
import { APP_IS_RUNNING_LOCALLY, stripLLMDetailsFromResponses, toStandardResponseFormat } from './backend/utils';
import InspectFooter from './InspectFooter';
import { escapeBraces } from './backend/template';
import LLMResponseInspectorDrawer from './LLMResponseInspectorDrawer';
// Whether we are running on localhost or not, and hence whether
// we have access to the Flask backend for, e.g., Python code evaluation.
@ -124,6 +125,7 @@ const CodeEvaluatorNode = ({ data, id, type: node_type }) => {
const pullInputData = useStore((state) => state.pullInputData);
const pingOutputNodes = useStore((state) => state.pingOutputNodes);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const bringNodeToFront = useStore((state) => state.bringNodeToFront);
const [status, setStatus] = useState('none');
const nodes = useStore((state) => state.nodes);
@ -136,6 +138,7 @@ const CodeEvaluatorNode = ({ data, id, type: node_type }) => {
// For a way to inspect responses without having to attach a dedicated node
const inspectModal = useRef(null);
const [uninspectedResponses, setUninspectedResponses] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
// The programming language for the editor. Also determines what 'execute'
// function will ultimately be called.
@ -295,7 +298,7 @@ const CodeEvaluatorNode = ({ data, id, type: node_type }) => {
})).flat()
});
if (status !== 'ready')
if (status !== 'ready' && !showDrawer)
setUninspectedResponses(true);
setStatus('ready');
@ -446,8 +449,18 @@ const CodeEvaluatorNode = ({ data, id, type: node_type }) => {
{ lastRunSuccess && lastResponses && lastResponses.length > 0 ?
(<InspectFooter label={<>Inspect results&nbsp;<IconSearch size='12pt'/></>}
onClick={showResponseInspector}
showNotificationDot={uninspectedResponses} />
) : <></>}
showNotificationDot={uninspectedResponses}
isDrawerOpen={showDrawer}
showDrawerButton={true}
onDrawerClick={() => {
setShowDrawer(!showDrawer);
setUninspectedResponses(false);
bringNodeToFront(id);
}}
/>) : <></>}
<LLMResponseInspectorDrawer jsonResponses={lastResponses} showDrawer={showDrawer} />
</BaseNode>
);
};

View File

@ -1,20 +1,35 @@
import { useState } from "react";
import { Button } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useMemo, useState } from "react";
import { Button, Tooltip } from "@mantine/core";
import { IconSearch, IconSquareArrowLeft, IconSquareArrowRight } from "@tabler/icons-react";
/**
* The footer at the bottom of a node, allowing a user to click it
* to inspect responses.
*/
const InspectFooter = ({ label, onClick, showNotificationDot }) => {
const InspectFooter = ({ label, onClick, showNotificationDot, showDrawerButton, onDrawerClick, isDrawerOpen }) => {
const [text, setText] = useState(label || (<>Inspect responses&nbsp;<IconSearch size='12pt' /></>));
const inspectBtnWidth = useMemo(() => (showDrawerButton ? '84%' : '100%'), [showDrawerButton]);
const drawerBtn = useMemo(() => {
if (showDrawerButton) return (
<Tooltip label={`${isDrawerOpen ? 'Close' : 'Open'} inspector drawer`} position='bottom' withArrow>
<Button color='blue' variant='subtle' w='16%' p='0px' onClick={onDrawerClick} style={{borderRadius: '0px', borderLeft: '1px solid #bdf', cursor: 'pointer'}}>
{isDrawerOpen ?
<IconSquareArrowLeft size='12pt' style={{flexShrink: '0'}} />
: <IconSquareArrowRight size='12pt' style={{flexShrink: '0'}} />}
</Button>
</Tooltip>);
else return undefined;
}, [showDrawerButton, onDrawerClick, isDrawerOpen]);
return (
<div className="eval-inspect-response-footer nodrag" onClick={onClick} style={{display: 'flex', justifyContent:'center'}}>
<Button color='blue' variant='subtle' w='100%' >
{text}
{ showNotificationDot ? <div className="something-changed-circle"></div> : <></>}
</Button>
<div className="eval-inspect-response-footer nodrag" style={{display: 'flex', justifyContent:'center'}}>
<Tooltip label="Open fullscreen inspector" position='bottom' withArrow>
<Button color='blue' variant='subtle' w={inspectBtnWidth} onClick={onClick} >
{text}
{ showNotificationDot ? <div className="something-changed-circle"></div> : <></>}
</Button>
</Tooltip>
{drawerBtn}
</div>
);
};

View File

@ -63,7 +63,7 @@ const InspectorNode = ({ data, id }) => {
customButtons={[
<button className="custom-button" key="export-data" onClick={() => exportToExcel(jsonResponses)}>Export data</button>
]} />
<div className='inspect-response-container nowheel nodrag'>
<div className='inspect-response-container nowheel nodrag' style={{marginTop: '-8pt'}}>
<LLMResponseInspector jsonResponses={jsonResponses} />
</div>
<Handle

View File

@ -12,6 +12,7 @@ import { LLMListContainer } from './LLMListComponent';
import LLMResponseInspectorModal from './LLMResponseInspectorModal';
import InspectFooter from './InspectFooter';
import { initLLMProviders } from './store';
import LLMResponseInspectorDrawer from './LLMResponseInspectorDrawer';
// The default prompt shown in gray highlights to give people a good example of an evaluation prompt.
const PLACEHOLDER_PROMPT = "Respond with 'true' if the text below has a positive sentiment, and 'false' if not. Do not reply with anything else.";
@ -32,10 +33,12 @@ const LLMEvaluatorNode = ({ data, id }) => {
const inspectModal = useRef(null);
const [uninspectedResponses, setUninspectedResponses] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const inputEdgesForNode = useStore((state) => state.inputEdgesForNode);
const pingOutputNodes = useStore((state) => state.pingOutputNodes);
const bringNodeToFront = useStore((state) => state.bringNodeToFront);
const apiKeys = useStore((state) => state.apiKeys);
const [lastResponses, setLastResponses] = useState([]);
@ -104,12 +107,13 @@ const LLMEvaluatorNode = ({ data, id }) => {
console.log(json.responses);
setLastResponses(json.responses);
setUninspectedResponses(true);
if (!showDrawer)
setUninspectedResponses(true);
setStatus('ready');
setProgress(undefined);
}).catch(handleError);
});
}, [inputEdgesForNode, promptText, llmScorers, apiKeys, pingOutputNodes, setStatus, alertModal]);
}, [inputEdgesForNode, promptText, llmScorers, apiKeys, pingOutputNodes, setStatus, showDrawer, alertModal]);
const handlePromptChange = useCallback((event) => {
// Store prompt text
@ -200,7 +204,17 @@ const LLMEvaluatorNode = ({ data, id }) => {
(<InspectFooter label={<>Inspect scores&nbsp;<IconSearch size='12pt'/></>}
onClick={showResponseInspector}
showNotificationDot={uninspectedResponses}
isDrawerOpen={showDrawer}
showDrawerButton={true}
onDrawerClick={() => {
setShowDrawer(!showDrawer);
setUninspectedResponses(false);
bringNodeToFront(id);
}}
/>) : <></>}
<LLMResponseInspectorDrawer jsonResponses={lastResponses} showDrawer={showDrawer} />
</BaseNode>
);
};

View File

@ -4,8 +4,8 @@
* Separated from ReactFlow node UI so that it can
* be deployed in multiple locations.
*/
import React, { useState, useEffect, useRef } from 'react';
import { Collapse, Radio, MultiSelect, Group, Table, NativeSelect, Checkbox, Flex } from '@mantine/core';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Collapse, Radio, MultiSelect, Group, Table, NativeSelect, Checkbox, Flex, Tabs } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconTable, IconLayoutList } from '@tabler/icons-react';
import * as XLSX from 'xlsx';
@ -339,7 +339,7 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
);
});
setResponses([(<Table key='table'>
setResponses([(<Table key='table' fontSize={wideFormat ? 'sm' : 'xs'}>
<thead>
<tr>{colnames.map(c => (<th key={c}>{c}</th>))}</tr>
</thead>
@ -360,14 +360,15 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
if (varnames.length === 0) {
// Base case. Display n response(s) to each single prompt, back-to-back:
let fixed_width = 100;
if (wideFormat && eatenvars.length > 0) {
let side_by_side_resps = wideFormat;
if (side_by_side_resps && eatenvars.length > 0) {
const num_llms = Array.from(new Set(resps.map(getLLMName))).length;
fixed_width = Math.max(20, Math.trunc(100 / num_llms)) - 1; // 20% width is lowest we will go (5 LLM response boxes max)
}
const resp_boxes = generateResponseBoxes(resps, eatenvars, fixed_width);
const className = eatenvars.length > 0 ? "response-group" : "";
const boxesClassName = eatenvars.length > 0 ? "response-boxes-wrapper" : "";
const flexbox = (wideFormat && fixed_width < 100) ? 'flex' : 'block';
const flexbox = (side_by_side_resps && fixed_width < 100) ? 'flex' : 'block';
const defaultOpened = !first_opened || eatenvars.length === 0 || eatenvars[eatenvars.length-1] === 'LLM';
first_opened = true;
leaf_id += 1;
@ -436,62 +437,62 @@ const LLMResponseInspector = ({ jsonResponses, wideFormat }) => {
setMultiSelectValue(new_val);
};
const sz = useMemo(() =>
(wideFormat ? 'sm' : 'xs')
, [wideFormat]);
return (<div style={{height: '100%'}}>
{wideFormat ?
<Radio.Group
name="viewFormat"
value={viewFormat}
onChange={setViewFormat}
>
<Group mt="0px" mb='xs'>
<Radio value="hierarchy" label={<span><IconLayoutList size='10pt' style={{marginBottom: '-1px'}}/> Grouped List</span>} />
<Radio value="table" label={<span><IconTable size='10pt' style={{marginBottom: '-1px'}}/> Table</span>} />
</Group>
</Radio.Group>
: <></>}
{viewFormat === "table" ?
<Flex gap='xl' align='end'>
<NativeSelect
value={tableColVar}
onChange={(event) => {
setTableColVar(event.currentTarget.value);
setUserSelectedTableCol(true);
}}
data={multiSelectVars}
label="Select the main variable to use for columns:"
mb="sm"
w="80%"
/>
<Checkbox checked={onlyShowScores}
label="Only show scores"
onChange={(e) => setOnlyShowScores(e.currentTarget.checked)}
mb='md'
display={showEvalScoreOptions ? 'inherit' : 'none'} />
</Flex>
: <></>}
{wideFormat === false || viewFormat === "hierarchy" ?
<Flex gap='xl' align='end'>
<MultiSelect ref={multiSelectRef}
onChange={handleMultiSelectValueChange}
className='nodrag nowheel inspect-multiselect'
label="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={wideFormat ? '80%' : '100%'} />
<Checkbox checked={onlyShowScores}
label="Only show scores"
onChange={(e) => setOnlyShowScores(e.currentTarget.checked)}
mb='xs'
display={showEvalScoreOptions ? 'inherit' : 'none'} />
</Flex>
: <></>}
<Tabs value={viewFormat} onTabChange={setViewFormat} styles={{tabLabel: {fontSize: wideFormat ? '12pt' : '9pt' }}}>
<Tabs.List>
<Tabs.Tab value="hierarchy"><IconLayoutList size="10pt" style={{marginBottom: wideFormat ? '0px' : '-4px'}}/>{wideFormat ? " Grouped List" : ""}</Tabs.Tab>
<Tabs.Tab value="table"><IconTable size="10pt" style={{marginBottom: wideFormat ? '0px' : '-4px'}}/>{wideFormat ? " Table View" : ""}</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="hierarchy" pt="xs">
<Flex gap='xl' align='end'>
<MultiSelect ref={multiSelectRef}
onChange={handleMultiSelectValueChange}
className='nodrag nowheel inspect-multiselect'
label="Group responses by (order matters):"
data={multiSelectVars}
placeholder="Pick vars to group responses, in order of importance"
size={sz}
value={multiSelectValue}
clearSearchOnChange={true}
clearSearchOnBlur={true}
w={wideFormat ? '80%' : '100%'} />
<Checkbox checked={onlyShowScores}
label="Only show scores"
onChange={(e) => setOnlyShowScores(e.currentTarget.checked)}
mb='xs'
size={sz}
display={showEvalScoreOptions ? 'inherit' : 'none'} />
</Flex>
</Tabs.Panel>
<Tabs.Panel value="table" pt="xs">
<Flex gap='xl' align='end'>
<NativeSelect
value={tableColVar}
onChange={(event) => {
setTableColVar(event.currentTarget.value);
setUserSelectedTableCol(true);
}}
data={multiSelectVars}
label="Select the main variable to use for columns:"
mb="sm"
size={sz}
w="80%"
/>
<Checkbox checked={onlyShowScores}
label="Only show scores"
onChange={(e) => setOnlyShowScores(e.currentTarget.checked)}
mb='md'
size={sz}
display={showEvalScoreOptions ? 'inherit' : 'none'} />
</Flex>
</Tabs.Panel>
</Tabs>
<div className="nowheel nodrag">
{responses}

View File

@ -0,0 +1,13 @@
import LLMResponseInspector from "./LLMResponseInspector";
const LLMResponseInspectorDrawer = ({jsonResponses, showDrawer}) => {
return (
<div className='inspect-responses-drawer' style={{display: showDrawer ? 'initial' : 'none'}}>
<div className='inspect-response-container nowheel nodrag' style={{margin: '0px 10px 10px 12px'}}>
<LLMResponseInspector jsonResponses={jsonResponses} />
</div>
</div>
);
};
export default LLMResponseInspectorDrawer;

View File

@ -14,6 +14,8 @@ import { escapeBraces } from './backend/template';
import ChatHistoryView from './ChatHistoryView';
import InspectFooter from './InspectFooter';
import { countNumLLMs, setsAreEqual, getLLMsInPulledInputData } from './backend/utils';
import LLMResponseInspector from './LLMResponseInspector';
import LLMResponseInspectorDrawer from './LLMResponseInspectorDrawer';
const getUniqueLLMMetavarKey = (responses) => {
const metakeys = new Set(responses.map(resp_obj => Object.keys(resp_obj.metavars)).flat());
@ -84,6 +86,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
const getImmediateInputNodeTypes = useStore((state) => state.getImmediateInputNodeTypes);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const pingOutputNodes = useStore((state) => state.pingOutputNodes);
const bringNodeToFront = useStore((state) => state.bringNodeToFront);
// API Keys (set by user in popup GlobalSettingsModal)
const apiKeys = useStore((state) => state.apiKeys);
@ -107,6 +110,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
const inspectModal = useRef(null);
const [uninspectedResponses, setUninspectedResponses] = useState(false);
const [responsesWillChange, setResponsesWillChange] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
// For continuing with prior LLMs toggle
const [contWithPriorLLMs, setContWithPriorLLMs] = useState(data.contChat !== undefined ? data.contChat : (node_type === 'chat' ? true : false));
@ -577,7 +581,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
return;
}
if (responsesWillChange)
if (responsesWillChange && !showDrawer)
setUninspectedResponses(true);
setResponsesWillChange(false);
@ -775,10 +779,21 @@ const PromptNode = ({ data, id, type: node_type }) => {
: <></>}
{ jsonResponses && jsonResponses.length > 0 && status !== 'loading' ?
(<InspectFooter onClick={showResponseInspector} showNotificationDot={uninspectedResponses} />
(<InspectFooter onClick={showResponseInspector}
showNotificationDot={uninspectedResponses}
isDrawerOpen={showDrawer}
showDrawerButton={true}
onDrawerClick={() => {
setShowDrawer(!showDrawer);
setUninspectedResponses(false);
bringNodeToFront(id);
}} />
) : <></>
}
</div>
<LLMResponseInspectorDrawer jsonResponses={jsonResponses} showDrawer={showDrawer} />
</BaseNode>
);
};

View File

@ -9,6 +9,7 @@ import LLMResponseInspectorModal from "./LLMResponseInspectorModal";
import useStore from "./store";
import fetch_from_backend from "./fetch_from_backend";
import { stripLLMDetailsFromResponses, toStandardResponseFormat } from "./backend/utils";
import LLMResponseInspectorDrawer from "./LLMResponseInspectorDrawer";
const createJSEvalCodeFor = (responseFormat, operation, value, valueType) => {
let responseObj = 'r.text'
@ -55,6 +56,7 @@ const SimpleEvalNode = ({data, id}) => {
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const pullInputData = useStore((state) => state.pullInputData);
const pingOutputNodes = useStore((state) => state.pingOutputNodes);
const bringNodeToFront = useStore((state) => state.bringNodeToFront);
const [pastInputs, setPastInputs] = useState([]);
const [status, setStatus] = useState('none');
@ -64,6 +66,7 @@ const SimpleEvalNode = ({data, id}) => {
const [uninspectedResponses, setUninspectedResponses] = useState(false);
const [lastResponses, setLastResponses] = useState([]);
const [lastRunSuccess, setLastRunSuccess] = useState(true);
const [showDrawer, setShowDrawer] = useState(false);
const [responseFormat, setResponseFormat] = useState(data.responseFormat || "response");
const [operation, setOperation] = useState(data.operation || "contains");
@ -149,12 +152,12 @@ const SimpleEvalNode = ({data, id}) => {
setLastResponses(stripLLMDetailsFromResponses(json.responses));
setLastRunSuccess(true);
if (status !== 'ready')
if (status !== 'ready' && !showDrawer)
setUninspectedResponses(true);
setStatus('ready');
}).catch((err) => rejected(err.message));
}, [handlePullInputs, pingOutputNodes, setStatus, alertModal, status, varValue, varValueType, responseFormat, textValue, valueFieldDisabled]);
}, [handlePullInputs, pingOutputNodes, setStatus, alertModal, status, varValue, varValueType, responseFormat, textValue, showDrawer, valueFieldDisabled]);
const showResponseInspector = useCallback(() => {
if (inspectModal && inspectModal.current && lastResponses) {
@ -298,7 +301,17 @@ const SimpleEvalNode = ({data, id}) => {
(<InspectFooter label={<>Inspect scores&nbsp;<IconSearch size='12pt'/></>}
onClick={showResponseInspector}
showNotificationDot={uninspectedResponses}
isDrawerOpen={showDrawer}
showDrawerButton={true}
onDrawerClick={() => {
setShowDrawer(!showDrawer);
setUninspectedResponses(false);
bringNodeToFront(id);
}}
/>) : <></>}
<LLMResponseInspectorDrawer jsonResponses={lastResponses} showDrawer={showDrawer} />
</BaseNode>
);
};

View File

@ -309,6 +309,14 @@ const useStore = create((set, get) => ({
})
});
},
bringNodeToFront: (id) => {
set({
nodes: get().nodes.map((n) => {
n.selected = n.id === id;
return n;
})
});
},
duplicateNode: (id, offset) => {
const nodes = get().nodes;
const node = nodes.find(n => n.id === id);

View File

@ -289,10 +289,10 @@
overflow-y: scroll;
min-width: 150px;
width: 280px;
min-height: 200px;
min-height: 270px;
height: 200px;
max-width: 650px;
max-height: 650px;
max-width: 1150px;
max-height: 750px;
resize: both;
}
.inspect-modal-response-container .response-var-header {
@ -341,6 +341,19 @@
padding-bottom: 20px;
min-width: 160px;
border-right: 1px solid #eee;
padding-left: 8px !important;
padding-right: 0px !important;
}
.inspect-responses-drawer {
position: absolute;
left: 100%;
top: 12px;
background-color: white;
border: 1px solid #999;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 2px;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.1) inset;
}
.response-group-component-header:hover {
color: #05e;
@ -433,9 +446,11 @@
margin: 4px 3px;
background-color: rgba(255, 255, 255, 0.4);
white-space: pre-wrap;
user-select: text;
cursor: text;
}
.small-response-metrics {
font-size: 8pt;
font-size: 10pt;
font-family: -apple-system, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-weight: 500;
text-align: center;
@ -501,6 +516,7 @@
padding: 2px 0px 1px 0px;
margin: 0px 2px 4px 2px;
border-radius: 5px;
min-width: 120px;
/* max-width: 30%; */
}

View File

@ -6,7 +6,7 @@ def readme():
setup(
name='chainforge',
version='0.2.7.7',
version='0.2.7.8',
packages=find_packages(),
author="Ian Arawjo",
description="A Visual Programming Environment for Prompt Engineering",