Show billing estimate and clarify session replacement

This commit is contained in:
2026-06-08 15:55:50 +08:00
parent 2afa73ac18
commit 3963d9ae2f
5 changed files with 90 additions and 6 deletions
+63 -2
View File
@@ -79,7 +79,7 @@ import {
import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import {
getImageQualityOptions,
getDefaultImageQuality,
@@ -220,6 +220,12 @@ const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
video: <VideoCameraOutlined />,
};
function formatCreditValue(value: number): string {
if (!Number.isFinite(value)) return "-";
if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
return Number(value.toFixed(2)).toString();
}
function WorkbenchPage({
isAuthenticated,
session,
@@ -464,6 +470,48 @@ function WorkbenchPage({
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
const billingEstimate = useMemo(() => {
if (activeMode === "image") {
return {
label: "预计 20 积分",
title: `图片生成按任务计费:${activeModel}${imageSettingsSummary},预计 20 积分`,
};
}
if (activeMode === "video") {
try {
const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
const credits = calculateEnterpriseVideoCredits({
model: activeModelValue,
resolution: videoQuality,
durationSeconds,
muted: false,
hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
});
return {
label: `预计 ${formatCreditValue(credits)} 积分`,
title: `${activeModel}${videoQualityLabel}${durationSeconds} 秒,预计 ${formatCreditValue(credits)} 积分`,
};
} catch {
return {
label: "计费以提交后为准",
title: "当前模型的预估计费暂不可用,实际扣费以服务端结算为准",
};
}
}
return {
label: "按 Token 结算",
title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
};
}, [
activeMode,
activeModel,
activeModelValue,
imageSettingsSummary,
referenceItems,
videoDuration,
videoQuality,
videoQualityLabel,
]);
const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -2545,7 +2593,15 @@ function WorkbenchPage({
}
};
const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3);
const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty
? "输入内容后可发送"
: generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
: billingEstimate.title;
const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2882,10 +2938,15 @@ function WorkbenchPage({
)}
</div>
<div className="wb-composer__toolbar-right">
<span className="wb-composer__billing-estimate" title={billingEstimate.title}>
{billingEstimate.label}
</span>
<button
type="button"
className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode,