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(null); export function GenerationOptionsProvider({ children }: { children: ReactNode }) { const [data, setData] = useState(null); const [err, setErr] = useState(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 (
页面暂时打不开,请稍后再试。 {err ? ( ({err}) ) : null}
); } if (!data) { return (
加载中…
); } return ( {children} ); } 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; }