This commit is contained in:
psychedelicious 2023-11-17 18:06:26 +11:00
parent 7b93b5e928
commit 2c979d1b68
12 changed files with 339 additions and 93 deletions

View File

@ -49,6 +49,7 @@
"back": "Back",
"batch": "Batch Manager",
"cancel": "Cancel",
"clickToEdit": "Click to Edit",
"close": "Close",
"on": "On",
"communityLabel": "Community",
@ -853,7 +854,7 @@
"noConnectionData": "No connection data",
"noConnectionInProgress": "No connection in progress",
"node": "Node",
"nodeOutputs": "Node Outputs",
"nodeOutputs": "Node Results",
"nodeSearch": "Search for nodes",
"nodeTemplate": "Node Template",
"nodeType": "Node Type",
@ -863,9 +864,9 @@
"noMatchingNodes": "No matching nodes",
"noNodeSelected": "No node selected",
"nodeOpacity": "Node Opacity",
"noOutputRecorded": "No outputs recorded",
"noOutputRecorded": "No results recorded",
"noOutputSchemaName": "No output schema name found in ref object",
"notes": "Notes",
"notes": "Node Notes",
"notesDescription": "Add notes about your workflow",
"oNNXModelField": "ONNX Model",
"oNNXModelFieldDescription": "ONNX model field.",
@ -943,7 +944,12 @@
"workflowValidation": "Workflow Validation Error",
"workflowVersion": "Version",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out"
"zoomOutNodes": "Zoom Out",
"tabDetails": "Details",
"tabNotes": "Notes",
"tabResults": "Results",
"tabData": "Data",
"tabTemplate": "Template"
},
"parameters": {
"aspectRatio": "Aspect Ratio",

View File

@ -1,15 +1,15 @@
import { FormControl, FormLabel } from '@chakra-ui/react';
import { FormControl, FormLabel, Flex } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNodeNotes } from 'features/nodes/hooks/useNodeNotes';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNodeData } from 'features/nodes/types/types';
import { isNil } from 'lodash-es';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch();
const data = useNodeData(nodeId);
const notes = useNodeNotes(nodeId);
const { t } = useTranslation();
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
@ -17,16 +17,17 @@ const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
},
[dispatch, nodeId]
);
if (!isInvocationNodeData(data)) {
if (isNil(notes)) {
return null;
}
return (
<FormControl>
<FormControl as={Flex} sx={{ flexDir: 'column', h: 'full' }}>
<FormLabel>{t('nodes.notes')}</FormLabel>
<IAITextarea
value={data?.notes}
value={notes}
onChange={handleNotesChanged}
rows={10}
resize="none"
h="full"
/>
</FormControl>
);

View File

@ -28,6 +28,10 @@ const NodeTitle = ({ nodeId, title }: Props) => {
const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback(
async (newTitle: string) => {
if (!newTitle.trim()) {
setLocalTitle(label || templateTitle || t('nodes.problemSettingTitle'));
return;
}
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')

View File

@ -22,9 +22,8 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa';
import { Node } from 'reactflow';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
import ScrollableContent from '../ScrollableContent';
import EditableNodeTitle from './details/EditableNodeTitle';
import InputFields from './details/InputFields';
const selector = createSelector(
stateSelector,
@ -82,42 +81,23 @@ const Content = (props: {
sx={{
flexDir: 'column',
position: 'relative',
p: 1,
gap: 2,
w: 'full',
}}
>
<EditableNodeTitle nodeId={props.node.data.id} />
<HStack>
<FormControl>
<FormLabel>Node Type</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.template.title}
</Text>
</FormControl>
<Flex
flexDir="row"
alignItems="center"
justifyContent="space-between"
w="full"
>
<FormControl isInvalid={needsUpdate}>
<FormLabel>Node Version</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.node.data.version}
</Text>
</FormControl>
{needsUpdate && (
<IAIIconButton
aria-label={t('nodes.updateNode')}
tooltip={t('nodes.updateNode')}
icon={<FaSync />}
onClick={updateNode}
/>
)}
</Flex>
</HStack>
<NotesTextarea nodeId={props.node.data.id} />
<FormControl>
<FormLabel>Type</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.template.title} ({props.template.type})
</Text>
</FormControl>
<FormControl>
<FormLabel>Description</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.template.description}
</Text>
</FormControl>
<InputFields nodeId={props.node.id} />
</Flex>
</ScrollableContent>
</Box>

View File

@ -0,0 +1,43 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { isInvocationNode } from 'features/nodes/types/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
const selector = createSelector(
stateSelector,
({ nodes }) => {
const lastSelectedNodeId =
nodes.selectedNodes[nodes.selectedNodes.length - 1];
const lastSelectedNode = nodes.nodes.find(
(node) => node.id === lastSelectedNodeId
);
if (!isInvocationNode(lastSelectedNode)) {
return;
}
return lastSelectedNode.id;
},
defaultSelectorOptions
);
const InspectorNotesTab = () => {
const nodeId = useAppSelector(selector);
const { t } = useTranslation();
if (!nodeId) {
return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
}
return <NotesTextarea nodeId={nodeId} />;
};
export default memo(InspectorNotesTab);

View File

@ -6,13 +6,41 @@ import {
TabPanels,
Tabs,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { isInvocationNode } from 'features/nodes/types/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import InspectorDataTab from './InspectorDataTab';
import InspectorOutputsTab from './InspectorOutputsTab';
import InspectorTemplateTab from './InspectorTemplateTab';
import InspectorDetailsTab from './InspectorDetailsTab';
import InspectorNotesTab from './InspectorNotesTab';
import InspectorResultsTab from './InspectorResultsTab';
import InspectorTemplateTab from './InspectorTemplateTab';
import EditableNodeTitle from './details/EditableNodeTitle';
const selector = createSelector(
stateSelector,
({ nodes }) => {
const lastSelectedNodeId =
nodes.selectedNodes[nodes.selectedNodes.length - 1];
const lastSelectedNode = nodes.nodes.find(
(node) => node.id === lastSelectedNodeId
);
if (!isInvocationNode(lastSelectedNode)) {
return;
}
return lastSelectedNode.id;
},
defaultSelectorOptions
);
const InspectorPanel = () => {
const { t } = useTranslation();
const nodeId = useAppSelector(selector);
return (
<Flex
layerStyle="first"
@ -25,15 +53,17 @@ const InspectorPanel = () => {
gap: 2,
}}
>
<EditableNodeTitle nodeId={nodeId} />
<Tabs
variant="line"
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
>
<TabList>
<Tab>Details</Tab>
<Tab>Outputs</Tab>
<Tab>Data</Tab>
<Tab>Template</Tab>
<Tab>{t('nodes.tabDetails')}</Tab>
<Tab>{t('nodes.tabNotes')}</Tab>
<Tab>{t('nodes.tabResults')}</Tab>
<Tab>{t('nodes.tabData')}</Tab>
<Tab>{t('nodes.tabTemplate')}</Tab>
</TabList>
<TabPanels>
@ -41,7 +71,10 @@ const InspectorPanel = () => {
<InspectorDetailsTab />
</TabPanel>
<TabPanel>
<InspectorOutputsTab />
<InspectorNotesTab />
</TabPanel>
<TabPanel>
<InspectorResultsTab />
</TabPanel>
<TabPanel>
<InspectorDataTab />

View File

@ -39,7 +39,7 @@ const selector = createSelector(
defaultSelectorOptions
);
const InspectorOutputsTab = () => {
const InspectorResultsTab = () => {
const { node, template, nes } = useAppSelector(selector);
const { t } = useTranslation();
@ -91,6 +91,6 @@ const InspectorOutputsTab = () => {
);
};
export default memo(InspectorOutputsTab);
export default memo(InspectorResultsTab);
const getKey = (result: AnyResult, i: number) => `${result.type}-${i}`;

View File

@ -3,20 +3,89 @@ import {
EditableInput,
EditablePreview,
Flex,
Text,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa';
type Props = {
nodeId: string;
title?: string;
type EditableNodeTitleProps = {
nodeId?: string;
};
const EditableNodeTitle = ({ nodeId, title }: Props) => {
const EditableNodeTitle = (props: EditableNodeTitleProps) => {
if (!props.nodeId) {
return (
<Text
sx={{
fontWeight: 600,
px: 1,
color: 'base.700',
_dark: { color: 'base.200' },
}}
>
No node selected
</Text>
);
}
return (
<Flex
sx={{
justifyContent: 'space-between',
alignItems: 'center',
px: 1,
color: 'base.700',
_dark: { color: 'base.200' },
}}
>
<EditableTitle nodeId={props.nodeId} />
<Version nodeId={props.nodeId} />
</Flex>
);
};
type VersionProps = {
nodeId: string;
};
const Version = memo(({ nodeId }: VersionProps) => {
const { version, needsUpdate, updateNode } = useNodeVersion(nodeId);
const { t } = useTranslation();
return (
<Flex alignItems="center" gap={1}>
<Text variant={needsUpdate ? 'error' : 'subtext'} fontWeight={600}>
v{version}
</Text>
{needsUpdate && (
<IAIIconButton
size="sm"
aria-label={t('nodes.updateNode')}
tooltip={t('nodes.updateNode')}
icon={<FaSync />}
variant="link"
onClick={updateNode}
/>
)}
</Flex>
);
});
Version.displayName = 'Version';
type EditableTitleProps = {
nodeId: string;
};
const EditableTitle = memo(({ nodeId }: EditableTitleProps) => {
const dispatch = useAppDispatch();
const label = useNodeLabel(nodeId);
const templateTitle = useNodeTemplateTitle(nodeId);
@ -25,12 +94,14 @@ const EditableNodeTitle = ({ nodeId, title }: Props) => {
const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback(
async (newTitle: string) => {
if (!newTitle.trim()) {
setLocalTitle(label || templateTitle || t('nodes.problemSettingTitle'));
return;
}
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')
);
setLocalTitle(label || templateTitle || t('nodes.problemSettingTitle'));
},
[dispatch, nodeId, title, templateTitle, label, t]
[dispatch, nodeId, templateTitle, label, t]
);
const handleChange = useCallback((newTitle: string) => {
@ -39,36 +110,28 @@ const EditableNodeTitle = ({ nodeId, title }: Props) => {
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')
);
}, [label, templateTitle, title, t]);
setLocalTitle(label || templateTitle || t('nodes.problemSettingTitle'));
}, [label, templateTitle, t]);
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
<Editable
as={Flex}
value={localTitle}
onChange={handleChange}
onSubmit={handleSubmit}
w="full"
>
<Editable
as={Flex}
value={localTitle}
onChange={handleChange}
onSubmit={handleSubmit}
w="full"
fontWeight={600}
>
<EditablePreview noOfLines={1} />
<EditableInput
className="nodrag"
_focusVisible={{ boxShadow: 'none' }}
/>
</Editable>
</Flex>
<EditablePreview p={0} fontWeight={600} noOfLines={1} />
<EditableInput
p={0}
className="nodrag"
fontWeight={700}
_focusVisible={{ boxShadow: 'none' }}
/>
</Editable>
);
};
});
EditableTitle.displayName = 'EditableTitle';
export default memo(EditableNodeTitle);

View File

@ -0,0 +1,20 @@
import { FormControl, FormLabel, Text } from '@chakra-ui/react';
import { useNodeInputFields } from 'features/nodes/hooks/useNodeInputFields';
import { memo } from 'react';
type Props = { nodeId: string };
const InputFields = ({ nodeId }: Props) => {
const inputs = useNodeInputFields(nodeId);
return (
<div>
{inputs?.map(({ fieldData, fieldTemplate }) => (
<FormControl key={fieldData.id}>
<FormLabel>{fieldData.label || fieldTemplate.title}</FormLabel>
<Text>{fieldData.type}</Text>
</FormControl>
))}
</div>
);
};
export default memo(InputFields);

View File

@ -0,0 +1,56 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useMemo } from 'react';
import {
InputFieldTemplate,
InputFieldValue,
isInvocationNode,
} from '../types/types';
export const useNodeInputFields = (
nodeId: string
): { fieldData: InputFieldValue; fieldTemplate: InputFieldTemplate }[] => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return [];
}
const template = nodes.nodeTemplates[node.data.type];
if (!template) {
return [];
}
const inputs = Object.values(node.data.inputs).reduce<
{
fieldData: InputFieldValue;
fieldTemplate: InputFieldTemplate;
}[]
>((acc, fieldData) => {
const fieldTemplate = template.inputs[fieldData.name];
if (fieldTemplate) {
acc.push({
fieldData,
fieldTemplate,
});
}
return acc;
}, []);
return inputs;
},
defaultSelectorOptions
),
[nodeId]
);
const inputs = useAppSelector(selector);
return inputs;
};

