feat(ui): iterate on builder (WIP)

This commit is contained in:
psychedelicious
2025-01-22 16:47:03 +11:00
parent bee0e8248f
commit bf60be99dc
5 changed files with 78 additions and 174 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')],
]), ]),
]), ],
]); ]);