fix: recognize runway video models in canvas chat
This commit is contained in:
parent
7e7c7f3a1a
commit
dae5581c76
@ -28,6 +28,10 @@ import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { Artifact } from "@/components/artifact";
|
||||
import { ProtocolDebugPanel } from "@/components/protocol-debug-panel";
|
||||
@ -55,6 +59,11 @@ function normalizeImageParamsForProvider(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (providerType === "runway") {
|
||||
normalized.imageCount = 1;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (providerType === "alibaba-image") {
|
||||
normalized.imageCount = Math.min(Math.max(currentCount, 1), 6);
|
||||
return normalized;
|
||||
@ -487,6 +496,7 @@ export function Chat({
|
||||
providerType: string;
|
||||
supportsVision?: boolean;
|
||||
supportsImageGen?: boolean;
|
||||
capabilities?: any;
|
||||
videoToolCapability?: VideoToolCapabilityContract | null;
|
||||
imageGenConfig?: {
|
||||
maxImages?: number;
|
||||
@ -503,13 +513,18 @@ export function Chat({
|
||||
}>("/api/models", fetcher);
|
||||
|
||||
const currentModel = modelsData?.models?.find(m => m.id === currentModelId);
|
||||
const isVideoModel =
|
||||
currentModel?.providerType === "volcano-video" ||
|
||||
currentModel?.providerType === "doubao-video" ||
|
||||
currentModel?.providerType === "qianwen-video" ||
|
||||
currentModel?.providerType === "fal-video" ||
|
||||
currentModel?.providerType === "ruxa-video";
|
||||
const isImageModel = currentModel?.supportsImageGen === true;
|
||||
const isVideoModel = isVideoModelDescriptor({
|
||||
providerType: currentModel?.providerType,
|
||||
modelId: currentModel?.modelId,
|
||||
capabilities: currentModel?.capabilities,
|
||||
supportsImageGen: currentModel?.supportsImageGen,
|
||||
});
|
||||
const isImageModel = isImageGenerationModelDescriptor({
|
||||
providerType: currentModel?.providerType,
|
||||
modelId: currentModel?.modelId,
|
||||
capabilities: currentModel?.capabilities,
|
||||
supportsImageGen: currentModel?.supportsImageGen,
|
||||
});
|
||||
|
||||
// 🔍 调试日志
|
||||
console.log('[Chat] Render - currentModelId:', currentModelId);
|
||||
@ -1074,16 +1089,27 @@ export function Chat({
|
||||
// 🆕 gemini-image 和 huanke-gemini 都视为 gemini 类型
|
||||
if (model?.providerType === "gemini" || model?.providerType === "gemini-image" || model?.providerType === "huanke-gemini") modelType = "gemini";
|
||||
else if (model?.providerType === "volcano-vision") modelType = "volcano-vision";
|
||||
else if (model?.providerType === "volcano-video" || model?.providerType === "doubao-video") modelType = "volcano-video";
|
||||
else if (model?.providerType === "qianwen-video") modelType = "qianwen-video";
|
||||
else if (model?.providerType === "fal-video") modelType = "fal-video";
|
||||
else if (model?.providerType === "ruxa-video") modelType = "ruxa-video";
|
||||
else if (
|
||||
model?.providerType === "alibaba-image" ||
|
||||
model?.providerType === "ruxa-image" ||
|
||||
model?.providerType === "fal-image" ||
|
||||
model?.providerType === "openai-image" ||
|
||||
model?.providerType === "midjourney"
|
||||
isVideoModelDescriptor({
|
||||
providerType: model?.providerType,
|
||||
modelId: model?.modelId,
|
||||
capabilities: model?.capabilities,
|
||||
supportsImageGen: model?.supportsImageGen,
|
||||
})
|
||||
) modelType = model?.providerType === "qianwen-video"
|
||||
? "qianwen-video"
|
||||
: model?.providerType === "fal-video"
|
||||
? "fal-video"
|
||||
: model?.providerType === "ruxa-video"
|
||||
? "ruxa-video"
|
||||
: "volcano-video";
|
||||
else if (
|
||||
isImageGenerationModelDescriptor({
|
||||
providerType: model?.providerType,
|
||||
modelId: model?.modelId,
|
||||
capabilities: model?.capabilities,
|
||||
supportsImageGen: model?.supportsImageGen,
|
||||
})
|
||||
) modelType = "alibaba-image";
|
||||
|
||||
// 智能自动模式下,如果当前轮输入包含图片,默认按图片生成链路跟踪
|
||||
|
||||
@ -3,6 +3,10 @@ import equal from "fast-deep-equal";
|
||||
import { ArrowDownIcon } from "lucide-react";
|
||||
import { memo, useEffect, useState, useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { useMessages } from "@/hooks/use-messages";
|
||||
import type { PendingGeneration } from "@/hooks/use-pending-generation";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
@ -121,11 +125,11 @@ function PureMessages({
|
||||
const isAlibabaImageModel = modelsData?.models?.some(
|
||||
(m: DynamicModel) =>
|
||||
m.id === selectedModelId &&
|
||||
(m.providerType === "alibaba-image" ||
|
||||
m.providerType === "ruxa-image" ||
|
||||
m.providerType === "fal-image" ||
|
||||
m.providerType === "openai-image" ||
|
||||
m.providerType === "midjourney")
|
||||
isImageGenerationModelDescriptor({
|
||||
providerType: m.providerType,
|
||||
modelId: m.modelId,
|
||||
supportsImageGen: m.supportsImageGen,
|
||||
})
|
||||
) ?? false;
|
||||
|
||||
// 🆕 获取当前模型的显示名称
|
||||
@ -140,14 +144,11 @@ function PureMessages({
|
||||
const isVideoModel = modelsData?.models?.some(
|
||||
(m: DynamicModel) =>
|
||||
m.id === selectedModelId &&
|
||||
(m.providerType === "volcano-video" ||
|
||||
m.providerType === "qianwen-video" ||
|
||||
m.providerType === "fal-video" ||
|
||||
m.providerType === "ruxa-video" ||
|
||||
m.providerType === "doubao-video" ||
|
||||
m.modelId?.toLowerCase().includes("video") ||
|
||||
m.modelId?.toLowerCase().includes("sora") ||
|
||||
m.displayName?.includes("视频"))
|
||||
isVideoModelDescriptor({
|
||||
providerType: m.providerType,
|
||||
modelId: m.modelId,
|
||||
supportsImageGen: m.supportsImageGen,
|
||||
})
|
||||
) ?? false;
|
||||
|
||||
// 控制等待动画延迟消失(Gemini 图片模型延迟 4 秒,其他模型 6 秒)
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import { Bot, ImageIcon, Sparkles, Video } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
|
||||
// 动态模型类型定义
|
||||
type DynamicModel = {
|
||||
@ -48,20 +52,22 @@ type PointsSummaryResponse = {
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
const VIDEO_PROVIDER_TYPES = new Set([
|
||||
"volcano-video",
|
||||
"doubao-video",
|
||||
"qianwen-video",
|
||||
"fal-video",
|
||||
"ruxa-video",
|
||||
]);
|
||||
|
||||
function isVideoProviderType(providerType?: string | null) {
|
||||
return VIDEO_PROVIDER_TYPES.has(String(providerType || ""));
|
||||
function isVideoProviderType(model?: Partial<DynamicModel> | null) {
|
||||
return isVideoModelDescriptor({
|
||||
providerType: model?.providerType,
|
||||
modelId: model?.modelId,
|
||||
capabilities: model?.capabilities,
|
||||
supportsImageGen: model?.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function isImageGenerationModel(model: DynamicModel) {
|
||||
return model.supportsImageGen === true && !isVideoProviderType(model.providerType);
|
||||
return isImageGenerationModelDescriptor({
|
||||
providerType: model.providerType,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
|
||||
@ -263,10 +269,10 @@ function PureMultimodalInput({
|
||||
(model) => isImageGenerationModel(model)
|
||||
);
|
||||
const videoMain = models.find(
|
||||
(model) => isVideoProviderType(model.providerType) && !model.isSubModel
|
||||
(model) => isVideoProviderType(model) && !model.isSubModel
|
||||
);
|
||||
const videoFallback = models.find((model) =>
|
||||
isVideoProviderType(model.providerType)
|
||||
isVideoProviderType(model)
|
||||
);
|
||||
return {
|
||||
image: nanoBananaImage || imageMain || imageFallback || null,
|
||||
@ -275,7 +281,7 @@ function PureMultimodalInput({
|
||||
}, [modelCatalog?.models]);
|
||||
|
||||
const activeQuickMode = useMemo<"image" | "video" | null>(() => {
|
||||
if (isVideoModel || isVideoProviderType(currentSelectedModel?.providerType)) {
|
||||
if (isVideoModel || isVideoProviderType(currentSelectedModel)) {
|
||||
return "video";
|
||||
}
|
||||
if (isImageModel || (currentSelectedModel && isImageGenerationModel(currentSelectedModel))) {
|
||||
@ -1779,28 +1785,13 @@ function PureModelSelectorCompact({
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center">
|
||||
{(() => {
|
||||
const providerType = model.providerType || "";
|
||||
if (
|
||||
providerType === "volcano-video" ||
|
||||
providerType === "doubao-video" ||
|
||||
providerType === "qianwen-video" ||
|
||||
providerType === "fal-video" ||
|
||||
providerType === "ruxa-video"
|
||||
) {
|
||||
if (isVideoProviderType(model)) {
|
||||
return <Video className="h-4 w-4" strokeWidth={2.2} />;
|
||||
}
|
||||
if (
|
||||
providerType === "gemini" ||
|
||||
providerType === "volcano-vision" ||
|
||||
providerType === "alibaba-image" ||
|
||||
providerType === "ruxa-image" ||
|
||||
providerType === "fal-image" ||
|
||||
providerType === "openai-image" ||
|
||||
providerType === "midjourney"
|
||||
) {
|
||||
if (isImageGenerationModel(model)) {
|
||||
return <ImageIcon className="h-4 w-4" strokeWidth={2.2} />;
|
||||
}
|
||||
if (providerType === "system") {
|
||||
if ((model.providerType || "") === "system") {
|
||||
return <Sparkles className="h-4 w-4" strokeWidth={2.2} />;
|
||||
}
|
||||
return <Bot className="h-4 w-4" strokeWidth={2.2} />;
|
||||
|
||||
@ -18,6 +18,7 @@ const PROVIDER_MODEL_LABEL: Record<string, string> = {
|
||||
"fal-video": "FAL 视频",
|
||||
"ruxa-video": "RUXA 视频",
|
||||
"volcano-video": "火山视频",
|
||||
runway: "Runway 视频",
|
||||
};
|
||||
|
||||
const PHASE_HINT: Record<VideoGenerationPhase, string> = {
|
||||
@ -33,6 +34,7 @@ const PROVIDER_GRADIENT_CLASS: Record<string, string> = {
|
||||
"fal-video": "from-sky-400 via-cyan-400 to-indigo-400",
|
||||
"ruxa-video": "from-violet-400 via-fuchsia-400 to-pink-400",
|
||||
"volcano-video": "from-orange-400 via-amber-400 to-yellow-400",
|
||||
runway: "from-rose-400 via-orange-400 to-amber-400",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user