From 4845d31857385279b75802ed3b7ea9c190c4bff4 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 26 Aug 2025 14:13:41 -0400 Subject: [PATCH] add option for video upsell, rearrange navigation bar and gallery tabs --- invokeai/frontend/web/public/locales/en.json | 14 +++- .../web/src/app/components/InvokeAIUI.tsx | 67 +++++++------------ .../frontend/web/src/app/components/types.ts | 43 ++++++++++++ .../app/store/nanostores/accountTypeText.ts | 3 + .../store/nanostores/videoUpsellComponent.ts | 4 ++ .../frontend/web/src/app/types/invokeai.ts | 4 +- .../features/gallery/components/Gallery.tsx | 17 ++--- .../gallery/components/VideoGallery.tsx | 16 +++++ .../web/src/features/queue/store/readiness.ts | 16 ++++- .../src/features/system/store/configSlice.ts | 2 + .../features/ui/components/VerticalNavBar.tsx | 10 +-- .../layouts/LaunchpadStartingFrameButton.tsx | 50 ++++++++++++++ .../ui/layouts/VideoLaunchpadPanel.tsx | 54 +++++++++++++++ .../ui/layouts/video-tab-auto-layout.tsx | 4 +- 14 files changed, 244 insertions(+), 60 deletions(-) create mode 100644 invokeai/frontend/web/src/app/components/types.ts create mode 100644 invokeai/frontend/web/src/app/store/nanostores/accountTypeText.ts create mode 100644 invokeai/frontend/web/src/app/store/nanostores/videoUpsellComponent.ts create mode 100644 invokeai/frontend/web/src/features/ui/layouts/LaunchpadStartingFrameButton.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/VideoLaunchpadPanel.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 3b7a630c90..067173a9f5 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1280,7 +1280,8 @@ "noNodesInGraph": "No nodes in graph", "systemDisconnected": "System disconnected", "promptExpansionPending": "Prompt expansion in progress", - "promptExpansionResultPending": "Please accept or discard your prompt expansion result" + "promptExpansionResultPending": "Please accept or discard your prompt expansion result", + "videoIsDisabled": "Video generation is not enabled for {{accountType}} accounts." }, "maskBlur": "Mask Blur", "negativePromptPlaceholder": "Negative Prompt", @@ -2593,7 +2594,7 @@ "panels": { "launchpad": "Launchpad", "workflowEditor": "Workflow Editor", - "imageViewer": "Image Viewer", + "imageViewer": "Viewer", "canvas": "Canvas", "video": "Video" }, @@ -2602,6 +2603,15 @@ "upscalingTitle": "Upscale and add detail.", "canvasTitle": "Edit and refine on Canvas.", "generateTitle": "Generate images from text prompts.", + "videoTitle": "Generate videos from text prompts.", + "video": { + "startingFrameCalloutTitle": "Add a Starting Frame", + "startingFrameCalloutDesc": "Add an image to control the first frame of your video." + }, + "addStartingFrame": { + "title": "Add a Starting Frame", + "description": "Add an image to control the first frame of your video." + }, "modelGuideText": "Want to learn what prompts work best for each model?", "modelGuideLink": "Check out our Model Guide.", "createNewWorkflowFromScratch": "Create a new Workflow from scratch", diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 3285f31ec0..21eee66513 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -1,16 +1,14 @@ import 'i18n'; -import type { Middleware } from '@reduxjs/toolkit'; -import type { StudioInitAction } from 'app/hooks/useStudioInitAction'; +import type { InvokeAIUIProps } from 'app/components/types'; import { $didStudioInit } from 'app/hooks/useStudioInitAction'; -import type { LoggingOverrides } from 'app/logging/logger'; import { $loggingOverrides, configureLogging } from 'app/logging/logger'; import { addStorageListeners } from 'app/store/enhancers/reduxRemember/driver'; import { $accountSettingsLink } from 'app/store/nanostores/accountSettingsLink'; +import { $accountTypeText } from 'app/store/nanostores/accountTypeText'; import { $authToken } from 'app/store/nanostores/authToken'; import { $baseUrl } from 'app/store/nanostores/baseUrl'; import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; -import type { CustomStarUi } from 'app/store/nanostores/customStarUI'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import { $logo } from 'app/store/nanostores/logo'; @@ -20,11 +18,10 @@ import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/proj import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId'; import { $store } from 'app/store/nanostores/store'; import { $toastMap } from 'app/store/nanostores/toastMap'; +import { $videoUpsellComponent } from 'app/store/nanostores/videoUpsellComponent'; import { $whatsNew } from 'app/store/nanostores/whatsNew'; import { createStore } from 'app/store/store'; -import type { PartialAppConfig } from 'app/types/invokeai'; import Loading from 'common/components/Loading/Loading'; -import type { WorkflowSortOption, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice'; import { $workflowLibraryCategoriesOptions, $workflowLibrarySortOptions, @@ -33,47 +30,13 @@ import { DEFAULT_WORKFLOW_LIBRARY_SORT_OPTIONS, DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES, } from 'features/nodes/store/workflowLibrarySlice'; -import type { WorkflowCategory } from 'features/nodes/types/workflow'; -import type { ToastConfig } from 'features/toast/toast'; -import type { PropsWithChildren, ReactNode } from 'react'; import React, { lazy, memo, useEffect, useLayoutEffect, useState } from 'react'; import { Provider } from 'react-redux'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { $socketOptions } from 'services/events/stores'; -import type { ManagerOptions, SocketOptions } from 'socket.io-client'; const App = lazy(() => import('./App')); -interface Props extends PropsWithChildren { - apiUrl?: string; - openAPISchemaUrl?: string; - token?: string; - config?: PartialAppConfig; - customNavComponent?: ReactNode; - accountSettingsLink?: string; - middleware?: Middleware[]; - projectId?: string; - projectName?: string; - projectUrl?: string; - queueId?: string; - studioInitAction?: StudioInitAction; - customStarUi?: CustomStarUi; - socketOptions?: Partial; - isDebugging?: boolean; - logo?: ReactNode; - toastMap?: Record; - whatsNew?: ReactNode[]; - workflowCategories?: WorkflowCategory[]; - workflowTagCategories?: WorkflowTagCategory[]; - workflowSortOptions?: WorkflowSortOption[]; - loggingOverrides?: LoggingOverrides; - /** - * If provided, overrides in-app navigation to the model manager - */ - onClickGoToModelManager?: () => void; - storagePersistDebounce?: number; -} - const InvokeAIUI = ({ apiUrl, openAPISchemaUrl, @@ -92,6 +55,8 @@ const InvokeAIUI = ({ isDebugging = false, logo, toastMap, + accountTypeText, + videoUpsellComponent, workflowCategories, workflowTagCategories, workflowSortOptions, @@ -99,7 +64,7 @@ const InvokeAIUI = ({ onClickGoToModelManager, whatsNew, storagePersistDebounce = 300, -}: Props) => { +}: InvokeAIUIProps) => { const [store, setStore] = useState | undefined>(undefined); const [didRehydrate, setDidRehydrate] = useState(false); @@ -180,6 +145,26 @@ const InvokeAIUI = ({ }; }, [customStarUi]); + useEffect(() => { + if (accountTypeText) { + $accountTypeText.set(accountTypeText); + } + + return () => { + $accountTypeText.set(''); + }; + }, [accountTypeText]); + + useEffect(() => { + if (videoUpsellComponent) { + $videoUpsellComponent.set(videoUpsellComponent); + } + + return () => { + $videoUpsellComponent.set(undefined); + }; + }, [videoUpsellComponent]); + useEffect(() => { if (customNavComponent) { $customNavComponent.set(customNavComponent); diff --git a/invokeai/frontend/web/src/app/components/types.ts b/invokeai/frontend/web/src/app/components/types.ts new file mode 100644 index 0000000000..dbec6a72a8 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/types.ts @@ -0,0 +1,43 @@ +import type { Middleware } from '@reduxjs/toolkit'; +import type { StudioInitAction } from 'app/hooks/useStudioInitAction'; +import type { LoggingOverrides } from 'app/logging/logger'; +import type { CustomStarUi } from 'app/store/nanostores/customStarUI'; +import type { PartialAppConfig } from 'app/types/invokeai'; +import type { SocketOptions } from 'dgram'; +import type { WorkflowSortOption, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice'; +import type { WorkflowCategory } from 'features/nodes/types/workflow'; +import type { ToastConfig } from 'features/toast/toast'; +import type { PropsWithChildren, ReactNode } from 'react'; +import type { ManagerOptions } from 'socket.io-client'; + +export interface InvokeAIUIProps extends PropsWithChildren { + apiUrl?: string; + openAPISchemaUrl?: string; + token?: string; + config?: PartialAppConfig; + customNavComponent?: ReactNode; + accountSettingsLink?: string; + middleware?: Middleware[]; + projectId?: string; + projectName?: string; + projectUrl?: string; + queueId?: string; + studioInitAction?: StudioInitAction; + customStarUi?: CustomStarUi; + socketOptions?: Partial; + isDebugging?: boolean; + logo?: ReactNode; + toastMap?: Record; + accountTypeText?: string; + videoUpsellComponent?: ReactNode; + whatsNew?: ReactNode[]; + workflowCategories?: WorkflowCategory[]; + workflowTagCategories?: WorkflowTagCategory[]; + workflowSortOptions?: WorkflowSortOption[]; + loggingOverrides?: LoggingOverrides; + /** + * If provided, overrides in-app navigation to the model manager + */ + onClickGoToModelManager?: () => void; + storagePersistDebounce?: number; +} diff --git a/invokeai/frontend/web/src/app/store/nanostores/accountTypeText.ts b/invokeai/frontend/web/src/app/store/nanostores/accountTypeText.ts new file mode 100644 index 0000000000..4008b86cef --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/accountTypeText.ts @@ -0,0 +1,3 @@ +import { atom } from 'nanostores'; + +export const $accountTypeText = atom(''); diff --git a/invokeai/frontend/web/src/app/store/nanostores/videoUpsellComponent.ts b/invokeai/frontend/web/src/app/store/nanostores/videoUpsellComponent.ts new file mode 100644 index 0000000000..f36512d744 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/videoUpsellComponent.ts @@ -0,0 +1,4 @@ +import { atom } from 'nanostores'; +import type { ReactNode } from 'react'; + +export const $videoUpsellComponent = atom(undefined); diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 91a4a57efc..b24f83a1b1 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -80,6 +80,7 @@ export const zAppConfig = z.object({ allowClientSideUpload: z.boolean(), allowPublishWorkflows: z.boolean(), allowPromptExpansion: z.boolean(), + allowVideo: z.boolean(), disabledTabs: z.array(zTabName), disabledFeatures: z.array(zAppFeature), disabledSDFeatures: z.array(zSDFeature), @@ -140,8 +141,9 @@ export const getDefaultAppConfig = (): AppConfig => ({ allowClientSideUpload: false, allowPublishWorkflows: false, allowPromptExpansion: false, + allowVideo: false, // used to determine if video is enabled vs upsell shouldShowCredits: false, - disabledTabs: ['video'], + disabledTabs: ['video'], // used to determine if video functionality is visible disabledFeatures: ['lightbox', 'faceRestore', 'batches'] satisfies AppFeature[], disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'] satisfies SDFeature[], sd: { diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index bc963d89be..11291bd5c7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -82,14 +82,7 @@ export const GalleryPanel = memo(() => { > {t('parameters.images')} - + {isVideoEnabled && ( )} + diff --git a/invokeai/frontend/web/src/features/gallery/components/VideoGallery.tsx b/invokeai/frontend/web/src/features/gallery/components/VideoGallery.tsx index 26bd003fc8..b0bfa1ae0a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/VideoGallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/VideoGallery.tsx @@ -1,6 +1,8 @@ import { Box, Flex, forwardRef, Grid, GridItem, Spinner, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; +import { $accountTypeText } from 'app/store/nanostores/accountTypeText'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { getFocusedRegion } from 'common/hooks/focus'; import { useRangeBasedVideoFetching } from 'features/gallery/hooks/useRangeBasedVideoFetching'; @@ -8,6 +10,7 @@ import type { selectGetVideoIdsQueryArgs } from 'features/gallery/store/galleryS import { selectGalleryImageMinimumWidth, selectLastSelectedItem } from 'features/gallery/store/gallerySelectors'; import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; +import { selectAllowVideo } from 'features/system/store/configSlice'; import type { MutableRefObject } from 'react'; import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import type { @@ -287,6 +290,19 @@ export const VideoGallery = memo(() => { const context = useMemo(() => ({ videoIds, queryArgs }), [videoIds, queryArgs]); + const isVideoEnabled = useAppSelector(selectAllowVideo); + const accountTypeText = useStore($accountTypeText); + + if (!isVideoEnabled) { + return ( + + + Video generation is not enabled for {accountTypeText} accounts + + + ); + } + if (isLoading) { return ( diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index a16f4aaa6b..34616ff0ad 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1,6 +1,7 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; +import { $accountTypeText } from 'app/store/nanostores/accountTypeText'; import { $false } from 'app/store/nanostores/util'; import type { AppDispatch, AppStore } from 'app/store/store'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; @@ -39,7 +40,7 @@ import { selectVideoSlice, type VideoState } from 'features/parameters/store/vid import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; import { getGridSize } from 'features/parameters/util/optimalDimension'; import { promptExpansionApi, type PromptExpansionRequestState } from 'features/prompt/PromptExpansion/state'; -import { selectConfigSlice } from 'features/system/store/configSlice'; +import { selectAllowVideo, selectConfigSlice } from 'features/system/store/configSlice'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { TabName } from 'features/ui/store/uiTypes'; import i18n from 'i18next'; @@ -93,6 +94,7 @@ const debouncedUpdateReasons = debounce( store: AppStore, isInPublishFlow: boolean, isChatGPT4oHighModelDisabled: (model: ParameterModel) => boolean, + isVideoEnabled: boolean, promptExpansionRequest: PromptExpansionRequestState, video: VideoState ) => { @@ -152,6 +154,7 @@ const debouncedUpdateReasons = debounce( params, promptExpansionRequest, dynamicPrompts, + isVideoEnabled, }); $reasonsWhyCannotEnqueue.set(reasons); } else { @@ -183,6 +186,7 @@ export const useReadinessWatcher = () => { const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $false); const isInPublishFlow = useStore($isInPublishFlow); const { isChatGPT4oHighModelDisabled } = useIsModelDisabled(); + const isVideoEnabled = useAppSelector(selectAllowVideo); const promptExpansionRequest = useStore(promptExpansionApi.$state); const video = useAppSelector(selectVideoSlice); useEffect(() => { @@ -206,6 +210,7 @@ export const useReadinessWatcher = () => { store, isInPublishFlow, isChatGPT4oHighModelDisabled, + isVideoEnabled, promptExpansionRequest, video ); @@ -229,6 +234,7 @@ export const useReadinessWatcher = () => { workflowSettings, isInPublishFlow, isChatGPT4oHighModelDisabled, + isVideoEnabled, promptExpansionRequest, video, ]); @@ -242,10 +248,16 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { params: ParamsState; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; + isVideoEnabled: boolean; }) => { - const { isConnected, video, params, dynamicPrompts, promptExpansionRequest } = arg; + const { isConnected, video, params, dynamicPrompts, promptExpansionRequest, isVideoEnabled } = arg; const { positivePrompt } = params; const reasons: Reason[] = []; + const accountTypeText = $accountTypeText.get(); + + if (!isVideoEnabled) { + reasons.push({ content: i18n.t('parameters.invoke.videoIsDisabled', { accountType: accountTypeText }) }); + } if (!isConnected) { reasons.push(disconnectedReason(i18n.t)); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 3e809e7b70..7de5d22838 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -71,6 +71,8 @@ export const selectIsModelsTabDisabled = createConfigSelector((config) => config export const selectIsClientSideUploadEnabled = createConfigSelector((config) => config.allowClientSideUpload); export const selectAllowPublishWorkflows = createConfigSelector((config) => config.allowPublishWorkflows); export const selectAllowPromptExpansion = createConfigSelector((config) => config.allowPromptExpansion); +export const selectAllowVideo = createConfigSelector((config) => config.allowVideo); + export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal); export const selectShouldShowCredits = createConfigSelector((config) => config.shouldShowCredits); const selectDisabledTabs = createConfigSelector((config) => config.disabledTabs); diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx index b1a92fa603..0bcde3a3ea 100644 --- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx +++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx @@ -1,4 +1,4 @@ -import { Flex, Spacer } from '@invoke-ai/ui-library'; +import { Divider, Flex, Spacer } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; import { useAppSelector } from 'app/store/storeHooks'; @@ -48,13 +48,15 @@ export const VerticalNavBar = memo(() => { {withGenerateTab && } label={t('ui.tabs.generate')} />} {withCanvasTab && } label={t('ui.tabs.canvas')} />} {withUpscalingTab && } label={t('ui.tabs.upscaling')} />} - {withWorkflowsTab && } label={t('ui.tabs.workflows')} />} - {withModelsTab && } label={t('ui.tabs.models')} />} - {withQueueTab && } label={t('ui.tabs.queue')} />} {withVideoTab && } label={t('ui.tabs.video')} />} + {withWorkflowsTab && } label={t('ui.tabs.workflows')} />} + {withModelsTab && } label={t('ui.tabs.models')} />} + {withQueueTab && } label={t('ui.tabs.queue')} />} + + {customNavComponent ? customNavComponent : } diff --git a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadStartingFrameButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadStartingFrameButton.tsx new file mode 100644 index 0000000000..a65d0a668d --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadStartingFrameButton.tsx @@ -0,0 +1,50 @@ +import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library'; +import { useAppStore } from 'app/store/storeHooks'; +import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/util'; +import { videoFrameFromImageDndTarget } from 'features/dnd/dnd'; +import { DndDropTarget } from 'features/dnd/DndDropTarget'; +import { startingFrameImageChanged } from 'features/parameters/store/videoSlice'; +import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiUploadBold, PiVideoBold } from 'react-icons/pi'; +import type { ImageDTO } from 'services/api/types'; + +const dndTargetData = videoFrameFromImageDndTarget.getData({ frame: 'start' }); + +export const LaunchpadStartingFrameButton = memo((props: { extraAction?: () => void }) => { + const { t } = useTranslation(); + const { dispatch } = useAppStore(); + + const uploadOptions = useMemo( + () => + ({ + onUpload: (imageDTO: ImageDTO) => { + dispatch(startingFrameImageChanged(imageDTOToImageWithDims(imageDTO))); + props.extraAction?.(); + }, + allowMultiple: false, + }) as const, + [dispatch, props] + ); + + const uploadApi = useImageUploadButton(uploadOptions); + + return ( + + + + {t('ui.launchpad.addStartingFrame.title')} + {t('ui.launchpad.addStartingFrame.description')} + + + + + + + + ); +}); + +LaunchpadStartingFrameButton.displayName = 'LaunchpadStartingFrameButton'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/VideoLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/VideoLaunchpadPanel.tsx new file mode 100644 index 0000000000..9ceb138cdc --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/VideoLaunchpadPanel.tsx @@ -0,0 +1,54 @@ +import { Button, Flex, Grid, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $videoUpsellComponent } from 'app/store/nanostores/videoUpsellComponent'; +import { useAppSelector } from 'app/store/storeHooks'; +import { VideoModelPicker } from 'features/settingsAccordions/components/VideoSettingsAccordion/VideoModelPicker'; +import { selectAllowVideo } from 'features/system/store/configSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { LaunchpadContainer } from './LaunchpadContainer'; +import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton'; +import { LaunchpadStartingFrameButton } from './LaunchpadStartingFrameButton'; + +export const VideoLaunchpadPanel = memo(() => { + const { t } = useTranslation(); + const isVideoEnabled = useAppSelector(selectAllowVideo); + const videoUpsellComponent = useStore($videoUpsellComponent); + + if (!isVideoEnabled) { + return ( + + + {videoUpsellComponent} + + + ); + } + + return ( + + + + + + {t('ui.launchpad.modelGuideText')}{' '} + + + + + + + + ); +}); +VideoLaunchpadPanel.displayName = 'VideoLaunchpadPanel'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx index 1b2b76a9dc..dbf9b44e4c 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx @@ -21,7 +21,6 @@ import { memo, useCallback, useEffect } from 'react'; import { DockviewTab } from './DockviewTab'; import { DockviewTabLaunchpad } from './DockviewTabLaunchpad'; import { DockviewTabProgress } from './DockviewTabProgress'; -import { GenerateLaunchpadPanel } from './GenerateLaunchpadPanel'; import { navigationApi } from './navigation-api'; import { PanelHotkeysLogical } from './PanelHotkeysLogical'; import { @@ -43,6 +42,7 @@ import { SETTINGS_PANEL_ID, VIEWER_PANEL_ID, } from './shared'; +import { VideoLaunchpadPanel } from './VideoLaunchpadPanel'; import { VideoTabLeftPanel } from './VideoTabLeftPanel'; const tabComponents = { @@ -52,7 +52,7 @@ const tabComponents = { }; const mainPanelComponents: AutoLayoutDockviewComponents = { - [LAUNCHPAD_PANEL_ID]: withPanelContainer(GenerateLaunchpadPanel), + [LAUNCHPAD_PANEL_ID]: withPanelContainer(VideoLaunchpadPanel), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), };