Compare commits

..

14 Commits

Author SHA1 Message Date
stringadmin 1049fa3218 Merge branch 'master' into feat/toolbox-preview-cards-and-responsive-polish 2026-06-08 10:59:20 +00:00
ludan 6f54ad92c0 feat: 工具盒卡片预览图替换与响应式视觉优化
本次提交包含以下改进:

## 1. 工具卡片真实预览图替换 (MorePage.tsx)
- 移除原有的CSS绘制Before/After对比图(ToolComparePanel/CompareScene)
- 新增ToolPreviewPanel组件,使用OSS真实截图展示每个工具的效果预览
- 建立toolPreviewImages映射表,为8个工具分别配置预览图URL
- 预览图支持hover悬浮放大效果(popover),桌面端鼠标悬停时展示大图
- 触摸设备通过@media (hover: none)隐藏popover,避免移动端误触

## 2. 核心工具区重构 (MorePage.tsx)
- 移除FeaturedTool独立接口和featuredTools数组,统一到tools体系
- 新增coreToolIds集合标记核心工具(workbench/inpaint/watermarkRemoval)
- 新增coreToolGradients和coreToolSteps独立配置每张核心卡的渐变和步骤
- 移除openFeaturedTool,统一使用openTool处理所有工具点击
- 核心卡片kick er改为显示分类标签(图像创作/视频创作)
- 视频生成分类标签更名为"视频创作"

## 3. 工具卡片视觉升级 (more.css)
- 新增CSS变量体系:--more-card-surface/surface-strong/border/border-strong
- 核心工具卡:三列网格布局、渐变背景叠加、预览图16:9占位区
- 普通工具卡:增大最小高度至392px、radial-gradient光晕、增强边框
- 卡片预览图:aspect-ratio容器、内边框光效、::after渐变叠加层
- Hover悬浮popover:从卡片底部弹出大图,160ms过渡动画
- CTA按钮强化:渐变背景、内阴影高光、font-weight 850
- :active状态按压反馈(translateY(0)消除位移)
- 阴影系统升级:更深、更柔和的阴影层次

## 4. Prompt案例弹窗响应式重构 (workbench.css)
- 720px断点:从垂直堆叠改为水平左右分栏布局(1.08:0.92)
- 侧边栏从底部面板移至右侧,带左分割线和投影
- 420px断点:紧凑水平分栏(0.9:1.1)、更小字号和间距
- 弹窗增加边框和圆角、关闭按钮毛玻璃效果
- 作者信息采用grid布局、描述文本line-clamp截断

## 5. 响应式细节完善
- more.css 860px: 双列核心卡、增大预览图、调整间距
- more.css 520px: 单列布局、筛选标签横向滚动、CTA按钮全宽
- workbench.css: 各断点prompt-case-modal精确调优
2026-06-08 18:57:07 +08:00
stringadmin 9b7e708f85 Merge pull request 'Codex/generation task reliability' (#27) from codex/generation-task-reliability into master
Reviewed-on: #27
2026-06-08 10:31:08 +00:00
stringadmin 4e97e706fd Add beta application email fields 2026-06-08 18:30:05 +08:00
stringadmin 30536ad15f Fix wan2.7 image quality selection 2026-06-08 18:26:44 +08:00
stringadmin e78cc05299 Merge remote-tracking branch 'origin/master' into codex/generation-task-reliability 2026-06-08 17:39:11 +08:00
stringadmin b88be66e7f Merge pull request 'feat: Workbench SaaS视觉升级与视图重置机制' (#26) from feat/workbench-saas-polish-and-reset into master
Reviewed-on: #26
2026-06-08 09:31:55 +00:00
stringadmin e351e93200 Center beta application review layout 2026-06-08 16:35:32 +08:00
stringadmin 117b9354eb Restore moderation page styles 2026-06-08 16:32:16 +08:00
stringadmin 446514dd06 Fix beta application review page scrolling 2026-06-08 16:26:38 +08:00
stringadmin 85a174bcb5 Avoid clearing sessions on permission errors 2026-06-08 16:20:52 +08:00
stringadmin 560a7baddc Restore image generation estimate to 20 credits 2026-06-08 16:07:04 +08:00
stringadmin 4f7f67a278 Scale generation billing estimates to 1-to-100 credits 2026-06-08 16:03:52 +08:00
stringadmin 3963d9ae2f Show billing estimate and clarify session replacement 2026-06-08 15:55:50 +08:00
20 changed files with 975 additions and 294 deletions
+4
View File
@@ -2,6 +2,7 @@ import { serverRequest } from "./serverConnection";
export interface BetaApplicationInput { export interface BetaApplicationInput {
name: string; name: string;
email: string;
phone: string; phone: string;
wechat: string; wechat: string;
industry: string; industry: string;
@@ -16,6 +17,7 @@ export interface BetaApplicationInput {
wantFeature: string[]; wantFeature: string[];
selfStatement: string; selfStatement: string;
signature: string; signature: string;
applicationDate: string;
agreeRules: boolean; agreeRules: boolean;
} }
@@ -72,6 +74,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem {
userId: readNumberOrNull(item.userId), userId: readNumberOrNull(item.userId),
username: readNullableString(item.username), username: readNullableString(item.username),
name: readString(item.name), name: readString(item.name),
email: readString(item.email),
phone: readString(item.phone), phone: readString(item.phone),
wechat: readString(item.wechat), wechat: readString(item.wechat),
industry: readString(item.industry), industry: readString(item.industry),
@@ -86,6 +89,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem {
wantFeature: readStringArray(item.wantFeature), wantFeature: readStringArray(item.wantFeature),
selfStatement: readString(item.selfStatement), selfStatement: readString(item.selfStatement),
signature: readString(item.signature), signature: readString(item.signature),
applicationDate: readString(item.applicationDate),
agreeRules: item.agreeRules === true, agreeRules: item.agreeRules === true,
status: normalizeStatus(item.status), status: normalizeStatus(item.status),
inviteCode: readNullableString(item.inviteCode), inviteCode: readNullableString(item.inviteCode),
+6 -1
View File
@@ -38,9 +38,14 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null {
const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled); const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled);
if (!enabled) return null; if (!enabled) return null;
const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value);
return { return {
value, 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, description: toStringValue(raw.description) || undefined,
badge: toStringValue(raw.badge) || undefined, badge: toStringValue(raw.badge) || undefined,
enabled, enabled,
+12
View File
@@ -248,6 +248,17 @@ function isNonAuthErrorCode(code: string | undefined): boolean {
].includes(code); ].includes(code);
} }
function isAuthFailureResponse(status: number, payload: unknown): boolean {
if (status === 401) return true;
if (status !== 403) return false;
const code = getPayloadCode(payload);
if (code === "SESSION_REPLACED" || code === "TOKEN_EXPIRED" || code === "ACCOUNT_DISABLED") return true;
const message = getPayloadMessage(payload) || "";
return /账号已禁用|登录已过期|登录状态|session|token|企业信息不存在/i.test(message);
}
function notifySessionExpired(status: number, response: Response, payload: unknown): void { function notifySessionExpired(status: number, response: Response, payload: unknown): void {
if (status !== 401 && status !== 403) return; if (status !== 401 && status !== 403) return;
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -263,6 +274,7 @@ function notifySessionExpired(status: number, response: Response, payload: unkno
// Non-auth 403 errors (enterprise model access, insufficient balance) must // Non-auth 403 errors (enterprise model access, insufficient balance) must
// not trigger session expiry. // not trigger session expiry.
if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return; if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return;
if (!isAuthFailureResponse(status, payload)) return;
const now = Date.now(); const now = Date.now();
if (now - lastSessionExpiredEventAt < 1500) return; if (now - lastSessionExpiredEventAt < 1500) return;
+12 -6
View File
@@ -10,6 +10,7 @@ interface BetaApplicationModalProps {
/* ── Form state ── */ /* ── Form state ── */
interface BetaFormData { interface BetaFormData {
name: string; name: string;
email: string;
phone: string; phone: string;
wechat: string; wechat: string;
industry: string; industry: string;
@@ -24,11 +25,13 @@ interface BetaFormData {
wantFeature: string[]; wantFeature: string[];
selfStatement: string; selfStatement: string;
signature: string; signature: string;
applicationDate: string;
agreeRules: boolean; agreeRules: boolean;
} }
const INITIAL_FORM: BetaFormData = { const INITIAL_FORM: BetaFormData = {
name: "", name: "",
email: "",
phone: "", phone: "",
wechat: "", wechat: "",
industry: "", industry: "",
@@ -43,6 +46,7 @@ const INITIAL_FORM: BetaFormData = {
wantFeature: [], wantFeature: [],
selfStatement: "", selfStatement: "",
signature: "", signature: "",
applicationDate: "",
agreeRules: false, agreeRules: false,
}; };
@@ -156,10 +160,12 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
const validate = () => { const validate = () => {
if (!form.name.trim()) return "请填写姓名 / 常用昵称"; if (!form.name.trim()) return "请填写姓名 / 常用昵称";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) return "请填写用于接收内测码的有效邮箱";
if (!form.phone.trim()) return "请填写联系手机号码"; if (!form.phone.trim()) return "请填写联系手机号码";
if (!form.wechat.trim()) return "请填写微信账号"; if (!form.wechat.trim()) return "请填写微信账号";
if (!form.selfStatement.trim()) return "请填写申请自述"; if (!form.selfStatement.trim()) return "请填写申请自述";
if (!form.signature.trim()) return "请填写申请人确认签字"; if (!form.signature.trim()) return "请填写申请人确认签字";
if (!form.applicationDate.trim()) return "请填写申请日期";
if (!form.agreeRules) return "请先阅读并同意内测规则"; if (!form.agreeRules) return "请先阅读并同意内测规则";
return null; return null;
}; };
@@ -178,6 +184,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
await betaApplicationClient.submit({ await betaApplicationClient.submit({
...form, ...form,
name: form.name.trim(), name: form.name.trim(),
email: form.email.trim(),
phone: form.phone.trim(), phone: form.phone.trim(),
wechat: form.wechat.trim(), wechat: form.wechat.trim(),
industry: form.industry.trim(), industry: form.industry.trim(),
@@ -190,9 +197,10 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
feedbackWilling: form.feedbackWilling.trim(), feedbackWilling: form.feedbackWilling.trim(),
selfStatement: form.selfStatement.trim(), selfStatement: form.selfStatement.trim(),
signature: form.signature.trim(), signature: form.signature.trim(),
applicationDate: form.applicationDate.trim(),
}); });
setForm(INITIAL_FORM); setForm(INITIAL_FORM);
setMessage({ tone: "success", text: "申请已提交,请留意站内通知。" }); setMessage({ tone: "success", text: "申请已提交,请留意预留邮箱中的审核结果。" });
} catch (error) { } catch (error) {
setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" }); setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" });
} finally { } finally {
@@ -229,6 +237,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<h3 className="beta-doc-section__title"></h3> <h3 className="beta-doc-section__title"></h3>
<div className="beta-doc-grid"> <div className="beta-doc-grid">
<TextField label="姓名 / 常用昵称" value={form.name} onChange={(v) => update("name", v)} /> <TextField label="姓名 / 常用昵称" value={form.name} onChange={(v) => update("name", v)} />
<TextField label="接收内测码邮箱" value={form.email} onChange={(v) => update("email", v)} placeholder="审核通过后内测码将发送到此邮箱" />
<TextField label="联系手机号码" value={form.phone} onChange={(v) => update("phone", v)} /> <TextField label="联系手机号码" value={form.phone} onChange={(v) => update("phone", v)} />
<TextField label="微信账号" value={form.wechat} onChange={(v) => update("wechat", v)} /> <TextField label="微信账号" value={form.wechat} onChange={(v) => update("wechat", v)} />
<TextField label="所在行业 / 职业" value={form.industry} onChange={(v) => update("industry", v)} /> <TextField label="所在行业 / 职业" value={form.industry} onChange={(v) => update("industry", v)} />
@@ -297,7 +306,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<li> <strong>500 50,000 </strong>使</li> <li> <strong>500 50,000 </strong>使</li>
<li>线</li> <li>线</li>
<li></li> <li></li>
<li> <strong>48 </strong> </li> <li> <strong>48 </strong> </li>
<li>线</li> <li>线</li>
</ol> </ol>
@@ -312,10 +321,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<div className="beta-doc-grid beta-doc-grid--two"> <div className="beta-doc-grid beta-doc-grid--two">
<TextField label="申请人确认签字" value={form.signature} onChange={(v) => update("signature", v)} placeholder="请签署姓名" /> <TextField label="申请人确认签字" value={form.signature} onChange={(v) => update("signature", v)} placeholder="请签署姓名" />
<div className="beta-text-field"> <TextField label="申请填写日期" value={form.applicationDate} onChange={(v) => update("applicationDate", v)} placeholder="例如:2026年6月8日" />
<span className="beta-text-field__label"></span>
<input type="text" className="beta-text-field__input" value="2026年 月 日" readOnly />
</div>
</div> </div>
</section> </section>
@@ -214,6 +214,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli
<h3></h3> <h3></h3>
<div className="beta-admin-field-grid"> <div className="beta-admin-field-grid">
<DetailField label="姓名 / 常用昵称" value={valueOrEmpty(selectedApplication.name)} /> <DetailField label="姓名 / 常用昵称" value={valueOrEmpty(selectedApplication.name)} />
<DetailField label="接收内测码邮箱" value={valueOrEmpty(selectedApplication.email)} />
<DetailField label="联系手机号码" value={valueOrEmpty(selectedApplication.phone)} /> <DetailField label="联系手机号码" value={valueOrEmpty(selectedApplication.phone)} />
<DetailField label="微信账号" value={valueOrEmpty(selectedApplication.wechat)} /> <DetailField label="微信账号" value={valueOrEmpty(selectedApplication.wechat)} />
<DetailField label="所在行业 / 职业" value={valueOrEmpty(selectedApplication.industry)} /> <DetailField label="所在行业 / 职业" value={valueOrEmpty(selectedApplication.industry)} />
@@ -248,6 +249,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli
<p className="beta-admin-statement">{selectedApplication.selfStatement || "未填写"}</p> <p className="beta-admin-statement">{selectedApplication.selfStatement || "未填写"}</p>
<div className="beta-admin-field-grid"> <div className="beta-admin-field-grid">
<DetailField label="申请人确认签字" value={valueOrEmpty(selectedApplication.signature)} /> <DetailField label="申请人确认签字" value={valueOrEmpty(selectedApplication.signature)} />
<DetailField label="申请填写日期" value={valueOrEmpty(selectedApplication.applicationDate)} />
<DetailField label="同意规则" value={selectedApplication.agreeRules ? "已同意" : "未同意"} /> <DetailField label="同意规则" value={selectedApplication.agreeRules ? "已同意" : "未同意"} />
<DetailField label="IP" value={valueOrEmpty(selectedApplication.ipAddress)} /> <DetailField label="IP" value={valueOrEmpty(selectedApplication.ipAddress)} />
<DetailField label="客户端" value={valueOrEmpty(selectedApplication.userAgent)} wide /> <DetailField label="客户端" value={valueOrEmpty(selectedApplication.userAgent)} wide />
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient"; import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient"; import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebCanvasWorkflow, WebUserSession } from "../../types"; import type { WebCanvasWorkflow, WebUserSession } from "../../types";
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils"; import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
import { canManageCommunityCases } from "./communityPermissions"; import { canManageCommunityCases } from "./communityPermissions";
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { communityClient, type ServerCommunityCase } from "../../api/communityClient"; import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
import { reportClient, type AdminReportItem } from "../../api/reportClient"; import { reportClient, type AdminReportItem } from "../../api/reportClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebUserSession } from "../../types"; import type { WebUserSession } from "../../types";
import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions"; import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions";
+171 -129
View File
@@ -37,117 +37,153 @@ interface MoreTool {
imageTool?: WebImageWorkbenchTool; imageTool?: WebImageWorkbenchTool;
ready: boolean; ready: boolean;
badge?: string; badge?: string;
featured?: boolean;
} }
type CompareScene = const toolPreviewImages: Record<string, string> = {
| "workbench" inpaint: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%B1%80%E9%83%A8%E9%87%8D%E7%BB%98.PNG",
| "inpaint" camera: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E9%95%9C%E5%A4%B4%E5%AE%9E%E9%AA%8C%E5%AE%A4.PNG",
| "camera" upscale: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%88%86%E8%BE%A8%E7%8E%87%E6%8F%90%E5%8D%87.PNG",
| "upscale" watermarkRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%8E%BB%E6%B0%B4%E5%8D%B0.PNG",
| "watermark" dialogGenerator: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%AF%B9%E8%AF%9D%E6%A1%86%E7%94%9F%E6%88%90%E5%99%A8.PNG",
| "dialog" subtitleRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%AD%97%E5%B9%95%E5%8E%BB%E9%99%A4.PNG",
| "subtitle" characterMix: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E8%A7%92%E8%89%B2%E8%BF%81%E7%A7%BB.PNG",
| "digital-human" avatarConsole: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E6%95%B0%E5%AD%97%E4%BA%BA%E6%8E%A7%E5%88%B6%E5%8F%B0.PNG",
| "character"
| "avatar";
const toolCompareScenes: Record<string, CompareScene> = {
workbench: "workbench",
inpaint: "inpaint",
camera: "camera",
upscale: "upscale",
watermarkRemoval: "watermark",
dialogGenerator: "dialog",
subtitleRemoval: "subtitle",
digitalHuman: "digital-human",
characterMix: "character",
avatarConsole: "avatar",
}; };
function ToolComparePanel({ scene }: { scene: CompareScene }) { function ToolPreviewPanel({ toolId }: { toolId: string }) {
const imageUrl = toolPreviewImages[toolId];
if (!imageUrl) return null;
return ( return (
<span className={`more-card__compare more-card__compare--${scene}`} aria-hidden="true"> <span className="more-card__preview" aria-hidden="true">
<span className="more-card__compare-labels"> <span className="more-card__preview-frame">
<span>Before</span> <img src={imageUrl} alt="" loading="lazy" decoding="async" />
<span>After</span>
</span>
<span className="more-card__compare-stage">
<span className="more-card__compare-side more-card__compare-side--before">
<span className="more-card__scene-subject" />
<span className="more-card__scene-detail" />
<span className="more-card__scene-overlay" />
</span>
<span className="more-card__compare-divider">
<span />
</span>
<span className="more-card__compare-side more-card__compare-side--after">
<span className="more-card__scene-subject" />
<span className="more-card__scene-detail" />
<span className="more-card__scene-overlay" />
</span>
</span> </span>
<img className="more-card__preview-popover" src={imageUrl} alt="" loading="lazy" decoding="async" />
</span> </span>
); );
} }
const tools: MoreTool[] = [ function getPreviewClassName(toolId: string) {
{ id: "workbench", title: "图片工作台", text: "融合、修复、局部增强", useCase: "适合商品图精修、创意合成和局部画面重做", tags: ["热门", "一站式", "商品图"], icon: <EditOutlined />, category: "image", imageTool: "workbench", ready: true, featured: true }, return toolPreviewImages[toolId] ? " more-card--has-preview" : " more-card--no-preview";
{ id: "inpaint", title: "局部重绘", text: "修掉瑕疵、替换物体、重做局部画面", useCase: "适合快速处理商品瑕疵、人物细节和背景杂物", tags: ["新手推荐", "精修"], icon: <HighlightOutlined />, category: "image", imageTool: "inpaint", ready: true },
{ id: "camera", title: "镜头实验室", text: "快速生成俯拍、特写、广角等商业镜头", useCase: "适合做产品主图、种草图和不同机位方案", tags: ["电商常用", "镜头"], icon: <CameraOutlined />, category: "image", imageTool: "camera", ready: true },
{ id: "upscale", title: "分辨率提升", text: "把低清图片或视频提升到可交付质感", useCase: "适合修复旧素材、放大商品图和增强短视频清晰度", tags: ["高清", "交付前"], icon: <ColumnWidthOutlined />, category: "image", target: "resolutionUpscale", ready: true },
{ id: "watermarkRemoval", title: "去水印", text: "智能去除图片水印、文字和遮挡元素", useCase: "适合整理素材、清理参考图和恢复画面干净度", tags: ["素材清理", "高频"], icon: <DeleteOutlined />, category: "image", target: "watermarkRemoval", ready: true },
{ id: "dialogGenerator", title: "交互式对话框生成器", text: "上传背景图,快速制作可拖拽编辑的对话框", useCase: "适合剧情海报、社媒截图和角色对白设计", tags: ["内容创作", "可编辑"], icon: <MessageOutlined />, category: "image", target: "dialogGenerator", ready: true },
{ id: "subtitleRemoval", title: "字幕去除", text: "擦除视频字幕,让画面重新变干净", useCase: "适合二创前素材整理、短视频重剪和画面修复", tags: ["视频增强", "素材清理"], icon: <DeleteOutlined />, category: "video", target: "subtitleRemoval", ready: true },
{ id: "digitalHuman", title: "数字人", text: "用一张人像和音频生成口播视频", useCase: "适合品牌讲解、课程口播和带货短视频", tags: ["热门", "口播", "视频"], icon: <CustomerServiceOutlined />, category: "video", target: "digitalHuman", ready: true, featured: true },
{ id: "characterMix", title: "角色迁移", text: "把人物图迁移到参考视频的动作里", useCase: "适合角色短片、动作复刻和虚拟人内容生产", tags: ["角色视频", "动作"], icon: <SwapOutlined />, category: "video", target: "characterMix", ready: true },
{ id: "avatarConsole", title: "数字人控制台", text: "管理形象、播报、互动与接入配置", useCase: "适合持续运营数字人、配置品牌形象和复用口播模板", tags: ["运营台", "企业"], icon: <DashboardOutlined />, category: "video", target: "avatarConsole", ready: true },
];
interface FeaturedTool {
id: string;
title: string;
desc: string;
kicker: string;
steps: string[];
outcome: string;
icon: ReactNode;
imageTool?: WebImageWorkbenchTool;
target?: WebViewKey;
category: ToolCategory;
gradient: string;
} }
const featuredTools: FeaturedTool[] = [ const tools: MoreTool[] = [
{ {
id: "workbench", id: "workbench",
title: "图片工作台", title: "图片工作台",
desc: "从一张素材开始,完成精修、合成和二次创作。", text: "融合、修复、局部增强",
kicker: "图片精修工作流", useCase: "适合商品图精修、创意合成和局部画面重做",
steps: ["上传素材", "局部修复", "高清导出"], tags: ["热门", "一站式", "商品图"],
outcome: "适合商品图、海报图和创意视觉",
icon: <EditOutlined />, icon: <EditOutlined />,
imageTool: "workbench",
category: "image", category: "image",
gradient: "linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(139, 92, 246, 0.06))", imageTool: "workbench",
ready: true,
},
{
id: "inpaint",
title: "局部重绘",
text: "修掉瑕疵、替换物体、重做局部画面",
useCase: "适合快速处理商品瑕疵、人物细节和背景杂物",
tags: ["新手推荐", "精修"],
icon: <HighlightOutlined />,
category: "image",
imageTool: "inpaint",
ready: true,
},
{
id: "camera",
title: "镜头实验室",
text: "快速生成俯拍、特写、广角等商业镜头",
useCase: "适合做产品主图、种草图和不同机位方案",
tags: ["电商常用", "镜头"],
icon: <CameraOutlined />,
category: "image",
imageTool: "camera",
ready: true,
},
{
id: "upscale",
title: "分辨率提升",
text: "把低清图片或视频提升到可交付质感",
useCase: "适合修复旧素材、放大商品图和增强短视频清晰度",
tags: ["高清", "交付前"],
icon: <ColumnWidthOutlined />,
category: "image",
target: "resolutionUpscale",
ready: true,
},
{
id: "watermarkRemoval",
title: "去水印",
text: "智能去除图片水印、文字和遮挡元素",
useCase: "适合整理素材、清理参考图和恢复画面干净度",
tags: ["素材清理", "高频"],
icon: <DeleteOutlined />,
category: "image",
target: "watermarkRemoval",
ready: true,
},
{
id: "dialogGenerator",
title: "交互式对话框生成器",
text: "上传背景图,快速制作可拖拽编辑的对话框",
useCase: "适合剧情海报、社媒截图和角色对白设计",
tags: ["内容创作", "可编辑"],
icon: <MessageOutlined />,
category: "image",
target: "dialogGenerator",
ready: true,
},
{
id: "subtitleRemoval",
title: "字幕去除",
text: "擦除视频字幕,让画面重新变干净",
useCase: "适合二创前素材整理、短视频重剪和画面修复",
tags: ["视频增强", "素材清理"],
icon: <DeleteOutlined />,
category: "video",
target: "subtitleRemoval",
ready: true,
}, },
{ {
id: "digitalHuman", id: "digitalHuman",
title: "数字人", title: "数字人",
desc: "用参考人像和音频,快速生成可交付口播视频", text: "用一张人像和音频生成口播视频",
kicker: "口播视频工作流", useCase: "适合品牌讲解、课程口播和带货短视频",
steps: ["选择人像", "上传音频", "生成视频"], tags: ["热门", "口播", "视频"],
outcome: "适合品牌讲解、课程和带货短视频",
icon: <CustomerServiceOutlined />, icon: <CustomerServiceOutlined />,
target: "digitalHuman",
category: "video", category: "video",
gradient: "linear-gradient(135deg, rgba(13, 148, 136, 0.12), rgba(6, 182, 212, 0.06))", target: "digitalHuman",
ready: true,
},
{
id: "characterMix",
title: "角色迁移",
text: "把人物图迁移到参考视频的动作里",
useCase: "适合角色短片、动作复刻和虚拟人内容生产",
tags: ["角色视频", "动作"],
icon: <SwapOutlined />,
category: "video",
target: "characterMix",
ready: true,
},
{
id: "avatarConsole",
title: "数字人控制台",
text: "管理形象、播报、互动与接入配置",
useCase: "适合持续运营数字人、配置品牌形象和复用口播模板",
tags: ["运营台", "企业"],
icon: <DashboardOutlined />,
category: "video",
target: "avatarConsole",
ready: true,
}, },
]; ];
const categoryLabels: Record<ToolCategory, string> = { const categoryLabels: Record<ToolCategory, string> = {
image: "图像创作", image: "图像创作",
video: "视频生成", video: "视频创作",
}; };
const categoryIcons: Record<ToolCategory, ReactNode> = { const categoryIcons: Record<ToolCategory, ReactNode> = {
@@ -162,6 +198,20 @@ const filters: { key: FilterKey; label: string }[] = [
{ key: "upcoming", label: "即将上线" }, { key: "upcoming", label: "即将上线" },
]; ];
const coreToolIds = new Set(["workbench", "inpaint", "watermarkRemoval"]);
const coreToolGradients: Record<string, string> = {
workbench: "linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(139, 92, 246, 0.06))",
inpaint: "linear-gradient(135deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.055))",
watermarkRemoval: "linear-gradient(135deg, rgba(16, 185, 129, 0.13), rgba(var(--accent-rgb), 0.055))",
};
const coreToolSteps: Record<string, string[]> = {
workbench: ["上传素材", "局部修复", "高清导出"],
inpaint: ["选定区域", "描述修改", "生成结果"],
watermarkRemoval: ["上传素材", "智能识别", "干净导出"],
};
const RECENT_STORAGE_KEY = "omniai:more-recent-tools"; const RECENT_STORAGE_KEY = "omniai:more-recent-tools";
const MAX_RECENT = 4; const MAX_RECENT = 4;
@@ -169,7 +219,9 @@ function getRecentToolIds(): string[] {
try { try {
const raw = localStorage.getItem(RECENT_STORAGE_KEY); const raw = localStorage.getItem(RECENT_STORAGE_KEY);
return raw ? JSON.parse(raw) : []; return raw ? JSON.parse(raw) : [];
} catch { return []; } } catch {
return [];
}
} }
function pushRecentToolId(id: string) { function pushRecentToolId(id: string) {
@@ -199,39 +251,29 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
} }
}, [onOpenImageTool, onSelectView]); }, [onOpenImageTool, onSelectView]);
const openFeaturedTool = useCallback((tool: FeaturedTool) => { const filteredTools = tools.filter((tool) => {
pushRecentToolId(tool.id); if (coreToolIds.has(tool.id)) return false;
setRecentIds(getRecentToolIds());
if (tool.imageTool && onOpenImageTool) {
onOpenImageTool(tool.imageTool);
return;
}
if (tool.target && onSelectView) {
onSelectView(tool.target);
}
}, [onOpenImageTool, onSelectView]);
const filteredTools = tools.filter((t) => {
if (t.featured) return false;
if (filter === "all") return true; if (filter === "all") return true;
if (filter === "upcoming") return !t.ready; if (filter === "upcoming") return !tool.ready;
return t.category === filter; return tool.category === filter;
}); });
const filterCounts: Record<FilterKey, number> = { const filterCounts: Record<FilterKey, number> = {
all: tools.filter((t) => !t.featured).length, all: tools.filter((tool) => !coreToolIds.has(tool.id)).length,
image: tools.filter((t) => !t.featured && t.category === "image").length, image: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "image").length,
video: tools.filter((t) => !t.featured && t.category === "video").length, video: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "video").length,
upcoming: tools.filter((t) => !t.featured && !t.ready).length, upcoming: tools.filter((tool) => !coreToolIds.has(tool.id) && !tool.ready).length,
}; };
const recentTools = recentIds const recentTools = recentIds
.map((id) => tools.find((t) => t.id === id)) .map((id) => tools.find((tool) => tool.id === id))
.filter((t): t is MoreTool => Boolean(t) && (t?.ready ?? false)); .filter((tool): tool is MoreTool => Boolean(tool) && (tool?.ready ?? false));
const groupedTools = filteredTools.reduce<Record<ToolCategory, MoreTool[]>>((acc, t) => { const coreTools = tools.filter((tool) => coreToolIds.has(tool.id));
if (!acc[t.category]) acc[t.category] = [];
acc[t.category].push(t); const groupedTools = filteredTools.reduce<Record<ToolCategory, MoreTool[]>>((acc, tool) => {
if (!acc[tool.category]) acc[tool.category] = [];
acc[tool.category].push(tool);
return acc; return acc;
}, {} as Record<ToolCategory, MoreTool[]>); }, {} as Record<ToolCategory, MoreTool[]>);
@@ -247,19 +289,19 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
</div> </div>
<div className="more-page-v2__header-meta" aria-label="工具盒概览"> <div className="more-page-v2__header-meta" aria-label="工具盒概览">
<span>{tools.filter((tool) => tool.ready).length} </span> <span>{tools.filter((tool) => tool.ready).length} </span>
<span>{featuredTools.length} </span> <span>{coreTools.length} </span>
</div> </div>
<nav className="more-page-v2__filters" aria-label="工具分类筛选"> <nav className="more-page-v2__filters" aria-label="工具分类筛选">
{filters.map((f) => ( {filters.map((item) => (
<button <button
key={f.key} key={item.key}
type="button" type="button"
className={filter === f.key ? "is-active" : ""} className={filter === item.key ? "is-active" : ""}
aria-pressed={filter === f.key} aria-pressed={filter === item.key}
onClick={() => setFilter(f.key)} onClick={() => setFilter(item.key)}
> >
<span>{f.label}</span> <span>{item.label}</span>
<em>{filterCounts[f.key]}</em> <em>{filterCounts[item.key]}</em>
</button> </button>
))} ))}
</nav> </nav>
@@ -298,27 +340,27 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
<ThunderboltOutlined /> <ThunderboltOutlined />
</h2> </h2>
<div className="more-page-v2__featured-grid"> <div className="more-page-v2__featured-grid">
{featuredTools.map((tool) => ( {coreTools.map((tool) => (
<button <button
key={tool.id} key={tool.id}
type="button" type="button"
className="more-card more-card--featured" className={`more-card more-card--featured${getPreviewClassName(tool.id)}`}
style={{ "--card-gradient": tool.gradient } as CSSProperties} style={{ "--card-gradient": coreToolGradients[tool.id] ?? "linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-rgb), 0.04))" } as CSSProperties}
aria-label={`打开核心工具:${tool.title}${tool.desc}`} aria-label={`打开核心工具:${tool.title}${tool.text}`}
onClick={() => openFeaturedTool(tool)} onClick={() => openTool(tool)}
> >
<span className="more-card__featured-icon">{tool.icon}</span> <span className="more-card__featured-icon">{tool.icon}</span>
<div className="more-card__featured-body"> <div className="more-card__featured-body">
<span className="more-card__featured-kicker">{tool.kicker}</span> <span className="more-card__featured-kicker">{categoryLabels[tool.category]}</span>
<strong>{tool.title}</strong> <strong>{tool.title}</strong>
<ToolComparePanel scene={toolCompareScenes[tool.id]} /> <ToolPreviewPanel toolId={tool.id} />
<span className="more-card__featured-desc">{tool.desc}</span> <span className="more-card__featured-desc">{tool.text}</span>
<span className="more-card__steps" aria-hidden="true"> <span className="more-card__steps" aria-hidden="true">
{tool.steps.map((step) => ( {(coreToolSteps[tool.id] ?? tool.tags.slice(0, 3)).map((step) => (
<span key={step}>{step}</span> <span key={step}>{step}</span>
))} ))}
</span> </span>
<span className="more-card__outcome">{tool.outcome}</span> <span className="more-card__outcome">{tool.useCase}</span>
<span className="more-card__cta">使 </span> <span className="more-card__cta">使 </span>
</div> </div>
</button> </button>
@@ -341,7 +383,7 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
<button <button
key={tool.id} key={tool.id}
type="button" type="button"
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}`} className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}${getPreviewClassName(tool.id)}`}
aria-label={tool.ready ? `打开工具:${tool.title}${tool.text}` : `${tool.title}暂未开放`} aria-label={tool.ready ? `打开工具:${tool.title}${tool.text}` : `${tool.title}暂未开放`}
onClick={() => openTool(tool)} onClick={() => openTool(tool)}
disabled={!tool.ready} disabled={!tool.ready}
@@ -353,7 +395,7 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
))} ))}
</span> </span>
<strong>{tool.title}</strong> <strong>{tool.title}</strong>
<ToolComparePanel scene={toolCompareScenes[tool.id]} /> <ToolPreviewPanel toolId={tool.id} />
<span className="more-card__desc">{tool.text}</span> <span className="more-card__desc">{tool.text}</span>
<span className="more-card__use-case">{tool.useCase}</span> <span className="more-card__use-case">{tool.useCase}</span>
<span className="more-card__action"> </span> <span className="more-card__action"> </span>
+1
View File
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, FlagOutlined, MailOutlined, PhoneOutlined } from "
import { useEffect, useState, type FormEvent } from "react"; import { useEffect, useState, type FormEvent } from "react";
import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient"; import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient";
import { reportClient, type ReportInput } from "../../api/reportClient"; import { reportClient, type ReportInput } from "../../api/reportClient";
import "../../styles/pages/compliance.css";
type SubmitState = "idle" | "loading" | "success" | "error"; type SubmitState = "idle" | "loading" | "success" | "error";
+94 -12
View File
@@ -67,7 +67,6 @@ import { downloadResultAsset } from "./workbenchDownload";
import { translateTaskError } from "../../utils/translateTaskError"; import { translateTaskError } from "../../utils/translateTaskError";
import { import {
buildLocalTimeoutMessage, buildLocalTimeoutMessage,
formatTextTokenUsage,
getTaskTimeoutPolicy, getTaskTimeoutPolicy,
isTaskLocallyTimedOut, isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle"; } from "../../utils/taskLifecycle";
@@ -79,10 +78,12 @@ import {
import { isViduModel } from "../../utils/viduRouting"; import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting"; import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy"; import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import { import {
getImageQualityOptions, getImageQualityOptions,
getImageQualityOptionsForContext,
getDefaultImageQuality, getDefaultImageQuality,
getDefaultImageQualityForContext,
getVideoQualityOptions, getVideoQualityOptions,
getDefaultVideoQuality, getDefaultVideoQuality,
getVideoQualityLabel, getVideoQualityLabel,
@@ -221,6 +222,12 @@ const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
video: <VideoCameraOutlined />, 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({ function WorkbenchPage({
isAuthenticated, isAuthenticated,
session, session,
@@ -464,11 +471,72 @@ function WorkbenchPage({
setSidebarCollapsed(!hasSidebarRecords); setSidebarCollapsed(!hasSidebarRecords);
}, [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 videoQualityOptions = getVideoQualityOptions(videoModel);
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality); const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`; 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 = const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder; referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down"; const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -1158,9 +1226,15 @@ function WorkbenchPage({
useEffect(() => { useEffect(() => {
if (!imageQualityOptions.some((option) => option.value === imageQuality)) { 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(() => { useEffect(() => {
if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return; if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;
@@ -2585,7 +2659,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 = [ const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode }, { text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2850,7 +2932,7 @@ function WorkbenchPage({
<SelectChip <SelectChip
chipId="image-grid-mode" chipId="image-grid-mode"
value={imageGridMode} value={imageGridMode}
options={GRID_MODE_OPTIONS} options={imageGridModeOptions}
disabled={disabled} disabled={disabled}
isOpen={toolbarMenuId === "image-grid-mode"} isOpen={toolbarMenuId === "image-grid-mode"}
onToggle={() => toggleToolbarMenu("image-grid-mode")} onToggle={() => toggleToolbarMenu("image-grid-mode")}
@@ -2922,10 +3004,15 @@ function WorkbenchPage({
)} )}
</div> </div>
<div className="wb-composer__toolbar-right"> <div className="wb-composer__toolbar-right">
<span className="wb-composer__billing-estimate" title={billingEstimate.title}>
{billingEstimate.label}
</span>
<button <button
type="button" type="button"
className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`} className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
disabled={sendDisabled || isGenerating} disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
onClick={() => { onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", { if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode, mode: activeMode,
@@ -3231,11 +3318,6 @@ function WorkbenchPage({
<span>{message.taskStatusLabel || generationStatus}</span> <span>{message.taskStatusLabel || generationStatus}</span>
</div> </div>
)} )}
{message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
<div className="ai-chat-task-billing-note">
{formatTextTokenUsage(message.taskUsage)}
</div>
)}
{(message.resultUrl || (message.result && message.status !== "thinking")) && ( {(message.resultUrl || (message.result && message.status !== "thinking")) && (
<ResultCard <ResultCard
message={message} message={message}
+1 -1
View File
@@ -231,7 +231,7 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe
})); }));
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [ 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: "wan2.7-image", label: "wan 2.7" },
{ value: "gpt-image-2", label: "omni-GPT" }, { value: "gpt-image-2", label: "omni-GPT" },
{ value: "gpt-image-2-vip", label: "omni-GPT VIP" }, { value: "gpt-image-2-vip", label: "omni-GPT VIP" },
+2 -2
View File
@@ -33,7 +33,7 @@ const initialState: SessionState = {
loginPromptOpen: false, loginPromptOpen: false,
pendingAction: null, pendingAction: null,
sessionReplacedOpen: false, sessionReplacedOpen: false,
sessionReplacedMessage: '您的账号已在其他设备登录,此设备的登录状态已失效。', sessionReplacedMessage: '当前账号已在其他设备登录,此设备的登录状态已失效。',
}; };
export const useSessionStore = create<SessionState & SessionActions>((set) => ({ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
@@ -55,7 +55,7 @@ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
showSessionReplaced: (message) => set({ showSessionReplaced: (message) => set({
sessionReplacedOpen: true, sessionReplacedOpen: true,
sessionReplacedMessage: message || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。', sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}), }),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }), hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
+19 -1
View File
@@ -3,7 +3,11 @@
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
width: min(1180px, calc(100vw - 48px)); width: min(1180px, calc(100vw - 48px));
margin: 0 auto;
height: 100%;
min-height: 0;
padding: 24px; padding: 24px;
overflow: hidden;
} }
.beta-admin-toolbar { .beta-admin-toolbar {
@@ -90,6 +94,8 @@
display: grid; display: grid;
grid-template-columns: 300px minmax(0, 1fr); grid-template-columns: 300px minmax(0, 1fr);
gap: 16px; gap: 16px;
flex: 1;
min-height: 0;
align-items: start; align-items: start;
} }
@@ -97,7 +103,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
max-height: calc(100vh - 220px); max-height: 100%;
overflow: auto; overflow: auto;
padding-right: 4px; padding-right: 4px;
} }
@@ -174,6 +180,10 @@
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
min-width: 0; min-width: 0;
max-height: 100%;
min-height: 0;
overflow: auto;
padding-right: 4px;
} }
.beta-admin-detail__header, .beta-admin-detail__header,
@@ -376,6 +386,7 @@
.beta-admin-page__inner { .beta-admin-page__inner {
width: min(100%, calc(100vw - 24px)); width: min(100%, calc(100vw - 24px));
padding: 16px 12px; padding: 16px 12px;
overflow: auto;
} }
.beta-admin-toolbar, .beta-admin-toolbar,
@@ -385,11 +396,18 @@
.beta-admin-layout { .beta-admin-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
overflow: visible;
} }
.beta-admin-list { .beta-admin-list {
max-height: none; max-height: none;
} }
.beta-admin-detail {
max-height: none;
overflow: visible;
padding-right: 0;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
+15
View File
@@ -11915,6 +11915,21 @@
gap: 6px; gap: 6px;
} }
.wb-composer__billing-estimate {
max-width: 138px;
padding: 6px 9px;
border: 2px solid #111;
background: #fffbe8;
color: #111;
font-size: 12px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 2px 0 #111;
}
.wb-composer__send-primary { .wb-composer__send-primary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+314 -99
View File
@@ -1,6 +1,11 @@
.more-page-v2 { .more-page-v2 {
--more-card-shadow: 0 18px 48px rgba(0, 0, 0, 0.24); --more-card-shadow: 0 22px 54px rgba(0, 0, 0, 0.3);
--more-card-glow: 0 0 0 1px rgba(255, 255, 255, 0.025), 0 18px 38px rgba(0, 0, 0, 0.16); --more-card-glow: 0 0 0 1px rgba(255, 255, 255, 0.035), 0 16px 34px rgba(0, 0, 0, 0.18);
--more-card-surface: rgba(19, 23, 24, 0.86);
--more-card-surface-strong: rgba(22, 27, 28, 0.94);
--more-card-border: rgba(255, 255, 255, 0.105);
--more-card-border-strong: rgba(var(--accent-rgb), 0.3);
--more-page-pad-x: clamp(18px, 2.3vw, 32px);
position: relative; position: relative;
display: grid; display: grid;
@@ -158,24 +163,24 @@
.more-page-v2__scroll { .more-page-v2__scroll {
overflow-y: auto; overflow-y: auto;
padding: 26px 28px 68px; padding: 28px var(--more-page-pad-x) 72px;
scrollbar-color: rgba(var(--accent-rgb), 0.26) transparent; scrollbar-color: rgba(var(--accent-rgb), 0.26) transparent;
} }
.more-page-v2__section { .more-page-v2__section {
margin-bottom: 30px; margin-bottom: 34px;
} }
.more-page-v2__section-title { .more-page-v2__section-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin: 0 0 15px; margin: 0 0 14px;
color: var(--fg-muted); color: color-mix(in srgb, var(--fg-muted) 86%, var(--fg-body));
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 850;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.055em;
} }
.more-page-v2__section-title .anticon { .more-page-v2__section-title .anticon {
@@ -199,27 +204,31 @@
.more-page-v2__recent-row { .more-page-v2__recent-row {
display: flex; display: flex;
gap: 12px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.more-page-v2__featured-grid { .more-page-v2__featured-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px; gap: 16px;
} }
.more-card--featured { .more-card--featured {
display: flex; display: grid;
align-items: flex-start; grid-template-columns: 54px minmax(0, 1fr);
gap: 18px; align-items: start;
justify-items: stretch;
gap: 16px;
min-height: 336px;
padding: 20px; padding: 20px;
border-color: rgba(var(--accent-rgb), 0.18); border-color: rgba(var(--accent-rgb), 0.2);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: background:
var(--card-gradient), var(--card-gradient),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012)), radial-gradient(circle at 14% 4%, rgba(var(--accent-rgb), 0.12), transparent 36%),
var(--bg-panel); linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.016)),
var(--more-card-surface-strong);
box-shadow: var(--more-card-glow); box-shadow: var(--more-card-glow);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -230,27 +239,27 @@
position: absolute; position: absolute;
inset: 0; inset: 0;
background: background:
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.035), transparent), linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.038), transparent),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 34%); linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 34%);
opacity: 0.5; opacity: 0.62;
pointer-events: none; pointer-events: none;
} }
.more-card--featured:hover { .more-card--featured:hover {
border-color: rgba(var(--accent-rgb), 0.45); border-color: rgba(var(--accent-rgb), 0.46);
transform: translateY(-3px); transform: translateY(-2px);
box-shadow: var(--more-card-shadow), 0 0 0 1px rgba(var(--accent-rgb), 0.1); box-shadow: var(--more-card-shadow), 0 0 0 1px rgba(var(--accent-rgb), 0.12);
} }
.more-card__featured-icon { .more-card__featured-icon {
display: grid; display: grid;
place-items: center; place-items: center;
width: 52px; width: 54px;
height: 52px; height: 54px;
border: 1px solid rgba(var(--accent-rgb), 0.22); border: 1px solid rgba(var(--accent-rgb), 0.24);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)), linear-gradient(180deg, rgba(var(--accent-rgb), 0.18), rgba(var(--accent-rgb), 0.08)),
var(--bg-inset); var(--bg-inset);
color: var(--accent); color: var(--accent);
font-size: 24px; font-size: 24px;
@@ -261,11 +270,32 @@
.more-card__featured-body { .more-card__featured-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 9px; gap: 10px;
justify-self: stretch;
width: 100%;
height: 100%;
min-width: 0; min-width: 0;
text-align: left; text-align: left;
} }
.more-card--featured .more-card__preview {
width: 100%;
min-height: 0;
aspect-ratio: 16 / 9;
}
.more-card--featured.more-card--no-preview {
min-height: 0;
}
.more-card--featured.more-card--no-preview .more-card__featured-body {
justify-content: flex-start;
}
.more-card--featured.more-card--no-preview .more-card__outcome {
margin-top: 4px;
}
.more-card__featured-kicker { .more-card__featured-kicker {
width: fit-content; width: fit-content;
color: var(--accent); color: var(--accent);
@@ -277,14 +307,14 @@
.more-card__featured-body strong { .more-card__featured-body strong {
color: var(--fg-body); color: var(--fg-body);
font-size: 18px; font-size: 20px;
font-weight: 800; font-weight: 850;
line-height: 1.25; line-height: 1.25;
} }
.more-card__featured-desc { .more-card__featured-desc {
font-size: 13px; font-size: 13px;
color: var(--fg-muted); color: color-mix(in srgb, var(--fg-muted) 88%, var(--fg-body));
line-height: 1.5; line-height: 1.5;
} }
@@ -327,20 +357,23 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
width: fit-content; width: fit-content;
min-height: 28px; min-height: 32px;
margin-top: 0; margin-top: auto;
padding: 0 10px; padding: 0 12px;
border: 1px solid rgba(var(--accent-rgb), 0.28); border: 1px solid rgba(var(--accent-rgb), 0.34);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: rgba(var(--accent-rgb), 0.08); background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.06);
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 850;
color: var(--accent) !important; color: var(--accent) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
} }
.more-page-v2__grid { .more-page-v2__grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(236px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px; gap: 16px;
} }
@@ -350,18 +383,22 @@
align-content: start; align-content: start;
justify-items: start; justify-items: start;
min-width: 0; min-width: 0;
gap: 10px; min-height: 392px;
gap: 12px;
padding: 18px; padding: 18px;
border: 1px solid var(--border-weak); border: 1px solid var(--more-card-border);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent 42%), radial-gradient(circle at 12% 0%, rgba(var(--accent-rgb), 0.055), transparent 34%),
var(--bg-panel); linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 42%),
var(--more-card-surface);
color: var(--fg-body); color: var(--fg-body);
font: inherit; font: inherit;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025); box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.035),
0 1px 0 rgba(255, 255, 255, 0.02);
transition: transition:
border-color 160ms ease, border-color 160ms ease,
background 160ms ease, background 160ms ease,
@@ -370,12 +407,19 @@
} }
.more-card:hover { .more-card:hover {
border-color: rgba(var(--accent-rgb), 0.38); border-color: var(--more-card-border-strong);
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 46%), radial-gradient(circle at 12% 0%, rgba(var(--accent-rgb), 0.085), transparent 36%),
var(--bg-hover, rgba(255, 255, 255, 0.03)); linear-gradient(180deg, rgba(255, 255, 255, 0.052), transparent 46%),
rgba(24, 29, 30, 0.94);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: var(--more-card-glow), 0 10px 26px rgba(0, 0, 0, 0.16); box-shadow: var(--more-card-glow), 0 14px 30px rgba(0, 0, 0, 0.18);
}
.more-card:active,
.more-page-v2__filters button:active,
.more-page-v2__empty-action:active {
transform: translateY(0);
} }
.more-card--pending { .more-card--pending {
@@ -395,17 +439,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
min-width: 150px; min-width: 164px;
min-height: 54px; min-height: 58px;
padding: 10px 14px; padding: 11px 14px;
border-color: rgba(var(--accent-rgb), 0.14); border-color: rgba(var(--accent-rgb), 0.16);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038), rgba(255, 255, 255, 0.016)),
rgba(18, 23, 24, 0.88);
} }
.more-card__icon { .more-card__icon {
display: grid; display: grid;
place-items: center; place-items: center;
width: 40px; width: 38px;
height: 40px; height: 38px;
border: 1px solid rgba(var(--accent-rgb), 0.16); border: 1px solid rgba(var(--accent-rgb), 0.16);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: background:
@@ -437,15 +484,15 @@
.more-card strong { .more-card strong {
max-width: 100%; max-width: 100%;
color: var(--fg-body); color: var(--fg-body);
font-size: 14px; font-size: 16px;
font-weight: 800; font-weight: 850;
line-height: 1.35; line-height: 1.28;
} }
.more-card__topline { .more-card__topline {
position: absolute; position: absolute;
top: 14px; top: 18px;
right: 14px; right: 18px;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
@@ -472,9 +519,9 @@
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
min-height: 92px; min-height: 104px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.28); border: 1px solid rgba(var(--accent-rgb), 0.24);
border-radius: 10px; border-radius: 10px;
background: background:
linear-gradient(135deg, rgba(var(--accent-rgb), 0.1), transparent 34%), linear-gradient(135deg, rgba(var(--accent-rgb), 0.1), transparent 34%),
@@ -483,8 +530,7 @@
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -1px 0 rgba(0, 0, 0, 0.34), inset 0 -1px 0 rgba(0, 0, 0, 0.34),
0 0 20px rgba(var(--accent-rgb), 0.08); 0 0 18px rgba(var(--accent-rgb), 0.07);
clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
isolation: isolate; isolation: isolate;
} }
@@ -506,7 +552,6 @@
inset: 5px; inset: 5px;
z-index: 3; z-index: 3;
border: 1px solid rgba(var(--accent-rgb), 0.16); border: 1px solid rgba(var(--accent-rgb), 0.16);
clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
content: ""; content: "";
pointer-events: none; pointer-events: none;
} }
@@ -880,15 +925,102 @@
border-radius: 8px; border-radius: 8px;
} }
.more-card__preview {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1.42 / 1;
overflow: visible;
isolation: isolate;
}
.more-card__preview-frame {
position: absolute;
inset: 0;
display: block;
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.22);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 50% 42%, rgba(var(--accent-rgb), 0.12), transparent 56%),
linear-gradient(135deg, rgba(var(--accent-rgb), 0.08), transparent 34%),
var(--bg-inset);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
0 0 18px rgba(var(--accent-rgb), 0.06);
}
.more-card__preview-frame::after {
position: absolute;
inset: 0;
z-index: 1;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 34%, rgba(0, 0, 0, 0.18)),
linear-gradient(90deg, rgba(255, 255, 255, 0.045), transparent 38%, rgba(255, 255, 255, 0.025));
content: "";
pointer-events: none;
}
.more-card__preview-frame img,
.more-card__preview-popover {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
transform: none;
transition:
filter 220ms ease;
}
.more-card:hover .more-card__preview-frame img {
filter: saturate(1.05) contrast(1.02);
}
.more-card__preview-popover {
position: absolute;
left: 50%;
bottom: calc(100% + 12px);
z-index: 20;
width: min(420px, calc(100vw - 48px));
height: auto;
max-height: min(360px, 58vh);
padding: 10px;
border: 1px solid rgba(var(--accent-rgb), 0.34);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 50% 20%, rgba(var(--accent-rgb), 0.12), transparent 52%),
rgba(10, 14, 14, 0.96);
box-shadow:
0 28px 68px rgba(0, 0, 0, 0.46),
0 0 0 1px rgba(255, 255, 255, 0.04);
opacity: 0;
pointer-events: none;
transform: translate(-50%, 8px) scale(0.96);
transform-origin: 50% 100%;
transition:
opacity 160ms ease,
transform 160ms ease;
}
.more-card__preview:hover .more-card__preview-popover {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
.more-card--featured .more-card__preview-popover {
display: none;
}
.more-card__desc { .more-card__desc {
color: var(--fg-muted); color: color-mix(in srgb, var(--fg-muted) 88%, var(--fg-body));
font-size: 12.5px; font-size: 12.5px;
line-height: 1.5; line-height: 1.55;
} }
.more-card__use-case { .more-card__use-case {
display: block; display: block;
min-height: 38px; min-height: 50px;
color: color-mix(in srgb, var(--fg-muted) 78%, var(--fg-body)); color: color-mix(in srgb, var(--fg-muted) 78%, var(--fg-body));
font-size: 12px; font-size: 12px;
line-height: 1.55; line-height: 1.55;
@@ -898,15 +1030,15 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
width: fit-content; width: fit-content;
min-height: 26px; min-height: 30px;
margin-top: 2px; margin-top: auto;
padding: 0 9px; padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: rgba(255, 255, 255, 0.035); background: rgba(255, 255, 255, 0.035);
color: var(--fg-body); color: var(--fg-body);
font-size: 11px; font-size: 11px;
font-weight: 800; font-weight: 850;
transition: transition:
border-color 160ms ease, border-color 160ms ease,
background 160ms ease, background 160ms ease,
@@ -915,8 +1047,8 @@
} }
.more-card:hover .more-card__action { .more-card:hover .more-card__action {
border-color: rgba(var(--accent-rgb), 0.28); border-color: rgba(var(--accent-rgb), 0.32);
background: rgba(var(--accent-rgb), 0.08); background: rgba(var(--accent-rgb), 0.1);
color: var(--accent); color: var(--accent);
transform: translateX(2px); transform: translateX(2px);
} }
@@ -936,14 +1068,15 @@
.more-page-v2__empty { .more-page-v2__empty {
display: grid; display: grid;
justify-items: center; justify-items: center;
gap: 10px; gap: 12px;
min-height: 220px; min-height: 238px;
padding: 34px 20px; padding: 38px 22px;
border: 1px solid var(--border-weak); border: 1px solid var(--more-card-border);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.065), transparent 64%), radial-gradient(circle at 50% 0%, rgba(var(--accent-rgb), 0.1), transparent 42%),
var(--bg-panel); linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 64%),
var(--more-card-surface);
color: var(--fg-muted); color: var(--fg-muted);
text-align: center; text-align: center;
} }
@@ -951,11 +1084,13 @@
.more-page-v2__empty-icon { .more-page-v2__empty-icon {
display: grid; display: grid;
place-items: center; place-items: center;
width: 48px; width: 52px;
height: 48px; height: 52px;
border: 1px solid rgba(var(--accent-rgb), 0.22); border: 1px solid rgba(var(--accent-rgb), 0.22);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: rgba(var(--accent-rgb), 0.1); background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.08);
color: var(--accent); color: var(--accent);
font-size: 20px; font-size: 20px;
} }
@@ -978,12 +1113,14 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 34px; min-height: 36px;
margin-top: 4px; margin-top: 4px;
padding: 0 12px; padding: 0 14px;
border: 1px solid rgba(var(--accent-rgb), 0.32); border: 1px solid rgba(var(--accent-rgb), 0.36);
border-radius: var(--radius-xs, 8px); border-radius: var(--radius-xs, 8px);
background: rgba(var(--accent-rgb), 0.08); background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.14), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.06);
color: var(--accent); color: var(--accent);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
@@ -1013,6 +1150,7 @@
.more-page-v2__header { .more-page-v2__header {
grid-template-columns: minmax(180px, auto) minmax(0, 1fr); grid-template-columns: minmax(180px, auto) minmax(0, 1fr);
gap: 14px;
} }
.more-page-v2__filters { .more-page-v2__filters {
@@ -1023,15 +1161,21 @@
@media (max-width: 860px) { @media (max-width: 860px) {
.more-page-v2 { .more-page-v2 {
--more-page-pad-x: 16px;
padding-left: 0; padding-left: 0;
} }
.more-page-v2__header { .more-page-v2__header {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
padding: 14px 16px 12px; padding: 16px 16px 14px;
gap: 12px; gap: 12px;
} }
.more-page-v2__header h1 {
font-size: 24px;
}
.more-page-v2__header-meta { .more-page-v2__header-meta {
gap: 6px; gap: 6px;
} }
@@ -1047,13 +1191,22 @@
padding-right: 16px; padding-right: 16px;
} }
.more-page-v2__filters button {
min-height: 31px;
padding: 0 10px;
}
.more-page-v2__scroll { .more-page-v2__scroll {
padding: 16px 16px 48px; padding: 18px 16px 52px;
}
.more-page-v2__section {
margin-bottom: 26px;
} }
.more-page-v2__grid { .more-page-v2__grid {
grid-template-columns: repeat(auto-fill, minmax(172px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px; gap: 14px;
} }
.more-page-v2__recent-row { .more-page-v2__recent-row {
@@ -1063,11 +1216,13 @@
} }
.more-page-v2__featured-grid { .more-page-v2__featured-grid {
grid-template-columns: 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px; gap: 12px;
} }
.more-card--featured { .more-card--featured {
grid-template-columns: 44px minmax(0, 1fr);
min-height: 0;
padding: 16px; padding: 16px;
gap: 12px; gap: 12px;
} }
@@ -1079,7 +1234,12 @@
} }
.more-card__featured-body strong { .more-card__featured-body strong {
font-size: 15px; font-size: 16px;
}
.more-card--featured .more-card__preview {
width: 100%;
min-height: 176px;
} }
.more-card__featured-kicker, .more-card__featured-kicker,
@@ -1097,8 +1257,13 @@
font-size: 10px; font-size: 10px;
} }
.more-card__compare { .more-card__preview {
min-height: 82px; min-height: 190px;
}
.more-card {
min-height: 394px;
padding: 16px;
} }
.more-card__topline { .more-card__topline {
@@ -1108,24 +1273,74 @@
} }
.more-card__use-case { .more-card__use-case {
min-height: 54px; min-height: 46px;
} }
} }
@media (max-width: 520px) { @media (max-width: 520px) {
.more-page-v2__header {
gap: 10px;
padding-top: 14px;
}
.more-page-v2__header-meta {
overflow-x: auto;
flex-wrap: nowrap;
margin-right: -16px;
padding-right: 16px;
scrollbar-width: none;
}
.more-page-v2__header-meta::-webkit-scrollbar {
display: none;
}
.more-page-v2__grid { .more-page-v2__grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.more-card { .more-page-v2__featured-grid {
gap: 9px; grid-template-columns: 1fr;
} }
.more-card__compare { .more-page-v2__section-title {
min-height: 94px; margin-bottom: 12px;
}
.more-card--featured {
grid-template-columns: 1fr;
padding: 15px;
}
.more-card__featured-icon {
width: 40px;
height: 40px;
}
.more-card {
gap: 10px;
min-height: 0;
padding: 15px;
}
.more-card__preview {
min-height: 190px;
} }
.more-card__use-case { .more-card__use-case {
min-height: 0; min-height: 0;
} }
.more-card__action,
.more-card__cta {
min-height: 32px;
width: 100%;
justify-content: center;
}
}
@media (hover: none) {
.more-card__preview-popover {
display: none;
}
} }
+284 -37
View File
@@ -443,23 +443,23 @@
@media (max-width: 720px) { @media (max-width: 720px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
align-items: stretch; align-items: center;
padding: calc(56px + env(safe-area-inset-top, 0px)) 0 0; padding: calc(56px + env(safe-area-inset-top, 0px) + 10px) 12px 12px;
background: background:
radial-gradient(circle at 50% 34%, rgba(var(--accent-rgb), 0.12), transparent 42%), radial-gradient(circle at 50% 34%, rgba(var(--accent-rgb), 0.12), transparent 42%),
rgba(0, 0, 0, 0.78); rgba(0, 0, 0, 0.78);
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: calc(100svh - 56px - env(safe-area-inset-top, 0px)); --prompt-case-modal-max-height: calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px);
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr) minmax(210px, 34%); grid-template-rows: minmax(0, 60%) minmax(220px, 40%);
width: 100%; width: min(100%, 520px);
height: var(--prompt-case-modal-max-height); height: var(--prompt-case-modal-max-height);
max-height: var(--prompt-case-modal-max-height); max-height: var(--prompt-case-modal-max-height);
overflow: hidden; overflow: hidden;
border: 0; border: 1px solid rgba(var(--accent-rgb), 0.34);
border-radius: 0; border-radius: 22px;
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)), linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)),
rgba(5, 8, 10, 0.96); rgba(5, 8, 10, 0.96);
@@ -467,6 +467,7 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media {
min-height: 0; min-height: 0;
align-content: center;
padding: 14px 14px 8px; padding: 14px 14px 8px;
overflow: hidden; overflow: hidden;
background: background:
@@ -479,7 +480,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: none; max-height: none;
border-radius: 16px; border-radius: 12px;
object-fit: contain; object-fit: contain;
background: rgba(0, 0, 0, 0.18); background: rgba(0, 0, 0, 0.18);
box-shadow: box-shadow:
@@ -490,12 +491,12 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) minmax(220px, 32%); grid-template-rows: minmax(0, 62%) minmax(220px, 38%);
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
padding: 12px 14px 8px; padding: 14px 14px 8px;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media img, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media img,
@@ -510,35 +511,36 @@
grid-template-rows: auto auto minmax(0, 1fr) auto; grid-template-rows: auto auto minmax(0, 1fr) auto;
min-height: 0; min-height: 0;
max-height: none; max-height: none;
gap: 11px; gap: 10px;
overflow-y: auto; overflow: hidden;
margin-top: -16px; margin-top: -14px;
padding: 20px 16px 16px; padding: 18px 16px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.12); border-top: 1px solid rgba(255, 255, 255, 0.12);
border-left: 0;
border-radius: 24px 24px 0 0; border-radius: 24px 24px 0 0;
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)),
rgba(12, 17, 19, 0.99); rgba(12, 17, 19, 0.99);
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.24); box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.28);
scrollbar-width: thin; scrollbar-width: thin;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
margin-top: -16px; margin-top: -14px;
padding: 20px 16px 16px; padding: 18px 16px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.12); border-top: 1px solid rgba(255, 255, 255, 0.12);
border-left: 0; border-left: 0;
border-radius: 24px 24px 0 0; border-radius: 24px 24px 0 0;
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.26); box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.28);
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__close { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__close {
top: 16px; top: 14px;
right: 16px; right: 14px;
z-index: 3; z-index: 3;
width: 34px; width: 32px;
height: 34px; height: 32px;
border-color: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.18);
background: rgba(8, 10, 11, 0.72); background: rgba(8, 10, 11, 0.72);
color: rgba(243, 245, 242, 0.78); color: rgba(243, 245, 242, 0.78);
@@ -546,29 +548,38 @@
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author {
gap: 10px; grid-template-columns: 32px minmax(0, 1fr);
gap: 9px;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author > span { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author > span {
width: 34px; width: 32px;
height: 34px; height: 32px;
font-size: 13px; font-size: 12px;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta {
gap: 7px; gap: 6px;
padding-bottom: 11px; min-height: 0;
padding-bottom: 9px;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta h2 { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta h2 {
font-size: 18px; font-size: 17px;
line-height: 1.28; line-height: 1.28;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt p { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt p {
font-size: 13px; font-size: 13px;
line-height: 1.58; line-height: 1.52;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-meta p { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-meta p {
@@ -579,17 +590,17 @@
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt {
gap: 7px; gap: 6px;
padding: 12px; padding: 12px;
border-radius: 14px; border-radius: 14px;
background: rgba(255, 255, 255, 0.035); background: rgba(255, 255, 255, 0.035);
overflow: auto; overflow: auto;
min-height: 0; min-height: 0;
max-height: 132px; max-height: none;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-prompt { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-prompt {
max-height: 118px; max-height: none;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-prompt, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-prompt,
@@ -608,13 +619,15 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 40px; min-height: 40px;
padding: 0 10px;
border-radius: 12px; border-radius: 12px;
font-size: 12px; font-size: 12px;
white-space: nowrap;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
grid-template-columns: 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@@ -625,14 +638,15 @@
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
grid-template-rows: minmax(0, 1fr) minmax(230px, 38%); grid-template-columns: 1fr;
border-radius: 0; grid-template-rows: minmax(0, 58%) minmax(230px, 42%);
border-radius: 20px;
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel, .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) minmax(230px, 38%); grid-template-rows: minmax(0, 60%) minmax(230px, 40%);
} }
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media {
@@ -1689,6 +1703,239 @@
} }
} }
@media (max-width: 720px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
align-items: center;
padding: calc(56px + env(safe-area-inset-top, 0px) + 10px) 10px 12px;
background:
radial-gradient(circle at 50% 28%, rgba(var(--accent-rgb), 0.12), transparent 42%),
rgba(0, 0, 0, 0.78);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: min(560px, calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px));
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(188px, 0.92fr);
grid-template-rows: 1fr;
width: min(calc(100vw - 20px), 660px);
height: var(--prompt-case-modal-max-height);
max-height: var(--prompt-case-modal-max-height);
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.32);
border-radius: 22px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018)),
rgba(5, 8, 10, 0.97);
box-shadow:
0 24px 64px rgba(0, 0, 0, 0.48),
inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: minmax(0, 0.96fr) minmax(190px, 1.04fr);
grid-template-rows: 1fr;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
display: grid;
place-items: center;
min-height: 0;
padding: 14px;
overflow: hidden;
background:
radial-gradient(circle at 50% 0%, rgba(var(--accent-rgb), 0.11), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 58%),
rgba(4, 8, 13, 0.98);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media img,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media img,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media img {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
border-radius: 12px;
object-fit: contain;
background: rgba(0, 0, 0, 0.18);
box-shadow:
0 18px 42px rgba(0, 0, 0, 0.32),
0 0 0 1px rgba(255, 255, 255, 0.08);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
position: relative;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
align-self: stretch;
min-height: 0;
max-height: none;
gap: 9px;
overflow: hidden;
margin-top: 0;
padding: 16px 14px 14px;
border: 0;
border-left: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.052), rgba(255, 255, 255, 0.018)),
rgba(12, 17, 19, 0.99);
box-shadow:
-14px 0 34px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(16px);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__close {
top: 14px;
right: 14px;
z-index: 3;
width: 32px;
height: 32px;
border-color: rgba(255, 255, 255, 0.18);
border-radius: 11px;
background: rgba(8, 10, 11, 0.72);
color: rgba(243, 245, 242, 0.82);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34);
backdrop-filter: blur(14px);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author {
grid-template-columns: 28px minmax(0, 1fr);
gap: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author > span {
width: 28px;
height: 28px;
font-size: 11px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author strong {
font-size: 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author em {
font-size: 11px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta {
min-height: 0;
gap: 5px;
padding-bottom: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta h2 {
font-size: 15px;
line-height: 1.28;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-meta p {
display: -webkit-box;
overflow: hidden;
color: rgba(226, 232, 240, 0.82);
font-size: 12px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-prompt {
min-height: 0;
max-height: none;
gap: 6px;
overflow: auto;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
background: rgba(255, 255, 255, 0.035);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt span {
font-size: 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt p {
font-size: 12px;
line-height: 1.48;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
position: static;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
align-self: end;
margin: 0;
padding: 5px 0 0;
background: transparent;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 36px;
padding: 0 7px;
border-radius: 11px;
font-size: 11px;
white-space: nowrap;
}
}
@media (max-width: 420px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
padding-right: 8px;
padding-left: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: min(520px, calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px));
grid-template-columns: minmax(0, 0.9fr) minmax(172px, 1.1fr);
grid-template-rows: 1fr;
border-radius: 20px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: minmax(0, 0.82fr) minmax(174px, 1.18fr);
grid-template-rows: 1fr;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
padding: 10px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
gap: 8px;
padding: 14px 10px 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 34px;
padding: 0 5px;
font-size: 10px;
}
}
@media (max-width: 980px) { @media (max-width: 980px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-home { .web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-home {
padding: 34px 18px 44px; padding: 34px 18px 44px;
+8
View File
@@ -1794,6 +1794,14 @@
box-shadow: none; box-shadow: none;
} }
.web-shell[data-ui-theme="dark-green"] .wb-composer__billing-estimate {
border: 1px solid var(--border-default);
border-radius: 999px;
background: var(--bg-elevated);
color: var(--fg-body);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) { .web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) {
background: var(--accent-hover); background: var(--accent-hover);
color: var(--dg-button-text); color: var(--dg-button-text);
+4 -3
View File
@@ -40,6 +40,7 @@ export const ENTERPRISE_VIDEO_RESOLUTION_OPTIONS = [
export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL; export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL;
export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P"; export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P";
const CREDITS_PER_CNY = 100;
export interface EnterpriseVideoPricingInput { export interface EnterpriseVideoPricingInput {
model: string; model: string;
@@ -74,11 +75,11 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
} }
if (model.includes("vidu")) { if (model.includes("vidu")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("pixverse")) { if (model.includes("pixverse")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("kling")) { if (model.includes("kling")) {
@@ -94,5 +95,5 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number { export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number {
const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1)); const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1));
return Number((getEnterpriseVideoCreditRate(input) * duration).toFixed(2)); return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
} }
+19
View File
@@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] {
: imageQualityOptions.filter((option) => option.value !== "4K"); : 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 { export function getDefaultImageQuality(model: string): string {
const options = getImageQualityOptions(model); const options = getImageQualityOptions(model);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K"; 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 ──────────────────────────────────────────────────────────── // ─── Video quality ────────────────────────────────────────────────────────────
function normalizeVideoModel(model: string): string { function normalizeVideoModel(model: string): string {
+5 -3
View File
@@ -32,8 +32,10 @@ export interface TextTokenUsage {
totalTokens?: number; totalTokens?: number;
} }
export const TEXT_INPUT_CREDITS_PER_MILLION = 2; const CREDITS_PER_CNY = 100;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5;
export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = { const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
submitTimeoutMs: 90_000, submitTimeoutMs: 90_000,
@@ -151,7 +153,7 @@ export function estimateTextTokenCredits(usage: TextTokenUsage): number {
} }
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string { export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
const rule = "文本计费规则:输入 Token 每百万 2 积分,输出 Token 每百万 5 积分,实际以服务端结算为准。"; const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
if (!usage) return rule; if (!usage) return rule;
const promptTokens = Math.max(0, Number(usage.promptTokens || 0)); const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
const completionTokens = Math.max(0, Number(usage.completionTokens || 0)); const completionTokens = Math.max(0, Number(usage.completionTokens || 0));