mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): iterate on builder (WIP)
This commit is contained in:
@@ -1,80 +0,0 @@
|
|||||||
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
|
||||||
import {
|
|
||||||
ContainerElementComponent,
|
|
||||||
useContainerContext,
|
|
||||||
} from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
|
|
||||||
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
|
|
||||||
import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
|
|
||||||
import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
|
|
||||||
import { TextElementComponent } from 'features/nodes/components/sidePanel/builder/TextElementComponent';
|
|
||||||
import type { ColumnChildElement, ColumnElement } from 'features/nodes/types/workflow';
|
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { createContext, memo, useContext, useMemo } from 'react';
|
|
||||||
import type { Equals } from 'tsafe';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
const _ColumnContext = createContext<{ columnId: string; columnNumber: number } | null>(null);
|
|
||||||
const ColumnContextProvider = ({
|
|
||||||
columnId,
|
|
||||||
columnNumber,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<{ columnId: string; columnNumber: number }>) => {
|
|
||||||
const ctx = useMemo(() => ({ columnId, columnNumber }), [columnId, columnNumber]);
|
|
||||||
return <_ColumnContext.Provider value={ctx}>{children}</_ColumnContext.Provider>;
|
|
||||||
};
|
|
||||||
export const useColumnContext = () => {
|
|
||||||
const context = useContext(_ColumnContext);
|
|
||||||
assert(context !== null);
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ColumnElementChildComponent = memo(({ element }: { element: ColumnChildElement }) => {
|
|
||||||
const { type, id } = element;
|
|
||||||
switch (type) {
|
|
||||||
case 'container':
|
|
||||||
return <ContainerElementComponent key={id} element={element} />;
|
|
||||||
case 'node-field':
|
|
||||||
return <NodeFieldElementComponent key={id} element={element} />;
|
|
||||||
case 'divider':
|
|
||||||
return <DividerElementComponent key={id} element={element} />;
|
|
||||||
case 'heading':
|
|
||||||
return <HeadingElementComponent key={id} element={element} />;
|
|
||||||
case 'text':
|
|
||||||
return <TextElementComponent key={id} element={element} />;
|
|
||||||
default:
|
|
||||||
assert<Equals<typeof type, never>>(false, `Unhandled type ${type}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ColumnElementChildComponent.displayName = 'ColumnElementChildComponent';
|
|
||||||
|
|
||||||
export const ColumnElementComponent = memo(({ element }: { element: ColumnElement }) => {
|
|
||||||
const containerCtx = useContainerContext();
|
|
||||||
const columnNumber = useMemo(
|
|
||||||
() => containerCtx.columnIds.indexOf(element.id) + 1,
|
|
||||||
[containerCtx.columnIds, element.id]
|
|
||||||
);
|
|
||||||
const withDivider = useMemo(
|
|
||||||
() => containerCtx.columnIds.indexOf(element.id) + 1 < containerCtx.columnIds.length,
|
|
||||||
[containerCtx.columnIds, element.id]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<ColumnContextProvider columnId={element.id} columnNumber={columnNumber}>
|
|
||||||
<>
|
|
||||||
<GridItem
|
|
||||||
as={Grid}
|
|
||||||
id={`column:${element.id}_${columnNumber}`}
|
|
||||||
gap={4}
|
|
||||||
gridAutoRows="min-content"
|
|
||||||
gridAutoFlow="row"
|
|
||||||
>
|
|
||||||
{element.data.elements.map((element) => (
|
|
||||||
<ColumnElementChildComponent key={element.id} element={element} />
|
|
||||||
))}
|
|
||||||
</GridItem>
|
|
||||||
{withDivider && <Flex w="1px" bg="base.800" flexShrink={0} />}
|
|
||||||
</>
|
|
||||||
</ColumnContextProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ColumnElementComponent.displayName = 'ColumnElementComponent';
|
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
import { Grid } from '@invoke-ai/ui-library';
|
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||||
import { ColumnElementComponent } from 'features/nodes/components/sidePanel/builder/ColumnElementComponent';
|
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
|
||||||
import type { ContainerElement } from 'features/nodes/types/workflow';
|
import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
|
||||||
import type { PropsWithChildren } from 'react';
|
import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
|
||||||
import { createContext, memo, useContext, useMemo } from 'react';
|
import { TextElementComponent } from 'features/nodes/components/sidePanel/builder/TextElementComponent';
|
||||||
|
import type { ContainerElement, FormElement } from 'features/nodes/types/workflow';
|
||||||
|
import { Fragment, memo } from 'react';
|
||||||
|
import type { Equals } from 'tsafe';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const _ContainerContext = createContext<{ containerId: string; columnIds: string[]; depth: number } | null>(null);
|
|
||||||
const ContainerContextProvider = ({
|
|
||||||
containerId,
|
|
||||||
columnIds,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<{ containerId: string; columnIds: string[] }>) => {
|
|
||||||
const parentCtx = useContext(_ContainerContext);
|
|
||||||
const ctx = useMemo(
|
|
||||||
() => ({ containerId, columnIds, depth: parentCtx ? parentCtx.depth + 1 : 0 }),
|
|
||||||
[columnIds, containerId, parentCtx]
|
|
||||||
);
|
|
||||||
return <_ContainerContext.Provider value={ctx}>{children}</_ContainerContext.Provider>;
|
|
||||||
};
|
|
||||||
export const useContainerContext = () => {
|
|
||||||
const context = useContext(_ContainerContext);
|
|
||||||
assert(context !== null);
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGridTemplateColumns = (count: number) => {
|
const getGridTemplateColumns = (count: number) => {
|
||||||
return Array.from({ length: count }, () => '1fr').join(' auto ');
|
return Array.from({ length: count }, () => '1fr').join(' auto ');
|
||||||
};
|
};
|
||||||
@@ -31,16 +15,43 @@ const getGridTemplateColumns = (count: number) => {
|
|||||||
export const ContainerElementComponent = memo(({ element }: { element: ContainerElement }) => {
|
export const ContainerElementComponent = memo(({ element }: { element: ContainerElement }) => {
|
||||||
const { id, data } = element;
|
const { id, data } = element;
|
||||||
const { columns } = data;
|
const { columns } = data;
|
||||||
const columnIds = useMemo(() => columns.map((column) => column.id), [columns]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContainerContextProvider containerId={id} columnIds={columnIds}>
|
<Grid id={id} gap={4} gridTemplateColumns={getGridTemplateColumns(columns.length)} gridAutoFlow="column">
|
||||||
<Grid id={id} gap={4} gridTemplateColumns={getGridTemplateColumns(columns.length)} gridAutoFlow="column">
|
{columns.map((elements, columnIndex) => {
|
||||||
{columns.map((element, i) => {
|
const key = `${element.id}_${columnIndex}`;
|
||||||
return <ColumnElementComponent key={`column:${id}_${i + 1}`} element={element} />;
|
const withDivider = columnIndex < columns.length - 1;
|
||||||
})}
|
return (
|
||||||
</Grid>
|
<Fragment key={key}>
|
||||||
</ContainerContextProvider>
|
<GridItem as={Grid} id={key} gap={4} gridAutoRows="min-content" gridAutoFlow="row">
|
||||||
|
{elements.map((element) => (
|
||||||
|
<FormElementComponent key={element.id} element={element} />
|
||||||
|
))}
|
||||||
|
</GridItem>
|
||||||
|
{withDivider && <Flex w="1px" bg="base.800" flexShrink={0} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
ContainerElementComponent.displayName = 'ContainerElementComponent';
|
ContainerElementComponent.displayName = 'ContainerElementComponent';
|
||||||
|
|
||||||
|
export const FormElementComponent = memo(({ element }: { element: FormElement }) => {
|
||||||
|
const { type, id } = element;
|
||||||
|
switch (type) {
|
||||||
|
case 'container':
|
||||||
|
return <ContainerElementComponent key={id} element={element} />;
|
||||||
|
case 'node-field':
|
||||||
|
return <NodeFieldElementComponent key={id} element={element} />;
|
||||||
|
case 'divider':
|
||||||
|
return <DividerElementComponent key={id} element={element} />;
|
||||||
|
case 'heading':
|
||||||
|
return <HeadingElementComponent key={id} element={element} />;
|
||||||
|
case 'text':
|
||||||
|
return <TextElementComponent key={id} element={element} />;
|
||||||
|
default:
|
||||||
|
assert<Equals<typeof type, never>>(false, `Unhandled type ${type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
FormElementComponent.displayName = 'FormElementComponent';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { ContainerElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
|
import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
|
||||||
import { data } from 'features/nodes/types/workflow';
|
import { data } from 'features/nodes/types/workflow';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const WorkflowBuilder = memo(() => {
|
|||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex w="full" h="full" justifyContent="center">
|
<Flex w="full" h="full" justifyContent="center">
|
||||||
<Flex w="full" h="full" maxW={512}>
|
<Flex w="full" h="full" maxW={512}>
|
||||||
<ContainerElementComponent element={data} />
|
<FormElementComponent element={data} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollableContent>
|
</ScrollableContent>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
import { WorkflowBuilder } from 'features/nodes/components/sidePanel/builder/WorkflowBuilder';
|
import { WorkflowBuilder } from 'features/nodes/components/sidePanel/builder/WorkflowBuilder';
|
||||||
|
import WorkflowLinearTab from 'features/nodes/components/sidePanel/workflow/WorkflowLinearTab';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const WorkflowFieldsLinearViewPanel = () => {
|
|||||||
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
|
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
|
||||||
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
|
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
|
||||||
<TabList>
|
<TabList>
|
||||||
|
<Tab>Builder</Tab>
|
||||||
<Tab>{t('common.linear')}</Tab>
|
<Tab>{t('common.linear')}</Tab>
|
||||||
<Tab>{t('common.details')}</Tab>
|
<Tab>{t('common.details')}</Tab>
|
||||||
<Tab>JSON</Tab>
|
<Tab>JSON</Tab>
|
||||||
@@ -21,6 +23,9 @@ const WorkflowFieldsLinearViewPanel = () => {
|
|||||||
<TabPanel>
|
<TabPanel>
|
||||||
<WorkflowBuilder />
|
<WorkflowBuilder />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<WorkflowLinearTab />
|
||||||
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<WorkflowGeneralTab />
|
<WorkflowGeneralTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|||||||
@@ -147,40 +147,18 @@ const divider = (): DividerElement => ({
|
|||||||
type: 'divider',
|
type: 'divider',
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ColumnElement = {
|
|
||||||
id: string;
|
|
||||||
type: 'column';
|
|
||||||
data: {
|
|
||||||
elements: ColumnChildElement[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const zColumnElement = zElementBase.extend({
|
|
||||||
type: z.literal('column'),
|
|
||||||
data: z.object({
|
|
||||||
elements: z.lazy(() => z.array(zColumnChildElement)),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const column = (elements: ColumnElement['data']['elements']): ColumnElement => ({
|
|
||||||
id: nanoid(),
|
|
||||||
type: 'column',
|
|
||||||
data: {
|
|
||||||
elements,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ContainerElement = {
|
export type ContainerElement = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'container';
|
type: 'container';
|
||||||
data: {
|
data: {
|
||||||
columns: ColumnElement[];
|
columns: FormElement[][];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const zContainerElement: z.ZodType<ContainerElement> = zElementBase.extend({
|
const zContainerElement: z.ZodType<ContainerElement> = zElementBase.extend({
|
||||||
type: z.literal('container'),
|
type: z.literal('container'),
|
||||||
data: z.object({
|
data: z.object({
|
||||||
columns: z.lazy(() => z.array(zColumnElement)),
|
columns: z.lazy(() => z.array(z.array(zFormElement))),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const container = (columns: ContainerElement['data']['columns']): ContainerElement => ({
|
const container = (columns: ContainerElement['data']['columns']): ContainerElement => ({
|
||||||
@@ -191,70 +169,60 @@ const container = (columns: ContainerElement['data']['columns']): ContainerEleme
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type CollapsibleElement = {
|
const zFormElement = z.union([zContainerElement, zNodeFieldElement, zHeadingElement, zTextElement, zDividerElement]);
|
||||||
// id: string;
|
|
||||||
// type: 'collapsible';
|
|
||||||
// columns: BuilderElement[];
|
|
||||||
// title: string;
|
|
||||||
// collapsed: boolean;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const zCollapsibleElement: z.ZodType<CollapsibleElement> = z.object({
|
export type FormElement = z.infer<typeof zFormElement>;
|
||||||
// type: z.literal('collapsible'),
|
|
||||||
// columns: z.lazy(() => z.array(zElement)),
|
|
||||||
// title: z.string(),
|
|
||||||
// collapsed: z.boolean(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
const zColumnChildElement = z.union([
|
|
||||||
zContainerElement,
|
|
||||||
// zCollapsibleElement
|
|
||||||
zNodeFieldElement,
|
|
||||||
zHeadingElement,
|
|
||||||
zTextElement,
|
|
||||||
zDividerElement,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type ColumnChildElement = z.infer<typeof zColumnChildElement>;
|
|
||||||
|
|
||||||
export const data: ContainerElement = container([
|
export const data: ContainerElement = container([
|
||||||
column([
|
[
|
||||||
heading('My Cool Workflow', 1),
|
heading('My Cool Workflow', 1),
|
||||||
text('This is a description of what my workflow does. It does things.', 'md'),
|
text('This is a description of what my workflow does. It does things.', 'md'),
|
||||||
divider(),
|
divider(),
|
||||||
heading('First Section', 2),
|
heading('First Section', 2),
|
||||||
text('The first section includes fields relevant to the first section. This note describes that fact.', 'sm'),
|
text('The first section includes fields relevant to the first section. This note describes that fact.', 'sm'),
|
||||||
container([
|
container([
|
||||||
column([nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')]),
|
[nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')],
|
||||||
column([nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')]),
|
[nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')],
|
||||||
column([nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')]),
|
[nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')],
|
||||||
]),
|
]),
|
||||||
nodeField('9c058600-8d73-4702-912b-0ccf37403bfd', 'value'),
|
nodeField('9c058600-8d73-4702-912b-0ccf37403bfd', 'value'),
|
||||||
nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value'),
|
nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value'),
|
||||||
nodeField('4e16cbf6-457c-46fb-9ab7-9cb262fa1e03', 'value'),
|
nodeField('4e16cbf6-457c-46fb-9ab7-9cb262fa1e03', 'value'),
|
||||||
nodeField('39cb5272-a9d7-4da9-9c35-32e02b46bb34', 'color'),
|
nodeField('39cb5272-a9d7-4da9-9c35-32e02b46bb34', 'color'),
|
||||||
container([
|
container([
|
||||||
column([
|
[
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
]),
|
],
|
||||||
column([
|
[
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
]),
|
],
|
||||||
|
[
|
||||||
|
container([
|
||||||
|
[
|
||||||
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
|
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value'),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
nodeField('14744f68-9000-4694-b4d6-cbe83ee231ee', 'model'),
|
nodeField('14744f68-9000-4694-b4d6-cbe83ee231ee', 'model'),
|
||||||
divider(),
|
divider(),
|
||||||
text('These are some text that are definitely super helpful.', 'sm'),
|
text('These are some text that are definitely super helpful.', 'sm'),
|
||||||
divider(),
|
divider(),
|
||||||
container([
|
container([
|
||||||
column([
|
[
|
||||||
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image'),
|
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image'),
|
||||||
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image'),
|
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image'),
|
||||||
]),
|
],
|
||||||
column([nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value')]),
|
[nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value')],
|
||||||
]),
|
]),
|
||||||
]),
|
],
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user