Skip to content

Commit 5393434

Browse files
authored
Merge pull request #103 from FlowTestAI/streamline-openai-flow
chore: streamline openai flow and integrate some ux feedbacks
2 parents 0d33cda + b0083fc commit 5393434

15 files changed

Lines changed: 201 additions & 97 deletions

File tree

packages/flowtest-electron/src/ipc/collection.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,20 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
206206
}
207207
});
208208

209+
ipcMain.handle('renderer:create-dotenv', async (event, collectionPath, content) => {
210+
try {
211+
createFile('.env', collectionPath, content || '');
212+
} catch (error) {
213+
return Promise.reject(error);
214+
}
215+
});
216+
209217
ipcMain.handle('renderer:addOrUpdate-dotEnvironment', async (event, collectionPath, variables) => {
210218
try {
211219
const pathname = path.join(collectionPath, '.env');
212220
// variables should be of format `k1=v1\nk2=v2`;
213221

214-
// Append to the .env file or create it if it doesn't exist
215-
fs.appendFile(pathname, variables, (err) => {
216-
if (err) {
217-
console.error('Error writing to .env file:', err);
218-
return Promise.reject(error);
219-
}
220-
console.log('.env file has been updated');
221-
});
222+
updateFile(pathname, variables);
222223
} catch (error) {
223224
return Promise.reject(error);
224225
}

src/components/atoms/Editor.css

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
.cm-editor {
2-
height: 380px;
3-
width: 380px;
2+
height: 100%;
3+
width: 100%;
4+
min-width: 288px;
5+
resize: both;
6+
overflow: auto !important;
47
}
58
.cm-scroller {
69
overflow: auto;

src/components/atoms/Editor.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import 'components/atoms/Editor.css';
1111

1212
export const Editor = ({ ...props }) => {
1313
const editor = useRef();
14-
//const [code, setCode] = useState('{}');
14+
const [view, setView] = useState(null);
15+
16+
if (view) {
17+
if (props.value != view.state.doc.toString()) {
18+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: props.value } });
19+
}
20+
}
1521

1622
const onUpdate = EditorView.updateListener.of((v) => {
1723
if (props.onChange) {
@@ -34,9 +40,11 @@ export const Editor = ({ ...props }) => {
3440
});
3541

3642
const view = new EditorView({ state, parent: editor.current });
43+
setView(view);
3744

3845
return () => {
3946
view.destroy();
47+
setView(null);
4048
};
4149
}, []);
4250

src/components/atoms/Tabs.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,16 @@ import { useTabStore } from 'stores/TabStore';
44
import ConfirmActionModal from 'components/molecules/modals/ConfirmActionModal';
55
import { isEqual } from 'lodash';
66
import { OBJ_TYPES } from 'constants/Common';
7-
import { compare } from './util';
7+
import { isSaveNeeded } from './util';
8+
import { saveHandle } from 'components/molecules/modals/SaveFlowModal';
89

910
const tabUnsavedChanges = (tab) => {
1011
if (tab.type === OBJ_TYPES.flowtest && tab.flowDataDraft) {
11-
const draftNodesSansOutput = tab.flowDataDraft.nodes.map((node) => {
12-
if (node.type === 'outputNode' && node.data.output) {
13-
const { ['output']: _, ...data } = node.data;
14-
return {
15-
...node,
16-
data,
17-
};
18-
}
19-
return node;
20-
});
21-
if (!isEqual(tab.flowData.nodes, draftNodesSansOutput) || !isEqual(tab.flowData.edges, tab.flowDataDraft.edges)) {
22-
console.log('Detected unsaved changes: ', compare(tab.flowData, tab.flowDataDraft));
23-
return true;
24-
}
12+
return isSaveNeeded(tab.flowData, tab.flowDataDraft);
2513
} else if (tab.type === OBJ_TYPES.environment && tab.variablesDraft && !isEqual(tab.variables, tab.variablesDraft)) {
2614
return true;
2715
} else {
28-
false;
16+
return false;
2917
}
3018
};
3119

@@ -36,7 +24,7 @@ const Tabs = () => {
3624
const focusTab = tabs.find((t) => t.id === focusTabId);
3725
const [confirmActionModalOpen, setConfirmActionModalOpen] = useState(false);
3826
const closeTab = useTabStore((state) => state.closeTab);
39-
const [closingTabId, setClosingTabId] = useState('');
27+
const [closingTab, setClosingTab] = useState('');
4028
const [closingCollectionId, setClosingCollectionId] = useState('');
4129
// ToDo: change color according to theme
4230
const activeTabStyles = 'bg-cyan-900 text-white';
@@ -48,7 +36,7 @@ const Tabs = () => {
4836
event.stopPropagation();
4937
event.preventDefault();
5038

51-
setClosingTabId(tab.id);
39+
setClosingTab(tab);
5240
setClosingCollectionId(tab.collectionId);
5341

5442
if (tabUnsavedChanges(tab)) {
@@ -94,13 +82,20 @@ const Tabs = () => {
9482
);
9583
})}
9684
<ConfirmActionModal
97-
closeFn={() => setConfirmActionModalOpen(false)}
85+
closeFn={() => {
86+
closeTab(closingTab.id, closingCollectionId);
87+
setConfirmActionModalOpen(false);
88+
}}
9889
open={confirmActionModalOpen}
9990
message={messageForConfirmActionModal}
10091
actionFn={() => {
101-
closeTab(closingTabId, closingCollectionId);
92+
saveHandle(closingTab);
93+
closeTab(closingTab.id, closingCollectionId);
10294
setConfirmActionModalOpen(false);
10395
}}
96+
closeModal={() => setConfirmActionModalOpen(false)}
97+
leftButtonMessage={'Close Withuout Saving'}
98+
rightButtonMessage={'Save And Close'}
10499
/>
105100
</div>
106101
);

