Skip to content

Commit bdb52cf

Browse files
feat(ui): set HF token in MM tab
- Display a toast on UI launch if the HF token is invalid - Show form in MM if token is invalid or unable to be verified, let user set the token via this form
1 parent 3f6f819 commit bdb52cf

File tree

8 files changed

+212
-1
lines changed

8 files changed

+212
-1
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,14 @@
638638
"huggingFacePlaceholder": "owner/model-name",
639639
"huggingFaceRepoID": "HuggingFace Repo ID",
640640
"huggingFaceHelper": "If multiple models are found in this repo, you will be prompted to select one to install.",
641+
"hfToken": "HuggingFace Token",
642+
"hfTokenHelperText": "A HF token is required to use checkpoint models. Click here to create or get your token.",
643+
"hfTokenInvalid": "Invalid or Missing HF Token",
644+
"hfTokenInvalidErrorMessage": "Invalid or missing HuggingFace token.",
645+
"hfTokenInvalidErrorMessage2": "Update it in the ",
646+
"hfTokenUnableToVerify": "Unable to Verify HF Token",
647+
"hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.",
648+
"hfTokenSaved": "HF Token Saved",
641649
"imageEncoderModelId": "Image Encoder Model ID",
642650
"installQueue": "Install Queue",
643651
"inplaceInstall": "In-place install",

invokeai/frontend/web/src/app/components/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
1111
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
1212
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
1313
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
14+
import { useHFLoginToast } from 'features/modelManagerV2/hooks/useHFLoginToast';
1415
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
1516
import { configChanged } from 'features/system/store/configSlice';
1617
import { languageSelector } from 'features/system/store/systemSelectors';
@@ -70,6 +71,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
7071
}, [dispatch]);
7172

7273
useStarterModelsToast();
74+
useHFLoginToast()
7375

