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 { ColumnElementComponent } from 'features/nodes/components/sidePanel/builder/ColumnElementComponent';
import type { ContainerElement } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useContext, useMemo } from 'react';
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
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 { ContainerElement, FormElement } from 'features/nodes/types/workflow';
import { Fragment, memo } from 'react';
import type { Equals } 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) => {
return Array.from({ length: count }, () => '1fr').join(' auto ');
};
@@ -31,16 +15,43 @@ const getGridTemplateColumns = (count: number) => {
export const ContainerElementComponent = memo(({ element }: { element: ContainerElement }) => {
const { id, data } = element;
const { columns } = data;
const columnIds = useMemo(() => columns.map((column) => column.id), [columns]);
return (
<ContainerContextProvider containerId={id} columnIds={columnIds}>
<Grid id={id} gap={4} gridTemplateColumns={getGridTemplateColumns(columns.length)} gridAutoFlow="column">
{columns.map((element, i) => {
return <ColumnElementComponent key={`column:${id}_${i + 1}`} element={element} />;
})}
</Grid>
</ContainerContextProvider>
<Grid id={id} gap={4} gridTemplateColumns={getGridTemplateColumns(columns.length)} gridAutoFlow="column">
{columns.map((elements, columnIndex) => {
const key = `${element.id}_${columnIndex}`;
const withDivider = columnIndex < columns.length - 1;
return (
<Fragment key={key}>
<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';
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 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 { memo } from 'react';
@@ -9,7 +9,7 @@ export const WorkflowBuilder = memo(() => {
<ScrollableContent>
<Flex w="full" h="full" justifyContent="center">
<Flex w="full" h="full" maxW={512}>
<ContainerElementComponent element={data} />
<FormElementComponent element={data} />
</Flex>
</Flex>
</ScrollableContent>

View File

@@ -1,5 +1,6 @@
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { WorkflowBuilder } from 'features/nodes/components/sidePanel/builder/WorkflowBuilder';
import WorkflowLinearTab from 'features/nodes/components/sidePanel/workflow/WorkflowLinearTab';
import { memo } from 'react';
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}>
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
<TabList>
<Tab>Builder</Tab>
<Tab>{t('common.linear')}</Tab>
<Tab>{t('common.details')}</Tab>
<Tab>JSON</Tab>
@@ -21,6 +23,9 @@ const WorkflowFieldsLinearViewPanel = () => {
<TabPanel>
<WorkflowBuilder />
</TabPanel>
<TabPanel>
<WorkflowLinearTab />
</TabPanel>
<TabPanel>
<WorkflowGeneralTab />
</TabPanel>

View File

@@ -147,40 +147,18 @@ const divider = (): DividerElement => ({
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 = {
id: string;
type: 'container';
data: {
columns: ColumnElement[];
columns: FormElement[][];
};
};
const zContainerElement: z.ZodType<ContainerElement> = zElementBase.extend({
type: z.literal('container'),
data: z.object({
columns: z.lazy(() => z.array(zColumnElement)),
columns: z.lazy(() => z.array(z.array(zFormElement))),
}),
});
const container = (columns: ContainerElement['data']['columns']): ContainerElement => ({
@@ -191,70 +169,60 @@ const container = (columns: ContainerElement['data']['columns']): ContainerEleme
},
});
// export type CollapsibleElement = {
// id: string;
// type: 'collapsible';
// columns: BuilderElement[];
// title: string;
// collapsed: boolean;
// };
const zFormElement = z.union([zContainerElement, zNodeFieldElement, zHeadingElement, zTextElement, zDividerElement]);
// const zCollapsibleElement: z.ZodType<CollapsibleElement> = z.object({
// 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 type FormElement = z.infer<typeof zFormElement>;
export const data: ContainerElement = container([
column([
[
heading('My Cool Workflow', 1),
text('This is a description of what my workflow does. It does things.', 'md'),
divider(),
heading('First Section', 2),
text('The first section includes fields relevant to the first section. This note describes that fact.', 'sm'),
container([
column([nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')]),
column([nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image')]),
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')],
]),
nodeField('9c058600-8d73-4702-912b-0ccf37403bfd', 'value'),
nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value'),
nodeField('4e16cbf6-457c-46fb-9ab7-9cb262fa1e03', 'value'),
nodeField('39cb5272-a9d7-4da9-9c35-32e02b46bb34', 'color'),
container([
column([
[
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'),
]),
],
[
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'),
divider(),
text('These are some text that are definitely super helpful.', 'sm'),
divider(),
container([
column([
[
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')],
]),
]),
],
]);