src/components/atoms/util.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,50 @@ export const compare = (a, b) => {
6363

6464
return result;
6565
};
66+
67+
export const isSaveNeeded = (flowData, flowDataDraft) => {
68+
if (flowData.nodes.length != flowDataDraft.nodes.length) {
69+
console.log('Detected unsaved changes: node count');
70+
return true;
71+
}
72+
73+
if (flowData.edges.length != flowDataDraft.edges.length) {
74+
console.log('Detected unsaved changes: edges count');
75+
return true;
76+
}
77+
78+
const unwrapEdge = function ({ source, sourceHandle, target, targetHandle }) {
79+
return { source, sourceHandle, target, targetHandle };
80+
};
81+
82+
const edges = flowData.edges.map((e) => unwrapEdge(e));
83+
const edgesDraft = flowDataDraft.edges.map((e) => unwrapEdge(e));
84+
85+
if (!isEqual(edges, edgesDraft)) {
86+
console.log('Detected unsaved changes: ', compare(edges, edgesDraft));
87+
return true;
88+
}
89+
90+
const unwrapNode = function ({ id, type, data, position }) {
91+
return { id, type, data, position };
92+
};
93+
94+
const nodes = flowData.nodes.map((e) => unwrapNode(e));
95+
const nodesDraft = flowDataDraft.nodes.map((node) => {
96+
if (node.type === 'outputNode' && node.data.output) {
97+
const { ['output']: _, ...data } = node.data;
98+
return unwrapNode({
99+
...node,
100+
data,
101+
});
102+
}
103+
return unwrapNode(node);
104+
});
105+
106+
if (!isEqual(nodes, nodesDraft)) {
107+
console.log('Detected unsaved changes: ', compare(nodes, nodesDraft));
108+
return true;
109+
}
110+
111+
return false;
112+
};

src/components/molecules/flow/AddNodes.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { PropTypes } from 'prop-types';
33
import { Popover, Transition } from '@headlessui/react';
44
import { PlusIcon, MinusIcon } from '@heroicons/react/20/solid';
55
import { Fragment } from 'react';
66
import { Disclosure } from '@headlessui/react';
7-
import { ChevronUpIcon } from '@heroicons/react/20/solid';
7+
import { ChevronDownIcon } from '@heroicons/react/20/solid';
88
import useCollectionStore from 'stores/CollectionStore';
99
import { orderNodesByTags } from './utils';
1010
import HorizontalDivider from 'components/atoms/common/HorizontalDivider';
@@ -64,9 +64,7 @@ const setVarNode = {
6464
};
6565

