feat(ui): get all tabs working w/ new layout

This commit is contained in:
psychedelicious
2025-06-20 11:49:06 +10:00
parent 7f222ffb9d
commit 16993cd216
8 changed files with 288 additions and 54 deletions

View File

@@ -0,0 +1,13 @@
import { Flex, Heading } from '@invoke-ai/ui-library';
import { memo } from 'react';
export const WorkflowsLaunchpadPanel = memo(() => {
return (
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={12} maxW={768}>
<Heading mb={4}>Go deep with Workflows.</Heading>
</Flex>
</Flex>
);
});
WorkflowsLaunchpadPanel.displayName = 'WorkflowsLaunchpadPanel';

View File

@@ -1,4 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { ReactFlowProvider } from '@xyflow/react';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
@@ -29,21 +30,23 @@ const NodeEditor = () => {
const { t } = useTranslation();
return (
<FocusRegionWrapper region="workflows" layerStyle="first" sx={FOCUS_REGION_STYLES}>
{data && (
<>
<Flow />
<AddNodeCmdk />
<TopLeftPanel />
<TopCenterPanel />
<TopRightPanel />
<BottomLeftPanel />
<MinimapPanel />
</>
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={PiFlowArrowBold} />}
</FocusRegionWrapper>
<ReactFlowProvider>
<FocusRegionWrapper region="workflows" layerStyle="first" sx={FOCUS_REGION_STYLES}>
{data && (
<>
<Flow />
<AddNodeCmdk />
<TopLeftPanel />
<TopCenterPanel />
<TopRightPanel />
<BottomLeftPanel />
<MinimapPanel />
</>
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={PiFlowArrowBold} />}
</FocusRegionWrapper>
</ReactFlowProvider>
);
};

View File

@@ -8,6 +8,7 @@ import { PublishWorkflowPanelContent } from 'features/nodes/components/sidePanel
import { ActiveWorkflowDescription } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription';
import { ActiveWorkflowNameAndActions } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowNameAndActions';
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
import QueueControls from 'features/queue/components/QueueControls';
import { memo } from 'react';
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
@@ -18,13 +19,16 @@ const WorkflowsTabLeftPanel = () => {
const isInPublishFlow = useStore($isInPublishFlow);
return (
<Flex w="full" h="full" gap={2} flexDir="column">
{isInPublishFlow && <PublishWorkflowPanelContent />}
{!isInPublishFlow && <ActiveWorkflowNameAndActions />}
{!isInPublishFlow && !isPublished && mode === 'view' && <ActiveWorkflowDescription />}
{!isInPublishFlow && !isPublished && mode === 'view' && <ViewModeLeftPanelContent />}
{!isInPublishFlow && !isPublished && mode === 'edit' && <EditModeLeftPanelContent />}
{isPublished && <PublishedWorkflowPanelContent />}
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Flex w="full" h="full" gap={2} flexDir="column">
{isInPublishFlow && <PublishWorkflowPanelContent />}
{!isInPublishFlow && <ActiveWorkflowNameAndActions />}
{!isInPublishFlow && !isPublished && mode === 'view' && <ActiveWorkflowDescription />}
{!isInPublishFlow && !isPublished && mode === 'view' && <ViewModeLeftPanelContent />}
{!isInPublishFlow && !isPublished && mode === 'edit' && <EditModeLeftPanelContent />}
{isPublished && <PublishedWorkflowPanelContent />}
</Flex>
</Flex>
);
};

View File

@@ -1,7 +1,5 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import InvocationCacheStatus from './InvocationCacheStatus';
@@ -11,18 +9,9 @@ import QueueTabQueueControls from './QueueTabQueueControls';
const QueueTabContent = () => {
const isInvocationCacheEnabled = useFeatureStatus('invocationCache');
const activeTabName = useAppSelector(selectActiveTab);
return (
<Flex
display={activeTabName === 'queue' ? undefined : 'none'}
hidden={activeTabName !== 'queue'}
borderRadius="base"
w="full"
h="full"
flexDir="column"
gap={2}
>
<Flex borderRadius="base" w="full" h="full" flexDir="column" gap={2}>
<Flex gap={2} w="full">
<QueueTabQueueControls />
<QueueStatus />

View File

@@ -78,11 +78,12 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('app', 'invokeFront', ['mod+shift+enter']);
addHotkey('app', 'cancelQueueItem', ['shift+x']);
addHotkey('app', 'clearQueue', ['mod+shift+x']);
addHotkey('app', 'selectCanvasTab', ['1']);
addHotkey('app', 'selectUpscalingTab', ['2']);
addHotkey('app', 'selectWorkflowsTab', ['3']);
addHotkey('app', 'selectModelsTab', ['4'], isModelManagerEnabled);
addHotkey('app', 'selectQueueTab', isModelManagerEnabled ? ['5'] : ['4']);
addHotkey('app', 'selectGenerateTab', ['1']);
addHotkey('app', 'selectCanvasTab', ['2']);
addHotkey('app', 'selectUpscalingTab', ['3']);
addHotkey('app', 'selectWorkflowsTab', ['4']);
addHotkey('app', 'selectModelsTab', ['5'], isModelManagerEnabled);
addHotkey('app', 'selectQueueTab', isModelManagerEnabled ? ['6'] : ['5']);
addHotkey('app', 'focusPrompt', ['alt+a']);
addHotkey('app', 'toggleLeftPanel', ['t', 'o']);
addHotkey('app', 'toggleRightPanel', ['g']);

View File

@@ -8,11 +8,16 @@ import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout';
import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-layout';
import { selectActiveTabIndex } from 'features/ui/store/uiSelectors';
import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { TabMountGate } from './TabMountGate';
import ModelManagerTab from './tabs/ModelManagerTab';
import QueueTab from './tabs/QueueTab';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
@@ -103,15 +108,36 @@ export const AppContent = memo(() => {
<VerticalNavBar />
</TabList>
<TabPanels w="full" h="full" p={0}>
<TabPanel w="full" h="full" p={0}>
<GenerateTabAutoLayout />
</TabPanel>
<TabPanel w="full" h="full" p={0}>
<CanvasTabAutoLayout />
</TabPanel>
<TabPanel w="full" h="full" p={0}>
<UpscalingTabAutoLayout />
</TabPanel>
<TabMountGate tab="generate">
<TabPanel w="full" h="full" p={0}>
<GenerateTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="canvas">
<TabPanel w="full" h="full" p={0}>
<CanvasTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="upscaling">
<TabPanel w="full" h="full" p={0}>
<UpscalingTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="workflows">
<TabPanel w="full" h="full" p={0}>
<WorkflowsTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="models">
<TabPanel w="full" h="full" p={0}>
<ModelManagerTab />
</TabPanel>
</TabMountGate>
<TabMountGate tab="queue">
<TabPanel w="full" h="full" p={0}>
<QueueTab />
</TabPanel>
</TabMountGate>
</TabPanels>
</Tabs>
);

View File

@@ -1,4 +1,3 @@
import { ReactFlowProvider } from '@xyflow/react';
import { useAppSelector } from 'app/store/storeHooks';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import NodeEditor from 'features/nodes/components/NodeEditor';
@@ -9,11 +8,7 @@ export const WorkflowsMainPanel = memo(() => {
const mode = useAppSelector(selectWorkflowMode);
if (mode === 'edit') {
return (
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
);
return <NodeEditor />;
}
return <ImageViewer />;

View File

@@ -0,0 +1,203 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { WorkflowsLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import NodeEditor from 'features/nodes/components/NodeEditor';
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { atom } from 'nanostores';
import { memo, useCallback, useRef, useState } from 'react';
import { useOnFirstVisible } from './use-on-first-visible';
const LAUNCHPAD_PANEL_ID = 'launchpad';
const WORKSPACE_PANEL_ID = 'workspace';
const VIEWER_PANEL_ID = 'viewer';
const PROGRESS_PANEL_ID = 'progress';
const dockviewComponents: IDockviewReactProps['components'] = {
[LAUNCHPAD_PANEL_ID]: WorkflowsLaunchpadPanel,
[WORKSPACE_PANEL_ID]: NodeEditor,
[VIEWER_PANEL_ID]: ImageViewerPanel,
[PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
});
api.addPanel({
id: WORKSPACE_PANEL_ID,
component: WORKSPACE_PANEL_ID,
title: 'Workflow Editor',
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: PROGRESS_PANEL_ID,
component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
return;
}
e.preventDefault();
}),
];
return () => {
disposables.forEach((disposable) => {
disposable.dispose();
});
};
};
const MainPanel = memo(() => {
return (
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={dockviewComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
);
});
MainPanel.displayName = 'MainPanel';
const LEFT_PANEL_ID = 'left';
const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
export const gridviewComponents: IGridviewReactProps['components'] = {
[LEFT_PANEL_ID]: WorkflowsTabLeftPanel,
[MAIN_PANEL_ID]: MainPanel,
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
export const initializeLayout = (api: GridviewApi) => {
api.addPanel({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
// priority: LayoutPriority.High,
});
api.addPanel({
id: LEFT_PANEL_ID,
component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: MAIN_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.addPanel({
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: MAIN_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.addPanel({
id: BOARDS_PANEL_ID,
component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: GALLERY_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const WorkflowsTabAutoLayout = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const $api = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$api.set(event.api);
initializeLayout(event.api);
},
[$api]
);
const resizeMainPanelOnFirstVisible = useCallback(() => {
const api = $api.get();
if (!api) {
return;
}
const mainPanel = api.getPanel(MAIN_PANEL_ID);
if (!mainPanel) {
return;
}
if (mainPanel.width !== 0) {
return;
}
let count = 0;
const setSize = () => {
if (count++ > 50) {
return;
}
mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
if (mainPanel.width === 0) {
requestAnimationFrame(setSize);
return;
}
};
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
return (
<AutoLayoutProvider $api={$api}>
<GridviewReact
ref={ref}
className="dockview-theme-invoke"
components={gridviewComponents}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</AutoLayoutProvider>
);
});
WorkflowsTabAutoLayout.displayName = 'WorkflowsTabAutoLayout';