View File

@ -0,0 +1,28 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useMemo } from 'react';
import { isInvocationNode } from '../types/types';
export const useNodeNotes = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node.data.notes;
},
defaultSelectorOptions
),
[nodeId]
);
const nodeNotes = useAppSelector(selector);
return nodeNotes;
};

View File

@ -1,10 +1,12 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppToaster } from 'app/components/Toaster';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { satisfies } from 'compare-versions';
import { cloneDeep, defaultsDeep } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Node } from 'reactflow';
import { AnyInvocationType } from 'services/events/types';
import { nodeReplaced } from '../store/nodesSlice';
@ -16,8 +18,6 @@ import {
isInvocationNode,
zParsedSemver,
} from '../types/types';
import { useAppToaster } from 'app/components/Toaster';
import { useTranslation } from 'react-i18next';
export const getNeedsUpdate = (
node?: Node<NodeData>,
@ -115,5 +115,17 @@ export const useNodeVersion = (nodeId: string) => {
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
}, [dispatch, node, nodeTemplate, t, toast]);
return { needsUpdate, mayUpdate, updateNode: _updateNode };
const version = useMemo(() => {
if (!isInvocationNode(node)) {
return '';
}
return node.data.version;
}, [node]);
return {
needsUpdate,
mayUpdate,
updateNode: _updateNode,
version,
};
};