feat(chat): default to Nano Banana Pro and add mode switch
This commit is contained in:
parent
26244a724c
commit
abadcdaa18
@ -8,6 +8,25 @@ import {
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
|
||||
|
||||
const VIDEO_PROVIDER_TYPES = new Set([
|
||||
"volcano-video",
|
||||
"doubao-video",
|
||||
"qianwen-video",
|
||||
"fal-video",
|
||||
]);
|
||||
|
||||
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
|
||||
const displayName = String(model.displayName || "").toLowerCase();
|
||||
const modelId = String(model.modelId || "").toLowerCase();
|
||||
return (
|
||||
displayName.includes("nano banana pro") ||
|
||||
displayName.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano_banana_pro") ||
|
||||
modelId.includes("nano-banana")
|
||||
);
|
||||
}
|
||||
|
||||
function sortModelsForDisplay<T extends { sortOrder?: number | null; displayName?: string | null }>(
|
||||
models: T[]
|
||||
) {
|
||||
@ -74,13 +93,26 @@ export async function GET() {
|
||||
imageGenConfig: model.imageGenConfig,
|
||||
sortOrder: model.sortOrder ?? 0,
|
||||
}));
|
||||
const preferredNanoBananaModel = models.find(
|
||||
(model) =>
|
||||
model.supportsImageGen &&
|
||||
!VIDEO_PROVIDER_TYPES.has(model.providerType || "") &&
|
||||
isNanoBananaProModel(model)
|
||||
);
|
||||
const preferredImageModel = models.find(
|
||||
(model) =>
|
||||
model.supportsImageGen &&
|
||||
!VIDEO_PROVIDER_TYPES.has(model.providerType || "")
|
||||
);
|
||||
|
||||
const resolvedDefaultModelId =
|
||||
(preferredNanoBananaModel?.id ??
|
||||
preferredImageModel?.id ??
|
||||
(defaultModel &&
|
||||
!isAiModelAgentAuto(defaultModel) &&
|
||||
models.some((model) => model.id === defaultModel.id)
|
||||
? defaultModel.id
|
||||
: models.find((model) => model.isDefault)?.id) ??
|
||||
: models.find((model) => model.isDefault)?.id)) ??
|
||||
models[0]?.id ??
|
||||
null;
|
||||
|
||||
|
||||
@ -15,6 +15,25 @@ import {
|
||||
} from "@/lib/db/queries";
|
||||
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
|
||||
|
||||
const VIDEO_PROVIDER_TYPES = new Set([
|
||||
"volcano-video",
|
||||
"doubao-video",
|
||||
"qianwen-video",
|
||||
"fal-video",
|
||||
]);
|
||||
|
||||
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
|
||||
const displayName = String(model.displayName || "").toLowerCase();
|
||||
const modelId = String(model.modelId || "").toLowerCase();
|
||||
return (
|
||||
displayName.includes("nano banana pro") ||
|
||||
displayName.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano_banana_pro") ||
|
||||
modelId.includes("nano-banana")
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex h-dvh" />}>
|
||||
@ -73,36 +92,61 @@ async function NewChatPage() {
|
||||
|
||||
const modelIdFromCookie = cookieStore.get("chat-model");
|
||||
|
||||
// 如果没有保存的模型 ID,从数据库获取默认模型
|
||||
let initialModelId = modelIdFromCookie?.value;
|
||||
|
||||
if (!initialModelId) {
|
||||
// 尝试获取默认模型
|
||||
const defaultModel = await getDefaultAiModel();
|
||||
const currentUser = session?.user?.id
|
||||
? await getUserById({ id: session.user.id }).catch(() => undefined)
|
||||
: undefined;
|
||||
const allowHiddenModels =
|
||||
session?.user?.type === "admin" || Boolean(currentUser?.isHighVip);
|
||||
const defaultModel = await getDefaultAiModel();
|
||||
const enabledModels = await getEnabledAiModelsWithProvider(false, true);
|
||||
const visibleModels = enabledModels.filter(
|
||||
({ model }) =>
|
||||
!isAiModelAgentAuto(model) &&
|
||||
(allowHiddenModels || !model.isHidden)
|
||||
);
|
||||
const preferredImageModels = visibleModels.filter(
|
||||
({ model, provider }) =>
|
||||
model.supportsImageGen &&
|
||||
!VIDEO_PROVIDER_TYPES.has(provider.type)
|
||||
);
|
||||
const nanoBananaModel = preferredImageModels.find(({ model }) =>
|
||||
isNanoBananaProModel(model)
|
||||
);
|
||||
const cookieModel = initialModelId
|
||||
? visibleModels.find(({ model }) => model.id === initialModelId)
|
||||
: undefined;
|
||||
const cookieIsVideoModel = Boolean(
|
||||
cookieModel && VIDEO_PROVIDER_TYPES.has(cookieModel.provider.type)
|
||||
);
|
||||
|
||||
const currentUser = session?.user?.id
|
||||
? await getUserById({ id: session.user.id }).catch(() => undefined)
|
||||
: undefined;
|
||||
const allowHiddenModels =
|
||||
session?.user?.type === "admin" || Boolean(currentUser?.isHighVip);
|
||||
|
||||
if (
|
||||
if (
|
||||
!initialModelId ||
|
||||
!cookieModel ||
|
||||
(cookieIsVideoModel && Boolean(nanoBananaModel))
|
||||
) {
|
||||
if (nanoBananaModel) {
|
||||
initialModelId = nanoBananaModel.model.id;
|
||||
} else if (
|
||||
defaultModel &&
|
||||
!isAiModelAgentAuto(defaultModel) &&
|
||||
(allowHiddenModels || !defaultModel.isHidden) &&
|
||||
preferredImageModels.some(({ model }) => model.id === defaultModel.id)
|
||||
) {
|
||||
initialModelId = defaultModel.id;
|
||||
} else if (preferredImageModels.length > 0) {
|
||||
const preferredDefaultImageModel = preferredImageModels.find(
|
||||
({ model }) => model.isDefault
|
||||
);
|
||||
initialModelId = (preferredDefaultImageModel || preferredImageModels[0]).model.id;
|
||||
} else if (
|
||||
defaultModel &&
|
||||
!isAiModelAgentAuto(defaultModel) &&
|
||||
(allowHiddenModels || !defaultModel.isHidden)
|
||||
) {
|
||||
initialModelId = defaultModel.id;
|
||||
} else {
|
||||
// 如果没有设置默认模型,获取第一个启用的模型
|
||||
const enabledModels = await getEnabledAiModelsWithProvider(false, true);
|
||||
const visibleModels = enabledModels.filter(
|
||||
({ model }) =>
|
||||
!isAiModelAgentAuto(model) &&
|
||||
(allowHiddenModels || !model.isHidden)
|
||||
);
|
||||
if (visibleModels.length > 0) {
|
||||
initialModelId = visibleModels[0].model.id;
|
||||
}
|
||||
} else if (visibleModels.length > 0) {
|
||||
initialModelId = visibleModels[0].model.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ type DynamicModel = {
|
||||
description: string | null;
|
||||
providerType?: string;
|
||||
supportsVision: boolean;
|
||||
supportsImageGen?: boolean;
|
||||
isDefault: boolean;
|
||||
providerName: string;
|
||||
isSubModel?: boolean; // 🆕 是否为子模型
|
||||
@ -43,6 +44,25 @@ type PointsSummaryResponse = {
|
||||
} | null;
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
const VIDEO_PROVIDER_TYPES = new Set([
|
||||
"volcano-video",
|
||||
"doubao-video",
|
||||
"qianwen-video",
|
||||
"fal-video",
|
||||
]);
|
||||
|
||||
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
|
||||
const displayName = String(model.displayName || "").toLowerCase();
|
||||
const modelId = String(model.modelId || "").toLowerCase();
|
||||
return (
|
||||
displayName.includes("nano banana pro") ||
|
||||
displayName.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano-banana-pro") ||
|
||||
modelId.includes("nano_banana_pro") ||
|
||||
modelId.includes("nano-banana")
|
||||
);
|
||||
}
|
||||
import { Trigger } from "@radix-ui/react-select";
|
||||
import type { UIMessage } from "ai";
|
||||
import equal from "fast-deep-equal";
|
||||
@ -184,6 +204,9 @@ function PureMultimodalInput({
|
||||
|
||||
// 获取网站设置(用于时间限制检查)
|
||||
const { data: siteSettings } = useSWR("/api/site-settings", fetcher);
|
||||
const { data: modelCatalog } = useSWR<{
|
||||
models: DynamicModel[];
|
||||
}>("/api/models", fetcher);
|
||||
const { data: membershipSummary } = useSWR<PointsSummaryResponse>(
|
||||
"/api/user/membership-summary",
|
||||
async (url: string) => {
|
||||
@ -205,6 +228,70 @@ function PureMultimodalInput({
|
||||
return currentTime >= startTime && currentTime <= endTime;
|
||||
}, [siteSettings]);
|
||||
|
||||
const currentSelectedModel = useMemo(
|
||||
() => modelCatalog?.models?.find((model) => model.id === selectedModelId),
|
||||
[modelCatalog?.models, selectedModelId]
|
||||
);
|
||||
|
||||
const quickSwitchTargets = useMemo(() => {
|
||||
const models = modelCatalog?.models || [];
|
||||
const nanoBananaImage = models.find(
|
||||
(model) =>
|
||||
model.supportsImageGen &&
|
||||
!VIDEO_PROVIDER_TYPES.has(model.providerType || "") &&
|
||||
isNanoBananaProModel(model)
|
||||
);
|
||||
const imageMain = models.find(
|
||||
(model) => model.supportsImageGen && !VIDEO_PROVIDER_TYPES.has(model.providerType || "") && !model.isSubModel
|
||||
);
|
||||
const imageFallback = models.find(
|
||||
(model) => model.supportsImageGen && !VIDEO_PROVIDER_TYPES.has(model.providerType || "")
|
||||
);
|
||||
const videoMain = models.find(
|
||||
(model) => VIDEO_PROVIDER_TYPES.has(model.providerType || "") && !model.isSubModel
|
||||
);
|
||||
const videoFallback = models.find((model) =>
|
||||
VIDEO_PROVIDER_TYPES.has(model.providerType || "")
|
||||
);
|
||||
return {
|
||||
image: nanoBananaImage || imageMain || imageFallback || null,
|
||||
video: videoMain || videoFallback || null,
|
||||
};
|
||||
}, [modelCatalog?.models]);
|
||||
|
||||
const activeQuickMode = useMemo<"image" | "video" | null>(() => {
|
||||
if (isVideoModel || VIDEO_PROVIDER_TYPES.has(currentSelectedModel?.providerType || "")) {
|
||||
return "video";
|
||||
}
|
||||
if (isImageModel || currentSelectedModel?.supportsImageGen) {
|
||||
return "image";
|
||||
}
|
||||
return null;
|
||||
}, [currentSelectedModel?.providerType, currentSelectedModel?.supportsImageGen, isImageModel, isVideoModel]);
|
||||
|
||||
const switchComposerMode = useCallback(
|
||||
(targetMode: "image" | "video") => {
|
||||
const targetModel = targetMode === "image" ? quickSwitchTargets.image : quickSwitchTargets.video;
|
||||
if (!targetModel) {
|
||||
toast.error(targetMode === "image" ? "当前没有可用的生图模型" : "当前没有可用的视频模型");
|
||||
return;
|
||||
}
|
||||
if (targetModel.id === selectedModelId) return;
|
||||
onModelChange?.(targetModel.id);
|
||||
startTransition(() => {
|
||||
saveChatModelAsCookie(targetModel.id);
|
||||
});
|
||||
toast.success(
|
||||
targetMode === "image"
|
||||
? `已切换到生图模型:${targetModel.displayName}`
|
||||
: `已切换到视频模型:${targetModel.displayName}`
|
||||
);
|
||||
},
|
||||
[onModelChange, quickSwitchTargets.image, quickSwitchTargets.video, selectedModelId]
|
||||
);
|
||||
|
||||
const showQuickModeSwitch = Boolean(quickSwitchTargets.image || quickSwitchTargets.video);
|
||||
|
||||
// 每分钟更新一次检查
|
||||
useEffect(() => {
|
||||
if (!siteSettings?.usageTimeEnabled) return;
|
||||
@ -1117,16 +1204,42 @@ function PureMultimodalInput({
|
||||
</div>
|
||||
<PromptInputToolbar className="!border-top-0 border-t-0! p-0 shadow-none dark:border-0 dark:border-transparent!">
|
||||
<PromptInputTools className="gap-0 sm:gap-0.5 flex-1 overflow-x-auto">
|
||||
{isVideoModel ? (
|
||||
<>
|
||||
{showQuickModeSwitch ? (
|
||||
<div className="mr-1 flex items-center gap-1 rounded-full border border-border/70 bg-muted/35 p-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-full border border-blue-500 bg-blue-50 px-3 text-blue-600 hover:bg-blue-100"
|
||||
className={cn(
|
||||
"h-7 rounded-full px-2 text-xs",
|
||||
activeQuickMode === "image"
|
||||
? "bg-white text-[#2563eb] shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
disabled={!quickSwitchTargets.image}
|
||||
onClick={() => switchComposerMode("image")}
|
||||
>
|
||||
<Video className="mr-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">视频</span>
|
||||
<ImageIcon className="mr-1 h-3.5 w-3.5" />
|
||||
生图
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-7 rounded-full px-2 text-xs",
|
||||
activeQuickMode === "video"
|
||||
? "bg-white text-[#2563eb] shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
disabled={!quickSwitchTargets.video}
|
||||
onClick={() => switchComposerMode("video")}
|
||||
>
|
||||
<Video className="mr-1 h-3.5 w-3.5" />
|
||||
视频
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{isVideoModel ? (
|
||||
<>
|
||||
{onVideoParamsChange && (
|
||||
<VideoParamsBar
|
||||
key={`video-params-${selectedModelId}-${isVideoModel}`}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user