Compare commits

..

3 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
10 changed files with 849 additions and 755 deletions
+1 -72
View File
@@ -4,7 +4,6 @@ import {
isRecord, isRecord,
readJsonResponse, readJsonResponse,
serverRequest, serverRequest,
isServerRequestError,
throwResponseError, throwResponseError,
} from "./serverConnection"; } from "./serverConnection";
import { isOptionalApiRouteMissing } from "./apiErrorUtils"; import { isOptionalApiRouteMissing } from "./apiErrorUtils";
@@ -248,46 +247,6 @@ let taskHistoryRouteMissing = false;
const TASK_SUBMIT_TIMEOUT_MS = 90_000; const TASK_SUBMIT_TIMEOUT_MS = 90_000;
const TASK_STATUS_TIMEOUT_MS = 20_000; const TASK_STATUS_TIMEOUT_MS = 20_000;
const NON_RETRYING_REQUEST = { maxRetries: 0 }; const NON_RETRYING_REQUEST = { maxRetries: 0 };
const PENDING_CANCEL_TASKS_KEY = "omniai:pending-task-cancellations";
function readPendingCancelTaskIds(): string[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(PENDING_CANCEL_TASKS_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string" && id.trim().length > 0) : [];
} catch {
return [];
}
}
function writePendingCancelTaskIds(taskIds: string[]): void {
if (typeof window === "undefined") return;
try {
const uniqueIds = Array.from(new Set(taskIds.filter(Boolean)));
if (uniqueIds.length) {
window.localStorage.setItem(PENDING_CANCEL_TASKS_KEY, JSON.stringify(uniqueIds));
} else {
window.localStorage.removeItem(PENDING_CANCEL_TASKS_KEY);
}
} catch {
// Pending cancellation recovery is best-effort.
}
}
function markTaskCancelPending(taskId: string): void {
writePendingCancelTaskIds([...readPendingCancelTaskIds(), taskId]);
}
function clearPendingTaskCancel(taskId: string): void {
writePendingCancelTaskIds(readPendingCancelTaskIds().filter((id) => id !== taskId));
}
function shouldRetryTaskCancel(error: unknown): boolean {
if (!isServerRequestError(error)) return true;
const status = error.status;
return status === 429 || status === undefined || status >= 500;
}
export const aiGenerationClient = { export const aiGenerationClient = {
async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> { async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
@@ -376,48 +335,18 @@ export const aiGenerationClient = {
}, },
async cancelTask(taskId: string): Promise<void> { async cancelTask(taskId: string): Promise<void> {
markTaskCancelPending(taskId);
try { try {
await serverRequest<void>(`ai/tasks/${taskId}/cancel`, { await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
method: "PATCH", method: "PATCH",
maxRetries: NON_RETRYING_REQUEST.maxRetries, maxRetries: NON_RETRYING_REQUEST.maxRetries,
fallbackMessage: "Task cancel failed", fallbackMessage: "Task cancel failed",
}); });
clearPendingTaskCancel(taskId);
} catch (error) { } catch (error) {
if (isOptionalApiRouteMissing(error) || !shouldRetryTaskCancel(error)) { if (isOptionalApiRouteMissing(error)) return;
clearPendingTaskCancel(taskId);
return;
}
throw error; throw error;
} }
}, },
cancelTaskOnUnload(taskId: string): void {
markTaskCancelPending(taskId);
const url = buildApiUrl(`ai/tasks/${encodeURIComponent(taskId)}/cancel`);
const headers = buildAuthHeaders();
const body = JSON.stringify({ reason: "page_unload" });
try {
void fetch(url, {
method: "PATCH",
headers,
body,
credentials: "include",
keepalive: true,
});
} catch {
// Page unload cancellation is best-effort.
}
},
flushPendingTaskCancellations(): void {
readPendingCancelTaskIds().forEach((taskId) => {
this.cancelTask(taskId).catch(() => {});
});
},
async getTaskStatus(taskId: string): Promise<AiTaskStatus> { async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, { return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
timeoutMs: TASK_STATUS_TIMEOUT_MS, timeoutMs: TASK_STATUS_TIMEOUT_MS,
-4
View File
@@ -21,10 +21,6 @@ function getEffectiveLimit(): number {
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS; return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
} }
export function getEffectiveGenerationLimit(): number {
return getEffectiveLimit();
}
export function getGenerationUserKey(userId?: string | number | null): string { export function getGenerationUserKey(userId?: string | number | null): string {
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId); return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
} }
+8 -12
View File
@@ -396,6 +396,7 @@ function CanvasPage({
const canvasUploadInputRef = useRef<HTMLInputElement>(null); const canvasUploadInputRef = useRef<HTMLInputElement>(null);
const imageNodeInputRef = useRef<HTMLInputElement>(null); const imageNodeInputRef = useRef<HTMLInputElement>(null);
const canvasRef = useRef<HTMLElement>(null); const canvasRef = useRef<HTMLElement>(null);
const videoGenerationInFlightRef = useRef(new Set<string>());
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>()); const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
const canvasDragCounterRef = useRef(0); const canvasDragCounterRef = useRef(0);
const [isCanvasDragging, setIsCanvasDragging] = useState(false); const [isCanvasDragging, setIsCanvasDragging] = useState(false);
@@ -416,7 +417,7 @@ function CanvasPage({
const { const {
textGenerationState, imageGenerationState, videoGenerationState, textGenerationState, imageGenerationState, videoGenerationState,
generationToast, setGenerationToast, generationToast, setGenerationToast,
imageGenerationInFlightRef, videoGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef, imageGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef,
canvasGenKeepaliveRestoredRef, canvasGenKeepaliveRestoredRef,
setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus, setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus,
restoreKeepaliveTasks, resetGenerationState, restoreKeepaliveTasks, resetGenerationState,
@@ -1886,14 +1887,13 @@ function CanvasPage({
setVideoGenerationStatus(nodeId, { status: "submitting", message: "正在提交视频生成", progress: 8 }); setVideoGenerationStatus(nodeId, { status: "submitting", message: "正在提交视频生成", progress: 8 });
setGenerationToast("视频正在生成"); setGenerationToast("视频正在生成");
let task: Awaited<ReturnType<typeof onCreateTask>> | null = null;
try { try {
const referenceUrls = await resolveConnectedImageReferenceUrls("video", nodeId); const referenceUrls = await resolveConnectedImageReferenceUrls("video", nodeId);
if (videoNode.videoMode === "img2video" && referenceUrls.length === 0) { if (videoNode.videoMode === "img2video" && referenceUrls.length === 0) {
throw new Error("图生视频需要先连接至少一个可用的图片节点"); throw new Error("图生视频需要先连接至少一个可用的图片节点");
} }
let requestModel = resolveVideoRequestModel({ model, referenceUrls }); let requestModel = resolveVideoRequestModel({ model, referenceUrls });
task = await onCreateTask({ const task = await onCreateTask({
title: videoNode.title || "视频节点生成", title: videoNode.title || "视频节点生成",
type: "video", type: "video",
prompt: prompt || "根据参考图片生成视频", prompt: prompt || "根据参考图片生成视频",
@@ -1916,12 +1916,10 @@ function CanvasPage({
if (task.status === "completed" && !task.outputUrl) { if (task.status === "completed" && !task.outputUrl) {
throw new Error("视频生成任务已完成,但服务器没有返回结果地址,请稍后重试"); throw new Error("视频生成任务已完成,但服务器没有返回结果地址,请稍后重试");
} }
const taskId = task.id;
addCanvasGenKeepalive(taskId, nodeId, "video", projectId || "");
setVideoGenerationStatus(nodeId, { status: "running", message: "视频生成中", progress: Math.max(18, Number(task.progress || 0)) }); setVideoGenerationStatus(nodeId, { status: "running", message: "视频生成中", progress: Math.max(18, Number(task.progress || 0)) });
const outputUrl = const outputUrl =
task.outputUrl || task.outputUrl ||
(await waitForVideoTaskResult(taskId, (status) => { (await waitForImageTaskResult(task.id, (status) => {
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0))); const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
const statusLabel = const statusLabel =
status.status === "pending" status.status === "pending"
@@ -1934,12 +1932,11 @@ function CanvasPage({
setVideoGenerationStatus(nodeId, { status: "running", message: statusLabel, progress }); setVideoGenerationStatus(nodeId, { status: "running", message: statusLabel, progress });
})); }));
setVideoGenerationStatus(nodeId, { status: "success", message: "视频生成完成", progress: 100 }); setVideoGenerationStatus(nodeId, { status: "success", message: "视频生成完成", progress: 100 });
removeCanvasGenKeepalive(taskId);
const immediateAssetRef = createCanvasAssetRefFromGeneratedResult({ const immediateAssetRef = createCanvasAssetRefFromGeneratedResult({
url: outputUrl, url: outputUrl,
mediaType: "video/mp4", mediaType: "video/mp4",
resultType: "video", resultType: "video",
taskId, taskId: task.id,
originalUrl: outputUrl, originalUrl: outputUrl,
}); });
setVideoNodes((currentNodes) => setVideoNodes((currentNodes) =>
@@ -1950,7 +1947,7 @@ function CanvasPage({
videoUrl: outputUrl, videoUrl: outputUrl,
assetRef: immediateAssetRef, assetRef: immediateAssetRef,
taskRef: { taskRef: {
taskId, taskId: task.id,
status: "completed", status: "completed",
resultUrl: outputUrl, resultUrl: outputUrl,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@@ -1964,7 +1961,7 @@ function CanvasPage({
url: outputUrl, url: outputUrl,
mediaType: "video/mp4", mediaType: "video/mp4",
resultType: "video", resultType: "video",
taskId, taskId: task.id,
originalUrl: outputUrl, originalUrl: outputUrl,
}); });
await delay(420); await delay(420);
@@ -1977,7 +1974,7 @@ function CanvasPage({
videoUrl: assetRef.url, videoUrl: assetRef.url,
assetRef, assetRef,
taskRef: { taskRef: {
taskId, taskId: task.id,
status: "completed", status: "completed",
resultUrl: assetRef.url, resultUrl: assetRef.url,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@@ -1994,7 +1991,6 @@ function CanvasPage({
}); });
} finally { } finally {
videoGenerationInFlightRef.current.delete(nodeId); videoGenerationInFlightRef.current.delete(nodeId);
if (task?.id) removeCanvasGenKeepalive(task.id);
} }
}; };
+2 -37
View File
@@ -6,7 +6,6 @@ import type {
CanvasVideoGenerationState, CanvasVideoGenerationState,
CanvasVideoNode, CanvasVideoNode,
} from "./canvasTypes"; } from "./canvasTypes";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence"; import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence";
import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils"; import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils";
@@ -42,13 +41,6 @@ export function removeCanvasGenKeepalive(taskId: string): void {
saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId)); saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId));
} }
export function cancelCanvasGenKeepaliveOnUnload(): void {
const entries = loadCanvasGenKeepalive();
if (!entries.length) return;
entries.forEach((entry) => aiGenerationClient.cancelTaskOnUnload(entry.taskId));
saveCanvasGenKeepalive([]);
}
export interface UseCanvasGenerationParams { export interface UseCanvasGenerationParams {
setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>; setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>;
setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>; setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
@@ -63,7 +55,6 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
const [generationToast, setGenerationToast] = useState<string | null>(null); const [generationToast, setGenerationToast] = useState<string | null>(null);
const imageGenerationInFlightRef = useRef(new Set<string>()); const imageGenerationInFlightRef = useRef(new Set<string>());
const videoGenerationInFlightRef = useRef(new Set<string>());
const textGenerationInFlightRef = useRef(new Set<string>()); const textGenerationInFlightRef = useRef(new Set<string>());
const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>()); const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>());
const canvasGenKeepaliveRestoredRef = useRef(false); const canvasGenKeepaliveRestoredRef = useRef(false);
@@ -134,7 +125,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
imageGenerationInFlightRef.current.delete(entry.nodeId); imageGenerationInFlightRef.current.delete(entry.nodeId);
}); });
} else if (entry.nodeKind === "video") { } else if (entry.nodeKind === "video") {
videoGenerationInFlightRef.current.add(entry.nodeId); imageGenerationInFlightRef.current.add(entry.nodeId);
setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 }); setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 });
void waitForVideoTaskResult(entry.taskId, (status) => { void waitForVideoTaskResult(entry.taskId, (status) => {
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0))); const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
@@ -163,7 +154,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
removeCanvasGenKeepalive(entry.taskId); removeCanvasGenKeepalive(entry.taskId);
setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" }); setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" });
}).finally(() => { }).finally(() => {
videoGenerationInFlightRef.current.delete(entry.nodeId); imageGenerationInFlightRef.current.delete(entry.nodeId);
}); });
} }
} }
@@ -174,36 +165,11 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
textGenerationAbortControllersRef.current.clear(); textGenerationAbortControllersRef.current.clear();
textGenerationInFlightRef.current.clear(); textGenerationInFlightRef.current.clear();
imageGenerationInFlightRef.current.clear(); imageGenerationInFlightRef.current.clear();
videoGenerationInFlightRef.current.clear();
setTextGenerationState({}); setTextGenerationState({});
setImageGenerationState({}); setImageGenerationState({});
setVideoGenerationState({}); setVideoGenerationState({});
}; };
useEffect(() => {
const handlePageHide = () => {
cancelCanvasGenKeepaliveOnUnload();
textGenerationAbortControllersRef.current.forEach((controller) => controller.abort());
textGenerationAbortControllersRef.current.clear();
textGenerationInFlightRef.current.clear();
imageGenerationInFlightRef.current.clear();
videoGenerationInFlightRef.current.clear();
setTextGenerationState({});
setImageGenerationState({});
setVideoGenerationState({});
};
const handleOnline = () => {
aiGenerationClient.flushPendingTaskCancellations();
};
window.addEventListener("pagehide", handlePageHide);
window.addEventListener("online", handleOnline);
aiGenerationClient.flushPendingTaskCancellations();
return () => {
window.removeEventListener("pagehide", handlePageHide);
window.removeEventListener("online", handleOnline);
};
}, []);
return { return {
textGenerationState, textGenerationState,
imageGenerationState, imageGenerationState,
@@ -211,7 +177,6 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
generationToast, generationToast,
setGenerationToast, setGenerationToast,
imageGenerationInFlightRef, imageGenerationInFlightRef,
videoGenerationInFlightRef,
textGenerationInFlightRef, textGenerationInFlightRef,
textGenerationAbortControllersRef, textGenerationAbortControllersRef,
canvasGenKeepaliveRestoredRef, canvasGenKeepaliveRestoredRef,
+59 -210
View File
@@ -7,213 +7,61 @@ interface CompliancePageProps {
kind: ComplianceKind; kind: ComplianceKind;
} }
const companyName = "南京万物可爱文化传媒有限公司"; const companyName = "OmniAI";
const platformName = "Omniai平台";
const contactEmail = "system@omniai.net.cn";
const contactPhone = "15155073618"; const contactPhone = "15155073618";
const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501"; const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼501";
const icpRecord = "苏ICP备2026021747号-1";
const effectiveDate = "2026年06月8日";
const privacyPolicyText = ` const agreementSections = [
隐私政策 {
更新日期:2026年06月8日 title: "服务范围",
生效日期:2026年06月8日 body: "平台提供 AI 图片、视频、脚本、数字人及相关创作辅助服务。具体功能、模型能力、消耗规则以页面展示和平台公告为准。",
欢迎您使用本平台服务! },
南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)是Omniai平台的运营者。我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与/或服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》(以下简称“本政策”)向您说明,在您使用我们的产品与/或服务时,我们如何收集、使用、保存、共享和转让您的个人信息,以及您所享有的个人信息权利。 {
本政策与您所使用的我们的产品与/或服务息息相关,请您务必仔细阅读并确认您已经充分理解本政策的内容。一旦您开始使用我们的产品与/或服务,即表示您已充分理解并同意本政策。 title: "账号与使用",
本政策将帮助您了解以下内容: body: "用户应保证注册信息真实有效,妥善保管账号与登录凭证,不得出租、转让账号或以自动化方式恶意占用平台资源。",
一、本政策的适用范围 },
二、我们如何收集和使用您的个人信息 {
三、我们如何使用Cookie和同类技术 title: "内容合规",
四、我们如何共享、转让、公开披露您的个人信息 body: "用户不得上传、生成、发布违法违规、侵权、涉政敏感、暴恐、色情、赌博、诈骗或侵犯他人合法权益的内容。平台有权对违规内容采取删除、限制功能、封禁账号等措施。",
五、我们如何保护和存储您的个人信息 },
六、您如何管理您的个人信息 {
七、我们如何保护未成年人的个人信息 title: "积分与付费",
八、本政策如何更新 body: "积分仅限平台内消费,不支持提现、转让或折现。充值、套餐、赠送积分的有效期、消耗顺序和退费规则以充值页面展示为准。",
九、其他声明与责任限制 },
十、如何联系我们 {
一、本政策的适用范围 title: "责任限制",
1. 本政策适用于您通过我们的网站、客户端应用程序、小程序以及其他技术形态(包括但不限于SDK、API等方式)访问和使用我们的产品与/或服务。 body: "AI 生成结果可能存在偏差,用户应自行审核输出内容并承担使用后果。因不可抗力、第三方服务异常、网络故障造成的服务中断,平台将在合理范围内修复。",
2. 本政策不适用于以下情况: },
(1)我们的产品与/或服务中包含的或链接至第三方提供的信息与/或服务(包括任何第三方应用、网站、产品、服务等)。这些服务由第三方负责运营,您使用该服务适用第三方另行向您说明的个人信息处理规则。 ];
(2)其他非我们向您提供的产品与/或服务。
(3)特别提示:用户间交易。您通过平台社区与其他用户进行作品、素材、版权等交易时,您主动向交易对方提供的个人信息(如联系方式、交付地址等)不适用本政策。我们建议您在交易过程中谨慎保护您的个人信息,并充分了解交易对方的信用状况。
二、我们如何收集和使用您的个人信息
我们会遵循合法、正当、必要、诚信的原则,基于本政策所述的目的,收集和使用您的个人信息。如果我们将您的个人信息用于本政策未载明的其他用途,我们将以合理方式另行向您告知并征得您的同意。
(一)注册、登录与认证
1. 必要信息:当您注册平台账号时,您需要提供您的手机号码及验证码/登录密码。手机号码是履行国家法律法规对网络实名制(真实身份信息认证)要求所需的必要信息,若您不提供,您将无法完成注册,仅能使用浏览、搜索等基本功能。
2. 企业用户:如您以企业身份注册,您还需提供企业名称、营业执照、法定代表人/联系人信息等,以便我们核验您的企业身份,为您提供企业级服务。
3. 第三方账号登录:您可以使用我们认可的第三方账号(如微信、QQ等)进行登录。我们会收集您在该第三方账号下的唯一标识、昵称、头像等信息,用于创建您在本平台的账号。
4. 账号信息完善:您可以选择填写或修改您的昵称、头像、个人简介、所在地区等信息。此类信息非必要,但有助于提升您的社区交互体验。
(二)浏览、搜索与内容发布
1. 内容浏览:当您浏览平台上的模型、素材、作品、文章等内容时,我们会收集您的设备信息、日志信息,包括IP地址、浏览器类型、操作系统版本、访问时间、点击记录、浏览记录、下载记录等。此类信息用于为您提供内容展示、优化推荐算法以及保障服务安全稳定运行。
2. 搜索功能:当您使用搜索服务时,我们会收集您的搜索关键字信息,用于向您展示搜索结果。
3. 内容发布与社区交互:当您在平台社区上传模型、发布作品、发表评论、点赞、收藏、分享时,我们会收集您主动发布的内容(包括但不限于文字、图片、视频、模型文件等)。您发布的内容中会展示您的昵称、头像等信息。
(三)SaaS软件服务
1. 使用SaaS工具:当您使用我们提供的SaaS软件工具(如图片编辑、模型训练、设计工具等)时,您主动上传、输入或导入的内容及指令将被收集,以便为您提供服务。
2. 生成内容:您通过SaaS工具生成的内容,其所有权归您所有。我们会保存该生成内容的记录,以便您进行查看、下载、管理或再次编辑。
3. 模型优化:在经过安全加密技术处理、严格去标识化且无法重新识别特定个人的前提下,我们可能会将您使用SaaS软件过程中产生的脱敏数据用于模型优化和改进服务。您可以通过平台设置选择是否允许我们将您的内容用于此目的。
(四)付费订阅与交易服务
1. 会员订阅:当您购买会员服务(软件订阅费)或充值积分时,我们会收集您的订单信息(包括商品/服务名称、金额、交易时间)和支付信息。支付信息(如银行卡号、第三方支付账号)由第三方支付机构直接收集,我们不会获取您完整的支付敏感信息。
2. 积分消耗:当您使用积分消耗大模型算力或兑换服务时,我们会记录您的积分变动情况。
3. 用户间交易:
(1)当您作为卖方(发布作品/素材进行售卖)时,我们需要收集您的实名认证信息(个人或企业)、收款账户信息(银行卡号或第三方支付账号),以便向您结算交易款项。
(2)当您作为买方(购买作品/素材)时,我们需要收集您的订单信息,并可能向卖方提供您的平台账号信息,以便卖方完成交付。您的真实联系方式(如手机号、地址)不会直接提供给卖方,除非您主动通过平台沟通工具披露。
(五)运营与安全保障
为了维护相关产品或服务的正常稳定运行,保护您或其他用户或公众的安全及合法利益,我们会收集如下必要信息:
1. 设备信息:包括设备型号、操作系统版本、唯一设备标识符、IP地址、MAC地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式/类型/状态等。
2. 日志信息:包括您的操作日志、服务日志,例如您对平台功能的点击、使用情况、崩溃数据、异常信息等。
3. 应用信息:用于预防恶意程序、保障运营质量,我们会收集安装的应用列表、软件列表或正在运行的进程信息。
4. IP归属地:根据相关法律法规要求,我们可能会在您的个人主页、作品发布等页面展示您的IP地址归属地信息。
(六)系统权限
为实现特定功能,我们可能会向您申请相机/摄像头、相册/存储、麦克风、通知、剪贴板等系统权限。您可以在设备设置中自主选择开启或关闭这些权限。关闭权限将可能导致对应功能无法使用,但不影响其他功能。
(七)征得授权同意的例外
根据相关法律法规,以下情形中收集您的个人信息无需征得您的授权同意:为订立、履行您作为一方当事人的合同所必需;为履行法定职责或者法定义务所必需;为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需;为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息;依照法律规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息;法律、行政法规规定的其他情形。
三、我们如何使用Cookie和同类技术
1. Cookie:为使您获得更轻松的访问体验,我们可能会使用Cookie技术收集和存储您的登录状态、浏览偏好、使用习惯等信息。您可以根据自己的偏好管理或删除Cookie,但请注意,禁用Cookie可能会导致您无法正常使用平台的某些功能。
2. 同类技术:我们可能会使用网站信标、像素标签等其他同类技术,用于分析您对我们服务的使用情况、评估广告效果等。
四、我们如何共享、转让、公开披露您的个人信息
(一)共享
我们不会与任何公司、组织和个人共享您的个人信息,但获得您的明确同意、履行法定义务、与授权合作伙伴共享、用户间交易、关联公司共享等情况除外。授权合作伙伴包括支付服务提供商、实名认证服务商、云服务提供商、推送服务提供商、安全与风控服务商等,我们仅共享实现目的所必要的信息。
(二)转让
我们不会将您的个人信息转让给任何公司、组织和个人,但获得您的明确同意后,或在涉及合并、收购、破产清算等情形时除外。如涉及个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受本政策约束,否则将要求其重新向您征求授权同意。
(三)公开披露
我们不会公开披露您的个人信息,但获得您的明确同意后、基于法律司法或行政程序要求、对违规账号或侵权行为进行必要公示等情况除外。
(四)共享、转让、公开披露的例外
在为订立或履行合同所必需、履行法定义务所必需、应对紧急情况、公共利益新闻报道或舆论监督、处理您自行公开或其他已合法公开信息、法律行政法规规定的其他情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意。
五、我们如何保护和存储您的个人信息
1. 存储地点:我们在中华人民共和国境内运营中收集和产生的个人信息,将存储在中华人民共和国境内。我们不会将您的个人信息传输至境外。
2. 存储期限:我们仅在为实现本政策所述目的所必需的最短期限内保留您的个人信息,除非法律法规有更强的存留要求。超出存储期限后,我们将对您的个人信息进行删除或匿名化处理。
3. 保护措施:我们采用SSL加密、数据脱敏、访问控制、入侵检测等符合行业标准的安全技术措施,保护您的个人信息免遭泄露、篡改、毁损或未经授权的访问。我们建立数据安全管理制度,与员工签署保密协议,定期开展安全审计。一旦发生个人信息安全事件,我们将依法及时告知您,并向监管部门报告。
六、您如何管理您的个人信息
您可以通过平台账号功能或联系我们,访问、更正、补充、删除您的个人信息,撤回同意或申请注销账号。账号注销后,我们将删除或匿名化处理您的个人信息(法律法规另有规定的除外),您将无法再使用该账号登录平台。对于合理的请求,我们原则上不收取费用;对于无端重复、超出合理限度的请求,我们可能会拒绝或收取合理成本费用。
七、我们如何保护未成年人的个人信息
1. 平台主要面向成年人。如您为18周岁以下的未成年人,应在您的父母或其他监护人监护、指导下共同阅读并同意本政策。
2. 如您为14周岁以下的儿童,请务必在监护人明确同意的前提下使用我们的服务。我们将只会在法律法规允许、监护人明确同意或保护儿童所必要的情况下收集、使用儿童的个人信息。
3. 如我们发现未经监护人同意收集了儿童个人信息,我们将尽快删除相关数据。
八、本政策如何更新
我们可能会根据法律法规变化、产品功能调整或业务发展需要适时修订本政策。本政策更新后,我们会在平台显著位置发布更新版本,并在生效前通过公告、站内信、弹窗等方式通知您。如您继续使用我们的服务,即表示您同意受修订后的本政策约束。
九、其他声明与责任限制
为提供更好的服务,我们的产品中可能包含第三方SDK。第三方SDK可能会收集您的设备信息、网络信息等,具体请查阅我们在平台公示的《第三方信息共享清单》。本政策适用中华人民共和国大陆地区法律。因本政策引起的任何争议,双方应友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地有管辖权的人民法院诉讼解决。
十、如何联系我们
如您对本政策有任何疑问、意见或建议,或需要进行投诉、举报,您可以通过以下方式与我们联系:
客服邮箱:system@omniai.net.cn
客服电话:15155073618
邮寄地址:江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501
我们将在15个工作日内回复您的请求。
`.trim();
const agreementText = ` const privacySections = [
Omniai平台用户协议 {
更新日期:2026年06月8日 title: "收集的信息",
生效日期:2026年06月8日 body: "我们会收集账号信息、登录状态、联系方式、创作输入、生成结果、用量记录、设备与网络日志,用于提供服务、安全审计和问题排查。",
欢迎您使用本平台服务。 },
本平台向您提供SaaS软件服务、社区交流、素材及作品交易等相关服务。欢迎您与南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)共同签署本《用户服务协议》(下称“本协议”),并使用Omniai平台服务! {
在您注册成为平台用户前,请您务必审慎阅读、充分理解本协议的全部内容,特别是免除或限制责任、法律适用和争议解决条款。如您对本协议内容有任何疑问,可通过平台客服进行咨询。如您未满18周岁,请在法定监护人陪同下仔细阅读并充分理解本协议,并征得监护人的同意后使用本平台服务。 title: "Cookie 与本地存储",
当您点击“同意”本协议、完成注册程序,或实际开始使用平台服务时,即表示您已充分阅读、理解并接受本协议的全部内容,并与我们达成一致,本协议即对您产生法律约束力。如您不同意本协议的任何条款,请立即停止注册或使用行为。 body: "我们使用 Cookie、localStorage 和 sessionStorage 保存登录状态、偏好设置、Cookie 同意状态、创作草稿和断点续传数据。",
我们可能不时修改本协议及相关平台规则,并通过网站公告、站内信等方式进行通知。若您在本协议修改后继续使用服务,即视为您已接受修改后的协议。 },
第一章 定义 {
1. 本平台:指由南京万物可爱文化传媒有限公司拥有并运营的,向用户提供SaaS软件工具、社区互动、内容上传、展示、分享、付费下载及交易等功能的网站、客户端应用程序及其他技术服务平台。 title: "信息使用",
2. SaaS服务:指我们基于软件即服务模式,向您提供的在线软件工具及相关技术服务,您可能需要支付软件订阅费(会员费)后方可使用全部或部分功能。 body: "信息用于身份验证、生成任务处理、资产管理、积分计费、客服支持、风控合规、服务优化和法律法规要求的备案审计。",
3. 社区服务:指我们在平台上为用户提供的,用于发布、展示、交流、分享及交易作品、素材、版权等内容的空间与功能。 },
4. 平台规则:指我们已经发布或后续可能发布、修改的与本平台相关的所有协议、政策、活动规则、公告、说明、站内信通知等,以及《隐私政策》等,均构成本协议不可分割的组成部分。 {
5. 用户:指使用本平台服务的任何自然人或组织,包括企业用户与个人用户,合称“您”。 title: "第三方处理",
6. 内容:指用户通过本平台上传、发布、生成、展示、交易的全部信息,包括但不限于模型、软件、素材、图片、视频、音频、文字、代码、设计图、评论等。 body: "为完成 AI 生成、对象存储、短信邮件、支付或错误监控,我们可能向必要的第三方服务提供最小范围数据,并要求其按约定保护数据安全。",
第二章 账号的注册、使用与管理 },
1. 账号注册 {
(1)您在使用本平台服务前,需通过实名手机号或我们认可的第三方账号进行注册。企业用户还应提供真实、有效的企业营业执照等信息。您在注册或使用Omniai平台服务时可能需要提供一些必要的信息,为保证您享用的平台服务安全有效且不断优化,您同意授权我们对您的必要个人信息进行验证和合理使用。您须保证所填写及提供的资料真实、准确、完整、合法有效。 title: "用户权利",
(2)您注册成功的账号仅限您本人/本企业自身正当使用,禁止以任何形式赠与、借用、出租、转让、售卖或授权他人使用该账号。 body: "你可以通过平台账号功能或联系方式申请访问、更正、删除个人信息,或撤回非必要授权。法律法规另有要求的记录可能需按规定保留。",
(3)特别提示:禁止恶意注册。您不得恶意批量注册账号,不得利用多个账号或其他技术手段实施干扰平台运营、规避平台规则、获取不当利益的行为。一经发现,我们有权立即冻结或收回相关账号,并追究您的违约责任。 },
(4)如您提供任何违法、不实或我们认为不适合的资料,或我们有理由怀疑您的行为属于恶意操作,我们有权暂停或终止您的账号。 ];
(5)我们及Omniai平台无须对任何用户的任何登记资料承担任何责任,包括但不限于鉴别、核实任何登记资料的真实性、正确性、完整性、适用性及是否为最新资料的责任。
2. 账号安全
(1)您应对账号及密码/验证码的安全性负完全责任,并对该账号下进行的所有活动承担责任。您应高度重视对账号与验证码的保密,在任何情况下不向他人透露账号及验证码。您的账号遭到未获授权的使用,或者发生其它任何安全问题时,您应立即通知我们。
(2)我们发现或合理认为使用者并非账号初始注册人,为保障账号安全,有权立即暂停或终止提供服务,并永久禁用该账号。
3. 账号注销与回收
(1)您可以按照平台公示的方式申请注销账号。账号注销后,我们将对账号内信息进行删除或匿名化处理,法律法规另有规定的除外。
(2)如您的账号连续六个月以上未登录,我们有权冻结、收回或者注销该账号。
第三章 平台服务与使用规范
1. 服务内容
(1)SaaS软件服务:我们向您提供在线SaaS软件工具,您可按需选择免费版或付费版(会员费)。使用部分高级功能或消耗大模型算力时,可能需要消耗积分,积分可通过充值等方式获得。
(2)社区服务:平台提供作品、素材、版权的上传、展示、付费下载、互动交流等服务。用户间可通过平台进行相关版权或素材的交易。
(3)交易手续费:为维持平台运营,对于用户间通过平台达成的交易,我们可能会向卖方或买方收取一定比例的交易手续费,具体费率以平台页面公示为准。
2. 用户行为规范:严禁侵权与违法内容
(1)您承诺,您在使用平台SaaS软件创作、或上传、发布、交易的所有内容,均由您原创或已获得合法、完整的授权,不存在侵犯任何第三方知识产权、肖像权、名誉权、隐私权等合法权益的情形。
(2)高风险警示:您不得使用本平台创作、上传、发布、传播任何违反中华人民共和国法律法规、社会主义制度、国家利益、社会公序良俗或包含淫秽、色情、暴力、赌博、恐怖、侮辱、诽谤、虚假信息、扰乱社会秩序等任何不良内容的信息或作品。
(3)您不得利用本平台进行任何危害网络安全的行为,包括但不限于使用插件、外挂、爬虫、病毒、反向工程、干扰系统正常运行等。
(4)您不得利用本平台实施洗钱、套现、诈骗、赌博等违法活动。
3. 禁止恶意“薅羊毛”与不正当竞争
您不得通过批量注册账号、使用外挂程序、虚构交易、虚假评价、套取平台补贴或奖励等方式,获取不当商业利益或损害平台及其他用户的权益。一经发现,平台有权扣划不当得利、限制或封禁账号、追究违约责任。
AI内容生成特别提示:您理解并同意,本平台提供的SaaS软件中可能包含基于人工智能技术的内容生成功能。鉴于AI技术的局限性,生成内容可能存在不确定性或不准确性。您应自行对生成内容进行审核和判断,并对其使用承担全部责任。我们不对生成内容的合法性、准确性、完整性、不侵权性作任何保证。
第四章 知识产权
1. 平台的知识产权
本平台软件、代码、界面设计、商标、标识等知识产权归我们所有,未经书面许可,您不得进行复制、修改、反向编译或用于任何商业目的。
2. 您的内容的知识产权
(1)权利归属:您通过本平台上传、发布的内容及使用SaaS软件独立创作生成的内容,其知识产权归您或原权利人所有。您对您的内容承担全部责任。
(2)授权许可:为了使您的内容能够在平台上进行展示、传播、交易,并为了我们能够持续改进SaaS软件和社区服务,您授予我们一项全球范围内、免费的、非排他性的、可分许可的权利,允许我们在平台及相关业务中使用、复制、修改、改编、分发您公开发布的内容。如果您不希望我们使用您的内容用于模型训练或改进,您可以通过平台设置或联系客服关闭此选项。对于您设置为非公开或仅用于交易的内容,我们将采取更严格的保护措施。
(3)交易授权:您通过平台社区进行作品、素材、版权的付费下载或交易,即授予购买方一项按照交易约定范围使用该内容的许可。您作为卖方,应清晰标识授权范围,并对授权内容的真实性和合法性承担全部责任。
3. 侵权内容处理
我们尊重他人知识产权,并已建立侵权投诉处理机制。任何第三方认为平台上的内容侵犯其合法权益的,可按照平台公示的投诉渠道提交书面通知及初步侵权证据。我们将在收到合格通知后,依法采取删除、屏蔽、断开链接等必要措施,并通知相关用户。对于重复侵权、恶意侵权或情节严重的用户,我们有权直接终止向其提供服务,并永久封禁其账号。
第五章 社区交易与责任限制
1. 交易主体与责任承担
(1)您充分理解并同意,本平台仅为用户提供作品、素材、版权的信息发布、展示、下载、支付结算等交易技术支持服务。平台不是交易合同的任何一方当事人,也并非作品的卖方或买方。
(2)核心免责声明:平台对于用户发布、展示、交易的内容的合法性、真实性、准确性、完整性、安全性、质量、是否侵权以及交易的履行等,不承担任何事先审查或担保责任。您与其他用户之间因内容交易产生的任何纠纷,均由交易双方自行协商解决,或通过司法、行政途径解决。
(3)如我们收到司法机关或行政机关的有效法律文书,或认为确有必要时,我们可以采取临时性措施以维护各方权益。
2. 社区内容的管理权利
我们有权依据法律法规、本协议及平台规则,对平台社区内的内容进行主动巡查。如发现或收到举报证明内容涉嫌侵权、违法或违反本协议,我们有权在不事先通知的情况下,直接删除、屏蔽相关内容,并对相关用户采取警示、限制功能、暂停服务或封禁账号等措施。
第六章 费用与支付
1. 软件订阅费(会员费):您购买会员服务时,应按照平台页面公示的价格和期限支付费用。会员服务为虚拟产品,一经开通,除法律另有规定或平台规则另有说明外,原则上不予退款。
2. 积分充值:积分用于消耗平台算力或兑换特定服务。充值后积分有效期以平台公示为准。平台有权根据运营情况调整积分获取或消耗规则,但会提前公告。
3. 交易手续费:平台有权就用户间通过平台达成的交易收取手续费,费率在交易前明确告知。平台有权根据市场情况调整手续费率,通过公告方式通知后生效。
4. 税费:您使用本平台所获得的收入(如售卖素材所得),根据中国法律规定,您应自行申报并缴纳相关税费。平台按照法律规定履行代扣代缴义务(如适用)。
第七章 违约与处理
1. 违约认定:发生违反本协议或平台规则、侵犯第三方权益、恶意批量注册、虚假交易、套取利益或其他违反法律法规并造成损害的行为,视为您违约。
2. 处理措施:我们有权独立判断并采取警告、拒绝发布、删除内容、限制使用功能、暂停服务、扣划不当得利、冻结或永久封禁账号、追究赔偿等一项或多项措施。
3. 赔偿责任:如因您的行为导致我们或第三方遭受任何损失,您应足额赔偿。
第八章 免责声明与责任限制
1. 服务现状:我们的服务按“现状”和“可得到”的状态提供,我们不作出任何明示或暗示的保证,包括但不限于服务无间断、无错误、安全可靠、适用于特定目的等。
2. AI生成内容免责:鉴于人工智能技术的局限性,我们无法保证通过SaaS软件生成内容的真实性、准确性、独创性及不侵权性。您应当对生成内容自行加以判断和验证,并承担使用该等内容所产生的全部风险与责任。
3. 不可抗力及第三方原因:因自然灾害、战争、政府行为、电力或通讯故障、第三方攻击等不可抗力或非我们故意或重大过失的原因导致的损失,我们不承担责任。
4. 服务中断与变更:我们可能会对平台进行升级、维护或修改,由此导致的服务中断或功能变更,我们将提前通过合理方式通知您。但我们不对因上述必要维护导致的任何损失承担责任,法律另有强制性规定的除外。
5. 第三方链接与内容:平台可能包含指向第三方网站或资源的链接。我们不对这些第三方网站或资源的可用性、内容、产品或服务承担任何责任。您访问第三方网站或使用第三方服务所产生的一切风险由您自行承担。
6. 责任上限:在法律允许的最大范围内,我们对于您因使用平台服务而遭受的任何间接、附带、特殊、惩罚性损失,即使已被告知可能发生该等损失,也不承担任何责任。我们的全部赔偿责任总额,不超过您在过去十二个月内向平台支付的费用总额。
第九章 个人信息保护
1. 我们非常重视您的个人信息保护。我们将按照平台公布的《隐私政策》收集、使用、存储、共享和保护您的个人信息。请您务必仔细阅读《隐私政策》。
2. 您同意我们根据《隐私政策》以及相关法律法规的要求处理您的个人信息。
3. 您知悉并同意,为方便您使用Omniai相关服务,我们有权在遵守法律法规的前提下,以明示的方式获取、使用、储存和分享您的个人信息。我们不会在未经您授权时,公开、编辑或透露您的个人信息及您保存在Omniai的非公开内容。
4. 您知悉并同意,我们有权通过cookie等技术收集您的产品或服务使用、行为数据,并在经过数据脱敏使之不再指向或关联到您个人身份信息时,自由使用脱敏后的纯商业数据。
第十章 协议的修改与终止
1. 协议的修改:我们有权根据法律法规变化或业务需要修改本协议。修改后的协议将在平台公示或以其他方式通知您。如您不接受修改后的协议,您应停止使用平台服务;如您继续使用,则视为同意接受修改后的协议。
2. 协议的终止:您可以申请注销账号终止本协议。我们有权在您违约或出现其他法定、约定情形时终止本协议。协议终止后,您仍需对协议终止前的行为承担责任。
第十一章 法律适用与争议解决
1. 本协议的订立、生效、履行、解释及争议解决,均适用中华人民共和国大陆地区法律。
2. 凡因本协议引起的或与本协议有关的任何争议,双方应首先友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地南京市浦口区有管辖权的人民法院通过诉讼解决。
第十二章 其他
1. 本协议条款标题仅为方便阅读而设,不影响条款含义的解释。
2. 本协议任何条款被认定为无效或不可执行,不影响其他条款的效力。
3. 如您对本协议有任何疑问、意见或建议,或需要投诉举报,您可以通过system@omniai.net.cn与我们联系。
4. 兜底免责声明:在法律法规允许的最大范围内,本协议未明确列明的、或因超出我们合理预见或控制范围所产生的任何直接或间接损失、责任或风险,我们均不承担责任,除非相关法律另有强制性规定。您理解并同意,使用本平台服务的风险由您自行承担,我们仅以普通注意义务对平台进行管理,不对任何未明确约定的商业成果或安全性作出保证。
(以下无正文)
`.trim();
function getDocumentLines(text: string) {
return text.split(/\n+/).map((line) => line.trim()).filter(Boolean);
}
function getLineClassName(line: string, index: number) {
if (index === 0) return "compliance-document__title";
if (/^(第[一二三四五六七八九十]+章|[一二三四五六七八九十]+、)/.test(line)) return "compliance-document__heading";
if (/^[一二三四五六七八九十]+/.test(line)) return "compliance-document__subheading";
if (/^[0-9]+\./.test(line) || /^[0-9]+/.test(line) || /^·/.test(line)) return "compliance-document__clause";
return "compliance-document__paragraph";
}
export default function CompliancePage({ kind }: CompliancePageProps) { export default function CompliancePage({ kind }: CompliancePageProps) {
const isPrivacy = kind === "privacy"; const isPrivacy = kind === "privacy";
const sections = isPrivacy ? privacySections : agreementSections;
const title = isPrivacy ? "隐私政策" : "用户协议"; const title = isPrivacy ? "隐私政策" : "用户协议";
const Icon = isPrivacy ? SafetyOutlined : FileTextOutlined; const Icon = isPrivacy ? SafetyOutlined : FileTextOutlined;
const lines = getDocumentLines(isPrivacy ? privacyPolicyText : agreementText);
return ( return (
<section className="compliance-page"> <section className="compliance-page">
@@ -223,26 +71,27 @@ export default function CompliancePage({ kind }: CompliancePageProps) {
<div> <div>
<span className="compliance-hero__eyebrow"></span> <span className="compliance-hero__eyebrow"></span>
<h1>{title}</h1> <h1>{title}</h1>
<p>{companyName}{platformName}{effectiveDate}</p> <p>{companyName} 2026 6 3 </p>
</div> </div>
</header> </header>
<article className="compliance-card compliance-document"> <div className="compliance-card">
{lines.map((line, index) => { {sections.map((section, index) => (
const className = getLineClassName(line, index); <article key={section.title} className="compliance-section">
if (className === "compliance-document__title") return <h2 key={`${index}-${line}`} className={className}>{line}</h2>; <span>{String(index + 1).padStart(2, "0")}</span>
if (className === "compliance-document__heading") return <h3 key={`${index}-${line}`} className={className}>{line}</h3>; <div>
if (className === "compliance-document__subheading") return <h4 key={`${index}-${line}`} className={className}>{line}</h4>; <h2>{section.title}</h2>
return <p key={`${index}-${line}`} className={className}>{line}</p>; <p>{section.body}</p>
})} </div>
</article> </article>
))}
</div>
<footer className="compliance-contact"> <footer className="compliance-contact">
<strong></strong> <strong></strong>
<span>{contactEmail}</span>
<span>{address}</span> <span>{address}</span>
<span>{contactPhone}</span> <span>{contactPhone}</span>
<span>{icpRecord}</span> <span>ICP备2026021747号-1</span>
</footer> </footer>
</div> </div>
</section> </section>
+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>
+9 -87
View File
@@ -37,7 +37,7 @@ import {
import "../../styles/pages/workbench.css"; import "../../styles/pages/workbench.css";
import type { WebGenerationPreviewTask, WebUserSession } from "../../types"; import type { WebGenerationPreviewTask, WebUserSession } from "../../types";
import { aiGenerationClient } from "../../api/aiGenerationClient"; import { aiGenerationClient } from "../../api/aiGenerationClient";
import { claimGenerationSlot, getActiveGenerationTaskCount, getEffectiveGenerationLimit, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency"; import { claimGenerationSlot, getActiveGenerationTaskCount, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
import { preUploadReference, resolvePreUploadedUrl } from "../../api/referenceUploadService"; import { preUploadReference, resolvePreUploadedUrl } from "../../api/referenceUploadService";
import { assetClient } from "../../api/assetClient"; import { assetClient } from "../../api/assetClient";
import { communityClient } from "../../api/communityClient"; import { communityClient } from "../../api/communityClient";
@@ -988,54 +988,6 @@ function WorkbenchPage({
persistKeepaliveTasks(rest); persistKeepaliveTasks(rest);
}; };
const releaseKeepaliveTaskLocally = useCallback((taskId: string, options?: { cancelServer?: boolean }) => {
const task = keepaliveTasksRef.current[taskId];
taskAbortControllersRef.current.get(taskId)?.abort();
taskAbortControllersRef.current.delete(taskId);
removeKeepaliveTask(taskId);
if (task && options?.cancelServer) {
aiGenerationClient.cancelTask(task.taskId).catch(() => {});
}
syncActiveGenerationUi();
}, [syncActiveGenerationUi]);
const releaseKeepaliveTaskAfterNetworkLoss = useCallback((task: WorkbenchKeepaliveTask, progress: number) => {
const latestTask = {
...task,
progress,
statusLabel: "网络中断,已释放提交按钮",
};
void patchConversationMessage(task.conversationId, task.assistantMessageId, {
status: "failed",
taskProgress: Math.max(progress, 100),
taskStatusLabel: "网络中断",
body: "网络中断,当前任务已停止等待并释放提交按钮。请确认网络恢复后重新提交任务。",
});
upsertKeepaliveTask(latestTask);
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
if (activeConversationIdRef.current === task.conversationId) {
setIsGenerating(false);
setGenerationStatus("网络中断,已释放提交按钮");
setGenerationProgress(0);
}
}, [patchConversationMessage, releaseKeepaliveTaskLocally]);
const cancelActiveKeepaliveTasksOnPageExit = useCallback(() => {
const tasks = Object.values(keepaliveTasksRef.current);
if (!tasks.length) return;
tasks.forEach((task) => {
taskAbortControllersRef.current.get(task.taskId)?.abort();
taskAbortControllersRef.current.delete(task.taskId);
releaseGenerationSlot(task.concurrencySlotId);
aiGenerationClient.cancelTaskOnUnload(task.taskId);
});
keepaliveTasksRef.current = {};
persistKeepaliveTasks({});
setIsGenerating(false);
setGenerationStatus("已释放未完成任务");
setGenerationProgress(0);
}, []);
const runKeepalivePoll = useCallback( const runKeepalivePoll = useCallback(
(task: WorkbenchKeepaliveTask) => { (task: WorkbenchKeepaliveTask) => {
if (taskAbortControllersRef.current.has(task.taskId)) return; if (taskAbortControllersRef.current.has(task.taskId)) return;
@@ -1062,10 +1014,6 @@ function WorkbenchPage({
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
if (attempt > 0) await sleep(3000); if (attempt > 0) await sleep(3000);
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
if (typeof navigator !== "undefined" && navigator.onLine === false) {
releaseKeepaliveTaskAfterNetworkLoss(task, lastKnownProgress);
return;
}
let status; let status;
try { try {
@@ -1079,8 +1027,7 @@ function WorkbenchPage({
taskProgress: 100, taskProgress: 100,
taskStatusLabel: "任务异常", taskStatusLabel: "任务异常",
}); });
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true }); removeKeepaliveTask(task.taskId);
onRefreshUsage?.();
return; return;
} }
continue; continue;
@@ -1308,24 +1255,6 @@ function WorkbenchPage({
}; };
}, [runKeepalivePoll]); }, [runKeepalivePoll]);
useEffect(() => {
const handlePageHide = () => {
cancelActiveKeepaliveTasksOnPageExit();
};
const handleOnline = () => {
Object.values(keepaliveTasksRef.current).forEach((task) => runKeepalivePoll(task));
syncActiveGenerationUi();
};
window.addEventListener("pagehide", handlePageHide);
window.addEventListener("online", handleOnline);
aiGenerationClient.flushPendingTaskCancellations();
return () => {
window.removeEventListener("pagehide", handlePageHide);
window.removeEventListener("online", handleOnline);
};
}, [cancelActiveKeepaliveTasksOnPageExit, runKeepalivePoll, syncActiveGenerationUi]);
useEffect(() => { useEffect(() => {
persistPromptHistory(promptHistory); persistPromptHistory(promptHistory);
}, [promptHistory]); }, [promptHistory]);
@@ -1956,7 +1885,7 @@ function WorkbenchPage({
const trimmedPrompt = (promptOverride ?? inputValue).trim(); const trimmedPrompt = (promptOverride ?? inputValue).trim();
if (!trimmedPrompt) return; if (!trimmedPrompt) return;
const userKey = getGenerationUserKey(session?.user.id); const userKey = getGenerationUserKey(session?.user.id);
if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= getEffectiveGenerationLimit()) return; if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= 3) return;
setReferencePreviewOpen(false); setReferencePreviewOpen(false);
let conversationId = activeConversationIdRef.current ?? activeConversationId; let conversationId = activeConversationIdRef.current ?? activeConversationId;
@@ -2435,11 +2364,8 @@ function WorkbenchPage({
setProjectError("仅支持对视频结果进行超分"); setProjectError("仅支持对视频结果进行超分");
return; return;
} }
const userKey = getGenerationUserKey(session?.user.id); if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
const activeCount = getActiveGenerationTaskCount(userKey); setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
const limit = getEffectiveGenerationLimit();
if (activeCount >= limit) {
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
return; return;
} }
if (!isAuthenticated) { if (!isAuthenticated) {
@@ -2560,11 +2486,8 @@ function WorkbenchPage({
setProjectError("仅支持对图片结果进行超分"); setProjectError("仅支持对图片结果进行超分");
return; return;
} }
const userKey = getGenerationUserKey(session?.user.id); if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
const activeCount = getActiveGenerationTaskCount(userKey); setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
const limit = getEffectiveGenerationLimit();
if (activeCount >= limit) {
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
return; return;
} }
if (!isAuthenticated) { if (!isAuthenticated) {
@@ -2737,14 +2660,13 @@ function WorkbenchPage({
}; };
const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)); const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const activeGenerationLimit = getEffectiveGenerationLimit(); const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= activeGenerationLimit;
const promptIsEmpty = !inputValue.trim(); const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached; const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty const sendButtonTitle = promptIsEmpty
? "输入内容后可发送" ? "输入内容后可发送"
: generationLimitReached : generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中(上限 ${activeGenerationLimit} 个),请等待任一任务完成` ? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
: billingEstimate.title; : billingEstimate.title;
const suggestedPrompts = [ const suggestedPrompts = [
-67
View File
@@ -788,65 +788,6 @@
overflow: hidden; overflow: hidden;
} }
.compliance-document {
gap: 0;
padding: 30px 34px 34px;
overflow: visible;
}
.compliance-document__title,
.compliance-document__heading,
.compliance-document__subheading,
.compliance-document__paragraph,
.compliance-document__clause {
margin: 0;
max-width: 100%;
color: var(--fg-body);
letter-spacing: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.compliance-document__title {
margin-bottom: 18px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-subtle);
font-size: clamp(24px, 3vw, 32px);
line-height: 1.22;
}
.compliance-document__heading {
margin: 26px 0 12px;
padding-top: 4px;
color: var(--accent);
font-size: 20px;
line-height: 1.35;
}
.compliance-document__subheading {
margin: 18px 0 8px;
font-size: 16px;
line-height: 1.5;
}
.compliance-document__paragraph,
.compliance-document__clause {
color: var(--fg-muted);
font-size: 14px;
line-height: 1.85;
}
.compliance-document__paragraph + .compliance-document__paragraph,
.compliance-document__clause + .compliance-document__clause,
.compliance-document__paragraph + .compliance-document__clause,
.compliance-document__clause + .compliance-document__paragraph {
margin-top: 8px;
}
.compliance-document__clause {
padding-left: 12px;
}
.compliance-section { .compliance-section {
display: grid; display: grid;
grid-template-columns: 52px minmax(0, 1fr); grid-template-columns: 52px minmax(0, 1fr);
@@ -951,12 +892,4 @@
.compliance-section { .compliance-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.compliance-document {
padding: 22px 18px 26px;
}
.compliance-document__clause {
padding-left: 0;
}
} }
+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;