diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 35644728938..218c92471ab 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -2109,5 +2109,65 @@
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",
"watchUiUpdatesOverview": "Watch UI Updates Overview"
+ },
+ "supportVideos": {
+ "supportVideos": "Support Videos",
+ "gettingStarted": "Getting Started",
+ "controlCanvas": "Control Canvas",
+ "watchOnYoutube": "Watch on YouTube",
+ "videos": {
+ "creatingYourFirstImage": {
+ "title": "Creating Your First Image",
+ "description": "Introduction to creating an image from scratch using Invoke's tools."
+ },
+ "usingControlLayersAndReferenceGuides": {
+ "title": "Using Control Layers and Reference Guides",
+ "description": "Learn how to guide your image creation with control layers and reference images."
+ },
+ "understandingImageToImageAndDenoising": {
+ "title": "Understanding Image-to-Image and Denoising",
+ "description": "Overview of image-to-image transformations and denoising in Invoke."
+ },
+ "exploringAIModelsAndConceptAdapters": {
+ "title": "Exploring AI Models and Concept Adapters",
+ "description": "Dive into AI models and how to use concept adapters for creative control."
+ },
+ "creatingAndComposingOnInvokesControlCanvas": {
+ "title": "Creating and Composing on Invoke's Control Canvas",
+ "description": "Learn to compose images using Invoke's control canvas."
+ },
+ "upscaling": {
+ "title": "Upscaling",
+ "description": "How to upscale images with Invoke's tools to enhance resolution."
+ },
+ "howDoIGenerateAndSaveToTheGallery": {
+ "title": "How Do I Generate and Save to the Gallery?",
+ "description": "Steps to generate and save images to the gallery."
+ },
+ "howDoIEditOnTheCanvas": {
+ "title": "How Do I Edit on the Canvas?",
+ "description": "Guide to editing images directly on the canvas."
+ },
+ "howDoIDoImageToImageTransformation": {
+ "title": "How Do I Do Image-to-Image Transformation?",
+ "description": "Tutorial on performing image-to-image transformations in Invoke."
+ },
+ "howDoIUseControlNetsAndControlLayers": {
+ "title": "How Do I Use Control Nets and Control Layers?",
+ "description": "Learn to apply control layers and controlnets to your images."
+ },
+ "howDoIUseGlobalIPAdaptersAndReferenceImages": {
+ "title": "How Do I Use Global IP Adapters and Reference Images?",
+ "description": "Introduction to adding reference images and global IP adapters."
+ },
+ "howDoIUseInpaintMasks": {
+ "title": "How Do I Use Inpaint Masks?",
+ "description": "How to apply inpaint masks for image correction and variation."
+ },
+ "howDoIOutpaint": {
+ "title": "How Do I Outpaint?",
+ "description": "Guide to outpainting beyond the original image borders."
+ }
+ }
}
}
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 67003f5b62e..2902c344adb 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -27,6 +27,7 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
+import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
import { configChanged } from 'features/system/store/configSlice';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
@@ -108,6 +109,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
+
);
};
diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx
new file mode 100644
index 00000000000..66e204c90dd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx
@@ -0,0 +1,30 @@
+import { ExternalLink, Flex, Spacer, Text } from '@invoke-ai/ui-library';
+import type { VideoData } from 'features/system/components/VideosModal/data';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+const formatTime = ({ minutes, seconds }: { minutes: number; seconds: number }) => {
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
+};
+
+export const VideoCard = memo(({ video }: { video: VideoData }) => {
+ const { t } = useTranslation();
+ const { tKey, link, length } = video;
+ return (
+
+
+
+ {t(`supportVideos.videos.${tKey}.title`)}
+
+
+ {formatTime(length)}
+
+
+
+ {t(`supportVideos.videos.${tKey}.description`)}
+
+
+ );
+});
+
+VideoCard.displayName = 'VideoCard';
diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx
new file mode 100644
index 00000000000..4140b05c03d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx
@@ -0,0 +1,20 @@
+import { Divider } from '@invoke-ai/ui-library';
+import { StickyScrollable } from 'features/system/components/StickyScrollable';
+import { gettingStartedVideos, type VideoData } from 'features/system/components/VideosModal/data';
+import { VideoCard } from 'features/system/components/VideosModal/VideoCard';
+import { Fragment, memo } from 'react';
+
+export const VideoCardList = memo(({ category, videos }: { category: string; videos: VideoData[] }) => {
+ return (
+
+ {videos.map((video, i) => (
+
+
+ {i < gettingStartedVideos.length - 1 && }
+
+ ))}
+
+ );
+});
+
+VideoCardList.displayName = 'VideoCardList';
diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx
new file mode 100644
index 00000000000..fb8ae679390
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx
@@ -0,0 +1,44 @@
+import {
+ Flex,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+} from '@invoke-ai/ui-library';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { buildUseDisclosure } from 'common/hooks/useBoolean';
+import { controlCanvasVideos, gettingStartedVideos } from 'features/system/components/VideosModal/data';
+import { VideoCardList } from 'features/system/components/VideosModal/VideoCardList';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const [useVideosModal] = buildUseDisclosure(false);
+
+export const VideosModal = memo(() => {
+ const { t } = useTranslation();
+ const videosModal = useVideosModal();
+
+ return (
+
+
+
+ {t('supportVideos.supportVideos')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+VideosModal.displayName = 'VideosModal';
diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx
new file mode 100644
index 00000000000..7436b8ec7f7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx
@@ -0,0 +1,20 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useVideosModal } from 'features/system/components/VideosModal/VideosModal';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiYoutubeLogoFill } from 'react-icons/pi';
+
+export const VideosModalButton = memo(() => {
+ const { t } = useTranslation();
+ const videosModal = useVideosModal();
+ return (
+ }
+ boxSize={8}
+ onClick={videosModal.open}
+ />
+ );
+});
+VideosModalButton.displayName = 'VideosModalButton';
diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts b/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts
new file mode 100644
index 00000000000..5f88500dfff
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts
@@ -0,0 +1,86 @@
+/**
+ * To add a support video, you'll need to add the video to the list below.
+ *
+ * The `tKey` is a sub-key in the translation file `invokeai/frontend/web/public/locales/en.json`.
+ * Add the title and description under `supportVideos.videos`, following the existing format.
+ */
+
+export type VideoData = {
+ tKey: string;
+ link: string;
+ length: {
+ minutes: number;
+ seconds: number;
+ };
+};
+
+export const gettingStartedVideos: VideoData[] = [
+ {
+ tKey: 'creatingYourFirstImage',
+ link: 'https://www.youtube.com/watch?v=jVi2XgSGrfY&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=1&t=29s&pp=iAQB',
+ length: { minutes: 6, seconds: 0 },
+ },
+ {
+ tKey: 'usingControlLayersAndReferenceGuides',
+ link: 'https://www.youtube.com/watch?v=crgw6bEgyrw&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=2&t=70s&pp=iAQB',
+ length: { minutes: 5, seconds: 30 },
+ },
+ {
+ tKey: 'understandingImageToImageAndDenoising',
+ link: 'https://www.youtube.com/watch?v=tvj8-0s6S2U&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=3&t=1s&pp=iAQB',
+ length: { minutes: 2, seconds: 37 },
+ },
+ {
+ tKey: 'exploringAIModelsAndConceptAdapters',
+ link: 'https://www.youtube.com/watch?v=iwBmBQMZ0UA&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=4&pp=iAQB',
+ length: { minutes: 8, seconds: 52 },
+ },
+ {
+ tKey: 'creatingAndComposingOnInvokesControlCanvas',
+ link: 'https://www.youtube.com/watch?v=MohWv5GZVGM&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=5&t=28s&pp=iAQB',
+ length: { minutes: 13, seconds: 56 },
+ },
+ {
+ tKey: 'upscaling',
+ link: 'https://www.youtube.com/watch?v=OCb19_P0nro&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=6&t=2s&pp=iAQB',
+ length: { minutes: 4, seconds: 0 },
+ },
+];
+
+export const controlCanvasVideos: VideoData[] = [
+ {
+ tKey: 'howDoIGenerateAndSaveToTheGallery',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=dbjmBc1iDAUpE1k5&t=26',
+ length: { minutes: 0, seconds: 49 },
+ },
+ {
+ tKey: 'howDoIEditOnTheCanvas',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=U_bFl9HsvSuejbxp&t=76',
+ length: { minutes: 0, seconds: 58 },
+ },
+ {
+ tKey: 'howDoIDoImageToImageTransformation',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=fjhTeY-yZ3qsEzEM&t=138',
+ length: { minutes: 0, seconds: 51 },
+ },
+ {
+ tKey: 'howDoIUseControlNetsAndControlLayers',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=x5KcYvkHbvR9ifsX&t=192',
+ length: { minutes: 1, seconds: 41 },
+ },
+ {
+ tKey: 'howDoIUseGlobalIPAdaptersAndReferenceImages',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=O940rNHiHGKXknK2&t=297',
+ length: { minutes: 0, seconds: 43 },
+ },
+ {
+ tKey: 'howDoIUseInpaintMasks',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=3DZhmerkzUmvJJSn&t=345',
+ length: { minutes: 1, seconds: 9 },
+ },
+ {
+ tKey: 'howDoIOutpaint',
+ link: 'https://youtu.be/Tl-69JvwJ2s?si=IIwkGZLq1PfLf80Q&t=420',
+ length: { minutes: 0, seconds: 48 },
+ },
+];
diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
index 2dbb87e8b50..a5925d8de58 100644
--- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
@@ -4,6 +4,7 @@ import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
+import { VideosModalButton } from 'features/system/components/VideosModal/VideosModalButton';
import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -39,6 +40,7 @@ export const VerticalNavBar = memo(() => {
+
{customNavComponent ? customNavComponent : }
);