fix(canvas): 创作工作区 - 收紧界面密度并改进 Agent 镂空标签

This commit is contained in:
ddcat666
2026-09-24 10:07:05 +08:00
parent 3ef40e1d21
commit 32a2e44040
9 changed files with 158 additions and 73 deletions

View File

@@ -5,6 +5,11 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试 # 待测试
## 工作区 100% 缩放下的界面密度
- 已收紧用户工作区侧栏、顶栏和导航行、画布桌面端 Dock、Agent 内边距及默认浮窗尺寸;Agent 旧的手动调整尺寸仍保留,可点标题栏「恢复 Agent 紧凑窗口」自行重置。技能组合标签维持透明底和彩色描边。
- [ ] 在已登录的真实画布和用户工作区以浏览器 100% 缩放验收明暗主题、侧栏展开/折叠、顶部/底部工具栏、Agent 历史与设置、输入区和标签横向滚动;再检查窄屏与触屏下是否截断或遮挡。当前本地独立浏览器会跳转登录页,未将静态检查视为视觉验收。
## 画布资源断链、前端发布兼容与画布热点 ## 画布资源断链、前端发布兼容与画布热点
- 已实现:统一递归识别持久资源字段(含嵌套 `videoPreview`);保存前和历史快照保存时校验当前资源归属/ready 状态,资源缺失返回带路径和 ID 的结构化 409。显式修复保留坏版本历史但要求新版本完整且 revision CAS 成功。后台 `repair-canvas-resources` 默认只读,仅可清除确认缺失的派生视频预览;主媒体问题报告为 unresolved。 - 已实现:统一递归识别持久资源字段(含嵌套 `videoPreview`);保存前和历史快照保存时校验当前资源归属/ready 状态,资源缺失返回带路径和 ID 的结构化 409。显式修复保留坏版本历史但要求新版本完整且 revision CAS 成功。后台 `repair-canvas-resources` 默认只读,仅可清除确认缺失的派生视频预览;主媒体问题报告为 unresolved。

View File

