|
|
|
@ -14,6 +14,7 @@ import { |
|
|
|
import { createPortal } from "react-dom"; |
|
|
|
import { useReactFlow, useViewport, ViewportPortal } from "@xyflow/react"; |
|
|
|
import { useProviderApiKeys, useWorkflowStore } from "@/store/workflowStore"; |
|
|
|
import { useModelStore } from "@/store/modelStore"; |
|
|
|
import { |
|
|
|
AspectRatio, |
|
|
|
EaseCurveNodeData, |
|
|
|
@ -253,6 +254,54 @@ function selectedModelFromProvider(model: ProviderModel): SelectedModel { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
function identifierFromUnknown(value: unknown): string | null { |
|
|
|
if (typeof value === "string" && value.trim().length > 0) return value.trim(); |
|
|
|
if (typeof value === "number" && Number.isFinite(value)) return String(Math.trunc(value)); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
function getSelectedModelIdentifiers(model: SelectedModel): Set<string> { |
|
|
|
return new Set( |
|
|
|
[ |
|
|
|
model.modelId, |
|
|
|
model.displayName, |
|
|
|
identifierFromUnknown(model.metadata?.aiModelId), |
|
|
|
identifierFromUnknown(model.metadata?.aiModelCode), |
|
|
|
identifierFromUnknown(model.metadata?.aiModelCodeAlias), |
|
|
|
] |
|
|
|
.filter((value): value is string => Boolean(value)) |
|
|
|
.map((value) => value.trim().toLowerCase()) |
|
|
|
.filter(Boolean) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function providerModelMatchesSelectedModel(model: ProviderModel, selectedModel: SelectedModel): boolean { |
|
|
|
if (model.provider !== selectedModel.provider) return false; |
|
|
|
const identifiers = getSelectedModelIdentifiers(selectedModel); |
|
|
|
return [ |
|
|
|
model.id, |
|
|
|
model.name, |
|
|
|
identifierFromUnknown(model.metadata?.aiModelId), |
|
|
|
identifierFromUnknown(model.metadata?.aiModelCode), |
|
|
|
identifierFromUnknown(model.metadata?.aiModelCodeAlias), |
|
|
|
] |
|
|
|
.filter((value): value is string => Boolean(value)) |
|
|
|
.some((value) => identifiers.has(value.trim().toLowerCase())); |
|
|
|
} |
|
|
|
|
|
|
|
function findProviderModelForSelectedModel(models: ProviderModel[], selectedModel: SelectedModel): ProviderModel | null { |
|
|
|
return models.find((model) => providerModelMatchesSelectedModel(model, selectedModel)) ?? null; |
|
|
|
} |
|
|
|
|
|
|
|
function selectedModelNeedsPopiserverMetadataLookup(model: SelectedModel): boolean { |
|
|
|
if (model.provider !== "popiserver") return false; |
|
|
|
const billingMethod = finiteIntegerFromUnknown(model.metadata?.billingMethod); |
|
|
|
if (billingMethod === null) return true; |
|
|
|
if (billingMethod !== 1) return false; |
|
|
|
const aiModelId = finiteIntegerFromUnknown(model.metadata?.aiModelId) ?? finiteIntegerFromUnknown(model.modelId); |
|
|
|
return !aiModelId; |
|
|
|
} |
|
|
|
|
|
|
|
function isSameSelectedModel(a: SelectedModel | undefined, b: SelectedModel | undefined): boolean { |
|
|
|
return Boolean(a?.provider && b?.provider && a.provider === b.provider && a.modelId === b.modelId); |
|
|
|
} |
|
|
|
@ -393,6 +442,10 @@ function getPopiserverModelName(model: SelectedModel): string { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function isPopiserverPerRunBillingModel(model: SelectedModel): boolean { |
|
|
|
return model.provider === "popiserver" && finiteIntegerFromUnknown(model.metadata?.billingMethod) === 1; |
|
|
|
} |
|
|
|
|
|
|
|
function buildSeedanceVideoEstimatePayload( |
|
|
|
draft: ComposerDraft, |
|
|
|
inputVideoDurationSeconds: number |
|
|
|
@ -456,6 +509,7 @@ function buildPopiserverTaskPricePayload( |
|
|
|
): PopiserverTaskPricePayload | null { |
|
|
|
const model = draft.selectedModel; |
|
|
|
if (model.provider !== "popiserver") return null; |
|
|
|
if (!isPopiserverPerRunBillingModel(model)) return null; |
|
|
|
|
|
|
|
const aiModelId = finiteIntegerFromUnknown(model.metadata?.aiModelId) ?? finiteIntegerFromUnknown(model.modelId); |
|
|
|
if (!aiModelId) return null; |
|
|
|
@ -906,6 +960,8 @@ export function GenerationComposer() { |
|
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
|
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); |
|
|
|
const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs); |
|
|
|
const popiModels = useModelStore((state) => state.all); |
|
|
|
const refreshPopiModels = useModelStore((state) => state.refreshPopiModels); |
|
|
|
const { newApiWGBaseUrl } = useProviderApiKeys(); |
|
|
|
const { screenToFlowPosition } = useReactFlow(); |
|
|
|
const { x: viewportX, y: viewportY, zoom } = useViewport(); |
|
|
|
@ -1053,6 +1109,79 @@ export function GenerationComposer() { |
|
|
|
? JSON.stringify(popiserverTaskPricePayload) |
|
|
|
: null; |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const selectedModel = draft.selectedModel; |
|
|
|
if (!selectedModelNeedsPopiserverMetadataLookup(selectedModel)) return; |
|
|
|
|
|
|
|
let cancelled = false; |
|
|
|
const applyResolvedModel = (models: ProviderModel[]): boolean => { |
|
|
|
const matchedModel = findProviderModelForSelectedModel(models, selectedModel); |
|
|
|
if (!matchedModel || finiteIntegerFromUnknown(matchedModel.metadata?.billingMethod) === null) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
const resolvedModel = selectedModelFromProvider(matchedModel); |
|
|
|
setDraft((current) => { |
|
|
|
if ( |
|
|
|
!isSameSelectedModel(current.selectedModel, selectedModel) || |
|
|
|
!selectedModelNeedsPopiserverMetadataLookup(current.selectedModel) |
|
|
|
) { |
|
|
|
return current; |
|
|
|
} |
|
|
|
|
|
|
|
const next = { |
|
|
|
...current, |
|
|
|
selectedModel: { |
|
|
|
...resolvedModel, |
|
|
|
capabilities: current.selectedModel.capabilities.length > 0 |
|
|
|
? current.selectedModel.capabilities |
|
|
|
: resolvedModel.capabilities, |
|
|
|
}, |
|
|
|
}; |
|
|
|
draftRef.current = next; |
|
|
|
return next; |
|
|
|
}); |
|
|
|
|
|
|
|
const activeContext = contextRef.current; |
|
|
|
const nodeModel = activeContext.node?.data && "selectedModel" in activeContext.node.data |
|
|
|
? (activeContext.node.data.selectedModel as SelectedModel | undefined) |
|
|
|
: undefined; |
|
|
|
if ( |
|
|
|
activeContext.mode === "node-edit" && |
|
|
|
activeContext.node && |
|
|
|
isSameSelectedModel(nodeModel, selectedModel) && |
|
|
|
nodeModel && |
|
|
|
selectedModelNeedsPopiserverMetadataLookup(nodeModel) |
|
|
|
) { |
|
|
|
updateNodeData(activeContext.node.id, { |
|
|
|
selectedModel: { |
|
|
|
...resolvedModel, |
|
|
|
capabilities: nodeModel.capabilities.length > 0 ? nodeModel.capabilities : resolvedModel.capabilities, |
|
|
|
}, |
|
|
|
}); |
|
|
|
} |
|
|
|
return true; |
|
|
|
}; |
|
|
|
|
|
|
|
if (applyResolvedModel(popiModels)) return; |
|
|
|
|
|
|
|
void refreshPopiModels().then((models) => { |
|
|
|
if (!cancelled) applyResolvedModel(models); |
|
|
|
}); |
|
|
|
|
|
|
|
return () => { |
|
|
|
cancelled = true; |
|
|
|
}; |
|
|
|
}, [ |
|
|
|
draft.selectedModel.displayName, |
|
|
|
draft.selectedModel.metadata, |
|
|
|
draft.selectedModel.modelId, |
|
|
|
draft.selectedModel.provider, |
|
|
|
popiModels, |
|
|
|
refreshPopiModels, |
|
|
|
updateNodeData, |
|
|
|
]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const selectedModel = draft.selectedModel; |
|
|
|
if ( |
|
|
|
|