Compare commits

...

9 Commits

Author SHA1 Message Date
psychedelicious
2c979d1b68 wip 2023-11-17 18:06:26 +11:00
psychedelicious
7b93b5e928 Merge branch 'main' into feat/arbitrary-field-types 2023-11-17 15:00:24 +11:00
Millun Atluri
136ff011b2 3.4.0post1 (#5115)
## What type of PR is this? (check all applicable)

3.4.0post1


## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:
2023-11-17 14:51:10 +11:00
Millun Atluri
3bc15a96d5 Update version to 3.4.0post1 2023-11-17 13:39:00 +11:00
Millun Atluri
43d5bb2038 Updated JS files 2023-11-17 13:36:50 +11:00
psychedelicious
8d39eab3a9 fix(ui): metadata error on img2img 2023-11-17 12:31:34 +11:00
psychedelicious
dc44debbab fix(ui): fix ts error with custom fields 2023-11-17 12:09:15 +11:00
psychedelicious
5ce2dc3a58 feat(ui): fix tooltips for custom types
We need to hold onto the original type of the field so they don't all just show up as "Unknown".
2023-11-17 12:01:39 +11:00
psychedelicious
27fd9071ba feat(ui): add support for custom field types
Node authors may now create their own arbitrary/custom field types. Any pydantic model is supported.

Two notes:
1. Your field type's class name must be unique.

Suggest prefixing fields with something related to the node pack as a kind of namespace.

2. Custom field types function as connection-only fields.

For example, if your custom field has string attributes, you will not get a text input for that attribute when you give a node a field with your custom type.

This is the same behaviour as other complex fields that don't have custom UIs in the workflow editor - like, say, a string collection.
2023-11-17 11:32:35 +11:00
47 changed files with 766 additions and 468 deletions

View File

@@ -244,7 +244,7 @@ class InvokeAiInstance:
"numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2
"urllib3~=1.26.0",
"requests~=2.28.0",
"torch~=2.1.0",
"torch==2.1.0",
"torchmetrics==0.11.4",
"torchvision>=0.14.1",
"--force-reinstall",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import{I as s,ie as T,v as l,$ as A,ig as R,aa as V,ih as z,ii as j,ij as D,ik as F,il as G,im as W,io as K,az as H,ip as U,iq as Y}from"./index-2ad466e0.js";import{M as Z}from"./MantineProvider-1fe3e660.js";var P=String.raw,E=P`
import{w as s,ie as T,v as l,_ as I,ig as R,aa as V,ih as z,ii as j,ij as D,ik as F,il as G,im as W,io as K,az as H,ip as U,iq as Y}from"./index-c553e366.js";import{M as Z}from"./MantineProvider-094ba0de.js";var P=String.raw,E=P`
:root,
:host {
--chakra-vh: 100vh;
@@ -277,4 +277,4 @@ import{I as s,ie as T,v as l,$ as A,ig as R,aa as V,ih as z,ii as j,ij as D,ik a
}
${E}
`}),g={light:"chakra-ui-light",dark:"chakra-ui-dark"};function Q(e={}){const{preventTransition:o=!0}=e,n={setDataset:r=>{const t=o?n.preventTransition():void 0;document.documentElement.dataset.theme=r,document.documentElement.style.colorScheme=r,t==null||t()},setClassName(r){document.body.classList.add(r?g.dark:g.light),document.body.classList.remove(r?g.light:g.dark)},query(){return window.matchMedia("(prefers-color-scheme: dark)")},getSystemTheme(r){var t;return((t=n.query().matches)!=null?t:r==="dark")?"dark":"light"},addListener(r){const t=n.query(),i=a=>{r(a.matches?"dark":"light")};return typeof t.addListener=="function"?t.addListener(i):t.addEventListener("change",i),()=>{typeof t.removeListener=="function"?t.removeListener(i):t.removeEventListener("change",i)}},preventTransition(){const r=document.createElement("style");return r.appendChild(document.createTextNode("*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(r),()=>{window.getComputedStyle(document.body),requestAnimationFrame(()=>{requestAnimationFrame(()=>{document.head.removeChild(r)})})}}};return n}var X="chakra-ui-color-mode";function L(e){return{ssr:!1,type:"localStorage",get(o){if(!(globalThis!=null&&globalThis.document))return o;let n;try{n=localStorage.getItem(e)||o}catch{}return n||o},set(o){try{localStorage.setItem(e,o)}catch{}}}}var ee=L(X),M=()=>{};function S(e,o){return e.type==="cookie"&&e.ssr?e.get(o):o}function O(e){const{value:o,children:n,options:{useSystemColorMode:r,initialColorMode:t,disableTransitionOnChange:i}={},colorModeManager:a=ee}=e,d=t==="dark"?"dark":"light",[u,p]=l.useState(()=>S(a,d)),[y,b]=l.useState(()=>S(a)),{getSystemTheme:w,setClassName:k,setDataset:x,addListener:$}=l.useMemo(()=>Q({preventTransition:i}),[i]),v=t==="system"&&!u?y:u,c=l.useCallback(m=>{const f=m==="system"?w():m;p(f),k(f==="dark"),x(f),a.set(f)},[a,w,k,x]);A(()=>{t==="system"&&b(w())},[]),l.useEffect(()=>{const m=a.get();if(m){c(m);return}if(t==="system"){c("system");return}c(d)},[a,d,t,c]);const C=l.useCallback(()=>{c(v==="dark"?"light":"dark")},[v,c]);l.useEffect(()=>{if(r)return $(c)},[r,$,c]);const N=l.useMemo(()=>({colorMode:o??v,toggleColorMode:o?M:C,setColorMode:o?M:c,forced:o!==void 0}),[v,C,c,o]);return s.jsx(R.Provider,{value:N,children:n})}O.displayName="ColorModeProvider";var te=["borders","breakpoints","colors","components","config","direction","fonts","fontSizes","fontWeights","letterSpacings","lineHeights","radii","shadows","sizes","space","styles","transition","zIndices"];function re(e){return V(e)?te.every(o=>Object.prototype.hasOwnProperty.call(e,o)):!1}function h(e){return typeof e=="function"}function oe(...e){return o=>e.reduce((n,r)=>r(n),o)}var ne=e=>function(...n){let r=[...n],t=n[n.length-1];return re(t)&&r.length>1?r=r.slice(0,r.length-1):t=e,oe(...r.map(i=>a=>h(i)?i(a):ae(a,i)))(t)},ie=ne(j);function ae(...e){return z({},...e,_)}function _(e,o,n,r){if((h(e)||h(o))&&Object.prototype.hasOwnProperty.call(r,n))return(...t)=>{const i=h(e)?e(...t):e,a=h(o)?o(...t):o;return z({},i,a,_)}}var q=l.createContext({getDocument(){return document},getWindow(){return window}});q.displayName="EnvironmentContext";function I(e){const{children:o,environment:n,disabled:r}=e,t=l.useRef(null),i=l.useMemo(()=>n||{getDocument:()=>{var d,u;return(u=(d=t.current)==null?void 0:d.ownerDocument)!=null?u:document},getWindow:()=>{var d,u;return(u=(d=t.current)==null?void 0:d.ownerDocument.defaultView)!=null?u:window}},[n]),a=!r||!n;return s.jsxs(q.Provider,{value:i,children:[o,a&&s.jsx("span",{id:"__chakra_env",hidden:!0,ref:t})]})}I.displayName="EnvironmentProvider";var se=e=>{const{children:o,colorModeManager:n,portalZIndex:r,resetScope:t,resetCSS:i=!0,theme:a={},environment:d,cssVarsRoot:u,disableEnvironment:p,disableGlobalStyle:y}=e,b=s.jsx(I,{environment:d,disabled:p,children:o});return s.jsx(D,{theme:a,cssVarsRoot:u,children:s.jsxs(O,{colorModeManager:n,options:a.config,children:[i?s.jsx(J,{scope:t}):s.jsx(B,{}),!y&&s.jsx(F,{}),r?s.jsx(G,{zIndex:r,children:b}):b]})})},le=e=>function({children:n,theme:r=e,toastOptions:t,...i}){return s.jsxs(se,{theme:r,...i,children:[s.jsx(W,{value:t==null?void 0:t.defaultOptions,children:n}),s.jsx(K,{...t})]})},de=le(j);const ue=()=>l.useMemo(()=>({colorScheme:"dark",fontFamily:"'Inter Variable', sans-serif",components:{ScrollArea:{defaultProps:{scrollbarSize:10},styles:{scrollbar:{"&:hover":{backgroundColor:"var(--invokeai-colors-baseAlpha-300)"}},thumb:{backgroundColor:"var(--invokeai-colors-baseAlpha-300)"}}}}}),[]),ce=L("@@invokeai-color-mode");function me({children:e}){const{i18n:o}=H(),n=o.dir(),r=l.useMemo(()=>ie({...U,direction:n}),[n]);l.useEffect(()=>{document.body.dir=n},[n]);const t=ue();return s.jsx(Z,{theme:t,children:s.jsx(de,{theme:r,colorModeManager:ce,toastOptions:Y,children:e})})}const ve=l.memo(me);export{ve as default};
`}),g={light:"chakra-ui-light",dark:"chakra-ui-dark"};function Q(e={}){const{preventTransition:o=!0}=e,n={setDataset:r=>{const t=o?n.preventTransition():void 0;document.documentElement.dataset.theme=r,document.documentElement.style.colorScheme=r,t==null||t()},setClassName(r){document.body.classList.add(r?g.dark:g.light),document.body.classList.remove(r?g.light:g.dark)},query(){return window.matchMedia("(prefers-color-scheme: dark)")},getSystemTheme(r){var t;return((t=n.query().matches)!=null?t:r==="dark")?"dark":"light"},addListener(r){const t=n.query(),i=a=>{r(a.matches?"dark":"light")};return typeof t.addListener=="function"?t.addListener(i):t.addEventListener("change",i),()=>{typeof t.removeListener=="function"?t.removeListener(i):t.removeEventListener("change",i)}},preventTransition(){const r=document.createElement("style");return r.appendChild(document.createTextNode("*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(r),()=>{window.getComputedStyle(document.body),requestAnimationFrame(()=>{requestAnimationFrame(()=>{document.head.removeChild(r)})})}}};return n}var X="chakra-ui-color-mode";function L(e){return{ssr:!1,type:"localStorage",get(o){if(!(globalThis!=null&&globalThis.document))return o;let n;try{n=localStorage.getItem(e)||o}catch{}return n||o},set(o){try{localStorage.setItem(e,o)}catch{}}}}var ee=L(X),M=()=>{};function S(e,o){return e.type==="cookie"&&e.ssr?e.get(o):o}function O(e){const{value:o,children:n,options:{useSystemColorMode:r,initialColorMode:t,disableTransitionOnChange:i}={},colorModeManager:a=ee}=e,d=t==="dark"?"dark":"light",[u,p]=l.useState(()=>S(a,d)),[y,b]=l.useState(()=>S(a)),{getSystemTheme:w,setClassName:k,setDataset:x,addListener:$}=l.useMemo(()=>Q({preventTransition:i}),[i]),v=t==="system"&&!u?y:u,c=l.useCallback(m=>{const f=m==="system"?w():m;p(f),k(f==="dark"),x(f),a.set(f)},[a,w,k,x]);I(()=>{t==="system"&&b(w())},[]),l.useEffect(()=>{const m=a.get();if(m){c(m);return}if(t==="system"){c("system");return}c(d)},[a,d,t,c]);const C=l.useCallback(()=>{c(v==="dark"?"light":"dark")},[v,c]);l.useEffect(()=>{if(r)return $(c)},[r,$,c]);const A=l.useMemo(()=>({colorMode:o??v,toggleColorMode:o?M:C,setColorMode:o?M:c,forced:o!==void 0}),[v,C,c,o]);return s.jsx(R.Provider,{value:A,children:n})}O.displayName="ColorModeProvider";var te=["borders","breakpoints","colors","components","config","direction","fonts","fontSizes","fontWeights","letterSpacings","lineHeights","radii","shadows","sizes","space","styles","transition","zIndices"];function re(e){return V(e)?te.every(o=>Object.prototype.hasOwnProperty.call(e,o)):!1}function h(e){return typeof e=="function"}function oe(...e){return o=>e.reduce((n,r)=>r(n),o)}var ne=e=>function(...n){let r=[...n],t=n[n.length-1];return re(t)&&r.length>1?r=r.slice(0,r.length-1):t=e,oe(...r.map(i=>a=>h(i)?i(a):ae(a,i)))(t)},ie=ne(j);function ae(...e){return z({},...e,_)}function _(e,o,n,r){if((h(e)||h(o))&&Object.prototype.hasOwnProperty.call(r,n))return(...t)=>{const i=h(e)?e(...t):e,a=h(o)?o(...t):o;return z({},i,a,_)}}var q=l.createContext({getDocument(){return document},getWindow(){return window}});q.displayName="EnvironmentContext";function N(e){const{children:o,environment:n,disabled:r}=e,t=l.useRef(null),i=l.useMemo(()=>n||{getDocument:()=>{var d,u;return(u=(d=t.current)==null?void 0:d.ownerDocument)!=null?u:document},getWindow:()=>{var d,u;return(u=(d=t.current)==null?void 0:d.ownerDocument.defaultView)!=null?u:window}},[n]),a=!r||!n;return s.jsxs(q.Provider,{value:i,children:[o,a&&s.jsx("span",{id:"__chakra_env",hidden:!0,ref:t})]})}N.displayName="EnvironmentProvider";var se=e=>{const{children:o,colorModeManager:n,portalZIndex:r,resetScope:t,resetCSS:i=!0,theme:a={},environment:d,cssVarsRoot:u,disableEnvironment:p,disableGlobalStyle:y}=e,b=s.jsx(N,{environment:d,disabled:p,children:o});return s.jsx(D,{theme:a,cssVarsRoot:u,children:s.jsxs(O,{colorModeManager:n,options:a.config,children:[i?s.jsx(J,{scope:t}):s.jsx(B,{}),!y&&s.jsx(F,{}),r?s.jsx(G,{zIndex:r,children:b}):b]})})},le=e=>function({children:n,theme:r=e,toastOptions:t,...i}){return s.jsxs(se,{theme:r,...i,children:[s.jsx(W,{value:t==null?void 0:t.defaultOptions,children:n}),s.jsx(K,{...t})]})},de=le(j);const ue=()=>l.useMemo(()=>({colorScheme:"dark",fontFamily:"'Inter Variable', sans-serif",components:{ScrollArea:{defaultProps:{scrollbarSize:10},styles:{scrollbar:{"&:hover":{backgroundColor:"var(--invokeai-colors-baseAlpha-300)"}},thumb:{backgroundColor:"var(--invokeai-colors-baseAlpha-300)"}}}}}),[]),ce=L("@@invokeai-color-mode");function me({children:e}){const{i18n:o}=H(),n=o.dir(),r=l.useMemo(()=>ie({...U,direction:n}),[n]);l.useEffect(()=>{document.body.dir=n},[n]);const t=ue();return s.jsx(Z,{theme:t,children:s.jsx(de,{theme:r,colorModeManager:ce,toastOptions:Y,children:e})})}const ve=l.memo(me);export{ve as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
margin: 0;
}
</style>
<script type="module" crossorigin src="./assets/index-2ad466e0.js"></script>
<script type="module" crossorigin src="./assets/index-c553e366.js"></script>
</head>
<body dir="ltr">

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

@@ -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

View File

@@ -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>
);

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,
};
};

View File

@@ -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.',

View File

@@ -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

View File

@@ -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) {

View File

@@ -60,6 +60,7 @@ const FIELD_VALUE_FALLBACK_MAP: {
UNetField: undefined,
VaeField: undefined,
VaeModelField: undefined,
Unknown: undefined,
};
export const buildInputFieldValue = (

View File

@@ -329,7 +329,7 @@ export const buildLinearImageToImageGraph = (
strength,
init_image: initialImage.imageName,
},
IMAGE_TO_LATENTS
LATENTS_TO_IMAGE
);
// Add Seamless To Graph

View File

@@ -350,7 +350,7 @@ export const buildLinearSDXLImageToImageGraph = (
positive_style_prompt: positiveStylePrompt,
negative_style_prompt: negativeStylePrompt,
},
IMAGE_TO_LATENTS
LATENTS_TO_IMAGE
);
// Add Seamless To Graph

View File

@@ -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;

View File

@@ -1 +1 @@
__version__ = "3.4.0"
__version__ = "3.4.0post1"