diff --git a/invokeai/frontend/web/src/common/components/InvModal/theme.ts b/invokeai/frontend/web/src/common/components/InvModal/theme.ts
index 3596feb7a2..af587d53b7 100644
--- a/invokeai/frontend/web/src/common/components/InvModal/theme.ts
+++ b/invokeai/frontend/web/src/common/components/InvModal/theme.ts
@@ -16,6 +16,7 @@ export const baseStyle = definePartsStyle(() => ({
header: {
fontWeight: 'semibold',
fontSize: 'lg',
+ color: 'base.300'
},
closeButton: {
opacity: 0.5,
diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx
index 285e8a76b1..b0ca291cbb 100644
--- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx
@@ -7,7 +7,6 @@ import {
useDisclosure,
} from '@chakra-ui/react';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
-import { InvHeading } from 'common/components/InvHeading/wrapper';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvInput } from 'common/components/InvInput/InvInput';
import {
@@ -22,6 +21,7 @@ import {
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import type { HotkeyGroup } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useHotkeyData } from 'features/system/components/HotkeysModal/useHotkeyData';
+import { StickyScrollable } from 'features/system/components/StickyScrollable';
import type { ChangeEventHandler, ReactElement } from 'react';
import {
cloneElement,
@@ -121,26 +121,9 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
- {filteredHotkeyGroups.map((group) => (
-
-
- {group.title}
-
-
+
+ {filteredHotkeyGroups.map((group) => (
+
{group.hotkeyListItems.map((hotkey, i) => (
{
{i < group.hotkeyListItems.length - 1 && }
))}
-
-
- ))}
- {!filteredHotkeyGroups.length && (
-
- )}
+
+ ))}
+ {!filteredHotkeyGroups.length && (
+
+ )}
+
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx
deleted file mode 100644
index b67ccc9099..0000000000
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Heading } from '@chakra-ui/react';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { InvButton } from 'common/components/InvButton/InvButton';
-import { InvText } from 'common/components/InvText/wrapper';
-import { resetCanvas } from 'features/canvas/store/canvasSlice';
-import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
-import { addToast } from 'features/system/store/systemSlice';
-import { memo, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- useClearIntermediatesMutation,
- useGetIntermediatesCountQuery,
-} from 'services/api/endpoints/images';
-import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
-
-import StyledFlex from './StyledFlex';
-
-const SettingsClearIntermediates = () => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
-
- const { data: intermediatesCount } = useGetIntermediatesCountQuery(
- undefined,
- {
- refetchOnMountOrArgChange: true,
- }
- );
-
- const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
- useClearIntermediatesMutation();
-
- const { data: queueStatus } = useGetQueueStatusQuery();
- const hasPendingItems =
- queueStatus &&
- (queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0);
-
- const handleClickClearIntermediates = useCallback(() => {
- if (hasPendingItems) {
- return;
- }
-
- clearIntermediates()
- .unwrap()
- .then((clearedCount) => {
- dispatch(controlAdaptersReset());
- dispatch(resetCanvas());
- dispatch(
- addToast({
- title: t('settings.intermediatesCleared', { count: clearedCount }),
- status: 'info',
- })
- );
- })
- .catch(() => {
- dispatch(
- addToast({
- title: t('settings.intermediatesClearedFailed'),
- status: 'error',
- })
- );
- });
- }, [t, clearIntermediates, dispatch, hasPendingItems]);
-
- return (
-
- {t('settings.clearIntermediates')}
-
- {t('settings.clearIntermediatesWithCount', {
- count: intermediatesCount ?? 0,
- })}
-
-
- {t('settings.clearIntermediatesDesc1')}
-
-
- {t('settings.clearIntermediatesDesc2')}
-
-
- {t('settings.clearIntermediatesDesc3')}
-
-
- );
-};
-
-export default memo(SettingsClearIntermediates);
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
index 4e9a0f2b61..f524450819 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
@@ -4,7 +4,6 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButton } from 'common/components/InvButton/InvButton';
import { InvControl } from 'common/components/InvControl/InvControl';
-import { InvHeading } from 'common/components/InvHeading/wrapper';
import {
InvModal,
InvModalBody,
@@ -16,8 +15,11 @@ import {
} from 'common/components/InvModal/wrapper';
import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import { InvText } from 'common/components/InvText/wrapper';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice';
+import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates';
+import { StickyScrollable } from 'features/system/components/StickyScrollable';
import {
setEnableImageDebugging,
setShouldConfirmOnDelete,
@@ -33,10 +35,8 @@ import { cloneElement, memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
-import SettingsClearIntermediates from './SettingsClearIntermediates';
import { SettingsLanguageSelect } from './SettingsLanguageSelect';
import { SettingsLogLevelSelect } from './SettingsLogLevelSelect';
-import StyledFlex from './StyledFlex';
const selector = createMemoizedSelector(
[stateSelector],
@@ -110,6 +110,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
}),
});
+ const {
+ clearIntermediates,
+ hasPendingItems,
+ intermediatesCount,
+ isLoading: isLoadingClearIntermediates,
+ } = useClearIntermediates();
+
const {
isOpen: isSettingsModalOpen,
onOpen: onSettingsModalOpen,
@@ -218,120 +225,143 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
isCentered
>
-
+
{t('common.settingsLabel')}
-
-
-
- {t('settings.general')}
-
-
-
-
-
-
- {t('settings.generation')}
-
-
-
-
-
-
-
-
-
- {t('settings.ui')}
-
-
-
-
-
-
-
-
-
- {shouldShowLocalizationToggle && }
-
-
-
-
-
- {shouldShowDeveloperSettings && (
-
- {t('settings.developer')}
-
+
+
+
+
+
-
-
+
+
+
+
-
- )}
+
+
+
+
- {shouldShowClearIntermediates && }
+
+
+
+
+
+
+
+
+
+
+ {shouldShowLocalizationToggle && }
+
+
+
+
-
- {t('settings.resetWebUI')}
-
- {t('settings.resetWebUI')}
-
- {shouldShowResetWebUiText && (
- <>
-
- {t('settings.resetWebUIDesc1')}
-
-
- {t('settings.resetWebUIDesc2')}
-
- >
+ {shouldShowDeveloperSettings && (
+
+
+
+
+
+
+
+
+
)}
-
-
+
+ {shouldShowClearIntermediates && (
+
+
+ {t('settings.clearIntermediatesWithCount', {
+ count: intermediatesCount ?? 0,
+ })}
+
+
+ {t('settings.clearIntermediatesDesc1')}
+
+
+ {t('settings.clearIntermediatesDesc2')}
+
+
+ {t('settings.clearIntermediatesDesc3')}
+
+
+ )}
+
+
+
+ {t('settings.resetWebUI')}
+
+ {shouldShowResetWebUiText && (
+ <>
+
+ {t('settings.resetWebUIDesc1')}
+
+
+ {t('settings.resetWebUIDesc2')}
+
+ >
+ )}
+
+
+
-
-
- {t('common.close')}
-
-
+
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts
new file mode 100644
index 0000000000..90f5dee8b0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts
@@ -0,0 +1,71 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import { resetCanvas } from 'features/canvas/store/canvasSlice';
+import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
+import { addToast } from 'features/system/store/systemSlice';
+import { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ useClearIntermediatesMutation,
+ useGetIntermediatesCountQuery,
+} from 'services/api/endpoints/images';
+import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
+
+export type UseClearIntermediatesReturn = {
+ intermediatesCount: number | undefined;
+ clearIntermediates: () => void;
+ isLoading: boolean;
+ hasPendingItems: boolean;
+};
+
+export const useClearIntermediates = (): UseClearIntermediatesReturn => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+
+ const { data: intermediatesCount } = useGetIntermediatesCountQuery(
+ undefined,
+ {
+ refetchOnMountOrArgChange: true,
+ }
+ );
+
+ const [_clearIntermediates, { isLoading }] = useClearIntermediatesMutation();
+
+ const { data: queueStatus } = useGetQueueStatusQuery();
+ const hasPendingItems = useMemo(
+ () =>
+ Boolean(
+ queueStatus &&
+ (queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0)
+ ),
+ [queueStatus]
+ );
+
+ const clearIntermediates = useCallback(() => {
+ if (hasPendingItems) {
+ return;
+ }
+
+ _clearIntermediates()
+ .unwrap()
+ .then((clearedCount) => {
+ dispatch(controlAdaptersReset());
+ dispatch(resetCanvas());
+ dispatch(
+ addToast({
+ title: t('settings.intermediatesCleared', { count: clearedCount }),
+ status: 'info',
+ })
+ );
+ })
+ .catch(() => {
+ dispatch(
+ addToast({
+ title: t('settings.intermediatesClearedFailed'),
+ status: 'error',
+ })
+ );
+ });
+ }, [t, _clearIntermediates, dispatch, hasPendingItems]);
+
+ return { intermediatesCount, clearIntermediates, isLoading, hasPendingItems };
+};
diff --git a/invokeai/frontend/web/src/features/system/components/StickyScrollable.tsx b/invokeai/frontend/web/src/features/system/components/StickyScrollable.tsx
new file mode 100644
index 0000000000..87c81f7e8a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/StickyScrollable.tsx
@@ -0,0 +1,49 @@
+import { Flex } from '@chakra-ui/layout';
+import { InvHeading } from 'common/components/InvHeading/wrapper';
+import type { PropsWithChildren } from 'react';
+import { memo } from 'react';
+
+export type StickyScrollableHeadingProps = {
+ title: string;
+};
+
+export const StickyScrollableHeading = memo(
+ (props: StickyScrollableHeadingProps) => {
+ return (
+
+ {props.title}
+
+ );
+ }
+);
+
+StickyScrollableHeading.displayName = 'StickyScrollableHeading';
+
+export type StickyScrollableContentProps = PropsWithChildren;
+
+export const StickyScrollableContent = memo(
+ (props: StickyScrollableContentProps) => {
+ return (
+
+ {props.children}
+
+ );
+ }
+);
+
+StickyScrollableContent.displayName = 'StickyScrollableContent';
+
+export type StickyScrollableProps = PropsWithChildren<{
+ title: string;
+}>;
+
+export const StickyScrollable = memo((props: StickyScrollableProps) => {
+ return (
+
+
+ {props.children}
+
+ );
+});
+
+StickyScrollable.displayName = 'StickyScrollable';