Gemini-VideoGeneration / web /frontend /src /context /GenerationOptionsContext.tsx
LehongWu's picture
Upload folder using huggingface_hub
38b7ac0 verified
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
import { apiFetch } from "../api";
export type LabeledModel = { value: string; label: string };
export type ThinkingLevelOption = { value: string; label: string };
/** Video model entry; `supports_reference_images` / `supports_4k` / `supports_end_frame` default to true if omitted. */
export type VideoModelOption = {
value: string;
label: string;
supports_reference_images?: boolean;
supports_4k?: boolean;
/** Start/end mode: if false, only start frame (e.g. Veo 3 family). */
supports_end_frame?: boolean;
};
export type GenerationOptions = {
image: {
models: LabeledModel[];
aspect_ratios: string[];
resolutions: string[];
/** @deprecated No longer used by the UI; image model + thinking are combined in ImageGen. */
thinking_levels?: ThinkingLevelOption[];
};
video: {
models: VideoModelOption[];
aspect_ratios: string[];
resolutions: string[];
durations_seconds: number[];
};
video_frames: {
models: VideoModelOption[];
aspect_ratios: string[];
resolutions: string[];
durations_seconds: number[];
};
};
const GenerationOptionsContext = createContext<GenerationOptions | null>(null);
export function GenerationOptionsProvider({ children }: { children: ReactNode }) {
const [data, setData] = useState<GenerationOptions | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
apiFetch("/api/config/generation-options")
.then(async (r) => {
if (r.status === 401) {
window.location.assign("/login");
return null;
}
const j = await r.json().catch(() => ({}));
if (!r.ok) {
const detail = (j as { detail?: unknown }).detail;
const msg =
typeof detail === "string"
? detail
: Array.isArray(detail)
? JSON.stringify(detail)
: r.statusText;
throw new Error(msg);
}
return j as GenerationOptions;
})
.then((opts) => {
if (cancelled || opts === null) return;
setData(opts);
})
.catch((e: unknown) => {
if (!cancelled)
setErr(e instanceof Error ? e.message : "Failed to load options");
});
return () => {
cancelled = true;
};
}, []);
if (err) {
return (
<div className="p-8 text-sm text-red-800">
页面暂时打不开,请稍后再试。
{err ? (
<span className="block mt-2 text-xs text-mist">({err})</span>
) : null}
</div>
);
}
if (!data) {
return (
<div className="p-8 text-sm text-mist">加载中…</div>
);
}
return (
<GenerationOptionsContext.Provider value={data}>
{children}
</GenerationOptionsContext.Provider>
);
}
const FAST_MODEL_IDS = new Set([
"gemini-3.1-flash-image-preview",
"veo-3.0-fast-generate-001",
"veo-3.1-fast-generate-preview",
]);
/** Prefer Flash/Fast id, else label 快速, else first or `fallback`. */
export function defaultFastModelValue(
models: { value: string; label?: string }[],
fallback: string,
): string {
const byId = models.find((m) => FAST_MODEL_IDS.has(m.value));
if (byId) return byId.value;
const byLabel = models.find((m) => m.label?.includes("快速"));
return byLabel?.value ?? models[0]?.value ?? fallback;
}
export function modelSupportsReferenceImages(
models: VideoModelOption[],
value: string,
): boolean {
const m = models.find((x) => x.value === value);
return m?.supports_reference_images !== false;
}
export function modelSupports4k(models: VideoModelOption[], value: string): boolean {
const m = models.find((x) => x.value === value);
return m?.supports_4k !== false;
}
export function modelSupportsEndFrame(models: VideoModelOption[], value: string): boolean {
const m = models.find((x) => x.value === value);
return m?.supports_end_frame !== false;
}
export function useGenerationOptions(): GenerationOptions {
const ctx = useContext(GenerationOptionsContext);
if (!ctx) {
throw new Error("useGenerationOptions must be used inside GenerationOptionsProvider");
}
return ctx;
}