added hf token validation
This commit is contained in:
parent
b6799a21a5
commit
035f765130
6 changed files with 148 additions and 0 deletions
|
|
@ -35,6 +35,7 @@ import {
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { cn, formatCompact } from "@/lib/utils";
|
||||
|
|
@ -103,10 +104,14 @@ export function DatasetStep() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -179,6 +184,23 @@ export function DatasetStep() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfModelSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -80,11 +81,15 @@ export function ModelSelectionStep() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -126,6 +131,23 @@ export function ModelSelectionStep() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -102,10 +103,14 @@ export function DatasetSection() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => {
|
||||
const ids = hfResults.map((r) => r.id);
|
||||
if (dataset && !ids.includes(dataset)) {
|
||||
|
|
@ -239,6 +244,23 @@ export function DatasetSection() {
|
|||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<HfDatasetSubsetSplitSelectors
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
useDebouncedValue,
|
||||
useGpuInfo,
|
||||
useHfModelSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -152,11 +153,15 @@ export function ModelSection() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => {
|
||||
const ids = hfResults.map((r) => r.id);
|
||||
if (selectedModel && !ids.includes(selectedModel)) {
|
||||
|
|
@ -555,6 +560,23 @@ export function ModelSection() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ export { useHardwareInfo } from "./use-hardware-info";
|
|||
export { useHfModelSearch } from "./use-hf-model-search";
|
||||
export { useHfDatasetSearch } from "./use-hf-dataset-search";
|
||||
export { useHfDatasetSplits } from "./use-hf-dataset-splits";
|
||||
export { useHfTokenValidation } from "./use-hf-token-validation";
|
||||
export { useInfiniteScroll } from "./use-infinite-scroll";
|
||||
|
|
|
|||
59
studio/frontend/src/hooks/use-hf-token-validation.ts
Normal file
59
studio/frontend/src/hooks/use-hf-token-validation.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { whoAmI } from "@huggingface/hub";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDebouncedValue } from "./use-debounced-value";
|
||||
|
||||
export interface HfTokenValidationState {
|
||||
isValid: boolean | null;
|
||||
error: string | null;
|
||||
isChecking: boolean;
|
||||
}
|
||||
|
||||
const INITIAL: HfTokenValidationState = {
|
||||
isValid: null,
|
||||
error: null,
|
||||
isChecking: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the Hugging Face token by calling the whoami-v2 API.
|
||||
* Debounces the token to avoid excessive requests while typing.
|
||||
* Returns validation state: isValid (null = not checked), error message, and isChecking.
|
||||
*/
|
||||
export function useHfTokenValidation(token: string): HfTokenValidationState {
|
||||
const debouncedToken = useDebouncedValue(token.trim(), 500);
|
||||
const [state, setState] = useState<HfTokenValidationState>(INITIAL);
|
||||
const versionRef = useRef(0);
|
||||
|
||||
const runCheck = useCallback(async (t: string) => {
|
||||
if (!t) {
|
||||
setState({ isValid: null, error: null, isChecking: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const v = ++versionRef.current;
|
||||
setState((prev) => ({ ...prev, isChecking: true, error: null }));
|
||||
|
||||
try {
|
||||
await whoAmI({ accessToken: t });
|
||||
if (versionRef.current !== v) return;
|
||||
setState({ isValid: true, error: null, isChecking: false });
|
||||
} catch {
|
||||
if (versionRef.current !== v) return;
|
||||
setState({
|
||||
isValid: false,
|
||||
error: "invalid or expired token",
|
||||
isChecking: false,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedToken) {
|
||||
setState(INITIAL);
|
||||
return;
|
||||
}
|
||||
runCheck(debouncedToken);
|
||||
}, [debouncedToken, runCheck]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue