|
|
|
@ -4,7 +4,7 @@ import { useMemo, useState, useCallback, useEffect, useRef } from "react"; |
|
|
|
import { createPortal } from "react-dom"; |
|
|
|
import { Node } from "@xyflow/react"; |
|
|
|
import { useWorkflowStore, saveNanoBananaDefaults, useProviderApiKeys } from "@/store/workflowStore"; |
|
|
|
import { NodeType, NanoBananaNodeData, GenerateVideoNodeData, Generate3DNodeData, GenerateAudioNodeData, EaseCurveNodeData, ConditionalSwitchNodeData, AspectRatio, Resolution, ModelType, MODEL_DISPLAY_NAMES, ProviderType, SelectedModel, MatchMode, ConditionalSwitchRule } from "@/types"; |
|
|
|
import { NodeType, NanoBananaNodeData, Generate3DNodeData, GenerateAudioNodeData, EaseCurveNodeData, ConditionalSwitchNodeData, AspectRatio, Resolution, ModelType, MODEL_DISPLAY_NAMES, ProviderType, SelectedModel, MatchMode, ConditionalSwitchRule } from "@/types"; |
|
|
|
import { ProviderModel, ModelCapability } from "@/lib/providers/types"; |
|
|
|
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; |
|
|
|
import { ModelParameters } from "./ModelParameters"; |
|
|
|
@ -21,8 +21,6 @@ import { isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; |
|
|
|
import { appendPopiModelQueryParams } from "@/utils/popiModelFilters"; |
|
|
|
import { SINGLE_INPUT_HANDLE_ID } from "@/utils/nodeHandles"; |
|
|
|
import { toSelectedModel } from "@/utils/selectedModel"; |
|
|
|
import { |
|
|
|
} from "@/utils/videoGenerationSettings"; |
|
|
|
|
|
|
|
// List of node types that have configurable parameters
|
|
|
|
const CONFIGURABLE_NODE_TYPES: NodeType[] = [ |
|
|
|
@ -150,9 +148,6 @@ export function ControlPanel() { |
|
|
|
{(selectedNode.type === "smartImage" || selectedNode.type === "nanoBanana") && ( |
|
|
|
<GenerateImageControls node={selectedNode} /> |
|
|
|
)} |
|
|
|
{selectedNode.type === "generateVideo" && ( |
|
|
|
<GenerateVideoControls node={selectedNode} /> |
|
|
|
)} |
|
|
|
{selectedNode.type === "generate3d" && ( |
|
|
|
<Generate3DControls node={selectedNode} /> |
|
|
|
)} |
|
|
|
@ -510,158 +505,6 @@ function GenerateImageControls({ node }: { node: Node }) { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// Generate Video Controls
|
|
|
|
function GenerateVideoControls({ node }: { node: Node }) { |
|
|
|
const nodeData = node.data as GenerateVideoNodeData; |
|
|
|
const { t } = useI18n(); |
|
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
|
const isRunning = useWorkflowStore((state) => state.isRunning); |
|
|
|
const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); |
|
|
|
const popiProviderMode = isPopiProviderMode(); |
|
|
|
|
|
|
|
const currentProvider: ProviderType = nodeData.selectedModel?.provider || (popiProviderMode ? POPI_PROVIDER_ID : "fal"); |
|
|
|
|
|
|
|
const handleParametersChange = useCallback( |
|
|
|
(parameters: Record<string, unknown>) => { |
|
|
|
updateNodeData(node.id, { parameters }); |
|
|
|
}, |
|
|
|
[node.id, updateNodeData] |
|
|
|
); |
|
|
|
|
|
|
|
const handleBrowseModelSelect = useCallback((model: ProviderModel) => { |
|
|
|
const newSelectedModel: SelectedModel = toSelectedModel(model); |
|
|
|
updateNodeData(node.id, { |
|
|
|
selectedModel: newSelectedModel, |
|
|
|
resolution: nodeData.resolution ?? DEFAULT_VIDEO_RESOLUTION, |
|
|
|
durationSeconds: nodeData.durationSeconds ?? DEFAULT_VIDEO_DURATION_SECONDS, |
|
|
|
soundEnabled: getDefaultVideoSoundEnabled(newSelectedModel), |
|
|
|
parameters: {}, |
|
|
|
}); |
|
|
|
setIsBrowseDialogOpen(false); |
|
|
|
}, [node.id, nodeData.durationSeconds, nodeData.resolution, updateNodeData]); |
|
|
|
|
|
|
|
const videoSoundSupported = modelSupportsVideoSound(nodeData.selectedModel); |
|
|
|
const hiddenVideoParameterNames = videoSoundSupported |
|
|
|
? VIDEO_FIRST_CLASS_PARAMETER_NAMES |
|
|
|
: [...VIDEO_DURATION_PARAMETER_NAMES, ...VIDEO_RESOLUTION_PARAMETER_NAMES]; |
|
|
|
|
|
|
|
return ( |
|
|
|
<> |
|
|
|
<div className="space-y-3"> |
|
|
|
{/* Model name + provider with link */} |
|
|
|
<div className="border-t border-neutral-700 pt-3"> |
|
|
|
<div className="flex items-start gap-2"> |
|
|
|
<div className="flex-1 min-w-0"> |
|
|
|
<div className="text-sm text-neutral-100 truncate"> |
|
|
|
{nodeData.selectedModel?.displayName || t("node.selectModel")} |
|
|
|
</div> |
|
|
|
{!popiProviderMode && ( |
|
|
|
<div className="flex items-center gap-1 mt-0.5"> |
|
|
|
<span className="text-[10px] text-neutral-500 truncate"> |
|
|
|
{getProviderDisplayName(currentProvider)} |
|
|
|
</span> |
|
|
|
{nodeData.selectedModel?.modelId && ( |
|
|
|
<a |
|
|
|
href={getModelPageUrl(currentProvider, nodeData.selectedModel.modelId) || "#"} |
|
|
|
target="_blank" |
|
|
|
rel="noopener noreferrer" |
|
|
|
className="text-neutral-500 hover:text-neutral-300 transition-colors" |
|
|
|
title={`View on ${getProviderDisplayName(currentProvider)}`} |
|
|
|
onClick={(e) => { |
|
|
|
if (!getModelPageUrl(currentProvider, nodeData.selectedModel?.modelId || "")) { |
|
|
|
e.preventDefault(); |
|
|
|
} |
|
|
|
}} |
|
|
|
> |
|
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> |
|
|
|
</svg> |
|
|
|
</a> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
<button |
|
|
|
onClick={() => setIsBrowseDialogOpen(true)} |
|
|
|
className="nodrag nopan shrink-0 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors" |
|
|
|
> |
|
|
|
{t("common.browse")} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2"> |
|
|
|
<label className="space-y-1 text-xs text-neutral-400"> |
|
|
|
<span>{t("node.duration")}</span> |
|
|
|
<input |
|
|
|
type="number" |
|
|
|
min="4" |
|
|
|
step="1" |
|
|
|
value={nodeData.durationSeconds ?? DEFAULT_VIDEO_DURATION_SECONDS} |
|
|
|
onChange={(event) => updateNodeData(node.id, { durationSeconds: normalizeVideoDurationSeconds(event.target.value) })} |
|
|
|
className="nodrag nopan w-full rounded bg-neutral-800 px-2 py-1.5 text-neutral-100 outline-none focus:ring-1 focus:ring-neutral-600" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label className="space-y-1 text-xs text-neutral-400"> |
|
|
|
<span>{t("node.resolution")}</span> |
|
|
|
<select |
|
|
|
value={nodeData.resolution ?? DEFAULT_VIDEO_RESOLUTION} |
|
|
|
onChange={(event) => updateNodeData(node.id, { resolution: event.target.value })} |
|
|
|
className="nodrag nopan w-full rounded bg-neutral-800 px-2 py-1.5 text-neutral-100 outline-none focus:ring-1 focus:ring-neutral-600" |
|
|
|
> |
|
|
|
{VIDEO_RESOLUTION_OPTIONS.map((item) => ( |
|
|
|
<option key={item} value={item}>{item}</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
</label> |
|
|
|
{videoSoundSupported && ( |
|
|
|
<label className="col-span-2 flex items-center gap-2 text-xs text-neutral-300"> |
|
|
|
<input |
|
|
|
type="checkbox" |
|
|
|
checked={nodeData.soundEnabled ?? true} |
|
|
|
onChange={(event) => updateNodeData(node.id, { soundEnabled: event.target.checked })} |
|
|
|
className="nodrag nopan h-3 w-3 rounded bg-neutral-800" |
|
|
|
/> |
|
|
|
<span>{t("node.sound")}</span> |
|
|
|
</label> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
{nodeData.selectedModel?.modelId && ( |
|
|
|
<ModelParameters |
|
|
|
modelId={nodeData.selectedModel.modelId} |
|
|
|
provider={currentProvider} |
|
|
|
parameters={nodeData.parameters || {}} |
|
|
|
onParametersChange={handleParametersChange} |
|
|
|
hiddenParameterNames={hiddenVideoParameterNames} |
|
|
|
/> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="flex justify-end"> |
|
|
|
<button |
|
|
|
onClick={() => regenerateNode(node.id)} |
|
|
|
disabled={isRunning} |
|
|
|
className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors" |
|
|
|
> |
|
|
|
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg> |
|
|
|
{isRunning ? t("toolbar.running") : t("toolbar.run")} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
{isBrowseDialogOpen && ( |
|
|
|
<ModelSearchDialog |
|
|
|
isOpen={isBrowseDialogOpen} |
|
|
|
onClose={() => setIsBrowseDialogOpen(false)} |
|
|
|
onModelSelected={handleBrowseModelSelect} |
|
|
|
initialCapabilityFilter="video" |
|
|
|
/> |
|
|
|
)} |
|
|
|
</> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// Generate 3D Controls
|
|
|
|
function Generate3DControls({ node }: { node: Node }) { |
|
|
|
const nodeData = node.data as Generate3DNodeData; |
|
|
|
|