6666
const AddNodes = ({ collectionId }) => {
67-
// const [open, setOpen] = useState(false);
68-
// const anchorRef = useRef(null);
69-
// const ps = useRef();
67+
const [searchFilter, setSearchFilter] = useState('');
7068

7169
const onDragStart = (event, node) => {
7270
event.dataTransfer.setData('application/reactflow', JSON.stringify(node));
@@ -75,7 +73,7 @@ const AddNodes = ({ collectionId }) => {
7573

7674
// Get all requests of this collections
7775
const collection = useCollectionStore.getState().collections.find((c) => c.id === collectionId);
78-
const nodesByTags = orderNodesByTags(collection.nodes);
76+
const nodesByTags = orderNodesByTags(collection.nodes, searchFilter);
7977

8078
return (
8179
<div className='absolute bottom-4 right-4 z-[2000]'>
@@ -110,7 +108,7 @@ const AddNodes = ({ collectionId }) => {
110108
<>
111109
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
112110
<span>Requests</span>
113-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5`} />
111+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5`} />
114112
</Disclosure.Button>
115113

116114
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
@@ -137,17 +135,28 @@ const AddNodes = ({ collectionId }) => {
137135
<>
138136
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
139137
<span>{collection.name}</span>
140-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
138+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
141139
</Disclosure.Button>
142140
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
143141
<div>
142+
<div className='flex items-center justify-between text-sm border rounded-md border-neutral-500 text-neutral-500 outline-0 focus:ring-0'>
143+
<input
144+
type='text'
145+
className='nodrag nowheel block h-9 w-full min-w-40 rounded-bl-md rounded-tl-md p-2.5'
146+
name='search-nodes'
147+
placeholder='Search Nodes'
148+
onChange={(e) => setSearchFilter(e.target.value)}
149+
value={searchFilter}
150+
/>
151+
</div>
152+
<HorizontalDivider />
144153
{Object.entries(nodesByTags).map(([tag, nodes], index) => (
145154
<Disclosure as='div' key={index}>
146155
{({ open }) => (
147156
<>
148157
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
149158
<span>{tag}</span>
150-
<ChevronUpIcon
159+
<ChevronDownIcon
151160
className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `}
152161
/>
153162
</Disclosure.Button>
@@ -189,7 +198,7 @@ const AddNodes = ({ collectionId }) => {
189198
<>
190199
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
191200
<span>Output</span>
192-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
201+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
193202
</Disclosure.Button>
194203
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
195204
<div
@@ -212,7 +221,7 @@ const AddNodes = ({ collectionId }) => {
212221
<>
213222
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
214223
<span>Assert</span>
215-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
224+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
216225
</Disclosure.Button>
217226
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
218227
<div
@@ -235,7 +244,7 @@ const AddNodes = ({ collectionId }) => {
235244
<>
236245
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
237246
<span>Delay</span>
238-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
247+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
239248
</Disclosure.Button>
240249
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
241250
<div
@@ -258,7 +267,7 @@ const AddNodes = ({ collectionId }) => {
258267
<>
259268
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
260269
<span>Authentication</span>
261-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
270+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
262271
</Disclosure.Button>
263272
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
264273
<div
@@ -281,7 +290,7 @@ const AddNodes = ({ collectionId }) => {
281290
<>
282291
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
283292
<span>Complex</span>
284-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
293+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
285294
</Disclosure.Button>
286295
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
287296
<div
@@ -304,7 +313,7 @@ const AddNodes = ({ collectionId }) => {
304313
<>
305314
<Disclosure.Button className='flex justify-between w-full px-4 py-2 text-lg font-medium text-left border-t border-b bg-gray-50 hover:bg-gray-100 focus:outline-none focus-visible:ring'>
306315
<span>Set Variable</span>
307-
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
316+
<ChevronDownIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 `} />
308317
</Disclosure.Button>
309318
<Disclosure.Panel className='px-4 pt-4 pb-2 text-sm border-l border-r'>
310319
<div

src/components/molecules/flow/flowtestai.js

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GENAI_MODELS } from 'constants/Common';
2+
import { addOrUpdateDotEnvironmentFile } from 'service/collection';
23
import useCollectionStore from 'stores/CollectionStore';
34

45
const translateGeneratedNodesToOpenApiNodes = (generatedNodes, openApiNodes) => {
@@ -34,27 +35,32 @@ const translateGeneratedNodesToOpenApiNodes = (generatedNodes, openApiNodes) =>
3435
return outputNodes;
3536
};
3637

37-
export const generateFlowData = async (instruction, modelName, collectionId) => {
38+
export const generateFlowData = async (instruction, modelName, modelKey, collectionId) => {
3839
try {
3940
const { ipcRenderer } = window;
4041

4142
const collection = useCollectionStore.getState().collections.find((c) => c.id === collectionId);
4243
if (collection) {
4344
if (modelName === GENAI_MODELS.openai) {
44-
const apiKey = collection.dotEnvVariables['OPENAI_APIKEY'];
45-
if (apiKey) {
46-
const generatedNodes = await ipcRenderer.invoke('renderer:generate-nodes-ai', instruction, collectionId, {
47-
name: GENAI_MODELS.openai,
48-
apiKey,
45+
if (!collection.dotEnvVariables) {
46+
await ipcRenderer.invoke('renderer:create-dotenv', collection.pathname, `OPENAI_APIKEY=${modelKey}`);
47+
} else if (
48+
!Object.prototype.hasOwnProperty.call(collection.dotEnvVariables, 'OPENAI_APIKEY') ||
49+
modelKey != collection.dotEnvVariables['OPENAI_APIKEY']
50+
) {
51+
await addOrUpdateDotEnvironmentFile(collectionId, {
52+
...collection.dotEnvVariables,
53+
OPENAI_APIKEY: modelKey,
4954
});
50-
const flowData = {
51-
nodes: translateGeneratedNodesToOpenApiNodes(generatedNodes, collection.nodes),
52-
};
53-
return flowData;
54-
} else {
55-
// prompt the user to add openai api key
56-
return Promise.reject(new Error(`OpenAI api key not added`));
5755
}
56+
const generatedNodes = await ipcRenderer.invoke('renderer:generate-nodes-ai', instruction, collectionId, {
57+
name: GENAI_MODELS.openai,
58+
apiKey: modelKey,
59+
});
60+
const flowData = {
61+
nodes: translateGeneratedNodesToOpenApiNodes(generatedNodes, collection.nodes),
62+
};
63+
return flowData;
5864
} else {
5965
return Promise.reject(new Error(`model: ${modelName} not supported`));
6066
}

src/components/molecules/flow/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ const Flow = ({ tab, collectionId }) => {
227227
setViewport(data);
228228
}}
229229
isValidConnection={isValidConnection}
230-
fitView
231230
>
232231
<Background variant='dots' gap={12} size={1} />
233232
<Controls

src/components/molecules/flow/nodes/OutputNode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const OutputNode = ({ id, data }) => {
1212
handleRight={true}
1313
handleRightData={{ type: 'source' }}
1414
>
15-
<div className='w-full text-xs text-gray-900 border border-gray-300 rounded-lg nodrag nowheel min-h-96 min-w-96 bg-gray-50 outline-blue-300 focus:border-blue-100 focus:ring-blue-100'>
15+
<div className='w-full text-xs text-gray-900 border border-gray-300 rounded-lg nodrag nowheel min-w-72 bg-gray-50 outline-blue-300 focus:border-blue-100 focus:ring-blue-100'>
1616
{data.output ? (
1717
<Editor name='output-text' value={JSON.stringify(data.output, null, 2)} readOnly={true} />
1818
) : (

0 commit comments

Comments
 (0)