feat: show generation point estimates
This commit is contained in:
parent
799dd3007b
commit
fd95b9547e
@ -192,6 +192,33 @@ const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||
const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
|
||||
const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
|
||||
const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const;
|
||||
const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record<
|
||||
(typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number],
|
||||
number
|
||||
> = {
|
||||
"480p": 1,
|
||||
"720p": 1.5,
|
||||
"1080p": 2,
|
||||
};
|
||||
|
||||
function formatCompactNumber(value: number, fractionDigits = 2) {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
const fixed = value.toFixed(fractionDigits);
|
||||
return fixed.replace(/\.?0+$/, "");
|
||||
}
|
||||
|
||||
function normalizeAdminMultiplier(value: unknown, fallback = 1) {
|
||||
const parsedValue = Number(value);
|
||||
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return fallback;
|
||||
return Math.min(20, Math.max(0.01, Number(parsedValue.toFixed(2))));
|
||||
}
|
||||
|
||||
function roundCreditCost(value: unknown) {
|
||||
const parsedValue = Number(value);
|
||||
return Number.isFinite(parsedValue) && parsedValue > 0
|
||||
? Math.max(1, Math.round(parsedValue))
|
||||
: 0;
|
||||
}
|
||||
|
||||
function createEmptyCanvasSkillTag(): CanvasSkillTag {
|
||||
return {
|
||||
@ -448,17 +475,72 @@ function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) {
|
||||
};
|
||||
}
|
||||
|
||||
function getVideoReferenceDurationCost(config: ImageGenConfig | undefined) {
|
||||
const durationCostEntries = Object.entries(
|
||||
config?.creditCostPerVideoByDuration || {}
|
||||
)
|
||||
.map(([durationKey, cost]) => ({
|
||||
duration: Number(durationKey),
|
||||
cost: roundCreditCost(cost),
|
||||
}))
|
||||
.filter((item) => Number.isFinite(item.duration) && item.cost > 0)
|
||||
.sort((left, right) => left.duration - right.duration);
|
||||
return (
|
||||
durationCostEntries[durationCostEntries.length - 1]?.cost ||
|
||||
roundCreditCost(config?.creditCostPerVideo) ||
|
||||
recommendedVideoDurationReferenceCosts[
|
||||
String(
|
||||
recommendedVideoDurationOptions[recommendedVideoDurationOptions.length - 1]
|
||||
)
|
||||
] ||
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
function getVideoResolutionMultiplier(
|
||||
config: ImageGenConfig | undefined,
|
||||
resolution: (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number]
|
||||
) {
|
||||
const referenceCost = getVideoReferenceDurationCost(config);
|
||||
const rawCost =
|
||||
config?.creditCostPerVideoByResolution?.[resolution] ??
|
||||
referenceCost * DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution];
|
||||
if (referenceCost <= 0) {
|
||||
return DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution];
|
||||
}
|
||||
return normalizeAdminMultiplier(
|
||||
Number(rawCost) / referenceCost,
|
||||
DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]
|
||||
);
|
||||
}
|
||||
|
||||
function getEnhanceProScaleMultiplier(
|
||||
pricing: PointsSystemConfig["enhanceProPricing"],
|
||||
factorKey: string,
|
||||
cost: number
|
||||
) {
|
||||
const baseCost = roundCreditCost(pricing.fixedCost);
|
||||
if (baseCost <= 0) return normalizeAdminMultiplier(factorKey);
|
||||
return normalizeAdminMultiplier(Number(cost) / baseCost, normalizeAdminMultiplier(factorKey));
|
||||
}
|
||||
|
||||
function applyRecommendedEnhanceProPricing(
|
||||
pricing: PointsSystemConfig["enhanceProPricing"]
|
||||
): PointsSystemConfig["enhanceProPricing"] {
|
||||
const recommended = defaultPointsSystemConfig.enhanceProPricing;
|
||||
const baseCost = roundCreditCost(pricing.fixedCost || recommended.fixedCost);
|
||||
return {
|
||||
...pricing,
|
||||
chargeMode: recommended.chargeMode,
|
||||
fixedCost: recommended.fixedCost,
|
||||
fixedCost: baseCost,
|
||||
chargeOnlyWhenUpscaling: recommended.chargeOnlyWhenUpscaling,
|
||||
batchSizeMultiplierEnabled: recommended.batchSizeMultiplierEnabled,
|
||||
scaleFactorCosts: { ...recommended.scaleFactorCosts },
|
||||
scaleFactorCosts: Object.fromEntries(
|
||||
Object.keys(recommended.scaleFactorCosts).map((factorKey) => [
|
||||
factorKey,
|
||||
roundCreditCost(baseCost * normalizeAdminMultiplier(factorKey)),
|
||||
])
|
||||
),
|
||||
modelMultipliers: { ...recommended.modelMultipliers },
|
||||
};
|
||||
}
|
||||
@ -2563,6 +2645,9 @@ export function AdminSettingsClient() {
|
||||
const configuredVideoDurationOptions = getConfiguredVideoDurationOptions(
|
||||
modelForm.imageGenConfig
|
||||
);
|
||||
const videoReferenceDurationCost = getVideoReferenceDurationCost(
|
||||
modelForm.imageGenConfig
|
||||
);
|
||||
const modelKindHint =
|
||||
(selectedModelProviderType === "runway" || selectedModelProviderType === "krea") &&
|
||||
!modelForm.modelId.trim()
|
||||
@ -3002,21 +3087,53 @@ export function AdminSettingsClient() {
|
||||
});
|
||||
};
|
||||
|
||||
const updateEnhanceProScaleFactorCost = (
|
||||
factorKey: string,
|
||||
nextCost: number
|
||||
) => {
|
||||
const updateEnhanceProFixedCost = (nextFixedCost: number) => {
|
||||
setSiteSettings((prev) => {
|
||||
const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
|
||||
const pricing = config.enhanceProPricing;
|
||||
const fixedCost = roundCreditCost(nextFixedCost);
|
||||
const scaleFactorCosts = Object.fromEntries(
|
||||
Object.entries(pricing.scaleFactorCosts).map(([factorKey, cost]) => {
|
||||
const multiplier = getEnhanceProScaleMultiplier(
|
||||
pricing,
|
||||
factorKey,
|
||||
Number(cost)
|
||||
);
|
||||
return [factorKey, roundCreditCost(fixedCost * multiplier)];
|
||||
})
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
pointsSystemConfig: {
|
||||
...config,
|
||||
enhanceProPricing: {
|
||||
...config.enhanceProPricing,
|
||||
...pricing,
|
||||
fixedCost,
|
||||
scaleFactorCosts,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const updateEnhanceProScaleFactorMultiplier = (
|
||||
factorKey: string,
|
||||
nextMultiplier: number
|
||||
) => {
|
||||
setSiteSettings((prev) => {
|
||||
const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
|
||||
const pricing = config.enhanceProPricing;
|
||||
const fixedCost = roundCreditCost(pricing.fixedCost);
|
||||
const multiplier = normalizeAdminMultiplier(nextMultiplier, Number(factorKey) || 1);
|
||||
return {
|
||||
...prev,
|
||||
pointsSystemConfig: {
|
||||
...config,
|
||||
enhanceProPricing: {
|
||||
...pricing,
|
||||
scaleFactorCosts: {
|
||||
...config.enhanceProPricing.scaleFactorCosts,
|
||||
[factorKey]: Math.max(0, Math.round(nextCost) || 0),
|
||||
...pricing.scaleFactorCosts,
|
||||
[factorKey]: roundCreditCost(fixedCost * multiplier),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -8531,7 +8648,7 @@ export function AdminSettingsClient() {
|
||||
图像增强 Pro 积分规则
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
默认按基础 20 积分计算;启用放大后按放大倍数折算,例如 1.25x 为 25 积分、2x 为 40 积分。
|
||||
按基础积分 × 档位倍率计算;启用放大后可为 1x、1.25x、1.5x、2x、3x、4x 分别设置倍率。
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
@ -8633,19 +8750,9 @@ export function AdminSettingsClient() {
|
||||
type="number"
|
||||
value={pointsSystemConfig.enhanceProPricing.fixedCost}
|
||||
onChange={(e) =>
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
pointsSystemConfig: {
|
||||
...pointsSystemConfig,
|
||||
enhanceProPricing: {
|
||||
...pointsSystemConfig.enhanceProPricing,
|
||||
fixedCost: Math.max(
|
||||
0,
|
||||
Number.parseInt(e.target.value || "0", 10) || 0
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
updateEnhanceProFixedCost(
|
||||
Number.parseInt(e.target.value || "0", 10) || 0
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -8681,34 +8788,52 @@ export function AdminSettingsClient() {
|
||||
|
||||
<div className="space-y-3 rounded-lg border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>放大倍率积分</Label>
|
||||
<Label>放大倍率档位</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
“放大倍率”计费模式下优先使用精确档位;未配置的小数倍率按基础价 × 倍数自动计算。
|
||||
“放大倍率”计费模式下按基础积分 × 档位倍率计算;未配置的小数倍率也会按基础积分 × 倍率自动估算。
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6">
|
||||
{Object.entries(
|
||||
pointsSystemConfig.enhanceProPricing.scaleFactorCosts
|
||||
)
|
||||
.sort(([left], [right]) => Number(left) - Number(right))
|
||||
.map(([factorKey, cost]) => (
|
||||
<div key={factorKey} className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{factorKey}x
|
||||
</Label>
|
||||
<Input
|
||||
min={0}
|
||||
type="number"
|
||||
value={cost}
|
||||
onChange={(e) =>
|
||||
updateEnhanceProScaleFactorCost(
|
||||
factorKey,
|
||||
Number.parseInt(e.target.value || "0", 10) || 0
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
.map(([factorKey, cost]) => {
|
||||
const multiplier = getEnhanceProScaleMultiplier(
|
||||
pointsSystemConfig.enhanceProPricing,
|
||||
factorKey,
|
||||
cost
|
||||
);
|
||||
const exactCost =
|
||||
roundCreditCost(
|
||||
pointsSystemConfig.enhanceProPricing.fixedCost
|
||||
) * multiplier;
|
||||
return (
|
||||
<div key={factorKey} className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{factorKey}x 倍率
|
||||
</Label>
|
||||
<Input
|
||||
min={0}
|
||||
step={0.05}
|
||||
type="number"
|
||||
value={formatCompactNumber(multiplier)}
|
||||
onChange={(e) =>
|
||||
updateEnhanceProScaleFactorMultiplier(
|
||||
factorKey,
|
||||
Number(e.target.value || factorKey)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
该档积分:{formatCompactNumber(exactCost)}
|
||||
{roundCreditCost(exactCost) !== exactCost
|
||||
? `,扣 ${roundCreditCost(exactCost)}`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12319,8 +12444,8 @@ export function AdminSettingsClient() {
|
||||
推荐视频积分规则
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
按 5/10/15 秒 = 1/1.5/2 倍,480p/720p/1080p 基础价 =
|
||||
100/200/250 积分计算。
|
||||
按 5/10/15 秒 = 165/350/600 积分,480p/720p/1080p =
|
||||
600 × 1/1.5/2 倍计算。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -12428,8 +12553,8 @@ export function AdminSettingsClient() {
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
系统会把最短秒数的参考积分作为 1
|
||||
倍,其它秒数按比例乘下方分辨率基础价。
|
||||
系统会把最长秒数的参考积分作为 1
|
||||
倍,其它秒数按比例乘下方分辨率倍率。
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -12439,8 +12564,15 @@ export function AdminSettingsClient() {
|
||||
)}
|
||||
|
||||
<div className="space-y-3 border-t pt-4">
|
||||
<div className="text-sm font-medium">
|
||||
分辨率基础积分
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">
|
||||
分辨率倍率
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前按 {videoReferenceDurationCost} 积分作为 1 倍参考;例如 720p
|
||||
填 1.5 时,该分辨率基准为{" "}
|
||||
{formatCompactNumber(videoReferenceDurationCost * 1.5)} 积分。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{VIDEO_PRICING_PREVIEW_RESOLUTIONS.map(
|
||||
@ -12449,27 +12581,25 @@ export function AdminSettingsClient() {
|
||||
key={resolutionKey}
|
||||
className="space-y-2"
|
||||
>
|
||||
<Label>{resolutionKey} 基础积分</Label>
|
||||
<Label>{resolutionKey} 倍率</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
step={0.1}
|
||||
value={
|
||||
modelForm.imageGenConfig
|
||||
?.creditCostPerVideoByResolution?.[
|
||||
resolutionKey
|
||||
] ??
|
||||
modelForm.imageGenConfig
|
||||
?.creditCostPerVideo ??
|
||||
0
|
||||
formatCompactNumber(
|
||||
getVideoResolutionMultiplier(
|
||||
modelForm.imageGenConfig,
|
||||
resolutionKey
|
||||
)
|
||||
)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const nextValue = Math.max(
|
||||
0,
|
||||
Number.parseInt(
|
||||
e.target.value,
|
||||
10
|
||||
) || 0
|
||||
const multiplier = normalizeAdminMultiplier(
|
||||
e.target.value,
|
||||
DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[
|
||||
resolutionKey
|
||||
]
|
||||
);
|
||||
setModelForm({
|
||||
...modelForm,
|
||||
@ -12479,18 +12609,39 @@ export function AdminSettingsClient() {
|
||||
...(modelForm.imageGenConfig
|
||||
?.creditCostPerVideoByResolution ||
|
||||
{}),
|
||||
[resolutionKey]: nextValue,
|
||||
[resolutionKey]: roundCreditCost(
|
||||
videoReferenceDurationCost *
|
||||
multiplier
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{videoReferenceDurationCost} ×{" "}
|
||||
{formatCompactNumber(
|
||||
getVideoResolutionMultiplier(
|
||||
modelForm.imageGenConfig,
|
||||
resolutionKey
|
||||
)
|
||||
)}{" "}
|
||||
={" "}
|
||||
{roundCreditCost(
|
||||
videoReferenceDurationCost *
|
||||
getVideoResolutionMultiplier(
|
||||
modelForm.imageGenConfig,
|
||||
resolutionKey
|
||||
)
|
||||
)}{" "}
|
||||
积分
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
配置后按用户选择的视频分辨率取基础价,再叠加上方秒数倍率;未命中再回退到秒数或统一价格。
|
||||
配置后按用户选择的视频分辨率倍率取基准价,再叠加上方秒数比例;未命中再回退到秒数或统一价格。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { getVideoCreditCostFromConfig } from "@/lib/ai/video-model-config";
|
||||
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type {
|
||||
@ -2916,6 +2917,10 @@ export function CanvasChatPanel({
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
creditCostPerImage?: number;
|
||||
creditCostPerVideo?: number;
|
||||
videoDurationOptions?: number[];
|
||||
creditCostPerVideoByDuration?: Record<string, number>;
|
||||
creditCostPerVideoByResolution?: Record<string, number>;
|
||||
} | null;
|
||||
}>;
|
||||
agentModel?: {
|
||||
@ -2938,6 +2943,10 @@ export function CanvasChatPanel({
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
creditCostPerImage?: number;
|
||||
creditCostPerVideo?: number;
|
||||
videoDurationOptions?: number[];
|
||||
creditCostPerVideoByDuration?: Record<string, number>;
|
||||
creditCostPerVideoByResolution?: Record<string, number>;
|
||||
} | null;
|
||||
} | null;
|
||||
}>("/api/models", fetcher);
|
||||
@ -7827,6 +7836,51 @@ export function CanvasChatPanel({
|
||||
membershipSummary?.summary?.currentCredits,
|
||||
]);
|
||||
|
||||
const estimatedPointsCost = useMemo(() => {
|
||||
if (membershipSummary?.isHighVip) return null;
|
||||
if (membershipSummary?.pointsSystemConfig?.enabled !== true) return null;
|
||||
if (activeComposerMode === "agent") return null;
|
||||
|
||||
if (isVideoModel) {
|
||||
const cost = getVideoCreditCostFromConfig(
|
||||
currentModel?.imageGenConfig,
|
||||
videoParams?.duration,
|
||||
videoParams?.resolution
|
||||
);
|
||||
return cost > 0
|
||||
? {
|
||||
cost,
|
||||
title: `本次预计消耗 ${cost} 积分`,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
const unitCost = Math.max(
|
||||
0,
|
||||
Math.round(Number(currentModel?.imageGenConfig?.creditCostPerImage ?? 0))
|
||||
);
|
||||
const imageCount = Math.max(
|
||||
1,
|
||||
Math.round(Number(imageParams?.imageCount || 1))
|
||||
);
|
||||
const cost = unitCost * imageCount;
|
||||
return cost > 0
|
||||
? {
|
||||
cost,
|
||||
title: `本次预计消耗 ${cost} 积分(${unitCost} × ${imageCount} 张)`,
|
||||
}
|
||||
: null;
|
||||
}, [
|
||||
activeComposerMode,
|
||||
currentModel?.imageGenConfig,
|
||||
imageParams?.imageCount,
|
||||
isVideoModel,
|
||||
membershipSummary?.isHighVip,
|
||||
membershipSummary?.pointsSystemConfig?.enabled,
|
||||
videoParams?.duration,
|
||||
videoParams?.resolution,
|
||||
]);
|
||||
|
||||
// 处理发送(图片已在上传时添加到画布,这里只发送消息)
|
||||
const handleSend = useCallback(async () => {
|
||||
const currentSendGuard = getSendGuard(activeChatId);
|
||||
@ -11478,6 +11532,24 @@ export function CanvasChatPanel({
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
{estimatedPointsCost ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background text-muted-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
title={estimatedPointsCost.title}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="font-medium text-foreground">
|
||||
{estimatedPointsCost.cost}
|
||||
</span>
|
||||
<span>积分</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
size={isWorkspacePresentation ? "default" : "icon"}
|
||||
className={cn(
|
||||
|
||||
@ -529,6 +529,11 @@ export function Chat({
|
||||
supportsNegativePrompt?: boolean;
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
creditCostPerImage?: number;
|
||||
creditCostPerVideo?: number;
|
||||
videoDurationOptions?: number[];
|
||||
creditCostPerVideoByDuration?: Record<string, number>;
|
||||
creditCostPerVideoByResolution?: Record<string, number>;
|
||||
} | null;
|
||||
}>;
|
||||
}>("/api/models", fetcher);
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { getVideoCreditCostFromConfig } from "@/lib/ai/video-model-config";
|
||||
|
||||
// 动态模型类型定义
|
||||
type DynamicModel = {
|
||||
@ -36,6 +37,10 @@ type DynamicModel = {
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
creditCostPerImage?: number;
|
||||
creditCostPerVideo?: number;
|
||||
videoDurationOptions?: number[];
|
||||
creditCostPerVideoByDuration?: Record<string, number>;
|
||||
creditCostPerVideoByResolution?: Record<string, number>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
@ -206,6 +211,10 @@ function PureMultimodalInput({
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
creditCostPerImage?: number;
|
||||
creditCostPerVideo?: number;
|
||||
videoDurationOptions?: number[];
|
||||
creditCostPerVideoByDuration?: Record<string, number>;
|
||||
creditCostPerVideoByResolution?: Record<string, number>;
|
||||
} | null;
|
||||
}) {
|
||||
// 🔍 调试日志:检查图片模型相关props
|
||||
@ -313,6 +322,51 @@ function PureMultimodalInput({
|
||||
|
||||
const showQuickModeSwitch = Boolean(quickSwitchTargets.image || quickSwitchTargets.video);
|
||||
|
||||
const estimatedPointsCost = useMemo(() => {
|
||||
if (membershipSummary?.isHighVip) return null;
|
||||
if (membershipSummary?.pointsSystemConfig?.enabled !== true) return null;
|
||||
|
||||
if (isVideoModel) {
|
||||
const cost = getVideoCreditCostFromConfig(
|
||||
imageGenConfig,
|
||||
videoParams?.duration,
|
||||
videoParams?.resolution
|
||||
);
|
||||
return cost > 0
|
||||
? {
|
||||
cost,
|
||||
title: `本次预计消耗 ${cost} 积分`,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!isImageModel) return null;
|
||||
const unitCost = Math.max(
|
||||
0,
|
||||
Math.round(Number(imageGenConfig?.creditCostPerImage ?? 0))
|
||||
);
|
||||
const imageCount = Math.max(
|
||||
1,
|
||||
Math.round(Number(imageParams?.imageCount || 1))
|
||||
);
|
||||
const cost = unitCost * imageCount;
|
||||
return cost > 0
|
||||
? {
|
||||
cost,
|
||||
title: `本次预计消耗 ${cost} 积分(${unitCost} × ${imageCount} 张)`,
|
||||
}
|
||||
: null;
|
||||
}, [
|
||||
imageGenConfig,
|
||||
imageParams?.imageCount,
|
||||
isImageModel,
|
||||
isVideoModel,
|
||||
membershipSummary?.isHighVip,
|
||||
membershipSummary?.pointsSystemConfig?.enabled,
|
||||
videoParams?.duration,
|
||||
videoParams?.resolution,
|
||||
]);
|
||||
|
||||
// 每分钟更新一次检查
|
||||
useEffect(() => {
|
||||
if (!siteSettings?.usageTimeEnabled) return;
|
||||
@ -1349,14 +1403,28 @@ function PureMultimodalInput({
|
||||
{status === "submitted" || status === "streaming" ? (
|
||||
<StopButton setMessages={setMessages} stop={stop} />
|
||||
) : (
|
||||
<PromptInputSubmit
|
||||
className="size-8 rounded-full bg-primary text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
data-testid="send-button"
|
||||
disabled={!input.trim() || uploadQueue.length > 0 || !isWithinUsageTime}
|
||||
status={status}
|
||||
>
|
||||
<ArrowUpIcon size={14} />
|
||||
</PromptInputSubmit>
|
||||
<>
|
||||
{estimatedPointsCost ? (
|
||||
<div
|
||||
className="flex h-8 items-center gap-1.5 rounded-full bg-muted px-2.5 text-xs text-muted-foreground"
|
||||
title={estimatedPointsCost.title}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="font-medium text-foreground">
|
||||
{estimatedPointsCost.cost}
|
||||
</span>
|
||||
<span>积分</span>
|
||||
</div>
|
||||
) : null}
|
||||
<PromptInputSubmit
|
||||
className="size-8 rounded-full bg-primary text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
data-testid="send-button"
|
||||
disabled={!input.trim() || uploadQueue.length > 0 || !isWithinUsageTime}
|
||||
status={status}
|
||||
>
|
||||
<ArrowUpIcon size={14} />
|
||||
</PromptInputSubmit>
|
||||
</>
|
||||
)}
|
||||
</PromptInputToolbar>
|
||||
</PromptInput>
|
||||
|
||||
@ -3,14 +3,14 @@ export type VideoResolutionCostMap = Record<string, number>;
|
||||
|
||||
export const recommendedVideoDurationOptions = [5, 10, 15] as const;
|
||||
export const recommendedVideoDurationReferenceCosts: VideoDurationCostMap = {
|
||||
"5": 100,
|
||||
"10": 150,
|
||||
"15": 200,
|
||||
"5": 165,
|
||||
"10": 350,
|
||||
"15": 600,
|
||||
};
|
||||
export const recommendedVideoResolutionBaseCosts: VideoResolutionCostMap = {
|
||||
"480p": 100,
|
||||
"720p": 200,
|
||||
"1080p": 250,
|
||||
"480p": 600,
|
||||
"720p": 900,
|
||||
"1080p": 1200,
|
||||
};
|
||||
|
||||
export type VideoImageGenConfigLike = {
|
||||
@ -172,7 +172,9 @@ export function getVideoCreditCostFromConfig(
|
||||
const resolutionBaseCost = resolutionCostMap[normalizedResolution] || 0;
|
||||
const durationCost = getDurationCost();
|
||||
const baseDurationCost =
|
||||
durationCostEntries.find((item) => item.cost > 0)?.cost ?? null;
|
||||
[...durationCostEntries]
|
||||
.reverse()
|
||||
.find((item) => item.cost > 0)?.cost ?? null;
|
||||
|
||||
if (durationCost !== null && baseDurationCost && baseDurationCost > 0) {
|
||||
return Math.max(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user