@@ -4,7 +4,7 @@ import { Tooltip } from "@/components/ui/base/tooltip";
import { useCallback, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react"; import { motion, useReducedMotion } from "motion/react";
import { ArrowUp, AtSign, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Eye, HelpCircle, ImagePlus, ListChecks, LoaderCircle, Pencil, Plus, RotateCcw, Sparkles, Square, X, XCircle } from "lucide-react"; import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, X, XCircle } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme"; import { canvasThemes } from "@/lib/canvas-theme";
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
@@ -595,13 +595,13 @@ export type AgentSceneBucket = {
}; };
/** 场景分类:与 presets.json 的 scene 字段、技能的 tag 字段共用同一套 key。 */ /** 场景分类:与 presets.json 的 scene 字段、技能的 tag 字段共用同一套 key。 */
export const AGENT_SCENE_DEFS: Array<{ key: string; label: string }> = [ export const AGENT_SCENE_DEFS: Array<{ key: string; label: string; icon: typeof Sparkles }> = [
{ key: "frequent", label: "我的常用" }, { key: "frequent", label: "我的常用", icon: Bookmark },
{ key: "drama", label: "短剧故事" }, { key: "drama", label: "短剧故事", icon: Clapperboard },
{ key: "ecommerce", label: "广告电商" }, { key: "ecommerce", label: "广告电商", icon: ShoppingBag },
{ key: "creative", label: "视觉创意" }, { key: "creative", label: "视觉创意", icon: Palette },
{ key: "social", label: "传播社媒" }, { key: "social", label: "传播社媒", icon: Share2 },
{ key: "others", label: "其他" }, { key: "others", label: "其他", icon: Shapes },
]; ];
export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = false, onPick, onPickSkill }: { export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = false, onPick, onPickSkill }: {
@@ -614,7 +614,7 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
}) { }) {
// 始终只占一排:默认显示场景分类,点某个场景后在同一排内就地切换内容。 // 始终只占一排:默认显示场景分类,点某个场景后在同一排内就地切换内容。
const [activeKey, setActiveKey] = useState<string | null>(null); const [activeKey, setActiveKey] = useState<string | null>(null);
const capsuleClass = "agent-scene-capsule shrink-0 rounded-lg px-3 py-1.5 text-left text-xs focus-visible:outline focus-visible:outline-2"; const capsuleClass = "agent-scene-capsule shrink-0";
const stop = { const stop = {
onMouseDown: (event: { stopPropagation(): void }) => event.stopPropagation(), onMouseDown: (event: { stopPropagation(): void }) => event.stopPropagation(),
onPointerDown: (event: { stopPropagation(): void }) => event.stopPropagation(), onPointerDown: (event: { stopPropagation(): void }) => event.stopPropagation(),
@@ -623,12 +623,12 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
const active = activeKey ? visible.find((bucket) => bucket.key === activeKey) || null : null; const active = activeKey ? visible.find((bucket) => bucket.key === activeKey) || null : null;
if (!visible.length) return null; if (!visible.length) return null;
return ( return (
<div className="agent-scene-capsules mx-3 mb-2 overflow-hidden rounded-xl" style={{ color: theme.node.text }}> <div className="agent-scene-capsules mx-3 mb-2 min-w-0" style={{ color: theme.node.text }}>
<div className="flex items-start gap-2 px-3 pt-2.5"> <div className="agent-scene-capsules-heading">
<Sparkles className="mt-[1px] size-3.5 shrink-0" style={{ color: theme.accent.primary }} /> <Sparkles aria-hidden="true" />
<span className="min-w-0 flex-1 text-xs font-semibold leading-5">{active ? active.label : "技能组合推荐"}</span> <span>{active ? active.label : "技能组合推荐"}</span>
</div> </div>
<div className="agent-scene-capsules-scroll thin-scrollbar flex gap-2 overflow-x-auto px-3 pb-1 pt-2"> <div className="agent-scene-capsules-scroll thin-scrollbar flex gap-2 overflow-x-auto px-1 py-2">
{active ? ( {active ? (
<> <>
<button <button
@@ -636,14 +636,15 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
disabled={disabled} disabled={disabled}
title="返回全部场景" title="返回全部场景"
className={capsuleClass} className={capsuleClass}
data-scene="back"
{...stop} {...stop}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setActiveKey(null); setActiveKey(null);
}} }}
> >
<span className="block whitespace-nowrap font-medium">← 全部场景</span> <ArrowLeft aria-hidden="true" />
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">返回</span> <span>全部场景</span>
</button> </button>
{active.presets.map((preset) => { {active.presets.map((preset) => {
const missing = preset.skillIds.filter((id) => !installedIds.has(id)).length; const missing = preset.skillIds.filter((id) => !installedIds.has(id)).length;
@@ -654,16 +655,16 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
disabled={disabled} disabled={disabled}
title={preset.rationale} title={preset.rationale}
className={capsuleClass} className={capsuleClass}
data-scene={active.key}
{...stop} {...stop}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onPick(preset); onPick(preset);
}} }}
> >
<span className="block whitespace-nowrap font-medium">{preset.name}</span> <Layers3 aria-hidden="true" />
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60"> <span className="agent-scene-capsule-label">{preset.name}</span>
{missing > 0 ? `${preset.skillIds.length} 个技能 · ${missing} 个待装` : `${preset.skillIds.length} 个技能`} <span className="agent-scene-capsule-meta">{missing > 0 ? `${preset.skillIds.length} 技能 · ${missing} 待装` : `${preset.skillIds.length} 技能`}</span>
</span>
</button> </button>
); );
})} })}
@@ -676,14 +677,16 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
disabled={disabled} disabled={disabled}
title={skill.description} title={skill.description}
className={capsuleClass} className={capsuleClass}
data-scene={active.key}
{...stop} {...stop}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onPickSkill(skill); onPickSkill(skill);
}} }}
> >
<span className="block max-w-[10rem] truncate font-medium">{skill.skillName}</span> <Sparkles aria-hidden="true" />
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">{owned}</span> <span className="agent-scene-capsule-label">{skill.skillName}</span>
<span className="agent-scene-capsule-meta">{owned}</span>
</button> </button>
); );
})} })}
@@ -691,6 +694,7 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
) : ( ) : (
visible.map((bucket) => { visible.map((bucket) => {
const count = bucket.presets.length + bucket.skills.length; const count = bucket.presets.length + bucket.skills.length;
const Icon = AGENT_SCENE_DEFS.find((definition) => definition.key === bucket.key)?.icon || Shapes;
return ( return (
<button <button
key={bucket.key} key={bucket.key}
@@ -698,20 +702,22 @@ export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = fa
disabled={disabled} disabled={disabled}
title={`查看「${bucket.label}」下的技能组合`} title={`查看「${bucket.label}」下的技能组合`}
className={capsuleClass} className={capsuleClass}
data-scene={bucket.key}
{...stop} {...stop}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setActiveKey(bucket.key); setActiveKey(bucket.key);
}} }}
> >
<span className="block whitespace-nowrap font-medium">{bucket.label}</span> <Icon aria-hidden="true" />
<span className="mt-0.5 block whitespace-nowrap text-[10px] leading-4 opacity-60">{count} 项</span> <span className="agent-scene-capsule-label">{bucket.label}</span>
<span className="agent-scene-capsule-meta">{count} 项</span>
</button> </button>
); );
}) })
)} )}
</div> </div>
<div className="px-3 pb-2 text-[10px] opacity-50">选择只对当前会话生效;未装的技能会添加到我的技能库。具体用哪张卡由 Agent 按任务检索。</div> <p className="agent-scene-capsules-note">仅本会话生效 · 缺失技能将加入技能库 · Agent 按任务调用</p>
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type Dispatch, type SetStateAction } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type Dispatch, type SetStateAction } from "react";
import { Button, Dropdown, Input } from "antd"; import { Button, Dropdown, Input } from "antd";
import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { ArrowLeft, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, MoveDiagonal2, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react"; import { ArrowLeft, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, MoveDiagonal2, RotateCcw, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export"; import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export";
import { markdownPlainText } from "@/lib/markdown-plain-text"; import { markdownPlainText } from "@/lib/markdown-plain-text";
@@ -840,6 +840,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
})().catch((cause) => setMessages((current) => appendAgentError(current, `export-${Date.now()}`, cause, "导出失败"))).finally(() => setExporting(false)); })().catch((cause) => setMessages((current) => appendAgentError(current, `export-${Date.now()}`, cause, "导出失败"))).finally(() => setExporting(false));
}} }}
onSettings={() => setView("settings")} onSettings={() => setView("settings")}
onResetLayout={panelLayout.reset}
onCollapse={onCollapse} onCollapse={onCollapse}
/> />
{run && connectionStatus !== "connected" ? ( {run && connectionStatus !== "connected" ? (
@@ -978,7 +979,7 @@ function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, onO
); );
} }
function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount, onNew, onHistory, onSettings, onCollapse, onExport, exporting }: { theme: CanvasTheme; hasMessages: boolean; statusLabel: string; statusColor: string; nodeCount: number; onNew: () => void; onHistory: () => void; onSettings: () => void; onCollapse: () => void; onExport: () => void; exporting: boolean }) { function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount, onNew, onHistory, onSettings, onResetLayout, onCollapse, onExport, exporting }: { theme: CanvasTheme; hasMessages: boolean; statusLabel: string; statusColor: string; nodeCount: number; onNew: () => void; onHistory: () => void; onSettings: () => void; onResetLayout: () => void; onCollapse: () => void; onExport: () => void; exporting: boolean }) {
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE; const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
return ( return (
<header data-agent-drag-handle className="agent-panel-header flex shrink-0 items-center gap-3"> <header data-agent-drag-handle className="agent-panel-header flex shrink-0 items-center gap-3">
@@ -993,6 +994,7 @@ function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount,
<div className="agent-panel-context">{appearance.agentName} · 当前画布 {nodeCount} 个节点</div> <div className="agent-panel-context">{appearance.agentName} · 当前画布 {nodeCount} 个节点</div>
</div> </div>
<div className="agent-header-actions flex items-center gap-0.5" style={{ color: theme.node.muted }}> <div className="agent-header-actions flex items-center gap-0.5" style={{ color: theme.node.muted }}>
<Button type="text" shape="circle" icon={<RotateCcw className="size-4" />} onClick={onResetLayout} aria-label="恢复 Agent 紧凑窗口" title="恢复默认窗口大小和位置" className="hidden sm:inline-flex" />
<Button type="text" shape="circle" icon={<Download className="size-4" />} loading={exporting} onClick={onExport} aria-label="导出 Agent 调试记录" title="导出对话、工具参数、审批和错误(分享前请检查隐私)" /> <Button type="text" shape="circle" icon={<Download className="size-4" />} loading={exporting} onClick={onExport} aria-label="导出 Agent 调试记录" title="导出对话、工具参数、审批和错误(分享前请检查隐私)" />
<Button type="text" shape="circle" icon={<MessageSquarePlus className="size-4" />} onClick={onNew} aria-label="新建对话" title="新建对话" /> <Button type="text" shape="circle" icon={<MessageSquarePlus className="size-4" />} onClick={onNew} aria-label="新建对话" title="新建对话" />
<Button type="text" shape="circle" icon={<History className="size-4" />} onClick={onHistory} aria-label="历史对话" title="历史对话" /> <Button type="text" shape="circle" icon={<History className="size-4" />} onClick={onHistory} aria-label="历史对话" title="历史对话" />

View File

@@ -30,8 +30,8 @@
} }
.agent-panel-header { .agent-panel-header {
min-height: 80px; min-height: 68px;
padding: var(--space-4) var(--space-5) var(--space-3) var(--space-6); padding: var(--space-3) var(--space-4) var(--space-2) var(--space-5);
} }
.agent-panel-title { .agent-panel-title {
@@ -55,8 +55,8 @@
} }
.agent-conversation { .agent-conversation {
padding: var(--space-4) var(--space-6) var(--space-6); padding: var(--space-3) var(--space-5) var(--space-5);
scroll-padding-block: var(--space-6); scroll-padding-block: var(--space-5);
} }
.agent-conversation-messages > * + * { .agent-conversation-messages > * + * {
@@ -193,11 +193,11 @@
} }
.agent-composer-wrap { .agent-composer-wrap {
padding: var(--space-2) var(--space-4) var(--space-4); padding: var(--space-2) var(--space-3) var(--space-3);
} }
.agent-composer-surface { .agent-composer-surface {
padding: var(--space-4) var(--space-4) var(--space-3); padding: var(--space-3) var(--space-3) var(--space-2-half);
border-radius: var(--agent-radius-surface, var(--radius-xl)); border-radius: var(--agent-radius-surface, var(--radius-xl));
background: var(--agent-surface-raised, var(--background)); background: var(--agent-surface-raised, var(--background));
} }
@@ -216,27 +216,84 @@
background: var(--agent-surface-raised, var(--background)); background: var(--agent-surface-raised, var(--background));
} }
/* 场景起步胶囊:选择作用于会话,未安装的技能会加入用户技能库。 */ /* 场景起步标签:透明底、彩色细描边,不叠加卡片表面。 */
.agent-scene-capsules { .agent-scene-capsules-heading {
border: 0; display: flex;
background: var(--agent-surface-soft, color-mix(in srgb, var(--foreground) 4%, transparent)); align-items: center;
gap: var(--space-2);
padding-inline: var(--space-1);
color: var(--agent-ink);
font-size: var(--fs-caption);
font-weight: 600;
}
.agent-scene-capsules-heading svg {
width: var(--space-4);
height: var(--space-4);
color: var(--palette-node-skill);
} }
.agent-scene-capsule { .agent-scene-capsule {
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); --scene-color: var(--palette-node-config);
background: var(--agent-surface-raised, var(--background)); display: inline-flex;
min-height: 36px;
max-width: 100%;
align-items: center;
gap: var(--space-2);
padding: var(--space-1-half) var(--space-3);
border: 1px solid color-mix(in srgb, var(--scene-color) 58%, var(--agent-ink));
border-radius: var(--r-full);
background: transparent;
color: color-mix(in srgb, var(--scene-color) 45%, var(--agent-ink));
font-size: var(--fs-caption);
line-height: 1.2;
white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease; transition: border-color 160ms ease, color 160ms ease, opacity 160ms ease;
}
.agent-scene-capsule[data-scene="frequent"] { --scene-color: var(--palette-node-skill); }
.agent-scene-capsule[data-scene="drama"] { --scene-color: var(--palette-node-script); }
.agent-scene-capsule[data-scene="ecommerce"] { --scene-color: var(--palette-node-image); }
.agent-scene-capsule[data-scene="creative"] { --scene-color: var(--palette-node-frame); }
.agent-scene-capsule[data-scene="social"] { --scene-color: var(--palette-node-audio); }
.agent-scene-capsule[data-scene="back"] { --scene-color: var(--agent-ink); }
.agent-scene-capsule svg {
width: var(--space-4);
height: var(--space-4);
flex: 0 0 auto;
stroke-width: 1.8;
color: color-mix(in srgb, var(--scene-color) 75%, var(--agent-ink));
}
.agent-scene-capsule-label {
min-width: 0;
max-width: 10rem;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.agent-scene-capsule-meta {
flex: 0 0 auto;
color: var(--agent-caption);
font-size: var(--fs-label);
font-variant-numeric: tabular-nums;
}
.agent-scene-capsule:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--scene-color) 82%, var(--agent-ink));
color: color-mix(in srgb, var(--scene-color) 70%, var(--agent-ink));
} }
.agent-scene-capsule:hover:not(:disabled),
.agent-scene-capsule:focus-visible { .agent-scene-capsule:focus-visible {
border-color: color-mix(in srgb, var(--agent-accent, var(--foreground)) 45%, transparent); outline: 2px solid var(--agent-focus-ring);
background: var(--agent-surface-soft, color-mix(in srgb, var(--foreground) 6%, transparent)); outline-offset: 2px;
} }
.agent-scene-capsule:active:not(:disabled) { .agent-scene-capsule:active:not(:disabled) {
transform: translateY(1px); opacity: 0.65;
} }
.agent-scene-capsule:disabled { .agent-scene-capsule:disabled {
@@ -244,6 +301,14 @@
opacity: 0.55; opacity: 0.55;
} }
.agent-scene-capsules-note {
margin: 0;
padding-inline: var(--space-1);
color: var(--agent-caption);
font-size: var(--fs-label);
line-height: 1.5;
}
.agent-scene-capsules-scroll { .agent-scene-capsules-scroll {
scrollbar-width: thin; scrollbar-width: thin;
} }
@@ -261,9 +326,6 @@
.agent-scene-capsule { .agent-scene-capsule {
transition: none; transition: none;
} }
.agent-scene-capsule:active:not(:disabled) {
transform: none;
}
} }
@container (max-width: 400px) { @container (max-width: 400px) {

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type PointerEvent, type KeyboardEvent } from "react"; import { useEffect, useRef, useState, type PointerEvent, type KeyboardEvent } from "react";
import { AGENT_PANEL_LAYOUT_KEY, changeAgentPanelLayout, clampAgentPanelLayout, restoreAgentPanelLayout, type AgentPanelGesture, type AgentPanelLayout } from "@/lib/canvas/agent-panel-layout"; import { AGENT_PANEL_LAYOUT_KEY, changeAgentPanelLayout, clampAgentPanelLayout, defaultAgentPanelLayout, restoreAgentPanelLayout, type AgentPanelGesture, type AgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight }); const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
@@ -69,6 +69,7 @@ export function useAgentPanelLayout() {
return { return {
style: compact ? { left: 0, top: 8, width: "100%", height: "calc(100dvh - 8px)" } : layout, style: compact ? { left: 0, top: 8, width: "100%", height: "calc(100dvh - 8px)" } : layout,
reset: () => setLayout(defaultAgentPanelLayout(viewport())),
onResizeKeyDown, onResizeKeyDown,
pointerHandlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp, onLostPointerCapture: () => { gestureRef.current = null; } }, pointerHandlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp, onLostPointerCapture: () => { gestureRef.current = null; } },
}; };

