feat(ui): add UI to reset hf token

This commit is contained in:
psychedelicious
2025-05-05 15:14:46 +10:00
parent fd20582cdd
commit ee4002607c
5 changed files with 111 additions and 51 deletions

View File

@@ -12,63 +12,45 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery, useSetHFTokenMutation } from 'services/api/endpoints/models';
import { UNAUTHORIZED_TOAST_ID } from 'services/events/onModelInstallError';
import {
useGetHFTokenStatusQuery,
useResetHFTokenMutation,
useSetHFTokenMutation,
} from 'services/api/endpoints/models';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const HFToken = () => {
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const [token, setToken] = useState('');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
const [trigger, { isLoading, isUninitialized }] = useSetHFTokenMutation();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
}, []);
const onClick = useCallback(() => {
trigger({ token })
.unwrap()
.then((res) => {
if (res === 'valid') {
setToken('');
toast({
id: UNAUTHORIZED_TOAST_ID,
title: t('modelManager.hfTokenSaved'),
status: 'success',
duration: 3000,
});
}
});
}, [t, token, trigger]);
const error = useMemo(() => {
if (!currentData || isUninitialized || isLoading) {
return null;
switch (currentData) {
case 'invalid':
return t('modelManager.hfTokenInvalidErrorMessage');
case 'unknown':
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
case 'valid':
case undefined:
return null;
default:
assert<Equals<never, typeof currentData>>(false, 'Unexpected HF token status');
}
if (currentData === 'invalid') {
return t('modelManager.hfTokenInvalidErrorMessage');
}
if (currentData === 'unknown') {
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
}
return null;
}, [currentData, isLoading, isUninitialized, t]);
}, [currentData, t]);
if (!currentData || currentData === 'valid') {
if (!currentData) {
return null;
}
return (
<Flex borderRadius="base" w="full">
<FormControl isInvalid={!isUninitialized && Boolean(error)} orientation="vertical">
<FormControl isInvalid={Boolean(error)} orientation="vertical">
<FormLabel>{t('modelManager.hfTokenLabel')}</FormLabel>
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={token} onChange={onChange} />
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
{t('common.save')}
</Button>
</Flex>
{error && <SetHFTokenInput />}
{!error && <ResetHFTokenButton />}
<FormHelperText>
<ExternalLink label={t('modelManager.hfTokenHelperText')} href="https://huggingface.co/settings/tokens" />
</FormHelperText>
@@ -77,3 +59,73 @@ export const HFToken = () => {
</Flex>
);
};
const PLACEHOLDER_TOKEN = Array.from({ length: 37 }, () => 'a').join('');
const ResetHFTokenButton = memo(() => {
const { t } = useTranslation();
const [resetHFToken, { isLoading }] = useResetHFTokenMutation();
const onClick = useCallback(() => {
resetHFToken()
.unwrap()
.then(() => {
toast({
title: t('modelManager.hfTokenReset'),
status: 'info',
});
});
}, [resetHFToken, t]);
return (
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={PLACEHOLDER_TOKEN} isDisabled />
<Button onClick={onClick} size="sm" isLoading={isLoading}>
{t('common.reset')}
</Button>
</Flex>
);
});
ResetHFTokenButton.displayName = 'ResetHFTokenButton';
const SetHFTokenInput = memo(() => {
const { t } = useTranslation();
const [token, setToken] = useState('');
const [trigger, { isLoading }] = useSetHFTokenMutation();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
}, []);
const onClick = useCallback(() => {
trigger({ token })
.unwrap()
.then((res) => {
switch (res) {
case 'valid':
setToken('');
toast({
title: t('modelManager.hfTokenSaved'),
status: 'success',
});
break;
case 'invalid':
case 'unknown':
default:
toast({
title: t('modelManager.hfTokenUnableToVerify'),
status: 'error',
});
break;
}
});
}, [t, token, trigger]);
return (
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={token} onChange={onChange} />
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
{t('common.save')}
</Button>
</Flex>
);
});
SetHFTokenInput.displayName = 'SetHFTokenInput';

View File

@@ -1,11 +1,10 @@
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { HFToken } from './HFToken';
import { HuggingFaceResults } from './HuggingFaceResults';
@@ -16,7 +15,6 @@ export const HuggingFaceForm = memo(() => {
const [errorMessage, setErrorMessage] = useState('');
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery();
const [installModel] = useInstallModel();
@@ -68,7 +66,7 @@ export const HuggingFaceForm = memo(() => {
<FormHelperText>{t('modelManager.huggingFaceHelper')}</FormHelperText>
{!!errorMessage.length && <FormErrorMessage>{errorMessage}</FormErrorMessage>}
</FormControl>
{currentData !== 'valid' && <HFToken />}
{isHFTokenEnabled && <HFToken />}
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
</Flex>
);

View File

@@ -4,13 +4,14 @@ import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import queryString from 'query-string';
import type { operations, paths } from 'services/api/schema';
import {
type AnyModelConfig,
type GetHFTokenStatusResponse,
isNonRefinerMainModelConfig,
type SetHFTokenArg,
type SetHFTokenResponse,
import type {
AnyModelConfig,
GetHFTokenStatusResponse,
ResetHFTokenResponse,
SetHFTokenArg,
SetHFTokenResponse,
} from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Param0 } from 'tsafe';
import type { ApiTagDescription } from '..';
@@ -293,6 +294,10 @@ export const modelsApi = api.injectEndpoints({
}
},
}),
resetHFToken: build.mutation<ResetHFTokenResponse, void>({
query: () => ({ url: buildModelsUrl('hf_login'), method: 'DELETE' }),
invalidatesTags: ['HFTokenStatus'],
}),
emptyModelCache: build.mutation<void, void>({
query: () => ({ url: buildModelsUrl('empty_model_cache'), method: 'POST' }),
}),
@@ -316,6 +321,7 @@ export const {
useGetStarterModelsQuery,
useGetHFTokenStatusQuery,
useSetHFTokenMutation,
useResetHFTokenMutation,
useEmptyModelCacheMutation,
} = modelsApi;

View File

@@ -321,6 +321,9 @@ export type GetHFTokenStatusResponse =
export type SetHFTokenResponse = NonNullable<
paths['/api/v2/models/hf_login']['post']['responses']['200']['content']['application/json']
>;
export type ResetHFTokenResponse = NonNullable<
paths['/api/v2/models/hf_login']['delete']['responses']['200']['content']['application/json']
>;
export type SetHFTokenArg = NonNullable<
paths['/api/v2/models/hf_login']['post']['requestBody']['content']['application/json']
>;