From 3963d9ae2fb317174be4d804aeea30287036cb00 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Mon, 8 Jun 2026 15:55:50 +0800 Subject: [PATCH 1/9] Show billing estimate and clarify session replacement --- src/features/workbench/WorkbenchPage.tsx | 65 +++++++++++++++++++++++- src/stores/useSessionStore.ts | 4 +- src/styles/pages/legacy-pages.css | 15 ++++++ src/styles/themes/dark-green.css | 8 +++ src/utils/enterpriseVideoPolicy.ts | 4 +- 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/features/workbench/WorkbenchPage.tsx b/src/features/workbench/WorkbenchPage.tsx index a91d243..ef91565 100644 --- a/src/features/workbench/WorkbenchPage.tsx +++ b/src/features/workbench/WorkbenchPage.tsx @@ -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 = { video: , }; +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({ )}
+ + {billingEstimate.label} +
)} - {message.role === "assistant" && message.mode === "chat" && message.status === "completed" && ( -
- {formatTextTokenUsage(message.taskUsage)} -
- )} {(message.resultUrl || (message.result && message.status !== "thinking")) && ( Date: Mon, 8 Jun 2026 16:35:32 +0800 Subject: [PATCH 7/9] Center beta application review layout --- src/styles/pages/beta-applications.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/pages/beta-applications.css b/src/styles/pages/beta-applications.css index bf618f7..2cebadb 100644 --- a/src/styles/pages/beta-applications.css +++ b/src/styles/pages/beta-applications.css @@ -3,6 +3,7 @@ flex-direction: column; gap: 18px; width: min(1180px, calc(100vw - 48px)); + margin: 0 auto; height: 100%; min-height: 0; padding: 24px; -- 2.52.0 From 30536ad15f509b87027effeb63542a53c5e0359c Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Mon, 8 Jun 2026 18:26:44 +0800 Subject: [PATCH 8/9] Fix wan2.7 image quality selection --- src/api/modelCapabilitiesClient.ts | 7 +++- src/features/workbench/WorkbenchPage.tsx | 35 +++++++++++++++++--- src/features/workbench/workbenchConstants.ts | 2 +- src/utils/modelOptions.ts | 19 +++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/api/modelCapabilitiesClient.ts b/src/api/modelCapabilitiesClient.ts index 7d284d0..22614bf 100644 --- a/src/api/modelCapabilitiesClient.ts +++ b/src/api/modelCapabilitiesClient.ts @@ -38,9 +38,14 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null { const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled); if (!enabled) return null; + const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value); + return { value, - label: toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value), + label: + value === "wan2.7-image-pro" + ? label.replace(/\s*4k\b/i, "").trim() || "wan 2.7 Pro" + : label, description: toStringValue(raw.description) || undefined, badge: toStringValue(raw.badge) || undefined, enabled, diff --git a/src/features/workbench/WorkbenchPage.tsx b/src/features/workbench/WorkbenchPage.tsx index bb60edb..47b0e1c 100644 --- a/src/features/workbench/WorkbenchPage.tsx +++ b/src/features/workbench/WorkbenchPage.tsx @@ -81,7 +81,9 @@ import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy"; import { getImageQualityOptions, + getImageQualityOptionsForContext, getDefaultImageQuality, + getDefaultImageQualityForContext, getVideoQualityOptions, getDefaultVideoQuality, getVideoQualityLabel, @@ -469,7 +471,26 @@ function WorkbenchPage({ setSidebarCollapsed(!hasSidebarRecords); }, [hasSidebarRecords]); - const imageQualityOptions = useMemo(() => getImageQualityOptions(imageModel), [imageModel]); + const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image"); + const isImageGridMode = activeMode === "image" && imageGridMode !== "single"; + const imageQualityContext = useMemo( + () => ({ + hasReferenceImages: hasImageReferences, + isGridMode: isImageGridMode, + }), + [hasImageReferences, isImageGridMode], + ); + const imageQualityOptions = useMemo( + () => getImageQualityOptionsForContext(imageModel, imageQualityContext), + [imageModel, imageQualityContext], + ); + const imageGridModeOptions = useMemo( + () => + String(imageModel || "").toLowerCase().startsWith("wan2.7-") + ? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25") + : GRID_MODE_OPTIONS, + [imageModel], + ); const videoQualityOptions = getVideoQualityOptions(videoModel); const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality); @@ -1205,9 +1226,15 @@ function WorkbenchPage({ useEffect(() => { if (!imageQualityOptions.some((option) => option.value === imageQuality)) { - setImageQuality(getDefaultImageQuality(imageModel)); + setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext)); } - }, [imageModel, imageQuality, imageQualityOptions]); + }, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]); + + useEffect(() => { + if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) { + setImageGridMode("single"); + } + }, [imageGridMode, imageGridModeOptions]); useEffect(() => { if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return; @@ -2905,7 +2932,7 @@ function WorkbenchPage({ toggleToolbarMenu("image-grid-mode")} diff --git a/src/features/workbench/workbenchConstants.ts b/src/features/workbench/workbenchConstants.ts index 3741f3b..b8a5b2b 100644 --- a/src/features/workbench/workbenchConstants.ts +++ b/src/features/workbench/workbenchConstants.ts @@ -231,7 +231,7 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe })); export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [ - { value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K" }, + { value: "wan2.7-image-pro", label: "wan 2.7 Pro" }, { value: "wan2.7-image", label: "wan 2.7" }, { value: "gpt-image-2", label: "omni-GPT" }, { value: "gpt-image-2-vip", label: "omni-GPT VIP" }, diff --git a/src/utils/modelOptions.ts b/src/utils/modelOptions.ts index 9b452d9..63d6371 100644 --- a/src/utils/modelOptions.ts +++ b/src/utils/modelOptions.ts @@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] { : imageQualityOptions.filter((option) => option.value !== "4K"); } +export function getImageQualityOptionsForContext( + model: string, + context?: { hasReferenceImages?: boolean; isGridMode?: boolean }, +): CanvasOption[] { + const options = getImageQualityOptions(model); + const shouldLimitTo2K = + String(model || "").toLowerCase() === "wan2.7-image-pro" && + (context?.hasReferenceImages || context?.isGridMode); + return shouldLimitTo2K ? options.filter((option) => option.value !== "4K") : options; +} + export function getDefaultImageQuality(model: string): string { const options = getImageQualityOptions(model); return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K"; } +export function getDefaultImageQualityForContext( + model: string, + context?: { hasReferenceImages?: boolean; isGridMode?: boolean }, +): string { + const options = getImageQualityOptionsForContext(model, context); + return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K"; +} + // ─── Video quality ──────────────────────────────────────────────────────────── function normalizeVideoModel(model: string): string { -- 2.52.0 From 4e97e706fdf2132a067c04434e00db4854d18bbb Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Mon, 8 Jun 2026 18:30:05 +0800 Subject: [PATCH 9/9] Add beta application email fields --- src/api/betaApplicationClient.ts | 4 ++++ src/components/BetaApplicationModal.tsx | 18 ++++++++++++------ .../beta-applications/BetaApplicationsPage.tsx | 2 ++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/api/betaApplicationClient.ts b/src/api/betaApplicationClient.ts index f0ad467..39921b2 100644 --- a/src/api/betaApplicationClient.ts +++ b/src/api/betaApplicationClient.ts @@ -2,6 +2,7 @@ import { serverRequest } from "./serverConnection"; export interface BetaApplicationInput { name: string; + email: string; phone: string; wechat: string; industry: string; @@ -16,6 +17,7 @@ export interface BetaApplicationInput { wantFeature: string[]; selfStatement: string; signature: string; + applicationDate: string; agreeRules: boolean; } @@ -72,6 +74,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem { userId: readNumberOrNull(item.userId), username: readNullableString(item.username), name: readString(item.name), + email: readString(item.email), phone: readString(item.phone), wechat: readString(item.wechat), industry: readString(item.industry), @@ -86,6 +89,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem { wantFeature: readStringArray(item.wantFeature), selfStatement: readString(item.selfStatement), signature: readString(item.signature), + applicationDate: readString(item.applicationDate), agreeRules: item.agreeRules === true, status: normalizeStatus(item.status), inviteCode: readNullableString(item.inviteCode), diff --git a/src/components/BetaApplicationModal.tsx b/src/components/BetaApplicationModal.tsx index 850dff2..619082a 100644 --- a/src/components/BetaApplicationModal.tsx +++ b/src/components/BetaApplicationModal.tsx @@ -10,6 +10,7 @@ interface BetaApplicationModalProps { /* ── Form state ── */ interface BetaFormData { name: string; + email: string; phone: string; wechat: string; industry: string; @@ -24,11 +25,13 @@ interface BetaFormData { wantFeature: string[]; selfStatement: string; signature: string; + applicationDate: string; agreeRules: boolean; } const INITIAL_FORM: BetaFormData = { name: "", + email: "", phone: "", wechat: "", industry: "", @@ -43,6 +46,7 @@ const INITIAL_FORM: BetaFormData = { wantFeature: [], selfStatement: "", signature: "", + applicationDate: "", agreeRules: false, }; @@ -156,10 +160,12 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => { const validate = () => { if (!form.name.trim()) return "请填写姓名 / 常用昵称"; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) return "请填写用于接收内测码的有效邮箱"; if (!form.phone.trim()) return "请填写联系手机号码"; if (!form.wechat.trim()) return "请填写微信账号"; if (!form.selfStatement.trim()) return "请填写申请自述"; if (!form.signature.trim()) return "请填写申请人确认签字"; + if (!form.applicationDate.trim()) return "请填写申请日期"; if (!form.agreeRules) return "请先阅读并同意内测规则"; return null; }; @@ -178,6 +184,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => { await betaApplicationClient.submit({ ...form, name: form.name.trim(), + email: form.email.trim(), phone: form.phone.trim(), wechat: form.wechat.trim(), industry: form.industry.trim(), @@ -190,9 +197,10 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => { feedbackWilling: form.feedbackWilling.trim(), selfStatement: form.selfStatement.trim(), signature: form.signature.trim(), + applicationDate: form.applicationDate.trim(), }); setForm(INITIAL_FORM); - setMessage({ tone: "success", text: "申请已提交,请留意站内通知。" }); + setMessage({ tone: "success", text: "申请已提交,请留意预留邮箱中的审核结果。" }); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" }); } finally { @@ -229,6 +237,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {

一、个人基础信息

update("name", v)} /> + update("email", v)} placeholder="审核通过后内测码将发送到此邮箱" /> update("phone", v)} /> update("wechat", v)} /> update("industry", v)} /> @@ -297,7 +306,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
  • 内测赠送 500 元等值 50,000 积分,仅限内测期间使用,不可提现、不可转让、不可兑换现金;
  • 内测版本含未上线测试功能,存在功能不稳定、界面调整、参数优化等情况,申请人自愿理解包容;
  • 严禁私自泄露内测专属工作流、内部功能、测试接口、未发布技术方案等内部资料;
  • -
  • 审核通过后,官方将在 48 小时 内发放内测账号、登录权限及免费积分;
  • +
  • 审核通过后,官方将在 48 小时 内通过预留邮箱发放内测码、登录权限及免费积分;
  • 正式版上线后,优质内测体验官可享受专属永久优惠权限与平台荣誉称号。
  • @@ -312,10 +321,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
    update("signature", v)} placeholder="请签署姓名" /> -
    - 申请填写日期 - -
    + update("applicationDate", v)} placeholder="例如:2026年6月8日" />
    diff --git a/src/features/beta-applications/BetaApplicationsPage.tsx b/src/features/beta-applications/BetaApplicationsPage.tsx index c6a0cf2..e4efccb 100644 --- a/src/features/beta-applications/BetaApplicationsPage.tsx +++ b/src/features/beta-applications/BetaApplicationsPage.tsx @@ -214,6 +214,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli

    一、个人基础信息

    + @@ -248,6 +249,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli

    {selectedApplication.selfStatement || "未填写"}

    + -- 2.52.0