Wire video canvas model selectors
This commit is contained in:
parent
af74e2197b
commit
9422c5de73
@ -13,8 +13,19 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import {
|
||||
type VideoCanvasFunctionConfig,
|
||||
@ -30,6 +41,26 @@ type SettingsResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
type AiModelOption = {
|
||||
id: string;
|
||||
modelId: string;
|
||||
displayName: string;
|
||||
providerName?: string;
|
||||
providerType?: string;
|
||||
supportsVision?: boolean;
|
||||
supportsImageGen?: boolean;
|
||||
isEnabled?: boolean;
|
||||
isHidden?: boolean;
|
||||
isMultimodal?: boolean;
|
||||
capabilities?: unknown;
|
||||
};
|
||||
|
||||
type AiModelsResponse = {
|
||||
models?: AiModelOption[];
|
||||
};
|
||||
|
||||
const INHERIT_MODEL_VALUE = "__inherit__";
|
||||
|
||||
const functionKindLabels: Record<VideoCanvasFunctionDefinition["kind"], string> = {
|
||||
audio: "音频",
|
||||
image: "图片生成",
|
||||
@ -40,13 +71,125 @@ const functionKindLabels: Record<VideoCanvasFunctionDefinition["kind"], string>
|
||||
workflow: "工作流",
|
||||
};
|
||||
|
||||
function getModelOptionValue(model: AiModelOption) {
|
||||
return String(model.modelId || model.id || "").trim();
|
||||
}
|
||||
|
||||
function getModelOptionLabel(model: AiModelOption) {
|
||||
const name = model.displayName || model.modelId || model.id;
|
||||
const id = model.modelId && model.modelId !== name ? ` · ${model.modelId}` : "";
|
||||
return `${name}${id}`;
|
||||
}
|
||||
|
||||
function isImageModelOption(model: AiModelOption) {
|
||||
return isImageGenerationModelDescriptor({
|
||||
providerType: model.providerType,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function isVideoModelOption(model: AiModelOption) {
|
||||
return isVideoModelDescriptor({
|
||||
providerType: model.providerType,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function filterModelsForFunctionKind(
|
||||
models: AiModelOption[],
|
||||
kind: VideoCanvasFunctionDefinition["kind"],
|
||||
) {
|
||||
const enabled = models.filter((model) => model.isEnabled !== false && getModelOptionValue(model));
|
||||
let filtered = enabled;
|
||||
if (kind === "image" || kind === "image-edit") {
|
||||
filtered = enabled.filter(isImageModelOption);
|
||||
} else if (kind === "video") {
|
||||
filtered = enabled.filter(isVideoModelOption);
|
||||
} else if (kind === "multimodal") {
|
||||
filtered = enabled.filter(
|
||||
(model) => model.isMultimodal || model.supportsVision || (!isImageModelOption(model) && !isVideoModelOption(model)),
|
||||
);
|
||||
}
|
||||
return filtered.length ? filtered : enabled;
|
||||
}
|
||||
|
||||
function ModelSelect({
|
||||
allowInherit = false,
|
||||
kind,
|
||||
models,
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
allowInherit?: boolean;
|
||||
kind: VideoCanvasFunctionDefinition["kind"];
|
||||
models: AiModelOption[];
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}) {
|
||||
const filteredModels = filterModelsForFunctionKind(models, kind);
|
||||
const normalizedValue = value?.trim() || (allowInherit ? INHERIT_MODEL_VALUE : "auto");
|
||||
const hasCurrent =
|
||||
normalizedValue === "auto" ||
|
||||
normalizedValue === INHERIT_MODEL_VALUE ||
|
||||
filteredModels.some((model) => getModelOptionValue(model) === normalizedValue);
|
||||
|
||||
return (
|
||||
<Select
|
||||
onValueChange={(nextValue) => {
|
||||
if (nextValue === INHERIT_MODEL_VALUE) {
|
||||
onChange("");
|
||||
return;
|
||||
}
|
||||
onChange(nextValue);
|
||||
}}
|
||||
value={normalizedValue}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allowInherit ? (
|
||||
<SelectItem value={INHERIT_MODEL_VALUE}>继承全局默认模型</SelectItem>
|
||||
) : (
|
||||
<SelectItem value="auto">自动选择可用模型</SelectItem>
|
||||
)}
|
||||
{!hasCurrent && normalizedValue ? (
|
||||
<SelectItem value={normalizedValue}>当前保存值:{normalizedValue}</SelectItem>
|
||||
) : null}
|
||||
{filteredModels.map((model) => {
|
||||
const optionValue = getModelOptionValue(model);
|
||||
return (
|
||||
<SelectItem key={`${model.id}-${optionValue}`} value={optionValue}>
|
||||
{getModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
{filteredModels.length === 0 ? (
|
||||
<SelectItem disabled value="__no_models__">
|
||||
暂无已启用模型
|
||||
</SelectItem>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoCanvasSettingsClient() {
|
||||
const { data, isLoading, mutate } = useSWR<SettingsResponse>(
|
||||
`/api/admin/settings?key=${VIDEO_CANVAS_FUNCTION_CONFIG_KEY}`,
|
||||
fetcher,
|
||||
);
|
||||
const { data: aiModelsData } = useSWR<AiModelsResponse>("/api/admin/ai-models", fetcher);
|
||||
const [config, setConfig] = useState<VideoCanvasFunctionConfig>(defaultVideoCanvasFunctionConfig);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const modelOptions = useMemo(
|
||||
() => (aiModelsData?.models || []).filter((model) => model.isEnabled !== false),
|
||||
[aiModelsData?.models],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
@ -135,7 +278,7 @@ export function VideoCanvasSettingsClient() {
|
||||
<div>
|
||||
<CardTitle className="text-xl">全局模型与开关</CardTitle>
|
||||
<CardDescription>
|
||||
默认模型可填写现有后端的模型标识;保持 auto 时由各接口按当前可用模型选择。
|
||||
默认模型从后台已启用模型中选择;保持自动时由各接口按当前可用模型选择。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@ -150,25 +293,28 @@ export function VideoCanvasSettingsClient() {
|
||||
<CardContent className="grid gap-4 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="defaultImageModel">默认图片/编辑模型</Label>
|
||||
<Input
|
||||
id="defaultImageModel"
|
||||
onChange={(event) => updateRoot({ defaultImageModel: event.target.value })}
|
||||
<ModelSelect
|
||||
kind="image-edit"
|
||||
models={modelOptions}
|
||||
onChange={(value) => updateRoot({ defaultImageModel: value || "auto" })}
|
||||
value={config.defaultImageModel}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="defaultVideoModel">默认视频模型</Label>
|
||||
<Input
|
||||
id="defaultVideoModel"
|
||||
onChange={(event) => updateRoot({ defaultVideoModel: event.target.value })}
|
||||
<ModelSelect
|
||||
kind="video"
|
||||
models={modelOptions}
|
||||
onChange={(value) => updateRoot({ defaultVideoModel: value || "auto" })}
|
||||
value={config.defaultVideoModel}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="defaultMultimodalModel">默认多模态模型</Label>
|
||||
<Input
|
||||
id="defaultMultimodalModel"
|
||||
onChange={(event) => updateRoot({ defaultMultimodalModel: event.target.value })}
|
||||
<ModelSelect
|
||||
kind="multimodal"
|
||||
models={modelOptions}
|
||||
onChange={(value) => updateRoot({ defaultMultimodalModel: value || "auto" })}
|
||||
value={config.defaultMultimodalModel}
|
||||
/>
|
||||
</div>
|
||||
@ -212,10 +358,11 @@ export function VideoCanvasSettingsClient() {
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_120px]">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`model-${item.key}`}>模型覆盖</Label>
|
||||
<Input
|
||||
id={`model-${item.key}`}
|
||||
onChange={(event) => updateFunction(item.key, { model: event.target.value })}
|
||||
placeholder="留空使用全局默认模型"
|
||||
<ModelSelect
|
||||
allowInherit
|
||||
kind={item.kind}
|
||||
models={modelOptions}
|
||||
onChange={(value) => updateFunction(item.key, { model: value })}
|
||||
value={item.model || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -19,6 +19,8 @@ const requestSchema = z.object({
|
||||
imageUrls: z.array(z.string().trim()).optional(),
|
||||
videoUrls: z.array(z.string().trim()).optional(),
|
||||
nodeTitle: z.string().trim().optional(),
|
||||
preferredModelRef: z.string().trim().optional(),
|
||||
model: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
function sanitizeUrls(value: unknown, limit = 6) {
|
||||
@ -263,6 +265,7 @@ export async function POST(request: Request) {
|
||||
const prompt = normalizeText(parsed.data.prompt);
|
||||
const imageUrls = sanitizeUrls(parsed.data.imageUrls, 4);
|
||||
const videoUrls = sanitizeUrls(parsed.data.videoUrls, 4);
|
||||
const preferredModelRef = normalizeText(parsed.data.preferredModelRef || parsed.data.model);
|
||||
if (!prompt && imageUrls.length === 0 && videoUrls.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "请至少提供脚本、图片或视频上下文" },
|
||||
@ -278,14 +281,25 @@ export async function POST(request: Request) {
|
||||
(item) => !item.model.isSubModel && item.model.isEnabled
|
||||
);
|
||||
|
||||
const preferredModel =
|
||||
preferredModelRef
|
||||
? compatibleModels.find(
|
||||
(item) =>
|
||||
item.model.id === preferredModelRef ||
|
||||
item.model.modelId === preferredModelRef,
|
||||
)
|
||||
: null;
|
||||
|
||||
let selectedModel =
|
||||
imageUrls.length > 0 &&
|
||||
agentModel &&
|
||||
supportsVision(agentModel.model)
|
||||
? agentModel
|
||||
: imageUrls.length > 0
|
||||
? compatibleModels.find((item) => supportsVision(item.model))
|
||||
: agentModel || compatibleModels[0];
|
||||
preferredModel && (imageUrls.length === 0 || supportsVision(preferredModel.model))
|
||||
? preferredModel
|
||||
: imageUrls.length > 0 &&
|
||||
agentModel &&
|
||||
supportsVision(agentModel.model)
|
||||
? agentModel
|
||||
: imageUrls.length > 0
|
||||
? compatibleModels.find((item) => supportsVision(item.model))
|
||||
: agentModel || compatibleModels[0];
|
||||
|
||||
if (!selectedModel) {
|
||||
selectedModel = compatibleModels[0];
|
||||
|
||||
@ -43,6 +43,10 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import capturedCanvasModules from "@/artifacts/liblib-frontend-capture-ed9a2c12570a49b9a1186017b2c38b52/canvas-modules.json";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import {
|
||||
canvasService,
|
||||
@ -201,6 +205,22 @@ type ComposerSettings = {
|
||||
resolution: string;
|
||||
};
|
||||
|
||||
type CanvasModelOption = {
|
||||
id: string;
|
||||
modelId?: string;
|
||||
displayName: string;
|
||||
providerName?: string;
|
||||
providerType?: string;
|
||||
supportsVision?: boolean;
|
||||
supportsImageGen?: boolean;
|
||||
isMultimodal?: boolean;
|
||||
isDefault?: boolean;
|
||||
isSubModel?: boolean;
|
||||
capabilities?: unknown;
|
||||
};
|
||||
|
||||
type CanvasModelKind = "image" | "video" | "multimodal" | "any";
|
||||
|
||||
type AddModuleKind = "text" | "image" | "video" | "video-clip" | "audio" | "script";
|
||||
type CanvasCreateKind = AddModuleKind;
|
||||
type TargetModuleActionLabel =
|
||||
@ -1178,9 +1198,30 @@ function getDefaultFunctionPrompt(key: string) {
|
||||
return defaultVideoCanvasFunctionConfig.functions[key]?.prompt || "";
|
||||
}
|
||||
|
||||
function normalizeModelRef(value: unknown) {
|
||||
const model = String(value || "").trim();
|
||||
return model && model !== "auto" ? model : "";
|
||||
}
|
||||
|
||||
function getFunctionModel(
|
||||
config: VideoCanvasFunctionConfig,
|
||||
definition: VideoCanvasFunctionDefinition,
|
||||
overrideModel?: string,
|
||||
) {
|
||||
const override = normalizeModelRef(overrideModel);
|
||||
if (override) return override;
|
||||
const fallback =
|
||||
definition.kind === "video"
|
||||
? config.defaultVideoModel
|
||||
: definition.kind === "multimodal"
|
||||
? config.defaultMultimodalModel
|
||||
: config.defaultImageModel;
|
||||
return normalizeModelRef(definition.model || fallback) || undefined;
|
||||
}
|
||||
|
||||
function getFunctionModelValue(
|
||||
config: VideoCanvasFunctionConfig,
|
||||
definition: VideoCanvasFunctionDefinition,
|
||||
) {
|
||||
const fallback =
|
||||
definition.kind === "video"
|
||||
@ -1188,8 +1229,57 @@ function getFunctionModel(
|
||||
: definition.kind === "multimodal"
|
||||
? config.defaultMultimodalModel
|
||||
: config.defaultImageModel;
|
||||
const model = (definition.model || fallback || "").trim();
|
||||
return model && model !== "auto" ? model : undefined;
|
||||
return String(definition.model || fallback || "auto").trim() || "auto";
|
||||
}
|
||||
|
||||
function getCanvasModelRef(model: CanvasModelOption) {
|
||||
return String(model.modelId || model.id || "").trim();
|
||||
}
|
||||
|
||||
function isCanvasImageModel(model: CanvasModelOption) {
|
||||
return isImageGenerationModelDescriptor({
|
||||
providerType: model.providerType,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function isCanvasVideoModel(model: CanvasModelOption) {
|
||||
return isVideoModelDescriptor({
|
||||
providerType: model.providerType,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function filterCanvasModelsByKind(models: CanvasModelOption[], kind: CanvasModelKind) {
|
||||
const usable = models.filter((model) => getCanvasModelRef(model));
|
||||
let filtered = usable;
|
||||
if (kind === "image") {
|
||||
filtered = usable.filter(isCanvasImageModel);
|
||||
} else if (kind === "video") {
|
||||
filtered = usable.filter(isCanvasVideoModel);
|
||||
} else if (kind === "multimodal") {
|
||||
filtered = usable.filter(
|
||||
(model) => model.isMultimodal || model.supportsVision || (!isCanvasImageModel(model) && !isCanvasVideoModel(model)),
|
||||
);
|
||||
}
|
||||
return filtered.length ? filtered : usable;
|
||||
}
|
||||
|
||||
function getCanvasModelDisplayName(
|
||||
modelRef: string | undefined,
|
||||
models: CanvasModelOption[],
|
||||
fallback = "自动模型",
|
||||
) {
|
||||
const normalized = normalizeModelRef(modelRef);
|
||||
if (!normalized) return fallback;
|
||||
const match = models.find(
|
||||
(model) => model.id === normalized || model.modelId === normalized || getCanvasModelRef(model) === normalized,
|
||||
);
|
||||
return match?.displayName || normalized;
|
||||
}
|
||||
|
||||
function composeFunctionPrompt(
|
||||
@ -1782,10 +1872,18 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
"/api/video-canvas/config",
|
||||
fetcher
|
||||
);
|
||||
const { data: canvasModelsData } = useSWR<{ models?: CanvasModelOption[] }>(
|
||||
"/api/models",
|
||||
fetcher
|
||||
);
|
||||
const videoCanvasFunctionConfig = useMemo(
|
||||
() => normalizeVideoCanvasFunctionConfig(videoCanvasFunctionSettings?.config),
|
||||
[videoCanvasFunctionSettings?.config]
|
||||
);
|
||||
const canvasModelOptions = useMemo(
|
||||
() => (canvasModelsData?.models || []).filter((model) => getCanvasModelRef(model)),
|
||||
[canvasModelsData?.models]
|
||||
);
|
||||
const [nodes, setNodes] = useState<CloneNode[]>([]);
|
||||
const [edges, setEdges] = useState<CloneEdge[]>([]);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
@ -2879,7 +2977,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const response = await imageGenerationService.generateImage({
|
||||
aspectRatio: payload.settings.ratio,
|
||||
imageCount: count,
|
||||
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||
model: getFunctionModel(videoCanvasFunctionConfig, definition, payload.settings.model),
|
||||
prompt: finalPrompt,
|
||||
referenceImage: sourceUrl,
|
||||
});
|
||||
@ -2947,7 +3045,12 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
);
|
||||
|
||||
const runPresetComposerSubmit = useCallback(
|
||||
async (nodeId: string, mode: Exclude<ActiveStarterScene, null>, draftPrompt: string) => {
|
||||
async (
|
||||
nodeId: string,
|
||||
mode: Exclude<ActiveStarterScene, null>,
|
||||
draftPrompt: string,
|
||||
settings?: ComposerSettings,
|
||||
) => {
|
||||
const node = nodes.find((item) => item.id === nodeId);
|
||||
if (!node) return;
|
||||
const incomingNodes = edges
|
||||
@ -2977,7 +3080,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
imageUrls,
|
||||
mode: "agent-plan",
|
||||
nodeTitle: node.title,
|
||||
prompt: finalPrompt,
|
||||
prompt,
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition, settings?.model),
|
||||
videoUrls,
|
||||
});
|
||||
if (!response.success) throw new Error(response.error || "故事脚本生成失败");
|
||||
@ -3013,7 +3117,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const prompt = composeFunctionPrompt(definition, draftPrompt);
|
||||
const response = await videoGenerationService.generateVideo({
|
||||
images: imageUrls.slice(0, 4),
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition, settings?.model),
|
||||
prompt,
|
||||
ratio: "16:9",
|
||||
resolution: "720p",
|
||||
@ -3120,6 +3224,11 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
.filter(Boolean);
|
||||
const action = String(node.draftValues?.action || "");
|
||||
const params = (node.draftValues?.params || {}) as Record<string, unknown>;
|
||||
const paramsSettings =
|
||||
params.settings && typeof params.settings === "object"
|
||||
? (params.settings as Record<string, unknown>)
|
||||
: {};
|
||||
const nodeModelRef = normalizeModelRef(params.model || paramsSettings.model);
|
||||
const rawPrompt = String(params.prompt || node.draftValues?.prompt || "");
|
||||
|
||||
try {
|
||||
@ -3146,7 +3255,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const response = await imageGenerationService.generateImage({
|
||||
aspectRatio: action === "IMAGE_GENERATE_NINE_GRID" ? "16:9" : undefined,
|
||||
imageCount: 1,
|
||||
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||
model: getFunctionModel(videoCanvasFunctionConfig, definition, nodeModelRef),
|
||||
prompt,
|
||||
referenceImage: sourceUrl || undefined,
|
||||
});
|
||||
@ -3188,7 +3297,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const prompt = composeFunctionPrompt(definition, rawPrompt || node.title);
|
||||
const response = await videoGenerationService.generateVideo({
|
||||
images: imageUrls.slice(0, 4),
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition, nodeModelRef),
|
||||
prompt,
|
||||
ratio: "16:9",
|
||||
resolution: "720p",
|
||||
@ -3232,6 +3341,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
mode: "agent-plan",
|
||||
nodeTitle: node.title,
|
||||
prompt,
|
||||
preferredModelRef: getFunctionModel(videoCanvasFunctionConfig, definition, nodeModelRef),
|
||||
videoUrls,
|
||||
});
|
||||
if (!response.success) throw new Error(response.error || "多模态分析失败");
|
||||
@ -3644,14 +3754,17 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
<PresetWorkflowControls
|
||||
anchor={selectedPresetAnchor}
|
||||
mode={selectedPresetMode ?? "character-three-view"}
|
||||
functionConfig={videoCanvasFunctionConfig}
|
||||
modelOptions={canvasModelOptions}
|
||||
nodes={nodes}
|
||||
onComposerSubmit={(payload) => submitSelectedImagePrompt(selectedPresetControlNode.id, payload)}
|
||||
onImageToolAction={(action, option) => runSelectedImageToolAction(selectedPresetControlNode.id, action, option)}
|
||||
onPresetSubmit={(prompt) =>
|
||||
onPresetSubmit={(prompt, settings) =>
|
||||
runPresetComposerSubmit(
|
||||
selectedPresetControlNode.id,
|
||||
selectedPresetMode ?? "character-three-view",
|
||||
prompt
|
||||
prompt,
|
||||
settings
|
||||
)
|
||||
}
|
||||
selectedNode={selectedPresetControlNode}
|
||||
@ -3661,8 +3774,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
{selectedNode && selectedTargetModuleKind === "text" && selectedTargetAnchor ? (
|
||||
<TextTargetModuleComposer
|
||||
anchor={selectedTargetAnchor}
|
||||
defaultModel={getFunctionModelValue(
|
||||
videoCanvasFunctionConfig,
|
||||
getConfiguredFunction(videoCanvasFunctionConfig, "story-script"),
|
||||
)}
|
||||
modelOptions={canvasModelOptions}
|
||||
node={selectedNode}
|
||||
onSubmit={(prompt) => {
|
||||
onSubmit={(prompt, model) => {
|
||||
setNodes((current) =>
|
||||
current.map((node) =>
|
||||
node.id === selectedNode.id
|
||||
@ -3675,6 +3793,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
manualTextComposer: true,
|
||||
params: {
|
||||
...((node.draftValues?.params as Record<string, unknown> | undefined) || {}),
|
||||
model,
|
||||
prompt,
|
||||
},
|
||||
},
|
||||
@ -4385,7 +4504,9 @@ function MagnetGlyph() {
|
||||
|
||||
function PresetWorkflowControls({
|
||||
anchor,
|
||||
functionConfig,
|
||||
mode,
|
||||
modelOptions,
|
||||
nodes,
|
||||
onComposerSubmit,
|
||||
onImageToolAction,
|
||||
@ -4393,19 +4514,25 @@ function PresetWorkflowControls({
|
||||
selectedNode,
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
functionConfig: VideoCanvasFunctionConfig;
|
||||
mode: Exclude<ActiveStarterScene, null>;
|
||||
modelOptions: CanvasModelOption[];
|
||||
nodes: CloneNode[];
|
||||
onComposerSubmit: (payload: ComposerSubmitPayload) => void;
|
||||
onImageToolAction: (action: ImageToolbarAction, option?: string) => void;
|
||||
onPresetSubmit: (prompt: string) => void;
|
||||
onPresetSubmit: (prompt: string, settings?: ComposerSettings) => void;
|
||||
selectedNode: CloneNode;
|
||||
}) {
|
||||
if (mode === "character-three-view") {
|
||||
const definition = getConfiguredFunction(functionConfig, "character-three-view");
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<CharacterPresetToolbar anchor={anchor} onToolAction={onImageToolAction} />
|
||||
<PromptComposer
|
||||
anchor={anchor}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
modelKind="image"
|
||||
modelOptions={modelOptions}
|
||||
nodes={nodes}
|
||||
onSubmit={onComposerSubmit}
|
||||
referenceThumbnailUrl={getNodeMediaUrl(selectedNode) || CHARACTER_IMAGE_URL}
|
||||
@ -4415,6 +4542,7 @@ function PresetWorkflowControls({
|
||||
}
|
||||
|
||||
if (mode === "story-script") {
|
||||
const definition = getConfiguredFunction(functionConfig, "story-script");
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<PresetFloatingToolbar
|
||||
@ -4429,6 +4557,9 @@ function PresetWorkflowControls({
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="8"
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
modelKind="multimodal"
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
prompt={STORY_SCRIPT_PROMPT}
|
||||
submitMessage="故事脚本生成接口待接入。"
|
||||
@ -4439,6 +4570,7 @@ function PresetWorkflowControls({
|
||||
}
|
||||
|
||||
if (mode === "first-frame") {
|
||||
const definition = getConfiguredFunction(functionConfig, "first-frame-video");
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<PresetFloatingToolbar
|
||||
@ -4453,6 +4585,9 @@ function PresetWorkflowControls({
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="28"
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
modelKind="video"
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
prompt="以当前图为首帧生成视频。"
|
||||
submitMessage="首帧图生视频接口待接入。"
|
||||
@ -4463,31 +4598,51 @@ function PresetWorkflowControls({
|
||||
);
|
||||
}
|
||||
|
||||
const definition = getConfiguredFunction(functionConfig, "audio-to-video");
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<AudioToVideoComposerPanel anchor={anchor} onSubmit={onPresetSubmit} />
|
||||
<AudioToVideoComposerPanel
|
||||
anchor={anchor}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextTargetModuleComposer({
|
||||
anchor,
|
||||
defaultModel = "auto",
|
||||
modelOptions = [],
|
||||
node,
|
||||
onSubmit,
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
defaultModel?: string;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
node: CloneNode;
|
||||
onSubmit: (prompt: string) => void;
|
||||
onSubmit: (prompt: string, model: string) => void;
|
||||
}) {
|
||||
const contentValues = node.draftValues?.content;
|
||||
const initialPrompt =
|
||||
Array.isArray(contentValues) && typeof contentValues[0] === "string" ? contentValues[0] : "";
|
||||
const initialModel = normalizeModelRef(
|
||||
((node.draftValues?.params as Record<string, unknown> | undefined)?.model as string | undefined) ||
|
||||
defaultModel,
|
||||
) || "auto";
|
||||
const [menu, setMenu] = useState<ComposerMenu>(null);
|
||||
const [selectedModel, setSelectedModel] = useState(initialModel);
|
||||
const [prompt, setPrompt] = useState(initialPrompt);
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(initialPrompt);
|
||||
}, [initialPrompt, node.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedModel(initialModel);
|
||||
}, [initialModel, node.id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute z-30 overflow-visible"
|
||||
@ -4504,6 +4659,17 @@ function TextTargetModuleComposer({
|
||||
className="relative h-[286px] w-[895px] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]"
|
||||
style={{ transform: `scale(${OFFICIAL_COMPOSER_SCALE})`, transformOrigin: "top left" }}
|
||||
>
|
||||
{menu === "model" ? (
|
||||
<ComposerPopover
|
||||
menu="model"
|
||||
modelKind="multimodal"
|
||||
modelOptions={modelOptions}
|
||||
onSelect={(option) => {
|
||||
setSelectedModel(option);
|
||||
setMenu(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<textarea
|
||||
className="h-[158px] w-full resize-none bg-transparent text-[20px] leading-8 text-white/64 outline-none placeholder:text-white/38"
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
@ -4513,10 +4679,11 @@ function TextTargetModuleComposer({
|
||||
/>
|
||||
<div className="absolute bottom-5 left-5 right-[84px] flex items-center gap-6 overflow-hidden text-[17px] text-white/78">
|
||||
<ComposerControl
|
||||
label="GVLM 3.1"
|
||||
active={menu === "model"}
|
||||
label={getCanvasModelDisplayName(selectedModel, modelOptions)}
|
||||
icon="⌘"
|
||||
dropdown
|
||||
onClick={() => toast.info("文本模型选择后端待接入。")}
|
||||
onClick={() => setMenu((current) => current === "model" ? null : "model")}
|
||||
/>
|
||||
<div className="min-w-0 flex-1" />
|
||||
<ComposerControl label="文" onClick={() => toast.info("翻译接口待接入。")} />
|
||||
@ -4524,7 +4691,7 @@ function TextTargetModuleComposer({
|
||||
</div>
|
||||
<button
|
||||
className="absolute bottom-5 right-5 flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] leading-none text-black transition hover:bg-white"
|
||||
onClick={() => onSubmit(prompt)}
|
||||
onClick={() => onSubmit(prompt, selectedModel)}
|
||||
type="button"
|
||||
>
|
||||
↑
|
||||
@ -4609,12 +4776,29 @@ function CharacterPresetToolbar({
|
||||
|
||||
function AudioToVideoComposerPanel({
|
||||
anchor,
|
||||
defaultModel = "auto",
|
||||
modelOptions = [],
|
||||
onSubmit,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
onSubmit?: (prompt: string) => void;
|
||||
defaultModel?: string;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
onSubmit?: (prompt: string, settings?: ComposerSettings) => void;
|
||||
}) {
|
||||
const [prompt, setPrompt] = useState(AUDIO_TO_VIDEO_PROMPT);
|
||||
const [menu, setMenu] = useState<ComposerMenu>(null);
|
||||
const [settings, setSettings] = useState<ComposerSettings>({
|
||||
camera: "摄像机",
|
||||
count: "1个",
|
||||
model: normalizeModelRef(defaultModel) || "auto",
|
||||
panorama: "全景",
|
||||
ratio: "16:9",
|
||||
resolution: "720P",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" }));
|
||||
}, [defaultModel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -4638,6 +4822,17 @@ function AudioToVideoComposerPanel({
|
||||
}
|
||||
>
|
||||
<div className="relative h-[260px] w-[1216px] rounded-[16px] border border-white/8 bg-[#252525]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
|
||||
{menu === "model" ? (
|
||||
<ComposerPopover
|
||||
menu="model"
|
||||
modelKind="video"
|
||||
modelOptions={modelOptions}
|
||||
onSelect={(option) => {
|
||||
setSettings((current) => ({ ...current, model: option }));
|
||||
setMenu(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<button className="absolute right-6 top-5 text-[32px] text-white/38 hover:text-white/70" onClick={() => toast.info("展开音频生视频编辑器待接入。")} type="button">↗</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{["文生视频", "全能参考", "图生视频", "首尾帧", "图片参考"].map((tab, index) => (
|
||||
@ -4686,7 +4881,7 @@ function AudioToVideoComposerPanel({
|
||||
value={prompt}
|
||||
/>
|
||||
<div className="absolute bottom-5 left-5 right-[86px] flex items-center gap-7 overflow-hidden text-[22px] text-white/82">
|
||||
<ComposerControl label="Seedance 2.0" icon="▥" dropdown onClick={() => toast.info("视频模型选择后端待接入。")} />
|
||||
<ComposerControl active={menu === "model"} label={getCanvasModelDisplayName(settings.model, modelOptions)} icon="▥" dropdown onClick={() => setMenu((current) => current === "model" ? null : "model")} />
|
||||
<span className="rounded-[5px] bg-[#f6c85f] px-1.5 py-0.5 text-[13px] font-bold text-[#2b2314]">VIP</span>
|
||||
<ComposerControl label="16:9" icon="▭" onClick={() => toast.info("比例选择后端待接入。")} />
|
||||
<ComposerControl label="720P" onClick={() => toast.info("清晰度选择后端待接入。")} />
|
||||
@ -4701,7 +4896,7 @@ function AudioToVideoComposerPanel({
|
||||
className="absolute bottom-5 right-5 flex h-14 w-14 items-center justify-center rounded-[14px] bg-white text-[34px] leading-none text-black hover:bg-white/90"
|
||||
onClick={() => {
|
||||
if (onSubmit) {
|
||||
onSubmit(prompt);
|
||||
onSubmit(prompt, settings);
|
||||
return;
|
||||
}
|
||||
toast.info("音频生视频生成接口待接入。");
|
||||
@ -5184,6 +5379,9 @@ function AudioResourcePanel() {
|
||||
function PresetComposerPanel({
|
||||
anchor,
|
||||
credit,
|
||||
defaultModel = "auto",
|
||||
modelKind = "image",
|
||||
modelOptions = [],
|
||||
onSubmit,
|
||||
prompt,
|
||||
submitMessage,
|
||||
@ -5192,18 +5390,34 @@ function PresetComposerPanel({
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
credit: string;
|
||||
onSubmit?: (prompt: string) => void;
|
||||
defaultModel?: string;
|
||||
modelKind?: CanvasModelKind;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
onSubmit?: (prompt: string, settings?: ComposerSettings) => void;
|
||||
prompt: string;
|
||||
submitMessage: string;
|
||||
thumbnailUrl?: string;
|
||||
title: string;
|
||||
}) {
|
||||
const [menu, setMenu] = useState<ComposerMenu>(null);
|
||||
const [draftPrompt, setDraftPrompt] = useState(prompt);
|
||||
const [settings, setSettings] = useState<ComposerSettings>({
|
||||
camera: "摄像机",
|
||||
count: "1张",
|
||||
model: normalizeModelRef(defaultModel) || "auto",
|
||||
panorama: "全景",
|
||||
ratio: "16:9",
|
||||
resolution: "2K",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDraftPrompt(prompt);
|
||||
}, [prompt]);
|
||||
|
||||
useEffect(() => {
|
||||
setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" }));
|
||||
}, [defaultModel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute overflow-visible"
|
||||
@ -5229,6 +5443,17 @@ function PresetComposerPanel({
|
||||
className="relative h-[238px] w-[895px] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]"
|
||||
style={{ transform: `scale(${OFFICIAL_COMPOSER_SCALE})`, transformOrigin: "top left" }}
|
||||
>
|
||||
{menu === "model" ? (
|
||||
<ComposerPopover
|
||||
menu="model"
|
||||
modelKind={modelKind}
|
||||
modelOptions={modelOptions}
|
||||
onSelect={(option) => {
|
||||
setSettings((current) => ({ ...current, model: option }));
|
||||
setMenu(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<button className="absolute right-5 top-5 text-[26px] text-white/45" onClick={() => toast.info("展开提示词编辑器待接入。")} type="button">↗</button>
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<span className="rounded-[10px] border border-white/10 bg-[#292929] px-3 py-2 text-[14px] text-white/62">{title}</span>
|
||||
@ -5245,7 +5470,7 @@ function PresetComposerPanel({
|
||||
value={draftPrompt}
|
||||
/>
|
||||
<div className="absolute bottom-5 left-5 right-[84px] flex items-center gap-6 overflow-hidden text-[17px] text-white/78">
|
||||
<ComposerControl label="Lib Nano Pro" icon="◉" dropdown onClick={() => toast.info("模型选择接口待接入。")} />
|
||||
<ComposerControl active={menu === "model"} label={getCanvasModelDisplayName(settings.model, modelOptions)} icon="◉" dropdown onClick={() => setMenu((current) => current === "model" ? null : "model")} />
|
||||
<ComposerControl label="16:9" icon="▭" dropdown onClick={() => toast.info("比例选择接口待接入。")} />
|
||||
<ComposerControl label="2K" dropdown onClick={() => toast.info("清晰度选择接口待接入。")} />
|
||||
<ComposerControl label="摄像机" icon="▣" onClick={() => toast.info("镜头配置接口待接入。")} />
|
||||
@ -5256,7 +5481,7 @@ function PresetComposerPanel({
|
||||
className="absolute bottom-5 right-5 flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] text-black hover:bg-white"
|
||||
onClick={() => {
|
||||
if (onSubmit) {
|
||||
onSubmit(draftPrompt);
|
||||
onSubmit(draftPrompt, settings);
|
||||
return;
|
||||
}
|
||||
toast.info(submitMessage);
|
||||
@ -5739,11 +5964,17 @@ function CharacterImagePanel({ imageUrl, label, wide = false }: { imageUrl: stri
|
||||
|
||||
function PromptComposer({
|
||||
anchor,
|
||||
defaultModel = "auto",
|
||||
modelKind = "image",
|
||||
modelOptions = [],
|
||||
nodes = [],
|
||||
onSubmit,
|
||||
referenceThumbnailUrl = CHARACTER_IMAGE_URL,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
defaultModel?: string;
|
||||
modelKind?: CanvasModelKind;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
nodes?: CloneNode[];
|
||||
onSubmit?: (payload: ComposerSubmitPayload) => void;
|
||||
referenceThumbnailUrl?: string;
|
||||
@ -5755,11 +5986,15 @@ function PromptComposer({
|
||||
const [settings, setSettings] = useState<ComposerSettings>({
|
||||
camera: "摄像机",
|
||||
count: "1张",
|
||||
model: "Lib Nano Pro",
|
||||
model: normalizeModelRef(defaultModel) || "auto",
|
||||
panorama: "全景",
|
||||
ratio: "16:9",
|
||||
resolution: "2K",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" }));
|
||||
}, [defaultModel]);
|
||||
const referenceNodes = useMemo(
|
||||
() => nodes.filter((node) => node.kind !== "script" && node.inputType !== "audio"),
|
||||
[nodes]
|
||||
@ -5883,7 +6118,12 @@ function PromptComposer({
|
||||
) : menu === "focus" ? (
|
||||
<ComposerFocusPopover onApply={(value) => applyComposerOption("focus", value)} />
|
||||
) : menu ? (
|
||||
<ComposerPopover menu={menu} onSelect={(option) => applyComposerOption(menu, option)} />
|
||||
<ComposerPopover
|
||||
menu={menu}
|
||||
modelKind={modelKind}
|
||||
modelOptions={modelOptions}
|
||||
onSelect={(option) => applyComposerOption(menu, option)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<textarea
|
||||
@ -5898,7 +6138,7 @@ function PromptComposer({
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-5 left-5 right-[84px] flex items-center gap-6 overflow-hidden text-[18px] text-white/78">
|
||||
<ComposerControl active={menu === "model"} label={settings.model} icon="◉" dropdown onClick={() => setMenu((current) => current === "model" ? null : "model")} />
|
||||
<ComposerControl active={menu === "model"} label={getCanvasModelDisplayName(settings.model, modelOptions)} icon="◉" dropdown onClick={() => setMenu((current) => current === "model" ? null : "model")} />
|
||||
<ComposerControl active={menu === "ratio"} label={settings.ratio} icon="▭" dropdown onClick={() => setMenu((current) => current === "ratio" ? null : "ratio")} />
|
||||
<ComposerControl active={menu === "resolution"} label={settings.resolution} dropdown onClick={() => setMenu((current) => current === "resolution" ? null : "resolution")} />
|
||||
<ComposerControl active={menu === "camera"} label={settings.camera} icon="▣" onClick={() => setMenu((current) => current === "camera" ? null : "camera")} />
|
||||
@ -6033,34 +6273,46 @@ function ComposerSlashPopover({ onSelect }: { onSelect: (scene: string) => void
|
||||
|
||||
function ComposerPopover({
|
||||
menu,
|
||||
modelKind = "image",
|
||||
modelOptions = [],
|
||||
onSelect,
|
||||
}: {
|
||||
menu: Exclude<ComposerMenu, "focus" | "reference" | "slash" | null>;
|
||||
modelKind?: CanvasModelKind;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
onSelect: (option: string) => void;
|
||||
}) {
|
||||
const options: Record<Exclude<ComposerMenu, "focus" | "reference" | "slash" | null>, string[]> = {
|
||||
camera: ["摄像机", "手持镜头", "固定镜头"],
|
||||
count: ["1张", "2张", "4张"],
|
||||
mark: ["添加人物标记", "添加构图标记", "添加动作标记"],
|
||||
model: ["Lib Nano Pro", "Lib Seedream Pro", "Lib Universal"],
|
||||
panorama: ["全景", "半身", "近景"],
|
||||
ratio: ["16:9", "9:16", "1:1", "4:3"],
|
||||
resolution: ["1K", "2K", "4K"],
|
||||
settings: ["提示词增强", "参考图权重", "高级参数"],
|
||||
style: ["写实", "电影感", "国风", "柔和自然光"],
|
||||
translate: ["中文", "English", "自动翻译"],
|
||||
const staticOptions: Record<Exclude<ComposerMenu, "focus" | "reference" | "slash" | null>, Array<{ label: string; value: string }>> = {
|
||||
camera: ["摄像机", "手持镜头", "固定镜头"].map((value) => ({ label: value, value })),
|
||||
count: ["1张", "2张", "4张"].map((value) => ({ label: value, value })),
|
||||
mark: ["添加人物标记", "添加构图标记", "添加动作标记"].map((value) => ({ label: value, value })),
|
||||
model: [],
|
||||
panorama: ["全景", "半身", "近景"].map((value) => ({ label: value, value })),
|
||||
ratio: ["16:9", "9:16", "1:1", "4:3"].map((value) => ({ label: value, value })),
|
||||
resolution: ["1K", "2K", "4K"].map((value) => ({ label: value, value })),
|
||||
settings: ["提示词增强", "参考图权重", "高级参数"].map((value) => ({ label: value, value })),
|
||||
style: ["写实", "电影感", "国风", "柔和自然光"].map((value) => ({ label: value, value })),
|
||||
translate: ["中文", "English", "自动翻译"].map((value) => ({ label: value, value })),
|
||||
};
|
||||
const modelItems = [
|
||||
{ label: "自动模型", value: "auto" },
|
||||
...filterCanvasModelsByKind(modelOptions, modelKind).map((model) => ({
|
||||
label: model.displayName || model.modelId || model.id,
|
||||
value: getCanvasModelRef(model),
|
||||
})),
|
||||
];
|
||||
const options = menu === "model" ? modelItems : staticOptions[menu];
|
||||
|
||||
return (
|
||||
<div className="absolute left-5 top-[102px] z-50 w-[260px] rounded-[14px] border border-white/10 bg-[#1f1f1f] p-2 shadow-[0_24px_80px_rgba(0,0,0,0.5)]">
|
||||
{options[menu].map((option) => (
|
||||
<div className="absolute left-5 top-[102px] z-50 max-h-[280px] w-[300px] overflow-y-auto rounded-[14px] border border-white/10 bg-[#1f1f1f] p-2 shadow-[0_24px_80px_rgba(0,0,0,0.5)]">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
className="flex h-9 w-full items-center justify-between rounded-[9px] px-3 text-left text-[13px] text-white/72 hover:bg-white/8 hover:text-white"
|
||||
key={option}
|
||||
onClick={() => onSelect(option)}
|
||||
key={option.value}
|
||||
onClick={() => onSelect(option.value)}
|
||||
type="button"
|
||||
>
|
||||
{option}
|
||||
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
||||
<span className="text-white/28">›</span>
|
||||
</button>
|
||||
))}
|
||||
@ -6094,7 +6346,7 @@ function ComposerControl({
|
||||
type="button"
|
||||
>
|
||||
{icon ? <span>{icon}</span> : null}
|
||||
{label ? <span>{label}</span> : null}
|
||||
{label ? <span className="max-w-[170px] truncate">{label}</span> : null}
|
||||
{dropdown ? <span className="text-white/42">⌄</span> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
@ -720,6 +720,7 @@ export const workflowAnalysisService = {
|
||||
imageUrls?: string[];
|
||||
videoUrls?: string[];
|
||||
nodeTitle?: string;
|
||||
preferredModelRef?: string;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
summary?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user