|
|
@ -1,35 +1,58 @@ |
|
|
"use client"; |
|
|
"use client"; |
|
|
|
|
|
|
|
|
import { useCallback, useMemo } from "react"; |
|
|
import { useCallback, useEffect, useMemo, useRef } from "react"; |
|
|
import { Button, Select } from "antd"; |
|
|
import { GenerateActionIcon } from "@/components/media/MediaToolbarIcons"; |
|
|
import { ArrowUpOutlined, ThunderboltFilled } from "@ant-design/icons"; |
|
|
|
|
|
import { PopiModelSelect } from "@/components/nodes/PopiModelSelect"; |
|
|
import { PopiModelSelect } from "@/components/nodes/PopiModelSelect"; |
|
|
import { useModelStore } from "@/store/modelStore"; |
|
|
import { useModelStore } from "@/store/modelStore"; |
|
|
|
|
|
import type { PopiModelDetail } from "@/store/modelStore"; |
|
|
import { useWorkflowStore } from "@/store/workflowStore"; |
|
|
import { useWorkflowStore } from "@/store/workflowStore"; |
|
|
import type { SmartImageNodeData, WorkflowNode } from "@/types"; |
|
|
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"; |
|
|
import { toSelectedModel } from "@/utils/selectedModel"; |
|
|
|
|
|
|
|
|
interface NodeHighDefinitionPanelProps { |
|
|
interface NodeHighDefinitionPanelProps { |
|
|
node: WorkflowNode; |
|
|
node: WorkflowNode; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const SCALE_OPTIONS = [ |
|
|
type SchemaOption = { key: string; label: string; value: unknown }; |
|
|
{ label: "2", value: 2 }, |
|
|
|
|
|
{ label: "4", value: 4 }, |
|
|
|
|
|
{ label: "6", value: 6 }, |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
const MODEL_MODE_OPTIONS = [ |
|
|
function schemaOptions(options: Array<{ label: string; value: unknown }> | undefined, fallbackValue: unknown): SchemaOption[] { |
|
|
{ label: "通用", value: HIGH_DEFINITION_MODEL_MODE }, |
|
|
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<string, unknown> | undefined): number { |
|
|
function selectedOptionKey(options: SchemaOption[], value: unknown): string | undefined { |
|
|
return typeof parameters?.scale === "number" && Number.isFinite(parameters.scale) ? parameters.scale : HIGH_DEFINITION_SCALE; |
|
|
if (value === undefined || value === null) return undefined; |
|
|
|
|
|
return options.find((option) => valueMatchesOption(option.value, value))?.key; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function readModelMode(parameters: Record<string, unknown> | undefined): string { |
|
|
function parameterLabel(parameter: ModelParameter): string { |
|
|
return typeof parameters?.modelMode === "string" && parameters.modelMode ? parameters.modelMode : HIGH_DEFINITION_MODEL_MODE; |
|
|
return parameter.label ?? parameter.name; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function extensionFieldLabel(field: ExtensionFieldSchema): string { |
|
|
|
|
|
return field.label ?? field.name; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) { |
|
|
export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) { |
|
|
@ -37,36 +60,128 @@ export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) |
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(node.id)); |
|
|
const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(node.id)); |
|
|
const highDefinitionModels = useModelStore((state) => state.byKind.highDefinition); |
|
|
const highDefinitionModels = useModelStore((state) => state.byKind.highDefinition); |
|
|
|
|
|
const fetchModelDetail = useModelStore((state) => state.fetchModelDetail); |
|
|
const data = node.data as SmartImageNodeData; |
|
|
const data = node.data as SmartImageNodeData; |
|
|
|
|
|
const hydratedKeyRef = useRef<string | null>(null); |
|
|
const selectedModel = useMemo( |
|
|
const selectedModel = useMemo( |
|
|
() => data.selectedModel ?? chooseHighDefinitionModel(highDefinitionModels), |
|
|
() => data.selectedModel ?? chooseHighDefinitionModel(highDefinitionModels), |
|
|
[data.selectedModel, highDefinitionModels] |
|
|
[data.selectedModel, highDefinitionModels] |
|
|
); |
|
|
); |
|
|
|
|
|
const activeModelDetail = useModelStore((state) => selectedModel.modelId ? state.detailsById[selectedModel.modelId] : undefined); |
|
|
const parameters = buildHighDefinitionParameters(data.parameters); |
|
|
const parameters = buildHighDefinitionParameters(data.parameters); |
|
|
const scale = readScale(parameters); |
|
|
const extraTaskParams = data.extraTaskParams ?? {}; |
|
|
const modelMode = readModelMode(parameters); |
|
|
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 updateParameters = useCallback((patch: Record<string, unknown>) => { |
|
|
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, { |
|
|
updateNodeData(node.id, { |
|
|
parameters: buildHighDefinitionParameters({ |
|
|
selectedModel: model, |
|
|
...(data.parameters ?? {}), |
|
|
parameters: hydratedConfig.parameters, |
|
|
...patch, |
|
|
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 updateParameter = useCallback((name: string, value: unknown) => { |
|
|
|
|
|
updateNodeData(node.id, { |
|
|
|
|
|
parameters: buildHighDefinitionParameters(updateModelParameterValue(data.parameters, name, value)), |
|
|
batchSize: HIGH_DEFINITION_BATCH_SIZE, |
|
|
batchSize: HIGH_DEFINITION_BATCH_SIZE, |
|
|
subType: HIGH_DEFINITION_SUBTYPE, |
|
|
|
|
|
}); |
|
|
}); |
|
|
}, [data.parameters, node.id, updateNodeData]); |
|
|
}, [data.parameters, node.id, updateNodeData]); |
|
|
|
|
|
|
|
|
const handleRun = useCallback(() => { |
|
|
const updateExtensionField = useCallback((name: string, value: unknown) => { |
|
|
updateNodeData(node.id, { |
|
|
updateNodeData(node.id, { |
|
|
selectedModel, |
|
|
extraTaskParams: updateModelParameterValue(data.extraTaskParams, name, value), |
|
|
parameters: buildHighDefinitionParameters(data.parameters), |
|
|
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, |
|
|
batchSize: HIGH_DEFINITION_BATCH_SIZE, |
|
|
subType: HIGH_DEFINITION_SUBTYPE, |
|
|
|
|
|
error: null, |
|
|
error: null, |
|
|
}); |
|
|
}); |
|
|
void regenerateNode(node.id); |
|
|
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 |
|
|
|
|
|
) => ( |
|
|
|
|
|
<div className="grid grid-cols-3 gap-1"> |
|
|
|
|
|
{options.map((option) => { |
|
|
|
|
|
const selected = option.key === selectedKey; |
|
|
|
|
|
return ( |
|
|
|
|
|
<button |
|
|
|
|
|
key={option.key} |
|
|
|
|
|
type="button" |
|
|
|
|
|
aria-pressed={selected} |
|
|
|
|
|
className={`h-8 rounded-lg border px-2 text-sm leading-5 transition-colors ${ |
|
|
|
|
|
selected |
|
|
|
|
|
? "border-[var(--border-strong)] bg-[var(--surface-hover)] text-[var(--text-primary)]" |
|
|
|
|
|
: "border-[var(--border-subtle)] bg-[var(--surface-1)] text-[var(--text-secondary)] hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]" |
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => onSelect(option.value)} |
|
|
|
|
|
> |
|
|
|
|
|
{option.label} |
|
|
|
|
|
</button> |
|
|
|
|
|
); |
|
|
|
|
|
})} |
|
|
|
|
|
</div> |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<section |
|
|
<section |
|
|
@ -84,63 +199,49 @@ export function NodeHighDefinitionPanel({ node }: NodeHighDefinitionPanelProps) |
|
|
<PopiModelSelect |
|
|
<PopiModelSelect |
|
|
kind="highDefinition" |
|
|
kind="highDefinition" |
|
|
value={selectedModel} |
|
|
value={selectedModel} |
|
|
onChange={(_, providerModel) => { |
|
|
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, { |
|
|
updateNodeData(node.id, { |
|
|
selectedModel: toSelectedModel(providerModel), |
|
|
selectedModel: nextModel, |
|
|
parameters: buildHighDefinitionParameters({}), |
|
|
parameters: hydratedConfig.parameters, |
|
|
|
|
|
extraTaskParams: hydratedConfig.extraTaskParams, |
|
|
batchSize: HIGH_DEFINITION_BATCH_SIZE, |
|
|
batchSize: HIGH_DEFINITION_BATCH_SIZE, |
|
|
subType: HIGH_DEFINITION_SUBTYPE, |
|
|
|
|
|
}); |
|
|
}); |
|
|
}} |
|
|
}} |
|
|
size="compact" |
|
|
size="compact" |
|
|
className="w-full" |
|
|
className="w-full" |
|
|
/> |
|
|
/> |
|
|
|
|
|
|
|
|
<span className="text-[var(--text-primary)]">模型</span> |
|
|
{parameterRows.map((row) => ( |
|
|
<Select |
|
|
<div key={row.parameter.name} className="contents"> |
|
|
value={modelMode} |
|
|
<span className="text-[var(--text-primary)]">{parameterLabel(row.parameter)}</span> |
|
|
options={MODEL_MODE_OPTIONS} |
|
|
{renderOptionButtons(row.selectedKey, row.options, (value) => updateParameter(row.parameter.name, value))} |
|
|
size="middle" |
|
|
</div> |
|
|
popupMatchSelectWidth={false} |
|
|
|
|
|
suffixIcon={<span className="text-[10px] text-[var(--text-muted)]">⌄</span>} |
|
|
|
|
|
onChange={(value) => updateParameters({ modelMode: value })} |
|
|
|
|
|
className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!border-[var(--border-subtle)] [&_.ant-select-selector]:!bg-[var(--surface-1)]" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<span className="text-[var(--text-primary)]">放大倍数</span> |
|
|
|
|
|
<div className="grid grid-cols-3 gap-1"> |
|
|
|
|
|
{SCALE_OPTIONS.map((option) => ( |
|
|
|
|
|
<button |
|
|
|
|
|
key={option.value} |
|
|
|
|
|
type="button" |
|
|
|
|
|
aria-pressed={scale === option.value} |
|
|
|
|
|
className={`h-8 rounded-lg border text-sm leading-5 transition-colors ${ |
|
|
|
|
|
scale === option.value |
|
|
|
|
|
? "border-[var(--border-strong)] bg-[var(--surface-hover)] text-[var(--text-primary)]" |
|
|
|
|
|
: "border-[var(--border-subtle)] bg-[var(--surface-1)] text-[var(--text-secondary)] hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]" |
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => updateParameters({ scale: option.value })} |
|
|
|
|
|
> |
|
|
|
|
|
{option.label} |
|
|
|
|
|
</button> |
|
|
|
|
|
))} |
|
|
))} |
|
|
|
|
|
|
|
|
|
|
|
{extensionFieldRows.map((row) => ( |
|
|
|
|
|
<div key={row.field.name} className="contents"> |
|
|
|
|
|
<span className="text-[var(--text-primary)]">{extensionFieldLabel(row.field)}</span> |
|
|
|
|
|
{renderOptionButtons(row.selectedKey, row.options, (value) => updateExtensionField(row.field.name, value))} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div className="mt-3 flex items-center justify-end gap-3"> |
|
|
<div className="mt-3 flex items-center justify-end"> |
|
|
<div className="flex items-center gap-1 text-xs text-[var(--text-muted)]"> |
|
|
<button |
|
|
<ThunderboltFilled className="text-[10px]" /> |
|
|
type="button" |
|
|
<span>15</span> |
|
|
aria-label={isRunning ? "生成中" : "生成"} |
|
|
</div> |
|
|
title={isRunning ? "生成中" : "生成"} |
|
|
<Button |
|
|
disabled={isRunning || !canRun} |
|
|
type="primary" |
|
|
|
|
|
shape="circle" |
|
|
|
|
|
icon={<ArrowUpOutlined />} |
|
|
|
|
|
loading={isRunning} |
|
|
|
|
|
disabled={!selectedModel.modelId && highDefinitionModels.length === 0} |
|
|
|
|
|
onClick={handleRun} |
|
|
onClick={handleRun} |
|
|
className="!h-8 !w-8" |
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--text-primary)] text-[var(--text-inverse)] transition-colors hover:bg-[var(--text-secondary)] disabled:cursor-not-allowed disabled:bg-[var(--surface-active)] disabled:text-[var(--text-disabled)]" |
|
|
/> |
|
|
> |
|
|
|
|
|
<GenerateActionIcon /> |
|
|
|
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
); |
|
|
); |
|
|
|