mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
5 Commits
v4.2.4
...
feat/ui/wo
Author | SHA1 | Date | |
---|---|---|---|
2c979d1b68 | |||
7b93b5e928 | |||
dc44debbab | |||
5ce2dc3a58 | |||
27fd9071ba |
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -47,8 +47,8 @@ const FieldHandle = (props: FieldHandleProps) => {
|
||||
isConnectionStartField,
|
||||
connectionError,
|
||||
} = props;
|
||||
const { name, type } = fieldTemplate;
|
||||
const { color: typeColor, title } = FIELDS[type];
|
||||
const { name, type, originalType } = fieldTemplate;
|
||||
const { color: typeColor } = FIELDS[type];
|
||||
|
||||
const styles: CSSProperties = useMemo(() => {
|
||||
const isCollectionType = COLLECTION_TYPES.includes(type);
|
||||
@ -102,13 +102,18 @@ const FieldHandle = (props: FieldHandleProps) => {
|
||||
|
||||
const tooltip = useMemo(() => {
|
||||
if (isConnectionInProgress && isConnectionStartField) {
|
||||
return title;
|
||||
return originalType;
|
||||
}
|
||||
if (isConnectionInProgress && connectionError) {
|
||||
return connectionError ?? title;
|
||||
return connectionError ?? originalType;
|
||||
}
|
||||
return title;
|
||||
}, [connectionError, isConnectionInProgress, isConnectionStartField, title]);
|
||||
return originalType;
|
||||
}, [
|
||||
connectionError,
|
||||
isConnectionInProgress,
|
||||
isConnectionStartField,
|
||||
originalType,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { useFieldData } from 'features/nodes/hooks/useFieldData';
|
||||
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||
import { FIELDS } from 'features/nodes/types/constants';
|
||||
import {
|
||||
isInputFieldTemplate,
|
||||
isInputFieldValue,
|
||||
@ -9,7 +8,6 @@ import {
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
@ -49,7 +47,7 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
)}
|
||||
{fieldTemplate && <Text>Type: {FIELDS[fieldTemplate.type].title}</Text>}
|
||||
{fieldTemplate && <Text>Type: {fieldTemplate.originalType}</Text>}
|
||||
{isInputTemplate && <Text>Input: {startCase(fieldTemplate.input)}</Text>}
|
||||
</Flex>
|
||||
);
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
@ -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);
|
@ -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 />
|
||||
|
@ -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}`;
|
@ -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);
|
||||
|
@ -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);
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -156,6 +156,11 @@ export const FIELDS: Record<FieldType, FieldUIConfig> = {
|
||||
description: 'Any field type is accepted.',
|
||||
title: 'Any',
|
||||
},
|
||||
Unknown: {
|
||||
color: 'gray.500',
|
||||
description: 'Unknown field type is accepted.',
|
||||
title: 'Unknown',
|
||||
},
|
||||
MetadataField: {
|
||||
color: 'gray.500',
|
||||
description: 'A metadata dict.',
|
||||
|
@ -133,6 +133,7 @@ export const zFieldType = z.enum([
|
||||
'UNetField',
|
||||
'VaeField',
|
||||
'VaeModelField',
|
||||
'Unknown',
|
||||
]);
|
||||
|
||||
export type FieldType = z.infer<typeof zFieldType>;
|
||||
@ -190,6 +191,7 @@ export type OutputFieldTemplate = {
|
||||
type: FieldType;
|
||||
title: string;
|
||||
description: string;
|
||||
originalType: string; // used for custom types
|
||||
} & _OutputField;
|
||||
|
||||
export const zInputFieldValueBase = zFieldValueBase.extend({
|
||||
@ -789,6 +791,11 @@ export const zAnyInputFieldValue = zInputFieldValueBase.extend({
|
||||
value: z.any().optional(),
|
||||
});
|
||||
|
||||
export const zUnknownInputFieldValue = zInputFieldValueBase.extend({
|
||||
type: z.literal('Unknown'),
|
||||
value: z.any().optional(),
|
||||
});
|
||||
|
||||
export const zInputFieldValue = z.discriminatedUnion('type', [
|
||||
zAnyInputFieldValue,
|
||||
zBoardInputFieldValue,
|
||||
@ -846,6 +853,7 @@ export const zInputFieldValue = z.discriminatedUnion('type', [
|
||||
zMetadataItemPolymorphicInputFieldValue,
|
||||
zMetadataInputFieldValue,
|
||||
zMetadataCollectionInputFieldValue,
|
||||
zUnknownInputFieldValue,
|
||||
]);
|
||||
|
||||
export type InputFieldValue = z.infer<typeof zInputFieldValue>;
|
||||
@ -856,6 +864,7 @@ export type InputFieldTemplateBase = {
|
||||
description: string;
|
||||
required: boolean;
|
||||
fieldKind: 'input';
|
||||
originalType: string; // used for custom types
|
||||
} & _InputField;
|
||||
|
||||
export type AnyInputFieldTemplate = InputFieldTemplateBase & {
|
||||
@ -863,6 +872,11 @@ export type AnyInputFieldTemplate = InputFieldTemplateBase & {
|
||||
default: undefined;
|
||||
};
|
||||
|
||||
export type UnknownInputFieldTemplate = InputFieldTemplateBase & {
|
||||
type: 'Unknown';
|
||||
default: undefined;
|
||||
};
|
||||
|
||||
export type IntegerInputFieldTemplate = InputFieldTemplateBase & {
|
||||
type: 'integer';
|
||||
default: number;
|
||||
@ -1259,7 +1273,8 @@ export type InputFieldTemplate =
|
||||
| MetadataItemCollectionInputFieldTemplate
|
||||
| MetadataInputFieldTemplate
|
||||
| MetadataItemPolymorphicInputFieldTemplate
|
||||
| MetadataCollectionInputFieldTemplate;
|
||||
| MetadataCollectionInputFieldTemplate
|
||||
| UnknownInputFieldTemplate;
|
||||
|
||||
export const isInputFieldValue = (
|
||||
field?: InputFieldValue | OutputFieldValue
|
||||
|
@ -81,6 +81,7 @@ import {
|
||||
T2IAdapterModelInputFieldTemplate,
|
||||
T2IAdapterPolymorphicInputFieldTemplate,
|
||||
UNetInputFieldTemplate,
|
||||
UnknownInputFieldTemplate,
|
||||
VaeInputFieldTemplate,
|
||||
VaeModelInputFieldTemplate,
|
||||
isArraySchemaObject,
|
||||
@ -981,6 +982,18 @@ const buildSchedulerInputFieldTemplate = ({
|
||||
return template;
|
||||
};
|
||||
|
||||
const buildUnknownInputFieldTemplate = ({
|
||||
baseField,
|
||||
}: BuildInputFieldArg): UnknownInputFieldTemplate => {
|
||||
const template: UnknownInputFieldTemplate = {
|
||||
...baseField,
|
||||
type: 'Unknown',
|
||||
default: undefined,
|
||||
};
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
export const getFieldType = (
|
||||
schemaObject: OpenAPIV3_1SchemaOrRef
|
||||
): string | undefined => {
|
||||
@ -1145,13 +1158,9 @@ const TEMPLATE_BUILDER_MAP: {
|
||||
UNetField: buildUNetInputFieldTemplate,
|
||||
VaeField: buildVaeInputFieldTemplate,
|
||||
VaeModelField: buildVaeModelInputFieldTemplate,
|
||||
Unknown: buildUnknownInputFieldTemplate,
|
||||
};
|
||||
|
||||
const isTemplatedFieldType = (
|
||||
fieldType: string | undefined
|
||||
): fieldType is keyof typeof TEMPLATE_BUILDER_MAP =>
|
||||
Boolean(fieldType && fieldType in TEMPLATE_BUILDER_MAP);
|
||||
|
||||
/**
|
||||
* Builds an input field from an invocation schema property.
|
||||
* @param fieldSchema The schema object
|
||||
@ -1161,7 +1170,8 @@ export const buildInputFieldTemplate = (
|
||||
nodeSchema: InvocationSchemaObject,
|
||||
fieldSchema: InvocationFieldSchema,
|
||||
name: string,
|
||||
fieldType: FieldType
|
||||
fieldType: FieldType,
|
||||
originalType: string
|
||||
) => {
|
||||
const {
|
||||
input,
|
||||
@ -1183,6 +1193,7 @@ export const buildInputFieldTemplate = (
|
||||
ui_order,
|
||||
ui_choice_labels,
|
||||
item_default,
|
||||
originalType,
|
||||
};
|
||||
|
||||
const baseField = {
|
||||
@ -1193,10 +1204,6 @@ export const buildInputFieldTemplate = (
|
||||
...extra,
|
||||
};
|
||||
|
||||
if (!isTemplatedFieldType(fieldType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const builder = TEMPLATE_BUILDER_MAP[fieldType];
|
||||
|
||||
if (!builder) {
|
||||
|
@ -60,6 +60,7 @@ const FIELD_VALUE_FALLBACK_MAP: {
|
||||
UNetField: undefined,
|
||||
VaeField: undefined,
|
||||
VaeModelField: undefined,
|
||||
Unknown: undefined,
|
||||
};
|
||||
|
||||
export const buildInputFieldValue = (
|
||||
|
@ -4,6 +4,7 @@ import { reduce, startCase } from 'lodash-es';
|
||||
import { OpenAPIV3_1 } from 'openapi-types';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import {
|
||||
FieldType,
|
||||
InputFieldTemplate,
|
||||
InvocationSchemaObject,
|
||||
InvocationTemplate,
|
||||
@ -103,7 +104,7 @@ export const parseSchema = (
|
||||
return inputsAccumulator;
|
||||
}
|
||||
|
||||
const fieldType = property.ui_type ?? getFieldType(property);
|
||||
let fieldType = property.ui_type ?? getFieldType(property);
|
||||
|
||||
if (!fieldType) {
|
||||
logger('nodes').warn(
|
||||
@ -118,6 +119,9 @@ export const parseSchema = (
|
||||
return inputsAccumulator;
|
||||
}
|
||||
|
||||
// stash this for custom types
|
||||
const originalType = fieldType;
|
||||
|
||||
if (fieldType === 'WorkflowField') {
|
||||
withWorkflow = true;
|
||||
return inputsAccumulator;
|
||||
@ -137,23 +141,24 @@ export const parseSchema = (
|
||||
}
|
||||
|
||||
if (!isFieldType(fieldType)) {
|
||||
logger('nodes').warn(
|
||||
logger('nodes').debug(
|
||||
{
|
||||
node: type,
|
||||
fieldName: propertyName,
|
||||
fieldType,
|
||||
field: parseify(property),
|
||||
},
|
||||
`Skipping unknown input field type: ${fieldType}`
|
||||
`Fallback handling for unknown input field type: ${fieldType}`
|
||||
);
|
||||
return inputsAccumulator;
|
||||
fieldType = 'Unknown';
|
||||
}
|
||||
|
||||
const field = buildInputFieldTemplate(
|
||||
schema,
|
||||
property,
|
||||
propertyName,
|
||||
fieldType
|
||||
fieldType as FieldType, // we have already checked that fieldType is a valid FieldType, and forced it to be Unknown if not
|
||||
originalType
|
||||
);
|
||||
|
||||
if (!field) {
|
||||
@ -220,26 +225,43 @@ export const parseSchema = (
|
||||
return outputsAccumulator;
|
||||
}
|
||||
|
||||
const fieldType = property.ui_type ?? getFieldType(property);
|
||||
let fieldType = property.ui_type ?? getFieldType(property);
|
||||
|
||||
if (!isFieldType(fieldType)) {
|
||||
if (!fieldType) {
|
||||
logger('nodes').warn(
|
||||
{ fieldName: propertyName, fieldType, field: parseify(property) },
|
||||
'Skipping unknown output field type'
|
||||
{
|
||||
node: type,
|
||||
fieldName: propertyName,
|
||||
fieldType,
|
||||
field: parseify(property),
|
||||
},
|
||||
'Missing output field type'
|
||||
);
|
||||
return outputsAccumulator;
|
||||
}
|
||||
|
||||
// stash for custom types
|
||||
const originalType = fieldType;
|
||||
|
||||
if (!isFieldType(fieldType)) {
|
||||
logger('nodes').debug(
|
||||
{ fieldName: propertyName, fieldType, field: parseify(property) },
|
||||
`Fallback handling for unknown input field type: ${fieldType}`
|
||||
);
|
||||
fieldType = 'Unknown';
|
||||
}
|
||||
|
||||
outputsAccumulator[propertyName] = {
|
||||
fieldKind: 'output',
|
||||
name: propertyName,
|
||||
title:
|
||||
property.title ?? (propertyName ? startCase(propertyName) : ''),
|
||||
description: property.description ?? '',
|
||||
type: fieldType,
|
||||
type: fieldType as FieldType,
|
||||
ui_hidden: property.ui_hidden ?? false,
|
||||
ui_type: property.ui_type,
|
||||
ui_order: property.ui_order,
|
||||
originalType,
|
||||
};
|
||||
|
||||
return outputsAccumulator;
|
||||
|
Reference in New Issue
Block a user