7476
return (
7577
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>

invokeai/frontend/web/src/app/types/invokeai.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export type AppFeature =
2525
| 'prependQueue'
2626
| 'invocationCache'
2727
| 'bulkDownload'
28-
| 'starterModels';
28+
| 'starterModels'
29+
| 'hfToken';
2930

3031
/**
3132
* A disable-able Stable Diffusion feature
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
Button,
3+
ExternalLink,
4+
Flex,
5+
FormControl,
6+
FormErrorMessage,
7+
FormHelperText,
8+
FormLabel,
9+
Input,
10+
useToast,
11+
} from '@invoke-ai/ui-library';
12+
import { skipToken } from '@reduxjs/toolkit/query';
13+
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
14+
import type { ChangeEvent } from 'react';
15+
import { useCallback, useMemo, useState } from 'react';
16+
import { useTranslation } from 'react-i18next';
17+
import { useGetHFTokenStatusQuery, useSetHFTokenMutation } from 'services/api/endpoints/models';
18+
19+
export const HFToken = () => {
20+
const { t } = useTranslation();
21+
const isEnabled = useFeatureStatus('hfToken').isFeatureEnabled;
22+
const [token, setToken] = useState('');
23+
const { currentData } = useGetHFTokenStatusQuery(isEnabled ? undefined : skipToken);
24+
const [trigger, { isLoading }] = useSetHFTokenMutation();
25+
const toast = useToast();
26+
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
27+
setToken(e.target.value);
28+
}, []);
29+
const onClick = useCallback(() => {
30+
trigger({ token })
31+
.unwrap()
32+
.then((res) => {
33+
if (res === 'valid') {
34+
setToken('');
35+
toast({
36+
title: t('modelManager.hfTokenSaved'),
37+
status: 'success',
38+
duration: 3000,
39+
});
40+
}
41+
});
42+
}, [t, toast, token, trigger]);
43+
44+
const error = useMemo(() => {
45+
if (!currentData || isLoading) {
46+
return null;
47+
}
48+
if (currentData === 'invalid') {
49+
return t('modelManager.hfTokenInvalidErrorMessage');
50+
}
51+
if (currentData === 'unknown') {
52+
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
53+
}
54+
return null;
55+
}, [currentData, isLoading, t]);
56+
57+
if (!currentData || currentData === 'valid') {
58+
return null;
59+
}
60+
61+
return (
62+
<Flex borderRadius="base" w="full">
63+
<FormControl isInvalid={Boolean(error)} orientation="vertical">
64+
<FormLabel>{t('modelManager.hfToken')}</FormLabel>
65+
<Flex gap={3} alignItems="center" w="full">
66+
<Input type="password" value={token} onChange={onChange} />
67+
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
68+
{t('common.save')}
69+
</Button>
70+
</Flex>
71+
<FormHelperText>
72+
<ExternalLink
73+
label={t('modelManager.hfTokenHelperText')}
74+
href="https://huggingface.co/settings/tokens"
75+
/>
76+
</FormHelperText>
77+
<FormErrorMessage>{error}</FormErrorMessage>
78+
</FormControl>
79+
</Flex>
80+
);
81+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Button, Text, useToast } from '@invoke-ai/ui-library';
2+
import { skipToken } from '@reduxjs/toolkit/query';
3+
import { useAppDispatch } from 'app/store/storeHooks';
4+
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
5+
import { setActiveTab } from 'features/ui/store/uiSlice';
6+
import { t } from 'i18next';
7+
import { useCallback, useEffect, useState } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { useGetHFTokenStatusQuery } from 'services/api/endpoints/models';
10+
import type { S } from 'services/api/types';
11+
12+
const FEATURE_ID = 'hfToken';
13+
14+
const getTitle = (token_status: S['HFTokenStatus']) => {
15+
switch (token_status) {
16+
case 'invalid':
17+
return t('modelManager.hfTokenInvalid');
18+
case 'unknown':
19+
return t('modelManager.hfTokenUnableToVerify');
20+
}
21+
};
22+
23+
export const useHFLoginToast = () => {
24+
const { t } = useTranslation();
25+
const isEnabled = useFeatureStatus(FEATURE_ID).isFeatureEnabled;
26+
const [didToast, setDidToast] = useState(false);
27+
const { data } = useGetHFTokenStatusQuery(isEnabled ? undefined : skipToken);
28+
const toast = useToast();
29+
30+
useEffect(() => {
31+
if (toast.isActive(FEATURE_ID)) {
32+
if (data === 'valid') {
33+
setDidToast(true);
34+
toast.close(FEATURE_ID);
35+
}
36+
return;
37+
}
38+
if (data && data !== 'valid' && !didToast && isEnabled) {
39+
const title = getTitle(data);
40+
toast({
41+
id: FEATURE_ID,
42+
title,
43+
description: <ToastDescription token_status={data} />,
44+
status: 'info',
45+
isClosable: true,
46+
duration: null,
47+
onCloseComplete: () => setDidToast(true),
48+
});
49+
}
50+
}, [data, didToast, isEnabled, t, toast]);
51+
};
52+
53+
type Props = {
54+
token_status: S['HFTokenStatus'];
55+
};
56+
57+
const ToastDescription = ({ token_status }: Props) => {
58+
const { t } = useTranslation();
59+
const dispatch = useAppDispatch();
60+
const toast = useToast();
61+
62+
const onClick = useCallback(() => {
63+
dispatch(setActiveTab('modelManager'));
64+
toast.close(FEATURE_ID);
65+
}, [dispatch, toast]);
66+
67+
if (token_status === 'invalid') {
68+
return (
69+
<Text fontSize="md">
70+
{t('modelManager.hfTokenInvalidErrorMessage')}{' '}
71+
{t('modelManager.hfTokenInvalidErrorMessage2')}
72+
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
73+
{t('modelManager.modelManager')}.
74+
</Button>
75+
</Text>
76+
);
77+
}
78+
79+
if (token_status === 'unknown') {
80+
return (
81+
<Text fontSize="md">
82+
{t('modelManager.hfTokenUnableToErrorMessage')}{' '}
83+
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
84+
{t('modelManager.modelManager')}.
85+
</Button>
86+
</Text>
87+
);
88+
}
89+
};

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Button, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
22
import { useAppDispatch } from 'app/store/storeHooks';
3+
import { HFToken } from 'features/modelManagerV2/components/HFToken';
34
import { SyncModelsButton } from 'features/modelManagerV2/components/SyncModels/SyncModelsButton';
45
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
56
import { useCallback } from 'react';
@@ -27,6 +28,7 @@ export const ModelManager = () => {
2728
</Button>
2829
</Flex>
2930
<Flex flexDir="column" layerStyle="second" p={4} gap={4} borderRadius="base" w="full" h="full">
31+
<HFToken />
3032
<ModelListNavigation />
3133
<ModelList />
3234
</Flex>

invokeai/frontend/web/src/services/api/endpoints/models.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ type GetModelConfigsResponse = NonNullable<
2727
paths['/api/v2/models/']['get']['responses']['200']['content']['application/json']
2828
>;
2929

30+
type GetHFTokenStatusResponse =
31+
paths['/api/v2/models/hf_login']['get']['responses']['200']['content']['application/json'];
32+
type SetHFTokenResponse = NonNullable<
33+
paths['/api/v2/models/hf_login']['post']['responses']['200']['content']['application/json']
34+
>;
35+
type SetHFTokenArg = NonNullable<
36+
paths['/api/v2/models/hf_login']['post']['requestBody']['content']['application/json']
37+
>;
38+
3039
export type GetStarterModelsResponse =
3140
paths['/api/v2/models/starter_models']['get']['responses']['200']['content']['application/json'];
3241

@@ -265,6 +274,22 @@ export const modelsApi = api.injectEndpoints({
265274
getStarterModels: build.query<GetStarterModelsResponse, void>({
266275
query: () => buildModelsUrl('starter_models'),
267276
}),
277+
getHFTokenStatus: build.query<GetHFTokenStatusResponse, void>({
278+
query: () => buildModelsUrl('hf_login'),
279+
providesTags: ['HFTokenStatus'],
280+
}),
281+
setHFToken: build.mutation<SetHFTokenResponse, SetHFTokenArg>({
282+
query: (body) => ({ url: buildModelsUrl('hf_login'), method: 'POST', body }),
283+
invalidatesTags: ['HFTokenStatus'],
284+
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
285+
try {
286+
const { data } = await queryFulfilled;
287+
dispatch(modelsApi.util.updateQueryData('getHFTokenStatus', undefined, () => data));
288+
} catch {
289+
// no-op
290+
}
291+
},
292+
}),
268293
}),
269294
});
270295

@@ -284,4 +309,6 @@ export const {
284309
useCancelModelInstallMutation,
285310
usePruneCompletedModelInstallsMutation,
286311
useGetStarterModelsQuery,
312+
useGetHFTokenStatusQuery,
313+
useSetHFTokenMutation,
287314
} = modelsApi;

invokeai/frontend/web/src/services/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const tagTypes = [
1212
'Board',
1313
'BoardImagesTotal',
1414
'BoardAssetsTotal',
15+
'HFTokenStatus',
1516
'Image',
1617
'ImageNameList',
1718
'ImageList',

0 commit comments

Comments
 (0)