diff --git a/src/components/MyCreationPickerModal.tsx b/src/components/MyCreationPickerModal.tsx index 04a8406e..4310a341 100644 --- a/src/components/MyCreationPickerModal.tsx +++ b/src/components/MyCreationPickerModal.tsx @@ -170,7 +170,7 @@ function getHistoryMediaUrl(item: HistoryItem, kind: HistoryTabKey): string | nu const mediaKind = getHistoryKind(item, kind); const result = firstResult(item); if (mediaKind === "image") { - return firstString(result?.images || undefined) || firstString(result?.originImages || undefined) || firstString(item.images) || null; + return firstString(result?.images || undefined) || null; } if (mediaKind === "video") return result?.video || item.video || null; return result?.voice || item.voice || item.audio || firstString(item.audios) || null; @@ -180,7 +180,7 @@ function getHistoryPreviewUrl(item: HistoryItem, kind: HistoryTabKey): string | const mediaKind = getHistoryKind(item, kind); const result = firstResult(item); if (mediaKind === "image") { - return firstString(result?.thumbs || undefined) || firstString(item.thumbs) || firstString(result?.images || undefined) || firstString(item.images) || null; + return firstString(result?.thumbs || undefined) || null; } if (mediaKind === "video") return result?.coverThumb || result?.cover || item.coverThumb || item.cover || result?.video || item.video || null; return null; diff --git a/src/components/composer/NodeHighDefinitionPanel.tsx b/src/components/composer/NodeHighDefinitionPanel.tsx index eddc70d2..ca383dbc 100644 --- a/src/components/composer/NodeHighDefinitionPanel.tsx +++ b/src/components/composer/NodeHighDefinitionPanel.tsx @@ -1,35 +1,58 @@ "use client"; -import { useCallback, useMemo } from "react"; -import { Button, Select } from "antd"; -import { ArrowUpOutlined, ThunderboltFilled } from "@ant-design/icons"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { GenerateActionIcon } from "@/components/media/MediaToolbarIcons"; import { PopiModelSelect } from "@/components/nodes/PopiModelSelect"; import { useModelStore } from "@/store/modelStore"; +import type { PopiModelDetail } from "@/store/modelStore"; import { useWorkflowStore } from "@/store/workflowStore"; import type { SmartImageNodeData, WorkflowNode } from "@/types"; -import { buildHighDefinitionParameters, chooseHighDefinitionModel, HIGH_DEFINITION_BATCH_SIZE, HIGH_DEFINITION_MODEL_MODE, HIGH_DEFINITION_SCALE, HIGH_DEFINITION_SUBTYPE } from "@/utils/highDefinitionNodes"; +import type { ModelParameter } from "@/lib/providers/types"; +import type { ExtensionFieldSchema, GenerationNodeConfig } from "@/utils/generationConfig"; +import { + buildGenerationConfigDraftFromModelSchema, + buildSubmittableGenerationConfig, + normalizeExtensionFields, + updateModelParameterValue, +} from "@/utils/generationConfig"; +import { buildHighDefinitionParameters, chooseHighDefinitionModel, HIGH_DEFINITION_BATCH_SIZE } from "@/utils/highDefinitionNodes"; import { toSelectedModel } from "@/utils/selectedModel"; interface NodeHighDefinitionPanelProps { node: WorkflowNode; } -const SCALE_OPTIONS = [ - { label: "2", value: 2 }, - { label: "4", value: 4 }, - { label: "6", value: 6 }, -]; +type SchemaOption = { key: string; label: string; value: unknown }; -const MODEL_MODE_OPTIONS = [ - { label: "通用", value: HIGH_DEFINITION_MODEL_MODE }, -]; +function schemaOptions(options: Array<{ label: string; value: unknown }> | undefined, fallbackValue: unknown): SchemaOption[] { + if (options && options.length > 0) { + return options.map((option, index) => ({ + key: String(option.value ?? index), + label: option.label, + value: option.value, + })); + } + if (fallbackValue !== undefined && fallbackValue !== null && fallbackValue !== "") { + return [{ key: String(fallbackValue), label: String(fallbackValue), value: fallbackValue }]; + } + return []; +} + +function valueMatchesOption(optionValue: unknown, value: unknown): boolean { + return Object.is(optionValue, value) || String(optionValue).toLowerCase() === String(value ?? "").toLowerCase(); +} -function readScale(parameters: Record | undefined): number { - return typeof parameters?.scale === "number" && Number.isFinite(parameters.scale) ? parameters.scale : HIGH_DEFINITION_SCALE; +function selectedOptionKey(options: SchemaOption[], value: unknown): string | undefined { + if (value === undefined || value === null) return undefined; + return options.find((option) => valueMatchesOption(option.value, value))?.key; } -function readModelMode(parameters: Record | undefined): string { - return typeof parameters?.modelMode === "string" && parameters.modelMode ? parameters.modelMode : HIGH_DEFINITION_MODEL_MODE; +function parameterLabel(parameter: ModelParameter): string { + return parameter.label ?? parameter.name; +} + +function extensionFieldLabel(field: ExtensionFieldSchema): string { + return field.label ?? field.name; } export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) { @@ -37,36 +60,128 @@ export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(node.id)); const highDefinitionModels = useModelStore((state) => state.byKind.highDefinition); + const fetchModelDetail = useModelStore((state) => state.fetchModelDetail); const data = node.data as SmartImageNodeData; + const hydratedKeyRef = useRef(null); const selectedModel = useMemo( () => data.selectedModel ?? chooseHighDefinitionModel(highDefinitionModels), [data.selectedModel, highDefinitionModels] ); + const activeModelDetail = useModelStore((state) => selectedModel.modelId ? state.detailsById[selectedModel.modelId] : undefined); const parameters = buildHighDefinitionParameters(data.parameters); - const scale = readScale(parameters); - const modelMode = readModelMode(parameters); + const extraTaskParams = data.extraTaskParams ?? {}; + const canRun = Boolean(selectedModel.modelId) || highDefinitionModels.length > 0; + + const buildConfig = useCallback(( + nextParameters = data.parameters ?? {}, + nextExtraTaskParams = data.extraTaskParams ?? {}, + model = selectedModel + ): GenerationNodeConfig => ({ + prompt: data.inputPrompt ?? "高清放大", + selectedModel: model, + inputImages: data.inputImages ?? [], + batchSize: HIGH_DEFINITION_BATCH_SIZE, + referenceSubjectList: data.referenceSubjectList ?? [], + parameters: buildHighDefinitionParameters(nextParameters), + extraTaskParams: nextExtraTaskParams, + }), [data.extraTaskParams, data.inputImages, data.inputPrompt, data.parameters, data.referenceSubjectList, selectedModel]); + + const applyModelDetailConfig = useCallback((model = selectedModel, detail: PopiModelDetail | null | undefined = activeModelDetail) => { + const config = buildConfig(data.parameters ?? {}, data.extraTaskParams ?? {}, model); + const hydratedConfig = buildGenerationConfigDraftFromModelSchema(config, detail); + updateNodeData(node.id, { + selectedModel: model, + parameters: hydratedConfig.parameters, + extraTaskParams: hydratedConfig.extraTaskParams, + batchSize: HIGH_DEFINITION_BATCH_SIZE, + }); + }, [activeModelDetail, buildConfig, data.extraTaskParams, data.parameters, node.id, selectedModel, updateNodeData]); + + useEffect(() => { + if (!selectedModel.modelId) return; + const key = `${node.id}:${selectedModel.modelId}`; + if (hydratedKeyRef.current === key) return; + hydratedKeyRef.current = key; + void fetchModelDetail(selectedModel.modelId).then((detail) => { + applyModelDetailConfig(selectedModel, detail); + }); + }, [applyModelDetailConfig, fetchModelDetail, node.id, selectedModel.modelId]); + + const parameterRows = useMemo(() => { + return (activeModelDetail?.parameters ?? []).map((parameter) => { + const options = schemaOptions(parameter.options, parameter.default); + return { + parameter, + options, + selectedKey: selectedOptionKey(options, parameters[parameter.name]), + }; + }).filter((row) => row.options.length > 0); + }, [activeModelDetail?.parameters, parameters]); + + const extensionFieldRows = useMemo(() => { + return normalizeExtensionFields(activeModelDetail?.extensionFields).map((field) => { + const options = schemaOptions(field.options, field.default); + return { + field, + options, + selectedKey: selectedOptionKey(options, extraTaskParams[field.name]), + }; + }).filter((row) => row.options.length > 0); + }, [activeModelDetail?.extensionFields, extraTaskParams]); - const updateParameters = useCallback((patch: Record) => { + const updateParameter = useCallback((name: string, value: unknown) => { updateNodeData(node.id, { - parameters: buildHighDefinitionParameters({ - ...(data.parameters ?? {}), - ...patch, - }), + parameters: buildHighDefinitionParameters(updateModelParameterValue(data.parameters, name, value)), batchSize: HIGH_DEFINITION_BATCH_SIZE, - subType: HIGH_DEFINITION_SUBTYPE, }); }, [data.parameters, node.id, updateNodeData]); - const handleRun = useCallback(() => { + const updateExtensionField = useCallback((name: string, value: unknown) => { updateNodeData(node.id, { - selectedModel, - parameters: buildHighDefinitionParameters(data.parameters), + extraTaskParams: updateModelParameterValue(data.extraTaskParams, name, value), + batchSize: HIGH_DEFINITION_BATCH_SIZE, + }); + }, [data.extraTaskParams, node.id, updateNodeData]); + + const handleRun = useCallback(async () => { + const detail = selectedModel.modelId ? await fetchModelDetail(selectedModel.modelId) : null; + const submitConfig = buildSubmittableGenerationConfig(buildConfig(), detail ?? activeModelDetail); + updateNodeData(node.id, { + selectedModel: submitConfig.selectedModel, + parameters: submitConfig.parameters, + extraTaskParams: submitConfig.extraTaskParams, batchSize: HIGH_DEFINITION_BATCH_SIZE, - subType: HIGH_DEFINITION_SUBTYPE, error: null, }); void regenerateNode(node.id); - }, [data.parameters, node.id, regenerateNode, selectedModel, updateNodeData]); + }, [activeModelDetail, buildConfig, fetchModelDetail, node.id, regenerateNode, selectedModel.modelId, updateNodeData]); + + const renderOptionButtons = ( + selectedKey: string | undefined, + options: SchemaOption[], + onSelect: (value: unknown) => void + ) => ( +
+ {options.map((option) => { + const selected = option.key === selectedKey; + return ( + + ); + })} +
+ ); return (
{ + onChange={async (_, providerModel) => { + const nextModel = toSelectedModel(providerModel); + const detail = nextModel.modelId ? await fetchModelDetail(nextModel.modelId) : null; + const baseConfig = buildConfig({}, {}, nextModel); + const hydratedConfig = buildGenerationConfigDraftFromModelSchema(baseConfig, detail); + hydratedKeyRef.current = `${node.id}:${nextModel.modelId}`; updateNodeData(node.id, { - selectedModel: toSelectedModel(providerModel), - parameters: buildHighDefinitionParameters({}), + selectedModel: nextModel, + parameters: hydratedConfig.parameters, + extraTaskParams: hydratedConfig.extraTaskParams, batchSize: HIGH_DEFINITION_BATCH_SIZE, - subType: HIGH_DEFINITION_SUBTYPE, }); }} size="compact" className="w-full" /> - 模型 -