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>