View File

@@ -58,10 +58,9 @@ type DockMetrics = {
distance: number; distance: number;
}; };
// magnification 收敛:base 略增以适配圆角方块,magnified 幅度从 +14 降到 +8, // 桌面 dock 收紧常态与悬浮尺寸;触屏尺寸单独保留以保证点击目标。
// 避免 dock 整体跳动,同时保留 Aceternity 接近放大身份。
const DOCK_METRICS: Record<NonNullable<FloatingDockProps["size"]>, DockMetrics> = { const DOCK_METRICS: Record<NonNullable<FloatingDockProps["size"]>, DockMetrics> = {
default: { base: 30, magnified: 38, icon: 15, iconMagnified: 18, distance: 100 }, default: { base: 28, magnified: 34, icon: 14, iconMagnified: 16, distance: 100 },
compact: { base: 26, magnified: 32, icon: 13, iconMagnified: 16, distance: 84 }, compact: { base: 26, magnified: 32, icon: 13, iconMagnified: 16, distance: 84 },
}; };
@@ -136,7 +135,7 @@ export const FloatingDock = forwardRef<HTMLDivElement, FloatingDockProps>(functi
: "h-9 gap-0.5 px-0.5 pb-0.5" : "h-9 gap-0.5 px-0.5 pb-0.5"
: size === "compact" : size === "compact"
? "h-8 gap-0.5 rounded-[var(--r-lg)] px-1 pb-1" ? "h-8 gap-0.5 rounded-[var(--r-lg)] px-1 pb-1"
: "h-10 gap-0.5 rounded-[var(--dock-radius)] px-1.5 pb-1", : "h-9 gap-0.5 rounded-[var(--dock-radius)] px-1.5 pb-0.5",
className, className,
)} )}
style={style} style={style}

