|
|
@ -23,6 +23,7 @@ import { |
|
|
type ComposerContext, |
|
|
type ComposerContext, |
|
|
} from "@/utils/composerNodeDescriptor"; |
|
|
} from "@/utils/composerNodeDescriptor"; |
|
|
import { |
|
|
import { |
|
|
|
|
|
buildGenerationConfigDraftFromModelSchema, |
|
|
buildInitialGenerationNodeData, |
|
|
buildInitialGenerationNodeData, |
|
|
buildSubmittableGenerationConfig, |
|
|
buildSubmittableGenerationConfig, |
|
|
filterTaskRoutingParameters, |
|
|
filterTaskRoutingParameters, |
|
|
@ -99,6 +100,7 @@ const PROMPT_LINE_HEIGHT_PX = 24; |
|
|
const PROMPT_MIN_HEIGHT_PX = PROMPT_LINE_HEIGHT_PX * 2; |
|
|
const PROMPT_MIN_HEIGHT_PX = PROMPT_LINE_HEIGHT_PX * 2; |
|
|
const PROMPT_MAX_HEIGHT_PX = PROMPT_LINE_HEIGHT_PX * 4; |
|
|
const PROMPT_MAX_HEIGHT_PX = PROMPT_LINE_HEIGHT_PX * 4; |
|
|
const EMPTY_EXTRA_TASK_PARAMS: Record<string, unknown> = {}; |
|
|
const EMPTY_EXTRA_TASK_PARAMS: Record<string, unknown> = {}; |
|
|
|
|
|
const ASPECT_RATIO_PARAMETER_NAMES = ["ratio", "aspectRatio", "aspect_ratio", "imageRatio", "image_ratio"]; |
|
|
|
|
|
|
|
|
const EMPTY_SELECTED_MODEL: SelectedModel = { |
|
|
const EMPTY_SELECTED_MODEL: SelectedModel = { |
|
|
provider: "popiserver", |
|
|
provider: "popiserver", |
|
|
@ -208,6 +210,18 @@ function buildComposerGenerationConfig(config: ComposerConfig, prompt: string): |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function readAspectRatioFromComposerConfig(config: Pick<GenerationNodeConfig, "parameters">): string | undefined { |
|
|
|
|
|
for (const name of ASPECT_RATIO_PARAMETER_NAMES) { |
|
|
|
|
|
const value = config.parameters?.[name]; |
|
|
|
|
|
if (typeof value === "string" && value.trim()) return value.trim(); |
|
|
|
|
|
} |
|
|
|
|
|
return undefined; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function composerConfigEquals(a: ComposerConfig, b: ComposerConfig): boolean { |
|
|
|
|
|
return JSON.stringify(a) === JSON.stringify(b); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function referenceSubjectsToMaterials(subjects: ComposerConfig["referenceSubjectList"]): ComposerReferenceMaterial[] { |
|
|
function referenceSubjectsToMaterials(subjects: ComposerConfig["referenceSubjectList"]): ComposerReferenceMaterial[] { |
|
|
return readReferenceSubjectList(subjects).map((subject, index) => ({ |
|
|
return readReferenceSubjectList(subjects).map((subject, index) => ({ |
|
|
id: index + 1, |
|
|
id: index + 1, |
|
|
@ -317,12 +331,14 @@ function buildComposerConfigPersistPatch(node: WorkflowNode, config: GenerationN |
|
|
const nodeType = node.type; |
|
|
const nodeType = node.type; |
|
|
const parameters = buildComposerPersistParameters(node, config); |
|
|
const parameters = buildComposerPersistParameters(node, config); |
|
|
if (nodeType === "smartImage" || nodeType === "nanoBanana") { |
|
|
if (nodeType === "smartImage" || nodeType === "nanoBanana") { |
|
|
|
|
|
const aspectRatio = readAspectRatioFromComposerConfig(config); |
|
|
return { |
|
|
return { |
|
|
inputPrompt: config.prompt.trim(), |
|
|
inputPrompt: config.prompt.trim(), |
|
|
selectedModel: config.selectedModel, |
|
|
selectedModel: config.selectedModel, |
|
|
batchSize: config.batchSize, |
|
|
batchSize: config.batchSize, |
|
|
parameters, |
|
|
parameters, |
|
|
extraTaskParams: config.extraTaskParams ?? {}, |
|
|
extraTaskParams: config.extraTaskParams ?? {}, |
|
|
|
|
|
...(aspectRatio ? { aspectRatio } : {}), |
|
|
} as Partial<NanoBananaNodeData>; |
|
|
} as Partial<NanoBananaNodeData>; |
|
|
} |
|
|
} |
|
|
if (nodeType === "smartVideo" || nodeType === "generateVideo") { |
|
|
if (nodeType === "smartVideo" || nodeType === "generateVideo") { |
|
|
@ -713,6 +729,7 @@ function ComposerEditor({ |
|
|
const suppressAutoPriceRefreshRef = useRef(false); |
|
|
const suppressAutoPriceRefreshRef = useRef(false); |
|
|
const modelChangeCompletionTimerRef = useRef<number | null>(null); |
|
|
const modelChangeCompletionTimerRef = useRef<number | null>(null); |
|
|
const priceRefreshTimerRef = useRef<number | null>(null); |
|
|
const priceRefreshTimerRef = useRef<number | null>(null); |
|
|
|
|
|
const hydratedModelDefaultsRef = useRef<Set<string>>(new Set()); |
|
|
const pricingInputsRef = useRef<PricingInputsSnapshot>({ |
|
|
const pricingInputsRef = useRef<PricingInputsSnapshot>({ |
|
|
config, |
|
|
config, |
|
|
prompt, |
|
|
prompt, |
|
|
@ -777,6 +794,41 @@ function ComposerEditor({ |
|
|
: storedModelFitsCapability(config.selectedModel, context.capability); |
|
|
: storedModelFitsCapability(config.selectedModel, context.capability); |
|
|
const canChooseModel = context.mode === "empty-create" || context.mode === "node-edit"; |
|
|
const canChooseModel = context.mode === "empty-create" || context.mode === "node-edit"; |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (isHidden) return; |
|
|
|
|
|
if (context.mode !== "node-edit" || !context.node) return; |
|
|
|
|
|
if (!activeModelDetail || !config.selectedModel.modelId) return; |
|
|
|
|
|
const hydrationKey = `${context.node.id}:${config.selectedModel.modelId}`; |
|
|
|
|
|
if (hydratedModelDefaultsRef.current.has(hydrationKey)) return; |
|
|
|
|
|
hydratedModelDefaultsRef.current.add(hydrationKey); |
|
|
|
|
|
|
|
|
|
|
|
const { prompt: _prompt, ...hydratedConfig } = buildGenerationConfigDraftFromModelSchema( |
|
|
|
|
|
buildGenerationConfigForContext(context), |
|
|
|
|
|
activeModelDetail |
|
|
|
|
|
); |
|
|
|
|
|
let shouldPersistAspectRatio = false; |
|
|
|
|
|
if (context.capability === "image") { |
|
|
|
|
|
const aspectRatio = readAspectRatioFromComposerConfig(hydratedConfig); |
|
|
|
|
|
if (aspectRatio) { |
|
|
|
|
|
onApplyNodeSize(context.node.id, aspectRatio); |
|
|
|
|
|
shouldPersistAspectRatio = (context.node.data as { aspectRatio?: unknown }).aspectRatio !== aspectRatio; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (composerConfigEquals(config, hydratedConfig) && !shouldPersistAspectRatio) return; |
|
|
|
|
|
|
|
|
|
|
|
setConfig(hydratedConfig); |
|
|
|
|
|
onPersistConfig(context, buildGenerationConfigForContext(context, hydratedConfig, prompt)); |
|
|
|
|
|
}, [ |
|
|
|
|
|
activeModelDetail, |
|
|
|
|
|
buildGenerationConfigForContext, |
|
|
|
|
|
config, |
|
|
|
|
|
context, |
|
|
|
|
|
isHidden, |
|
|
|
|
|
onApplyNodeSize, |
|
|
|
|
|
onPersistConfig, |
|
|
|
|
|
prompt, |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
const connectedReferenceImages = context.mode === "node-edit" ? connectedInputs?.images ?? [] : []; |
|
|
const connectedReferenceImages = context.mode === "node-edit" ? connectedInputs?.images ?? [] : []; |
|
|
const connectedReferenceAudio = context.mode === "node-edit" ? connectedInputs?.audio ?? [] : []; |
|
|
const connectedReferenceAudio = context.mode === "node-edit" ? connectedInputs?.audio ?? [] : []; |
|
|
const referenceImages = connectedReferenceImages.length > 0 ? connectedReferenceImages : config.inputImages; |
|
|
const referenceImages = connectedReferenceImages.length > 0 ? connectedReferenceImages : config.inputImages; |
|
|
@ -910,7 +962,7 @@ function ComposerEditor({ |
|
|
generationConfig, |
|
|
generationConfig, |
|
|
detail ?? inputs.activeModelDetail ?? { parameters: inputs.modelSchema, extensionFields: inputs.extensionFields } |
|
|
detail ?? inputs.activeModelDetail ?? { parameters: inputs.modelSchema, extensionFields: inputs.extensionFields } |
|
|
); |
|
|
); |
|
|
const quoteRequest = buildPopiserverPriceQuoteRequest(buildComposerPricingContext({ |
|
|
const pricingContextInput = { |
|
|
config: normalizedConfig, |
|
|
config: normalizedConfig, |
|
|
prompt: nextPrompt, |
|
|
prompt: nextPrompt, |
|
|
capability: context.capability, |
|
|
capability: context.capability, |
|
|
@ -918,12 +970,17 @@ function ComposerEditor({ |
|
|
modelDetail: detail, |
|
|
modelDetail: detail, |
|
|
media: inputs.popiserverPricingMedia, |
|
|
media: inputs.popiserverPricingMedia, |
|
|
inputVideoDurationSeconds: inputs.connectedVideoDurationSeconds, |
|
|
inputVideoDurationSeconds: inputs.connectedVideoDurationSeconds, |
|
|
})); |
|
|
}; |
|
|
|
|
|
console.info("[points quote] build input params", pricingContextInput); |
|
|
|
|
|
|
|
|
|
|
|
const quoteRequest = buildPopiserverPriceQuoteRequest(buildComposerPricingContext(pricingContextInput)); |
|
|
if (!quoteRequest) { |
|
|
if (!quoteRequest) { |
|
|
setPriceState({ amount: null, isLoading: false }); |
|
|
setPriceState({ amount: null, isLoading: false }); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.info("[points quote] request params", quoteRequest); |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
const response = await fetch("/api/points/quote", { |
|
|
const response = await fetch("/api/points/quote", { |
|
|
method: "POST", |
|
|
method: "POST", |
|
|
@ -961,7 +1018,6 @@ function ComposerEditor({ |
|
|
}, [config, context.node, updateNodeExtraTaskParams]); |
|
|
}, [config, context.node, updateNodeExtraTaskParams]); |
|
|
|
|
|
|
|
|
const schedulePriceRefresh = useCallback(() => { |
|
|
const schedulePriceRefresh = useCallback(() => { |
|
|
console.log('priceRefreshTimerRef -----------', priceRefreshTimerRef.current) |
|
|
|
|
|
if (priceRefreshTimerRef.current !== null) { |
|
|
if (priceRefreshTimerRef.current !== null) { |
|
|
window.clearTimeout(priceRefreshTimerRef.current); |
|
|
window.clearTimeout(priceRefreshTimerRef.current); |
|
|
} |
|
|
} |
|
|
@ -971,7 +1027,6 @@ function ComposerEditor({ |
|
|
if (latest.isHidden) return; |
|
|
if (latest.isHidden) return; |
|
|
if (suppressAutoPriceRefreshRef.current) return; |
|
|
if (suppressAutoPriceRefreshRef.current) return; |
|
|
if (!latest.config.selectedModel.modelId) return; |
|
|
if (!latest.config.selectedModel.modelId) return; |
|
|
console.log('refreshPrice ---------', latest) |
|
|
|
|
|
void refreshPrice(latest.config, latest.prompt, latest.activeModelDetail, latest); |
|
|
void refreshPrice(latest.config, latest.prompt, latest.activeModelDetail, latest); |
|
|
}, 200); |
|
|
}, 200); |
|
|
}, [refreshPrice]); |
|
|
}, [refreshPrice]); |
|
|
|