|
|
|
@ -14,6 +14,7 @@ import { |
|
|
|
POPI_PROVIDER_ID, |
|
|
|
} from "@/lib/providerMode"; |
|
|
|
import { useModelStore } from "@/store/modelStore"; |
|
|
|
import { getPopiCapabilitiesForKind, getPopiKindForSubType } from "@/utils/popiModelClassification"; |
|
|
|
|
|
|
|
// localStorage cache for models (persists across dev server restarts)
|
|
|
|
const MODELS_CACHE_KEY = "node-banana-models-cache-v2"; |
|
|
|
@ -117,9 +118,9 @@ function getPaneCenter() { |
|
|
|
} |
|
|
|
|
|
|
|
// Capability filter options
|
|
|
|
type CapabilityFilter = "all" | "image" | "video" | "3d" | "audio"; |
|
|
|
type CapabilityFilter = "all" | "image" | "video" | "3d" | "audio" | "llm"; |
|
|
|
|
|
|
|
const POPI_MEDIA_FILTERS: Exclude<CapabilityFilter, "all">[] = ["image", "video", "audio", "3d"]; |
|
|
|
const POPI_MEDIA_FILTERS: Exclude<CapabilityFilter, "all">[] = ["image", "video", "audio", "llm"]; |
|
|
|
|
|
|
|
function getEffectiveCapabilityFilter( |
|
|
|
capabilityFilter: CapabilityFilter, |
|
|
|
@ -130,15 +131,38 @@ function getEffectiveCapabilityFilter( |
|
|
|
|
|
|
|
function modelMatchesCapability(model: ProviderModel, filter: CapabilityFilter): boolean { |
|
|
|
if (filter === "all") return true; |
|
|
|
if (filter === "image") return model.capabilities.some((capability) => capability === "text-to-image" || capability === "image-to-image"); |
|
|
|
if (filter === "video") return model.capabilities.some((capability) => |
|
|
|
capability === "text-to-video" || |
|
|
|
capability === "image-to-video" || |
|
|
|
capability === "video-to-video" || |
|
|
|
capability === "audio-to-video" |
|
|
|
); |
|
|
|
if (filter === "audio") return model.capabilities.some((capability) => capability === "text-to-audio"); |
|
|
|
return model.capabilities.some((capability) => capability === "text-to-3d" || capability === "image-to-3d"); |
|
|
|
if (filter === "3d") return model.capabilities.some((capability) => capability === "text-to-3d" || capability === "image-to-3d"); |
|
|
|
return modelHasPopiKind(model, filter); |
|
|
|
} |
|
|
|
|
|
|
|
function getModelSubTypes(model: ProviderModel): number[] { |
|
|
|
const subTypes = model.metadata?.subTypes; |
|
|
|
if (Array.isArray(subTypes)) { |
|
|
|
return subTypes.map((subType) => Number(subType)).filter((subType) => Number.isFinite(subType)); |
|
|
|
} |
|
|
|
const subType = Number(model.metadata?.subType); |
|
|
|
return Number.isFinite(subType) ? [subType] : []; |
|
|
|
} |
|
|
|
|
|
|
|
function modelHasPopiKind(model: ProviderModel, filter: Exclude<CapabilityFilter, "all" | "3d">): boolean { |
|
|
|
if (getModelSubTypes(model).some((subType) => getPopiKindForSubType(subType) === filter)) return true; |
|
|
|
return model.capabilities.some((capability) => getPopiCapabilitiesForKind(filter).includes(capability)); |
|
|
|
} |
|
|
|
|
|
|
|
function capabilitiesForFilter(filter: CapabilityFilter, fallback: ModelCapability[]): ModelCapability[] { |
|
|
|
if (filter === "image" || filter === "video" || filter === "audio" || filter === "llm") { |
|
|
|
return getPopiCapabilitiesForKind(filter); |
|
|
|
} |
|
|
|
if (filter === "3d") return ["text-to-3d", "image-to-3d"]; |
|
|
|
return fallback; |
|
|
|
} |
|
|
|
|
|
|
|
function capabilityFilterToNodeType(filter: CapabilityFilter): "nanoBanana" | "generateVideo" | "generate3d" | "generateAudio" | "llmGenerate" { |
|
|
|
if (filter === "video") return "generateVideo"; |
|
|
|
if (filter === "3d") return "generate3d"; |
|
|
|
if (filter === "audio") return "generateAudio"; |
|
|
|
if (filter === "llm") return "llmGenerate"; |
|
|
|
return "nanoBanana"; |
|
|
|
} |
|
|
|
|
|
|
|
function filterModelsBySearch(models: ProviderModel[], searchQuery: string): ProviderModel[] { |
|
|
|
@ -288,15 +312,7 @@ export function ModelSearchDialog({ |
|
|
|
params.set("provider", activeProviderFilter); |
|
|
|
} |
|
|
|
if (effectiveCapabilityFilter !== "all") { |
|
|
|
const capabilities = |
|
|
|
effectiveCapabilityFilter === "image" |
|
|
|
? "text-to-image,image-to-image" |
|
|
|
: effectiveCapabilityFilter === "video" |
|
|
|
? "text-to-video,image-to-video,audio-to-video" |
|
|
|
: effectiveCapabilityFilter === "3d" |
|
|
|
? "text-to-3d,image-to-3d" |
|
|
|
: "text-to-audio"; |
|
|
|
params.set("capabilities", capabilities); |
|
|
|
params.set("capabilities", capabilitiesForFilter(effectiveCapabilityFilter, []).join(",")); |
|
|
|
} |
|
|
|
if (bypassCache) { |
|
|
|
params.set("refresh", "true"); |
|
|
|
@ -428,7 +444,13 @@ export function ModelSearchDialog({ |
|
|
|
|
|
|
|
// If onModelSelected is provided, use it to update an existing node
|
|
|
|
if (onModelSelected) { |
|
|
|
onModelSelected(model); |
|
|
|
const effectiveCapabilityFilter = getEffectiveCapabilityFilter(capabilityFilter, popiProviderMode); |
|
|
|
onModelSelected({ |
|
|
|
...model, |
|
|
|
capabilities: popiProviderMode |
|
|
|
? capabilitiesForFilter(effectiveCapabilityFilter, model.capabilities) |
|
|
|
: model.capabilities, |
|
|
|
}); |
|
|
|
onClose(); |
|
|
|
return; |
|
|
|
} |
|
|
|
@ -441,6 +463,7 @@ export function ModelSearchDialog({ |
|
|
|
}); |
|
|
|
|
|
|
|
// Determine node type based on model capabilities
|
|
|
|
const effectiveCapabilityFilter = getEffectiveCapabilityFilter(capabilityFilter, popiProviderMode); |
|
|
|
const isVideoModel = model.capabilities.some( |
|
|
|
(cap) => cap === "text-to-video" || cap === "image-to-video" || cap === "audio-to-video" |
|
|
|
); |
|
|
|
@ -450,15 +473,29 @@ export function ModelSearchDialog({ |
|
|
|
const isAudioModel = model.capabilities.some( |
|
|
|
(cap) => cap === "text-to-audio" |
|
|
|
); |
|
|
|
|
|
|
|
const nodeType = isVideoModel ? "generateVideo" : is3DModel ? "generate3d" : isAudioModel ? "generateAudio" : "nanoBanana"; |
|
|
|
const isLlmModel = modelMatchesCapability(model, "llm"); |
|
|
|
|
|
|
|
const nodeType = popiProviderMode |
|
|
|
? capabilityFilterToNodeType(effectiveCapabilityFilter) |
|
|
|
: isLlmModel |
|
|
|
? "llmGenerate" |
|
|
|
: isVideoModel |
|
|
|
? "generateVideo" |
|
|
|
: is3DModel |
|
|
|
? "generate3d" |
|
|
|
: isAudioModel |
|
|
|
? "generateAudio" |
|
|
|
: "nanoBanana"; |
|
|
|
const selectedCapabilities = popiProviderMode |
|
|
|
? capabilitiesForFilter(effectiveCapabilityFilter, model.capabilities) |
|
|
|
: model.capabilities; |
|
|
|
|
|
|
|
addNode(nodeType, position, { |
|
|
|
selectedModel: { |
|
|
|
provider: model.provider, |
|
|
|
modelId: model.id, |
|
|
|
displayName: model.name, |
|
|
|
capabilities: model.capabilities, |
|
|
|
capabilities: selectedCapabilities, |
|
|
|
metadata: model.metadata, |
|
|
|
pricing: model.pricing |
|
|
|
? { |
|
|
|
@ -472,7 +509,7 @@ export function ModelSearchDialog({ |
|
|
|
|
|
|
|
onClose(); |
|
|
|
}, |
|
|
|
[screenToFlowPosition, addNode, onClose, onModelSelected, trackModelUsage] |
|
|
|
[screenToFlowPosition, addNode, onClose, onModelSelected, trackModelUsage, capabilityFilter, popiProviderMode] |
|
|
|
); |
|
|
|
|
|
|
|
// Handle escape key
|
|
|
|
@ -574,24 +611,7 @@ export function ModelSearchDialog({ |
|
|
|
if (effectiveCapabilityFilter === "all") return true; |
|
|
|
if (!matchingModel) return true; // Show if we can't verify capabilities
|
|
|
|
|
|
|
|
const isImage = matchingModel.capabilities.some( |
|
|
|
(cap) => cap === "text-to-image" || cap === "image-to-image" |
|
|
|
); |
|
|
|
const isVideo = matchingModel.capabilities.some( |
|
|
|
(cap) => cap === "text-to-video" || cap === "image-to-video" || cap === "audio-to-video" |
|
|
|
); |
|
|
|
const is3D = matchingModel.capabilities.some( |
|
|
|
(cap) => cap === "text-to-3d" || cap === "image-to-3d" |
|
|
|
); |
|
|
|
const isAudio = matchingModel.capabilities.some( |
|
|
|
(cap) => cap === "text-to-audio" |
|
|
|
); |
|
|
|
|
|
|
|
if (effectiveCapabilityFilter === "image") return isImage; |
|
|
|
if (effectiveCapabilityFilter === "video") return isVideo; |
|
|
|
if (effectiveCapabilityFilter === "3d") return is3D; |
|
|
|
if (effectiveCapabilityFilter === "audio") return isAudio; |
|
|
|
return true; |
|
|
|
return modelMatchesCapability(matchingModel, effectiveCapabilityFilter); |
|
|
|
}) |
|
|
|
.slice(0, 4); // Show max 4
|
|
|
|
}, [recentModels, models, capabilityFilter, popiProviderMode]); |
|
|
|
@ -667,6 +687,10 @@ export function ModelSearchDialog({ |
|
|
|
color = "bg-violet-500/20 text-violet-300"; |
|
|
|
label = "audio→vid"; |
|
|
|
break; |
|
|
|
case "text-to-text": |
|
|
|
color = "bg-sky-500/20 text-sky-300"; |
|
|
|
label = "txt→txt"; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (label) { |
|
|
|
@ -688,6 +712,7 @@ export function ModelSearchDialog({ |
|
|
|
if (filter === "image") return t("modelSearch.image"); |
|
|
|
if (filter === "video") return t("modelSearch.video"); |
|
|
|
if (filter === "audio") return t("modelSearch.audio"); |
|
|
|
if (filter === "llm") return t("modelSearch.llm"); |
|
|
|
if (filter === "3d") return "3D"; |
|
|
|
return t("modelSearch.allTypes"); |
|
|
|
}; |
|
|
|
@ -880,8 +905,8 @@ export function ModelSearchDialog({ |
|
|
|
<option value="all">{t("modelSearch.allTypes")}</option> |
|
|
|
<option value="image">{t("modelSearch.image")}</option> |
|
|
|
<option value="video">{t("modelSearch.video")}</option> |
|
|
|
<option value="3d">3D</option> |
|
|
|
<option value="audio">{t("modelSearch.audio")}</option> |
|
|
|
<option value="llm">{t("modelSearch.llm")}</option> |
|
|
|
</select> |
|
|
|
)} |
|
|
|
|
|
|
|
|