added hf token validation

This commit is contained in:
samit 2026-02-19 21:05:27 -08:00
commit 035f765130
6 changed files with 148 additions and 0 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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";

View 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;
}