View File

@@ -20,7 +20,7 @@ export function clampAgentPanelLayout(layout: AgentPanelLayout, viewport: AgentP
} }
export function restoreAgentPanelLayout(raw: string | null, viewport: AgentPanelViewport): AgentPanelLayout { export function restoreAgentPanelLayout(raw: string | null, viewport: AgentPanelViewport): AgentPanelLayout {
const fallback = { width: 448, height: 720, left: viewport.width - 464, top: viewport.height - 732 }; const fallback = defaultAgentPanelLayout(viewport);
if (!raw) return clampAgentPanelLayout(fallback, viewport); if (!raw) return clampAgentPanelLayout(fallback, viewport);
try { try {
const parsed: unknown = JSON.parse(raw); const parsed: unknown = JSON.parse(raw);
@@ -33,6 +33,12 @@ export function restoreAgentPanelLayout(raw: string | null, viewport: AgentPanel
return clampAgentPanelLayout(fallback, viewport); return clampAgentPanelLayout(fallback, viewport);
} }
export function defaultAgentPanelLayout(viewport: AgentPanelViewport): AgentPanelLayout {
const width = 420;
const height = 640;
return clampAgentPanelLayout({ width, height, left: viewport.width - width - MARGIN, top: viewport.height - height - MARGIN }, viewport);
}
export function changeAgentPanelLayout(start: AgentPanelLayout, gesture: AgentPanelGesture, dx: number, dy: number, viewport: AgentPanelViewport): AgentPanelLayout { export function changeAgentPanelLayout(start: AgentPanelLayout, gesture: AgentPanelGesture, dx: number, dy: number, viewport: AgentPanelViewport): AgentPanelLayout {
if (gesture === "move") return clampAgentPanelLayout({ ...start, left: start.left + dx, top: start.top + dy }, viewport); if (gesture === "move") return clampAgentPanelLayout({ ...start, left: start.left + dx, top: start.top + dy }, viewport);
const right = start.left + start.width; const right = start.left + start.width;

View File

@@ -249,7 +249,7 @@
--canvas-inset-x: 16px; /* 浮层水平安全区(dock inset、顶栏 px)*/ --canvas-inset-x: 16px; /* 浮层水平安全区(dock inset、顶栏 px)*/
--canvas-inset-y: 16px; /* 浮层垂直安全区(dock 底部)*/ --canvas-inset-y: 16px; /* 浮层垂直安全区(dock 底部)*/
--canvas-topbar-h: 64px; /* 顶部栏高度 */ --canvas-topbar-h: 64px; /* 顶部栏高度 */
--canvas-topbar-offset: 72px; /* 顶栏下方浮层顶部偏移(topbar-h + 8px 间距)*/ --canvas-topbar-offset: calc(var(--canvas-topbar-h) + 8px); /* 顶栏下方浮层顶部偏移 */
--canvas-dock-popover-offset: 50px; /* dock 弹出面板底部偏移 */ --canvas-dock-popover-offset: 50px; /* dock 弹出面板底部偏移 */
/* --- 7. 动效(对应 #99 动效 token)--- */ /* --- 7. 动效(对应 #99 动效 token)--- */
@@ -1691,9 +1691,9 @@
--workspace-accent-soft: rgba(17, 17, 17, 0.06); --workspace-accent-soft: rgba(17, 17, 17, 0.06);
--workspace-nav-active-bg: rgba(17, 17, 17, 0.075); --workspace-nav-active-bg: rgba(17, 17, 17, 0.075);
--workspace-nav-active-hover-bg: rgba(17, 17, 17, 0.11); --workspace-nav-active-hover-bg: rgba(17, 17, 17, 0.11);
--workspace-sidebar-width: 312px; --workspace-sidebar-width: 200px;
--workspace-sidebar-nav-width: 312px; --workspace-sidebar-nav-width: 200px;
--workspace-sidebar-collapsed-width: 86px; --workspace-sidebar-collapsed-width: 64px;
--workspace-shell-gutter: 0px; --workspace-shell-gutter: 0px;
--workspace-card-gap: 0px; --workspace-card-gap: 0px;
--workspace-topbar-height: 56px; --workspace-topbar-height: 56px;
@@ -1719,10 +1719,10 @@
/* 画布编辑器保持独立的全屏工作区,但沿用用户端的安全区和控件密度。 */ /* 画布编辑器保持独立的全屏工作区,但沿用用户端的安全区和控件密度。 */
.app-user-workspace.app-spatial-workspace { .app-user-workspace.app-spatial-workspace {
--canvas-topbar-h: 64px; --canvas-topbar-h: 56px;
--canvas-inset-x: 16px; --canvas-inset-x: 12px;
--canvas-inset-y: 16px; --canvas-inset-y: 12px;
--canvas-dock-popover-offset: 50px; --canvas-dock-popover-offset: 46px;
} }
/* 画布编辑器和管理后台不继承用户端的圆角卡片外壳。 */ /* 画布编辑器和管理后台不继承用户端的圆角卡片外壳。 */
@@ -15180,12 +15180,12 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
/* ===== 2026-09 user workspace alignment ===== /* ===== 2026-09 user workspace alignment =====
这一组规则只作用于真实用户端壳层和首页,后台与画布编辑器保持原有职责和交互。 这一组规则只作用于真实用户端壳层和首页,后台与画布编辑器保持原有职责和交互。
几何基准:侧栏 216px、顶部 64px、导航行 44px、首页内容最大宽度 1180px。 */ 几何基准:侧栏 200px、顶部 56px、导航行 40px、首页内容最大宽度 1180px。 */
.app-user-workspace { .app-user-workspace {
--workspace-reference-sidebar: 216px; --workspace-reference-sidebar: 200px;
--workspace-reference-collapsed: 70px; --workspace-reference-collapsed: 64px;
--workspace-reference-topbar: 64px; --workspace-reference-topbar: 56px;
--workspace-reference-row: 44px; --workspace-reference-row: 40px;
--workspace-reference-page-bg: #ffffff; --workspace-reference-page-bg: #ffffff;
background: var(--workspace-reference-page-bg); background: var(--workspace-reference-page-bg);
} }
@@ -17844,8 +17844,8 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
.app-user-workspace .app-workspace-sidebar-scroll-area { padding: 22px 12px 12px !important; gap: 24px !important; } .app-user-workspace .app-workspace-sidebar-scroll-area { padding: 22px 12px 12px !important; gap: 24px !important; }
.app-user-workspace .app-workspace-sidebar-footer { padding: 12px 12px 14px !important; border-top: 0 !important; } .app-user-workspace .app-workspace-sidebar-footer { padding: 12px 12px 14px !important; border-top: 0 !important; }
.app-user-workspace .app-workspace-nav-link { .app-user-workspace .app-workspace-nav-link {
min-height: 44px !important; min-height: var(--workspace-reference-row) !important;
padding: 4px 10px !important; padding: 3px 10px !important;
border-radius: 13px !important; border-radius: 13px !important;
color: var(--user-ink-muted) !important; color: var(--user-ink-muted) !important;
font-size: 13px !important; font-size: 13px !important;
@@ -17860,13 +17860,13 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
.app-user-workspace .app-workspace-nav-link:focus-visible:not(.is-active) { background: var(--user-surface-hover) !important; color: var(--user-ink) !important; } .app-user-workspace .app-workspace-nav-link:focus-visible:not(.is-active) { background: var(--user-surface-hover) !important; color: var(--user-ink) !important; }
.app-user-workspace .app-workspace-nav-group-toggle { color: var(--user-ink-soft) !important; } .app-user-workspace .app-workspace-nav-group-toggle { color: var(--user-ink-soft) !important; }
.app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-sidebar-scroll-area { padding-inline: 8px !important; } .app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-sidebar-scroll-area { padding-inline: 8px !important; }
.app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-nav-link { width: 54px !important; } .app-user-workspace .app-workspace-sidebar.is-collapsed .app-workspace-nav-link { width: 48px !important; }
.app-user-workspace .app-workspace-topbar { .app-user-workspace .app-workspace-topbar {
min-height: 64px !important; min-height: var(--workspace-reference-topbar) !important;
height: 64px !important; height: var(--workspace-reference-topbar) !important;
overflow: visible; overflow: visible;
padding-inline: 24px !important; padding-inline: 20px !important;
border-bottom: 0 !important; border-bottom: 0 !important;
background: color-mix(in srgb, var(--user-page-bg) 92%, transparent) !important; background: color-mix(in srgb, var(--user-page-bg) 92%, transparent) !important;
color: var(--user-ink) !important; color: var(--user-ink) !important;

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { changeAgentPanelLayout, clampAgentPanelLayout, restoreAgentPanelLayout } from "@/lib/canvas/agent-panel-layout"; import { changeAgentPanelLayout, clampAgentPanelLayout, defaultAgentPanelLayout, restoreAgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
import { agentErrorPresentation, agentSubmissionErrorTitle } from "@/lib/canvas/agent-error-presentation"; import { agentErrorPresentation, agentSubmissionErrorTitle } from "@/lib/canvas/agent-error-presentation";
import { ApiError } from "@/services/api/request"; import { ApiError } from "@/services/api/request";
@@ -15,6 +15,10 @@ describe("Agent window layout", () => {
expect(layout.height).toBe(626); expect(layout.height).toBe(626);
expect(layout.top).toBe(12); expect(layout.top).toBe(12);
}); });
it("offers a compact reset without altering a saved window until requested", () => {
expect(restoreAgentPanelLayout(JSON.stringify(start), viewport)).toEqual(start);
expect(defaultAgentPanelLayout(viewport)).toEqual({ left: 848, top: 148, width: 420, height: 640 });
});
it("keeps the bottom/right anchor when resizing from top/left", () => { it("keeps the bottom/right anchor when resizing from top/left", () => {
const layout = changeAgentPanelLayout(start, "northwest", -120, 60, viewport); const layout = changeAgentPanelLayout(start, "northwest", -120, 60, viewport);
expect(layout).toEqual({ left: 696, top: 128, width: 568, height: 660 }); expect(layout).toEqual({ left: 696, top: 128, width: 568, height: 660 });