File size: 4,292 Bytes
6cc3d86
 
 
 
 
 
 
 
982bad4
 
6cc3d86
 
 
38b7ac0
6cc3d86
 
 
 
a70128c
38b7ac0
 
6cc3d86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982bad4
6cc3d86
06e36cd
 
 
 
6cc3d86
 
 
 
 
 
 
 
 
 
 
 
 
 
06e36cd
 
6cc3d86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6249f41
6cc3d86
 
 
df31fd7
6cc3d86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a70128c
 
 
 
 
38b7ac0
 
 
 
 
6cc3d86
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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;
}