diff --git a/.gitignore b/.gitignore index e4eb66865..b206b6e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,8 @@ assets/prototypes/.*.drawio.bkp camera_language_guide.md .superdesign .playwright-cli/ +# Local Playwright / browser verification artifacts. +/output/ /project-memory/ .workbuddy/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 412b38cb8..16a3690d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- 新增画布多选工具栏和节点右键菜单的「发送到 Agent」快捷入口,点击后自动带入节点引用。 + ## v1.3.0 - 发布正式版本 `v1.3.0`,提供 `ghcr.io/ddcat-ai/open-ai-canvas-web:1.3.0` 与 `ghcr.io/ddcat-ai/open-ai-canvas-backend:1.3.0` 的 Linux amd64/arm64 镜像,并更新 `latest` 镜像。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 05dad2f44..1a61b474c 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 画布节点快速引用 Agent + +- 多选节点后,选区浮动工具栏应显示 `@` 图标;点击「发送到 Agent」后,Agent 输入框应自动带入所有选中节点引用。 +- 单节点右键菜单(图片、视频、文本及其他节点)应显示「发送到 Agent」;点击后仅带入当前节点引用。 +- 引用应以节点 mention token 保存,发送前可以继续补充操作描述。 + ## Agent 对话模型选择器复用创造页样式 - Agent 底部 Composer 与设置页「对话模型」改为复用画布节点 / 创造页的 `ModelPicker` `variant="creation"`:先选品牌、再选模型,不再把品牌挤成一行。 diff --git a/web/src/components/canvas/canvas-cloud-agent-panel.tsx b/web/src/components/canvas/canvas-cloud-agent-panel.tsx index f72038b01..b5c133903 100644 --- a/web/src/components/canvas/canvas-cloud-agent-panel.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-panel.tsx @@ -32,11 +32,11 @@ import { CanvasCloudAgentSettings, agentPermissionLabel, agentPermissionMenuItem import { useAgentPanelLayout } from "./use-agent-panel-layout"; import "./canvas-cloud-agent.css"; -type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void }; +type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void }; type ApprovalState = { approvalId: string; detail: Record; reason: string }; type AgentPanelView = "chat" | "history" | "settings"; -export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) { +export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) { const theme = canvasThemes[useActiveTheme()]; const config = useEffectiveConfig(); const updateConfig = useConfigStore((state) => state.updateConfig); @@ -47,6 +47,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re const [connectionEpoch, setConnectionEpoch] = useState(0); const [messages, setMessages] = useState([]); const [prompt, setPrompt] = useState(""); + const lastPrefillPromptRef = useRef(""); const [reasoningMode, setReasoningMode] = useState("off"); const [profileView, setProfileView] = useState(null); const [profileLoading, setProfileLoading] = useState(false); @@ -112,6 +113,14 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re const statusLabel = status === "waiting_approval" ? "等待审批" : status === "running" || status === "queued" ? "运行中" : status === "completed" ? "已完成" : status === "failed" ? "异常" : status === "cancelled" ? "已停止" : status === "rejected" ? "已拒绝" : "待命"; const statusColor = status === "failed" ? "#e66b6b" : status === "rejected" || status === "cancelled" ? theme.node.muted : status === "waiting_approval" ? "#d6a24a" : status === "running" || status === "queued" ? "#69c29b" : theme.node.muted; + useEffect(() => { + const value = prefillPrompt?.trim(); + if (!value || value === lastPrefillPromptRef.current) return; + lastPrefillPromptRef.current = value; + setPrompt(value); + setView("chat"); + }, [prefillPrompt]); + useEffect(() => { if (!open || view !== "chat") setSkillsOpen(false); }, [open, view]); diff --git a/web/src/components/canvas/canvas-context-menu.tsx b/web/src/components/canvas/canvas-context-menu.tsx index 913ba9ada..cd2a8dbbe 100644 --- a/web/src/components/canvas/canvas-context-menu.tsx +++ b/web/src/components/canvas/canvas-context-menu.tsx @@ -1,6 +1,6 @@ import { AnimatePresence } from "motion/react"; import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; -import { ArrowLeft, Check, ChevronRight, Clipboard, CloudUpload, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, LayoutTemplate, Link2, Maximize2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Undo2, Upload, UserRound } from "lucide-react"; +import { ArrowLeft, AtSign, Check, ChevronRight, Clipboard, CloudUpload, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, LayoutTemplate, Link2, Maximize2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Undo2, Upload, UserRound } from "lucide-react"; import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu"; import { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category"; @@ -55,6 +55,7 @@ type CanvasNodeContextMenuProps = { onSpreadSelection?: () => void; onCopySelection?: () => void; onDeleteSelection?: () => void; + onSendToAgent?: () => void; }; export function CanvasNodeContextMenu({ @@ -94,6 +95,7 @@ export function CanvasNodeContextMenu({ onSpreadSelection, onCopySelection, onDeleteSelection, + onSendToAgent, }: CanvasNodeContextMenuProps) { const theme = canvasThemes[useActiveTheme()]; const [addOpen, setAddOpen] = useState(false); @@ -200,6 +202,7 @@ export function CanvasNodeContextMenu({ } label="自适应整理画布" detail="保持相对布局并加大边距" disabled={!onSpreadSelection} onClick={() => onSpreadSelection && runAction(onSpreadSelection)} /> } label={`复制 ${selectedCount} 个节点`} shortcut="⌘C" disabled={!onCopySelection} onClick={() => onCopySelection && runAction(onCopySelection)} /> + } label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} /> } label={`删除 ${selectedCount} 个节点`} danger disabled={!onDeleteSelection} onClick={() => onDeleteSelection && runAction(onDeleteSelection)} /> ) : menu.type === "node" ? ( @@ -211,6 +214,7 @@ export function CanvasNodeContextMenu({ } label="查看角色详情" onClick={() => runAction(onEditText)} /> + } label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} /> } label="复制角色引用" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> } label="创建引用副本" shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> } label="删除节点" danger onClick={() => runAction(onDelete)} /> @@ -224,6 +228,7 @@ export function CanvasNodeContextMenu({ {isImage ? } label="上传到方舟素材库" onClick={() => runAction(onUploadToArkPrivateAsset)} /> : null} + } label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} /> } label="复制节点" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> {isImage ? } label="复制图片" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null} } label={isImage ? "复制图片地址" : "复制视频地址"} disabled={!canCopyMediaUrl} onClick={() => runAction(onCopyMediaUrl)} /> @@ -241,6 +246,7 @@ export function CanvasNodeContextMenu({ {isText ? } label="用文本生图" disabled={!canGenerateFromText} onClick={() => runAction(onGenerateImage)} /> : null} + } label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} /> } label={isFrame ? `复制${isFolder ? "文件夹" : "背板"}及内容` : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> {isText ? } label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null} } label={isFrame ? `创建${isFolder ? "文件夹" : "背板"}副本` : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index 65fe0e5d3..2751a7a43 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -159,7 +159,7 @@ export function CanvasToolbar({ onToggleSettingsPanel: () => { setAddOpen(false); setAppearanceOpen(false); setSettingsOpen((value) => !value); }, onDeleteSelected: onDelete, // 以下为多选/节点悬停工具栏回调,主工具栏不使用,用 no-op 占位 - onAlign: () => {}, onArrange: () => {}, onCreateStoryboard: () => {}, onCreateReferenceGroup: () => {}, onBatchConnect: () => {}, onMergeVideos: () => {}, + onAlign: () => {}, onArrange: () => {}, onCreateStoryboard: () => {}, onCreateReferenceGroup: () => {}, onBatchConnect: () => {}, onMergeVideos: () => {}, onSendSelectionToAgent: () => {}, onNodeInfo: () => {}, onNodeDelete: () => {}, onNodeRetry: () => {}, onNodeEditText: () => {}, onNodeDecreaseFont: () => {}, onNodeIncreaseFont: () => {}, onNodeToggleDialog: () => {}, onNodeAnnotate: () => {}, onNodeGenerateImage: () => {}, onNodeUpload: () => {}, onNodeDownload: () => {}, onNodeSaveAsset: () => {}, onNodeMaskEdit: () => {}, onNodeEmotion: () => {}, onNodePortraitTexture: () => {}, onNodeCrop: () => {}, onNodeSplit: () => {}, onNodeUpscale: () => {}, diff --git a/web/src/components/layout/workspace-sidebar-nav.tsx b/web/src/components/layout/workspace-sidebar-nav.tsx index 99b9ccb5b..6fb39e199 100644 --- a/web/src/components/layout/workspace-sidebar-nav.tsx +++ b/web/src/components/layout/workspace-sidebar-nav.tsx @@ -1,5 +1,6 @@ import { Popover } from "antd"; import { Bell, ChevronDown, ChevronRight, CircleUserRound, History as HistoryIcon, Infinity as InfinityIcon, PanelLeftClose, PanelLeftOpen, Plus } from "lucide-react"; +import { LayoutGroup, motion, useReducedMotion } from "motion/react"; import { useEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from "react"; import { Link, useLocation, useNavigate, useSearchParams } from "react-router"; @@ -8,6 +9,7 @@ import { Kbd } from "@/components/ui/base/kbd"; import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools"; import { useWorkspaceLogout } from "@/hooks/use-workspace-logout"; import { SystemAnnouncementCenter } from "@/components/layout/system-announcement-center"; +import { aceternityMotion } from "@/lib/aceternity-motion"; import { cn } from "@/lib/utils"; import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules"; import { useUserStore, type FeatureAvailability } from "@/stores/use-user-store"; @@ -139,6 +141,7 @@ function NavItem({ const isActive = activeId === item.id || (item.id === "settings" && activeId.startsWith("settings:")); const hasChildren = Boolean(item.children?.length); const [isOpen, setIsOpen] = useState(false); + const reducedMotion = useReducedMotion(); // 激活分支自动展开(如设置分区子项),保证当前位置可见。 useEffect(() => { @@ -172,10 +175,18 @@ function NavItem({ ); const rowClassName = cn( - "app-workspace-nav-link group flex min-h-11 w-full items-center justify-between gap-2 rounded-[var(--r-md)] px-3 py-2 text-[var(--fs-body)] transition-[background-color,color,transform] duration-200 select-none", + "app-workspace-nav-link group relative isolate flex min-h-11 w-full items-center justify-between gap-2 rounded-[var(--r-md)] px-3 py-2 text-[var(--fs-body)] transition-[color,transform] duration-200 select-none", collapsed && "is-collapsed", isActive ? "is-active font-medium" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground", ); + const activePill = isActive ? ( + + ) : null; const handleClick = () => { if (item.action === "search") { @@ -211,10 +222,12 @@ function NavItem({ onPointerDown={() => preloadWorkspaceRoute(linkTo)} onPointerEnter={() => preloadWorkspaceRoute(linkTo)} > + {activePill} {rowContent} ) : ( )} @@ -299,6 +312,7 @@ export function WorkspaceSidebarNav({ collapsed, onNavigate, onOpenSearch, onExp
+
void handleLogout()} collapsed={collapsed} /> ))}
+
diff --git a/web/src/components/ui/aceternity/hover-border-gradient.tsx b/web/src/components/ui/aceternity/hover-border-gradient.tsx new file mode 100644 index 000000000..974130405 --- /dev/null +++ b/web/src/components/ui/aceternity/hover-border-gradient.tsx @@ -0,0 +1,86 @@ +import { motion, useReducedMotion } from "motion/react"; +import { useEffect, useState, type ComponentPropsWithoutRef, type ElementType, type MouseEvent, type ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT"; + +type HoverBorderGradientProps = { + as?: T; + containerClassName?: string; + className?: string; + duration?: number; + clockwise?: boolean; + children?: ReactNode; +} & Omit, "as" | "children">; + +const movingMap: Record = { + TOP: "radial-gradient(20.7% 50% at 50% 0%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)", + LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)", + BOTTOM: "radial-gradient(20.7% 50% at 50% 100%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)", + RIGHT: "radial-gradient(16.2% 41.2% at 100% 50%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)", +}; + +const highlight = "radial-gradient(75% 181% at 50% 50%, color-mix(in srgb, var(--user-ink) 22%, transparent) 0%, transparent 100%)"; + +function rotateDirection(current: Direction, clockwise: boolean): Direction { + const directions: Direction[] = clockwise + ? ["TOP", "LEFT", "BOTTOM", "RIGHT"] + : ["TOP", "RIGHT", "BOTTOM", "LEFT"]; + const index = directions.indexOf(current); + return directions[(index - 1 + directions.length) % directions.length]; +} + +// Aceternity Hover Border Gradient:中性墨色描边,不用彩色高光。 +export function HoverBorderGradient({ + children, + containerClassName, + className, + as, + duration = 1, + clockwise = true, + onMouseEnter, + onMouseLeave, + ...props +}: HoverBorderGradientProps) { + const Tag = (as || "button") as ElementType; + const reducedMotion = useReducedMotion(); + const [hovered, setHovered] = useState(false); + const [direction, setDirection] = useState("TOP"); + + useEffect(() => { + if (reducedMotion || hovered) return; + const interval = window.setInterval(() => { + setDirection((current) => rotateDirection(current, clockwise)); + }, duration * 1000); + return () => window.clearInterval(interval); + }, [clockwise, duration, hovered, reducedMotion]); + + return ( + ) => { + setHovered(true); + onMouseEnter?.(event as never); + }} + onMouseLeave={(event: MouseEvent) => { + setHovered(false); + onMouseLeave?.(event as never); + }} + > +
{children}
+ {reducedMotion ? null : ( + + )} +
+ + ); +} diff --git a/web/src/components/ui/aceternity/spotlight-surface.tsx b/web/src/components/ui/aceternity/spotlight-surface.tsx index 5288fef66..0acd1c9de 100644 --- a/web/src/components/ui/aceternity/spotlight-surface.tsx +++ b/web/src/components/ui/aceternity/spotlight-surface.tsx @@ -7,11 +7,12 @@ type SpotlightSurfaceProps = Omit, "children"> & { children?: ReactNode; spotlightColor: string; spotlightRadius?: number; + contentClassName?: string; }; // 基于 Aceternity Card Spotlight 改造:只保留中性指针高光,避免高频工具面板持续动画。 export const SpotlightSurface = forwardRef(function SpotlightSurface( - { children, className, spotlightColor, spotlightRadius = 220, onPointerEnter, onPointerLeave, onPointerMove, ...props }, + { children, className, contentClassName, spotlightColor, spotlightRadius = 220, onPointerEnter, onPointerLeave, onPointerMove, ...props }, ref, ) { const mouseX = useMotionValue(0); @@ -45,7 +46,7 @@ export const SpotlightSurface = forwardRef -
{children}
+
{children}
); }); diff --git a/web/src/lib/canvas/tool-registry/definitions/selection-toolbar-tools.tsx b/web/src/lib/canvas/tool-registry/definitions/selection-toolbar-tools.tsx index de1c54a18..96bb80d67 100644 --- a/web/src/lib/canvas/tool-registry/definitions/selection-toolbar-tools.tsx +++ b/web/src/lib/canvas/tool-registry/definitions/selection-toolbar-tools.tsx @@ -1,4 +1,4 @@ -import { AlignHorizontalJustifyCenter, AlignHorizontalJustifyEnd, AlignHorizontalJustifyStart, AlignHorizontalSpaceAround, AlignHorizontalSpaceBetween, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, AlignVerticalSpaceAround, AlignVerticalSpaceBetween, Film, FolderTree, Grid3X3, LayoutTemplate, Link2, LoaderCircle, Workflow } from "lucide-react"; +import { AlignHorizontalJustifyCenter, AlignHorizontalJustifyEnd, AlignHorizontalJustifyStart, AlignHorizontalSpaceAround, AlignHorizontalSpaceBetween, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, AlignVerticalSpaceAround, AlignVerticalSpaceBetween, AtSign, Film, FolderTree, Grid3X3, LayoutTemplate, Link2, LoaderCircle, Workflow } from "lucide-react"; import { registerToolbarTools, type ToolDefinition } from "@/lib/canvas/tool-registry"; @@ -18,6 +18,7 @@ export const selectionToolbarTools: ToolDefinition[] = [ { id: "selection-create-storyboard", toolbar: "selection", category: "selection", label: "创建分镜组", icon: , defaultVisible: true, defaultOrder: 130, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateStoryboard() }, { id: "selection-create-reference-group", toolbar: "selection", category: "selection", label: "创建引用组", icon: , defaultVisible: false, defaultOrder: 140, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateReferenceGroup() }, { id: "selection-batch-connect", toolbar: "selection", category: "selection", label: "批量连接", icon: , defaultVisible: true, defaultOrder: 145, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onBatchConnect() }, + { id: "selection-send-to-agent", toolbar: "selection", category: "selection", label: "发送到 Agent", icon: , defaultVisible: true, defaultOrder: 148, disabled: (ctx) => ctx.selectedCount < 1, run: (ctx) => ctx.handlers.onSendSelectionToAgent() }, { id: "selection-merge-videos", toolbar: "selection", diff --git a/web/src/lib/canvas/tool-registry/tool-definition.ts b/web/src/lib/canvas/tool-registry/tool-definition.ts index 79ec6cb4d..8faf9f24f 100644 --- a/web/src/lib/canvas/tool-registry/tool-definition.ts +++ b/web/src/lib/canvas/tool-registry/tool-definition.ts @@ -68,6 +68,7 @@ export type ToolbarHandlers = { onCreateReferenceGroup: () => void; onBatchConnect: () => void; onMergeVideos: () => void; + onSendSelectionToAgent: () => void; // 节点悬停工具栏——节点操作(均接收当前节点) onNodeInfo: (node: CanvasNodeData) => void; onNodeDelete: (node: CanvasNodeData) => void; diff --git a/web/src/pages/canvas/canvas-project-context-menu.tsx b/web/src/pages/canvas/canvas-project-context-menu.tsx index 6732eb1af..fc065cb91 100644 --- a/web/src/pages/canvas/canvas-project-context-menu.tsx +++ b/web/src/pages/canvas/canvas-project-context-menu.tsx @@ -42,6 +42,7 @@ type CanvasProjectContextMenuProps = { onSpreadSelection: () => void; onCopySelection: () => void; onDeleteSelection: () => void; + onSendToAgent: () => void; }; export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props }: CanvasProjectContextMenuProps) { @@ -114,6 +115,7 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props onSpreadSelection={props.onSpreadSelection} onCopySelection={props.onCopySelection} onDeleteSelection={props.onDeleteSelection} + onSendToAgent={props.onSendToAgent} /> ); } diff --git a/web/src/pages/canvas/canvas-project-selection-toolbar.tsx b/web/src/pages/canvas/canvas-project-selection-toolbar.tsx index 1847a6bd8..b5de7c2e4 100644 --- a/web/src/pages/canvas/canvas-project-selection-toolbar.tsx +++ b/web/src/pages/canvas/canvas-project-selection-toolbar.tsx @@ -20,13 +20,14 @@ type CanvasProjectSelectionToolbarProps = { onCreateReferenceGroup: () => void; onBatchConnect: () => void; onMergeVideos: () => void; + onSendSelectionToAgent: () => void; }; -export function CanvasProjectSelectionToolbar({ anchorRef, containerRef, count, selectedVideoCount, mergingVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos }: CanvasProjectSelectionToolbarProps) { +export function CanvasProjectSelectionToolbar({ anchorRef, containerRef, count, selectedVideoCount, mergingVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onSendSelectionToAgent }: CanvasProjectSelectionToolbarProps) { const theme = canvasThemes[useCanvasThemeStore((state) => state.theme)]; const handlers = { - onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, + onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onSendSelectionToAgent, } as Partial as ToolbarHandlers; const ctx: ToolContext = { diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 3620fcf97..5656afe49 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -77,6 +77,7 @@ import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt"; import { applyCanvasConnectionPromptSync, buildCanvasAgentMentionReferences, + canvasResourceMentionToken, buildCanvasNodeMentionReferenceMap, buildCanvasResourceReferences, getContextResourceNodes, @@ -292,6 +293,7 @@ function InfiniteCanvasPage() { const [selectedConnectionId, setSelectedConnectionId] = useState(null); const [hoveredNodeId, setHoveredNodeId] = useState(null); const [contextMenu, setContextMenu] = useState(null); + const [agentPrefillPrompt, setAgentPrefillPrompt] = useState(""); const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [canvasAppearance, setCanvasAppearance] = useState(() => canvasAppearanceForTheme(colorTheme)); const [backgroundMode, setBackgroundMode] = useState(DEFAULT_CANVAS_BACKGROUND_MODE); @@ -333,6 +335,15 @@ function InfiniteCanvasPage() { const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false); const { assistantOpen, closeAgent, openAgent } = useCanvasAssistantVisibility(); const agentMentionReferences = useMemo(() => buildCanvasAgentMentionReferences(nodes), [nodes]); + + const sendSelectionToAgent = useCallback((nodeId?: string) => { + const ids = nodeId ? [nodeId] : Array.from(selectedNodeIdsRef.current); + const references = ids.map((id) => agentMentionReferences.find((reference) => reference.nodeId === id)).filter((reference): reference is CanvasResourceReference => Boolean(reference)); + if (!references.length) return; + setAgentPrefillPrompt(`${references.map(canvasResourceMentionToken).join(" ")} `); + openAgent(); + setContextMenu(null); + }, [agentMentionReferences, openAgent]); const { tasks: activeTasks } = useCanvasActiveTasks(projectId, projectLoaded); const { focusMode, enterFocusMode, exitFocusMode, toggleFocusMode } = useFocusMode(); const [focusDockRevealed, setFocusDockRevealed] = useState(false); @@ -2582,7 +2593,7 @@ function InfiniteCanvasPage() { ) : null}
- { + { if (!nodesRef.current.some((node) => node.id === nodeId)) { message.info("该节点已删除或尚未同步到画布"); return; } focusCanvasNode(nodeId); }} /> @@ -2697,6 +2708,7 @@ function InfiniteCanvasPage() { onCreateReferenceGroup={createReferenceGroup} onBatchConnect={() => beginBatchConnectionMode(Array.from(selectedNodeIds))} onMergeVideos={() => void mergeSelectedVideos()} + onSendSelectionToAgent={() => sendSelectionToAgent()} /> ) : null} @@ -2843,6 +2855,7 @@ function InfiniteCanvasPage() { onSpreadSelection={spreadSelectedNodes} onCopySelection={copySelectedNodes} onDeleteSelection={() => deleteNodes(selectedNodeIds)} + onSendToAgent={() => sendSelectionToAgent(contextMenu?.type === "node" && selectedNodeIds.size <= 1 ? contextMenu.nodeId : undefined)} /> diff --git a/web/src/pages/create/creation-product.css b/web/src/pages/create/creation-product.css index c2aabe7d4..579e67c79 100644 --- a/web/src/pages/create/creation-product.css +++ b/web/src/pages/create/creation-product.css @@ -1,21 +1,13 @@ /* Creation-specific geometry. Shared controls still use the workspace tokens. */ .creation-mode-tabs { position: relative; isolation: isolate; display: grid; width: min(456px, 100%); margin: 0 auto 20px; padding: 4px; border-radius: 18px; background: var(--user-surface-muted); } -.creation-mode-indicator { position: absolute; z-index: -1; top: 4px; left: 4px; bottom: 4px; border-radius: 14px; background: var(--user-surface); box-shadow: 0 2px 8px color-mix(in srgb, var(--user-ink) 5%, transparent); } -.creation-mode-button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 0; height: 44px; padding: 0 12px; border: 0; border-radius: 14px; background: transparent; color: var(--user-ink-muted); font-size: 14px; font-weight: 600; cursor: pointer; transition: color var(--user-motion), transform var(--user-motion); } -.creation-mode-button svg { width: 18px; height: 18px; stroke-width: 1.8; } -.creation-mode-button[data-mode="video"] svg { color: var(--user-accent); } -.creation-mode-button[data-mode="image"] svg { color: var(--product-accent-green); } -.creation-mode-button[data-mode="text"] svg { color: var(--product-accent-orange); } -.creation-mode-button[data-mode="agent"] svg { color: var(--product-accent-blue); } +.creation-mode-indicator { position: absolute; z-index: -1; inset: 0; border-radius: 14px; background: var(--user-surface); box-shadow: 0 2px 8px color-mix(in srgb, var(--user-ink) 5%, transparent); } +.creation-mode-button { position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 0; height: 44px; padding: 0 12px; border: 0; border-radius: 14px; background: transparent; color: var(--user-ink-muted); font-size: 14px; font-weight: 600; cursor: pointer; transition: color var(--user-motion), transform var(--user-motion); } +.creation-mode-button svg { width: 18px; height: 18px; stroke-width: 1.8; color: currentColor; } .creation-mode-button[aria-pressed="true"], .creation-mode-button:hover { color: var(--user-ink); } .creation-mode-button:active { transform: scale(.97); } .creation-mode-button:focus-visible { outline: 2px solid var(--user-accent); outline-offset: 2px; } -.creation-mode-spark { width: 5px; height: 5px; border-radius: 50%; background: var(--user-accent); box-shadow: 0 0 0 3px var(--user-accent-soft); } -.creation-mode-tabs[data-active-mode="agent"] .creation-mode-indicator { background: linear-gradient(115deg, var(--user-accent-soft), color-mix(in srgb, var(--product-accent-blue) 12%, var(--user-surface))); } -.creation-mode-tabs[data-active-mode="video"] .creation-mode-indicator { background: linear-gradient(115deg, var(--user-surface), var(--user-accent-soft)); } -.creation-mode-tabs[data-active-mode="image"] .creation-mode-indicator { background: color-mix(in srgb, var(--product-accent-green) 9%, var(--user-surface)); } -.creation-mode-tabs[data-active-mode="text"] .creation-mode-indicator { background: color-mix(in srgb, var(--product-accent-orange) 9%, var(--user-surface)); } -.creation-agent-entry { display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: center; gap: 16px 24px; padding: 28px; margin-bottom: 20px; border-radius: var(--user-radius-card); background: radial-gradient(ellipse at top right, var(--user-accent-soft), transparent 60%), var(--user-surface); animation: product-card-enter var(--product-enter) var(--product-ease) backwards; } +.creation-mode-spark { width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: .42; box-shadow: none; } +.creation-agent-entry { display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: center; gap: 16px 24px; padding: 28px; margin-bottom: 20px; border-radius: var(--user-radius-card); background: var(--user-surface); animation: product-card-enter var(--product-enter) var(--product-ease) backwards; } .creation-agent-orb { grid-row: span 2; } .creation-agent-eyebrow { font-size: 10px; font-weight: 650; letter-spacing: .14em; color: var(--user-accent); } .creation-agent-copy h2 { margin: 4px 0 8px; font-size: 21px; font-weight: 650; color: var(--user-ink); } @@ -69,34 +61,27 @@ min-width: 36px !important; height: 36px !important; border-radius: 999px !important; - background: color-mix(in srgb, var(--product-accent-blue) 12%, var(--user-surface-muted)) !important; - color: var(--product-accent-blue) !important; + background: var(--user-surface-muted) !important; + color: var(--user-ink-muted) !important; transition: background var(--user-motion), color var(--user-motion), transform var(--user-motion), box-shadow var(--user-motion) !important; } .app-user-workspace .creation-voice-trigger.ant-btn:hover, .app-user-workspace .creation-voice-trigger.ant-btn:focus-visible { - background: color-mix(in srgb, var(--product-accent-blue) 20%, var(--user-surface)) !important; - color: var(--product-accent-blue) !important; + background: var(--user-surface-hover) !important; + color: var(--user-ink) !important; transform: translateY(-1px); - box-shadow: 0 8px 18px color-mix(in srgb, var(--product-accent-blue) 18%, transparent); + box-shadow: 0 8px 18px color-mix(in srgb, var(--user-ink) 8%, transparent); } .app-user-workspace .creation-chat-control > svg:first-child, -.app-user-workspace .creation-model-picker .canvas-model-picker-trigger-icon { transition: transform var(--user-motion), color var(--user-motion); } +.app-user-workspace .creation-model-picker .canvas-model-picker-trigger-icon { transition: transform var(--user-motion), color var(--user-motion); color: currentColor; } .app-user-workspace .creation-chat-control:hover > svg:first-child, -.app-user-workspace .creation-model-picker:hover .canvas-model-picker-trigger-icon { transform: scale(1.08); } -.app-user-workspace .creation-chat-control:has(.lucide-wand-sparkles) > svg:first-child { color: var(--product-accent-violet); } -.app-user-workspace .creation-chat-control:has(.lucide-clock-3) > svg:first-child { color: var(--product-accent-orange); } -.app-user-workspace .creation-chat-control:has(.lucide-sliders-horizontal) > svg:first-child { color: var(--product-accent-green); } -.app-user-workspace .creation-chat-control:has(.lucide-waves) > svg:first-child { color: var(--product-accent-cyan); } -.app-user-workspace .creation-chat-control:has(.lucide-brain) > svg:first-child { color: var(--product-accent-blue); } -.app-user-workspace .creation-chat-control.is-clear > svg:first-child { color: #c45b5b; } -.app-user-workspace .creation-chat-control.is-clear:hover { background: color-mix(in srgb, #c45b5b 12%, var(--user-surface)) !important; color: #c45b5b !important; } -.app-user-workspace .creation-model-picker .canvas-model-picker-trigger-icon { color: var(--product-accent-orange); } +.app-user-workspace .creation-model-picker:hover .canvas-model-picker-trigger-icon { transform: scale(1.06); } +.app-user-workspace .creation-chat-control.is-clear:hover { background: color-mix(in srgb, var(--status-error, #c45b5b) 12%, var(--user-surface)) !important; color: var(--status-error, #c45b5b) !important; } .app-user-workspace .creation-launchpad .creation-reference-add-button:hover:not(:disabled) { border-style: solid; - border-color: var(--user-accent); - background: color-mix(in srgb, var(--user-accent) 12%, var(--user-surface)); - color: var(--user-accent); + border-color: var(--user-border-strong); + background: var(--user-surface-hover); + color: var(--user-ink); transform: translateY(-1px); } .app-user-workspace .creation-launchpad .creation-reference-add-button:active:not(:disabled) { transform: scale(.97); } diff --git a/web/src/pages/create/creation-workspace.tsx b/web/src/pages/create/creation-workspace.tsx index 7387c8e00..de4e260ec 100644 --- a/web/src/pages/create/creation-workspace.tsx +++ b/web/src/pages/create/creation-workspace.tsx @@ -5,7 +5,7 @@ import { App, Button, Dropdown, Popover } from "antd"; import { AppDrawer } from "@/components/ui/product/app-drawer"; import { AppModal } from "@/components/ui/product/app-modal"; import { Tooltip } from "@/components/ui/base/tooltip"; -import { Reorder, motion, useReducedMotion } from "motion/react"; +import { Reorder, LayoutGroup, motion, useReducedMotion } from "motion/react"; import { ArrowDown, ArrowUp, Brain, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, MoreHorizontal, Music2, Pencil, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, UserRound, WandSparkles, Waves, X } from "lucide-react"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; @@ -20,7 +20,10 @@ import { CachedResourceImage } from "@/components/cached-resource-image"; import { CanvasImagePreview } from "@/components/canvas/canvas-image-preview"; import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea"; import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button"; +import { HoverBorderGradient } from "@/components/ui/aceternity/hover-border-gradient"; +import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { ModelPicker } from "@/components/model-picker"; +import { aceternityMotion } from "@/lib/aceternity-motion"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { ASSET_CATEGORY_LABELS } from "@/lib/asset-category"; import { formatShotOrdinal } from "@/lib/shot-label"; @@ -482,7 +485,13 @@ export function CreationComposer(props: ComposerProps) { } return undefined; }; - const composer =
+ const composer = + {props.variant === "thread" ?
: null}
@@ -600,7 +609,8 @@ export function CreationComposer(props: ComposerProps) { setPreviewUrl("")} /> -
; + + ; if (!promptOptimizerOpen) return composer; @@ -630,18 +640,26 @@ export function CreationModeTabs({ mode, onModeChange, agentActive = false, onAg { mode: "image", icon: , label: "图片" }, { mode: "text", icon: , label: "文本" }, ]; - const activeIndex = agentActive ? 3 : items.findIndex((item) => item.mode === mode); - const vertical = orientation === "vertical"; - return
- + const indicator = (pressed: boolean) => pressed ? ( + + ) : null; + return +
{items.map((item) => ( ))} - {onAgentSelect ? : null} -
; + {onAgentSelect ? : null} +
+ ; } function ModePicker({ mode, onModeChange }: { mode: CreationMode; onModeChange: (mode: CreationMode) => void }) { @@ -735,22 +753,43 @@ const creationEmptySuggestions: Array<{ mode: CreationMode; icon: typeof Clapper ]; export function CreationEmptySuggest({ onStartPrompt, onOpenLibrary }: { onStartPrompt: (mode: CreationMode, prompt: string) => void; onOpenLibrary: () => void }) { - return
+ const reducedMotion = useReducedMotion(); + const [hovered, setHovered] = useState(null); + return +
{creationEmptySuggestions.map((item) => { const Icon = item.icon; const start = () => { if (item.openLibrary) onOpenLibrary(); else onStartPrompt(item.mode, item.prompt); }; - return ; + ; })} -
; +
+ ; } diff --git a/web/src/pages/welcome/contributors-parse.ts b/web/src/pages/welcome/contributors-parse.ts new file mode 100644 index 000000000..6f67761cd --- /dev/null +++ b/web/src/pages/welcome/contributors-parse.ts @@ -0,0 +1,90 @@ +export type WelcomeContributor = { + name: string; + avatar: string; + signature: string; + email?: string; + wechat?: string; + isFounder?: boolean; +}; + +function plainText(value: string) { + return value + .replace(//gi, " · ") + .replace(/<[^>]+>/g, "") + .replace(/\[([^\]]+)]\([^)]+\)/g, "$1") + .replace(/\s+/g, " ") + .trim(); +} + +function wechatFrom(details: string) { + return details.match(/微信\s*[::]\s*([^·]+)/)?.[1]?.trim(); +} + +function parsePipeContributor(line: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] { + const columns = line + .slice(1, line.lastIndexOf("|")) + .split("|") + .map((column) => column.trim()); + const image = columns[0]?.match(/src="([^"]+)"[^>]*alt="([^"]*)"/i); + const nameColumn = columns[1] ?? ""; + const name = plainText(nameColumn.split(//i)[0] ?? image?.[2] ?? ""); + const avatarPath = image?.[1]; + if (!avatarPath || !name) return []; + + const avatar = resolveAvatar(avatarPath); + if (!avatar) return []; + + const nameDetails = plainText(nameColumn); + const email = plainText(columns[2] ?? ""); + + return [ + { + name, + avatar, + signature: plainText(columns[3] ?? ""), + email: email || undefined, + wechat: wechatFrom(nameDetails), + isFounder: nameDetails.includes("项目发起者"), + }, + ]; +} + +function parseHtmlContributor(cell: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] { + const avatarPath = cell.match(/]*\bsrc="([^"]+)"/i)?.[1]; + const alt = cell.match(/]*\balt="([^"]*)"/i)?.[1] ?? ""; + const strong = cell.match(/([\s\S]*?)<\/strong>/i)?.[1] ?? ""; + const name = plainText(strong.split(//i)[0] ?? "") || plainText(alt); + if (!avatarPath || !name) return []; + + const avatar = resolveAvatar(avatarPath); + if (!avatar) return []; + + const nameDetails = plainText(strong) || name; + const email = decodeURIComponent(cell.match(/mailto:([^"'>\s]+)/i)?.[1] ?? "") || plainText(cell.match(/]*>([\s\S]*?)<\/a>/i)?.[1] ?? ""); + + return [ + { + name, + avatar, + signature: plainText(cell.match(/([\s\S]*?)<\/em>/i)?.[1] ?? ""), + email: email || undefined, + wechat: wechatFrom(nameDetails), + isFounder: nameDetails.includes("项目发起者"), + }, + ]; +} + +export function parseContributors(markdown: string, resolveAvatar: (path: string) => string | undefined): WelcomeContributor[] { + const section = markdown.match(/## 贡献者与团队([\s\S]*?)(?=\n##\s|$)/)?.[1]; + if (!section) return []; + + const htmlCells = section.match(//gi) ?? []; + if (htmlCells.length) { + return htmlCells.flatMap((cell) => parseHtmlContributor(cell, resolveAvatar)); + } + + return section + .split("\n") + .filter((line) => /^\|\s* parsePipeContributor(line, resolveAvatar)); +} diff --git a/web/src/pages/welcome/contributors.ts b/web/src/pages/welcome/contributors.ts index 96c4aa087..7eb037f67 100644 --- a/web/src/pages/welcome/contributors.ts +++ b/web/src/pages/welcome/contributors.ts @@ -1,65 +1,13 @@ import readmeMarkdown from "../../../../README.md?raw"; +import { parseContributors } from "./contributors-parse"; + const contributorAvatarModules = import.meta.glob("../../../../assets/user-*", { eager: true, import: "default", query: "?url", }) as Record; -export type WelcomeContributor = { - name: string; - avatar: string; - signature: string; - email?: string; - wechat?: string; - isFounder?: boolean; -}; +export type { WelcomeContributor } from "./contributors-parse"; -function plainText(value: string) { - return value - .replace(//gi, " · ") - .replace(/<[^>]+>/g, "") - .replace(/\[([^\]]+)]\([^)]+\)/g, "$1") - .replace(/\s+/g, " ") - .trim(); -} - -function parseContributors(markdown: string): WelcomeContributor[] { - const section = markdown.match(/## 贡献者与团队([\s\S]*?)(?=\n##\s)/)?.[1]; - if (!section) return []; - - return section - .split("\n") - .filter((line) => /^\|\s* { - const columns = line - .slice(1, line.lastIndexOf("|")) - .split("|") - .map((column) => column.trim()); - const image = columns[0]?.match(/src="([^"]+)"[^>]*alt="([^"]*)"/i); - const nameColumn = columns[1] ?? ""; - const name = plainText(nameColumn.split(//i)[0] ?? image?.[2] ?? ""); - const avatarPath = image?.[1]; - if (!avatarPath || !name) return []; - - const avatar = contributorAvatarModules[`../../../../${avatarPath}`]; - if (!avatar) return []; - - const nameDetails = plainText(nameColumn); - const email = plainText(columns[2] ?? ""); - const wechat = nameDetails.match(/微信\s*[::]\s*([^·]+)/)?.[1]?.trim(); - - return [ - { - name, - avatar, - signature: plainText(columns[3] ?? ""), - email: email || undefined, - wechat, - isFounder: nameDetails.includes("项目发起者"), - }, - ]; - }); -} - -export const welcomeContributors = parseContributors(readmeMarkdown); +export const welcomeContributors = parseContributors(readmeMarkdown, (avatarPath) => contributorAvatarModules[`../../../../${avatarPath}`]); diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index d3f4ab21d..b0662e58f 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -17707,8 +17707,8 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 height: 28px; flex: 0 0 auto; border-radius: 8px; - background: #f3f2ff; - color: #6657d9; + background: var(--user-surface-muted); + color: var(--user-ink-muted); } .app-user-workspace .creation-empty-suggest .suggest-copy, @@ -19379,7 +19379,7 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .app-workspace-nav-link .app-workspace-nav-main { gap: 10px !important; } .app-user-workspace .app-workspace-nav-link .app-workspace-nav-icon svg, .app-user-workspace .app-workspace-nav-link .app-workspace-nav-main > svg { width: 16px !important; height: 16px !important; } -.app-user-workspace .app-workspace-nav-link.is-active { background: var(--user-surface-muted) !important; color: var(--user-ink) !important; font-weight: 680 !important; } +.app-user-workspace .app-workspace-nav-link.is-active { background: transparent !important; color: var(--user-ink) !important; font-weight: 680 !important; } .app-user-workspace .app-workspace-nav-link:hover:not(.is-active), .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; } @@ -19491,27 +19491,40 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .creation-launchpad > .creation-mode-capsule .creation-chat-control svg { width: 16px !important; height: 16px !important; } .app-user-workspace .creation-empty-composer { width: 100% !important; margin: 0 0 20px !important; } +.app-user-workspace .creation-composer-shell { + width: 100%; + border-radius: 22px; + background: transparent; + box-shadow: 0 6px 16px color-mix(in srgb, var(--user-ink) 8%, transparent); +} +.dark .app-user-workspace .creation-composer-shell { + box-shadow: 0 6px 16px rgba(0, 0, 0, .28); +} .app-user-workspace .creation-chat-composer { position: relative; isolation: isolate; overflow: hidden; border: 0 !important; - border-radius: 22px !important; - background: - radial-gradient(920px 180px at 8% -30%, color-mix(in srgb, var(--user-accent) 11%, transparent), transparent 58%), - var(--user-surface) !important; - box-shadow: 0 6px 16px color-mix(in srgb, var(--user-ink) 8%, transparent) !important; + border-radius: 21px !important; + background: var(--user-surface) !important; + box-shadow: none !important; transition: box-shadow var(--user-motion), transform var(--user-motion) !important; } +.app-user-workspace .creation-chat-composer .creation-composer-mode-row, +.app-user-workspace .creation-chat-composer .creation-chat-writing-surface, +.app-user-workspace .creation-chat-composer .creation-chat-dock { + position: relative; + z-index: 1; +} .app-user-workspace .creation-chat-composer:focus-within { - box-shadow: 0 8px 18px color-mix(in srgb, var(--user-ink) 10%, transparent) !important; + box-shadow: none !important; transform: none; } .dark .app-user-workspace .creation-chat-composer { - box-shadow: 0 6px 16px rgba(0, 0, 0, .28) !important; + box-shadow: none !important; } .dark .app-user-workspace .creation-chat-composer:focus-within { - box-shadow: 0 8px 18px rgba(0, 0, 0, .34) !important; + box-shadow: none !important; } .app-user-workspace .creation-chat-composer.is-empty .creation-chat-writing-surface { min-height: 216px !important; @@ -19579,9 +19592,9 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .creation-launchpad .creation-reference-add-button { border-style: dashed; - border-color: color-mix(in srgb, var(--user-accent) 28%, var(--user-border)); - background: color-mix(in srgb, var(--user-accent) 7%, var(--user-surface-muted)); - color: var(--user-accent); + border-color: var(--user-border); + background: var(--user-surface-muted); + color: var(--user-ink-muted); font-size: 12px; transition: background var(--user-motion), border-color var(--user-motion), color var(--user-motion), transform var(--user-motion); } @@ -19598,9 +19611,9 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .creation-model-picker-menu .canvas-model-picker-subheading small { color: var(--user-ink-soft); font-size: 10px; font-weight: 500; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand { display: flex; min-height: 58px; align-items: center; gap: 10px; border: 1px solid transparent; border-radius: 13px; padding: 8px 10px; background: transparent; color: var(--user-ink); text-align: left; transition: background-color 160ms ease, border-color 160ms ease, transform 160ms cubic-bezier(.16,1,.3,1); } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand:hover { border-color: var(--user-border); background: var(--user-surface-hover); transform: translateX(2px); } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand-icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 11px; background: linear-gradient(135deg, color-mix(in srgb, var(--user-accent) 20%, var(--user-surface)), color-mix(in srgb, var(--user-accent) 6%, var(--user-surface))); color: var(--user-accent); } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand:nth-child(3n) .canvas-model-picker-brand-icon { background: linear-gradient(135deg, color-mix(in srgb, #e99a3d 24%, var(--user-surface)), color-mix(in srgb, #e99a3d 7%, var(--user-surface))); color: #d57b16; } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand:nth-child(4n) .canvas-model-picker-brand-icon { background: linear-gradient(135deg, color-mix(in srgb, #41a87b 22%, var(--user-surface)), color-mix(in srgb, #41a87b 7%, var(--user-surface))); color: #25845d; } +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand-icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 11px; background: var(--user-surface-muted); color: var(--user-ink-muted); } +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand:nth-child(3n) .canvas-model-picker-brand-icon, +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand:nth-child(4n) .canvas-model-picker-brand-icon { background: var(--user-surface-muted); color: var(--user-ink-muted); } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand-copy { display: grid; min-width: 0; flex: 1; gap: 4px; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand-copy strong { overflow: hidden; font-size: 13px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-brand-copy small { color: var(--user-ink-muted); font-size: 10px; } @@ -19613,9 +19626,9 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .creation-model-picker-menu .canvas-model-picker-secondary-head strong { color: var(--user-ink); font-size: 13px; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-secondary-head small { color: var(--user-ink-soft); font-size: 10px; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-option { min-height: 55px; border-radius: 12px; padding: 8px 9px; } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option > span:first-child > span:first-child { width: 32px; height: 32px; border-radius: 10px; background: color-mix(in srgb, var(--user-accent) 10%, var(--user-surface)) !important; color: var(--user-accent); } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option:nth-child(3n) > span:first-child > span:first-child { color: #d57b16; background: color-mix(in srgb, #e99a3d 12%, var(--user-surface)) !important; } -.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option:nth-child(4n) > span:first-child > span:first-child { color: #25845d; background: color-mix(in srgb, #41a87b 12%, var(--user-surface)) !important; } +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option > span:first-child > span:first-child { width: 32px; height: 32px; border-radius: 10px; background: var(--user-surface-muted) !important; color: var(--user-ink-muted); } +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option:nth-child(3n) > span:first-child > span:first-child, +.app-user-workspace .creation-model-picker-menu .canvas-model-picker-option:nth-child(4n) > span:first-child > span:first-child { color: var(--user-ink-muted); background: var(--user-surface-muted) !important; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-option .text-amber-600, .app-user-workspace .creation-model-picker-menu .canvas-model-picker-option .dark\:text-amber-300 { display: inline-flex; align-items: center; gap: 5px; border-radius: 999px; background: color-mix(in srgb, #e28a17 12%, var(--user-surface)); color: #c66d00 !important; padding: 5px 8px; font-size: 11px; font-weight: 700; } .app-user-workspace .creation-model-picker-menu .canvas-model-picker-option .text-amber-600 svg { width: 13px; height: 13px; } @@ -19636,6 +19649,8 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .creation-empty-suggest .creation-hero-prompt, .app-user-workspace .creation-empty-suggest .suggest-card { + position: relative; + isolation: isolate; min-height: 80px !important; margin: 0 !important; padding: 12px !important; @@ -19646,12 +19661,22 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 color: var(--user-ink) !important; transition: transform var(--user-motion), background var(--user-motion), box-shadow var(--user-motion), border-color var(--user-motion) !important; } +.app-user-workspace .creation-empty-suggest .suggest-card-hover { + position: absolute; + inset: 0; + z-index: 0; + border-radius: inherit; + background: var(--user-surface-hover); + pointer-events: none; +} +.app-user-workspace .creation-empty-suggest .suggest-card > .suggest-icon, +.app-user-workspace .creation-empty-suggest .suggest-card > .suggest-copy { position: relative; z-index: 1; } .app-user-workspace .creation-empty-suggest .creation-hero-prompt { grid-column: auto !important; } .app-user-workspace .creation-empty-suggest .creation-hero-prompt:hover, .app-user-workspace .creation-empty-suggest .suggest-card:hover, .app-user-workspace .creation-empty-suggest .creation-hero-prompt:focus-visible, -.app-user-workspace .creation-empty-suggest .suggest-card:focus-visible { transform: translateY(-2px) !important; border-color: var(--user-border-strong) !important; background: var(--user-surface-hover) !important; box-shadow: var(--user-shadow-hover) !important; } -.app-user-workspace .creation-empty-suggest .suggest-icon { width: 32px !important; height: 32px !important; margin-bottom: 0 !important; flex-shrink: 0; border: 0 !important; border-radius: 12px !important; background: var(--user-accent-soft) !important; color: var(--user-accent) !important; } +.app-user-workspace .creation-empty-suggest .suggest-card:focus-visible { transform: none !important; border-color: transparent !important; background: transparent !important; box-shadow: none !important; } +.app-user-workspace .creation-empty-suggest .suggest-icon { width: 32px !important; height: 32px !important; margin-bottom: 0 !important; flex-shrink: 0; border: 0 !important; border-radius: 12px !important; background: var(--user-surface-muted) !important; color: var(--user-ink-muted) !important; } .app-user-workspace .creation-empty-suggest .suggest-copy, .app-user-workspace .creation-empty-suggest .creation-hero-prompt-copy { gap: 4px !important; } .app-user-workspace .creation-empty-suggest .suggest-copy strong, diff --git a/web/src/styles/workspace-product.css b/web/src/styles/workspace-product.css index 995d43f40..a4df26146 100644 --- a/web/src/styles/workspace-product.css +++ b/web/src/styles/workspace-product.css @@ -471,49 +471,51 @@ .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-profile { width: 48px; min-width: 48px; flex: 0 0 48px; margin-inline: auto; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-notification-spacer { display: none; } -.app-user-workspace .app-workspace-nav-link { --nav-accent: var(--user-ink-muted); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="home"] { --nav-accent: var(--product-accent-orange); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="projects"] { --nav-accent: var(--product-accent-blue); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="canvas"] { --nav-accent: var(--product-accent-green); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="assets"] { --nav-accent: #c4891a; } -.app-user-workspace .app-workspace-nav-link[data-nav-id="skills"] { --nav-accent: var(--product-accent-pink); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="plugins"] { --nav-accent: var(--product-accent-violet); } -.app-user-workspace .app-workspace-nav-link[data-nav-id="tasks"] { --nav-accent: var(--product-accent-cyan); } -.dark .app-user-workspace .app-workspace-nav-link[data-nav-id="assets"] { --nav-accent: #e8c37a; } +.app-user-workspace .app-workspace-nav-link { + position: relative; + isolation: isolate; +} +.app-user-workspace .app-workspace-nav-active-pill { + position: absolute; + inset: 0; + z-index: 0; + border-radius: inherit; + background: var(--user-surface-muted); + pointer-events: none; +} +.app-user-workspace .app-workspace-nav-link > :not(.app-workspace-nav-active-pill) { + position: relative; + z-index: 1; +} .app-user-workspace .app-workspace-nav-icon { display: grid; - width: 28px; - height: 28px; - flex: 0 0 28px; + width: 20px; + height: 20px; + flex: 0 0 20px; place-items: center; - border-radius: 9px; - background: color-mix(in srgb, var(--nav-accent) 14%, transparent); - color: var(--nav-accent); - transition: background var(--product-state) var(--product-ease), transform var(--product-state) var(--product-ease), box-shadow var(--product-state) var(--product-ease); + border-radius: 0; + background: transparent; + color: currentColor; + transition: transform var(--product-state) var(--product-ease); } .app-user-workspace .app-workspace-nav-icon svg { - color: var(--nav-accent) !important; + color: currentColor !important; opacity: 1 !important; transform-origin: center; transition: transform var(--product-state) var(--product-ease); } .app-user-workspace .app-workspace-nav-link:hover .app-workspace-nav-icon, .app-user-workspace .app-workspace-nav-link:focus-visible .app-workspace-nav-icon { - background: color-mix(in srgb, var(--nav-accent) 22%, transparent); transform: translateY(-1px); } -.app-user-workspace .app-workspace-nav-link:hover .app-workspace-nav-icon svg, -.app-user-workspace .app-workspace-nav-link:focus-visible .app-workspace-nav-icon svg { - animation: workspace-nav-icon-nudge 420ms var(--product-ease); -} .app-user-workspace .app-workspace-nav-link.is-active { - background: color-mix(in srgb, var(--nav-accent) 12%, var(--user-surface-muted)) !important; + background: transparent !important; } .app-user-workspace .app-workspace-nav-link.is-active .app-workspace-nav-icon { - background: color-mix(in srgb, var(--nav-accent) 24%, transparent); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--nav-accent) 10%, transparent); + background: transparent; + box-shadow: none; } -.app-user-workspace .app-workspace-nav-link:active .app-workspace-nav-icon { transform: scale(.94); } +.app-user-workspace .app-workspace-nav-link:active .app-workspace-nav-icon { transform: scale(.96); } .app-user-workspace .app-workspace-nav-group > div > div .app-workspace-nav-link { animation: workspace-nav-item-enter var(--product-enter) var(--product-ease) both; } @@ -523,11 +525,6 @@ .app-user-workspace .app-workspace-nav-group > div > div:nth-child(4) .app-workspace-nav-link { animation-delay: 120ms; } .app-user-workspace .app-workspace-nav-group > div > div:nth-child(n + 5) .app-workspace-nav-link { animation-delay: 160ms; } -@keyframes workspace-nav-icon-nudge { - 0% { transform: rotate(0deg) scale(1); } - 42% { transform: rotate(-10deg) scale(1.08); } - 100% { transform: rotate(0deg) scale(1); } -} @keyframes workspace-nav-item-enter { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: translateX(0); } @@ -545,16 +542,14 @@ .app-user-workspace .creation-chat-composer.is-thread .creation-composer-mode-row { grid-column: 1; grid-row: 1 / 3; min-width: 0; padding: 8px 0 8px 8px; } .app-user-workspace .creation-chat-composer.is-thread .creation-composer-mode-row .creation-mode-tabs { display: grid; width: 100%; height: 100%; grid-template-rows: repeat(3, minmax(52px, 1fr)); align-self: stretch; margin: 0; padding: 4px; border-radius: 16px; transform: none; } .app-user-workspace .creation-chat-composer.is-thread .creation-composer-mode-row .creation-mode-button { height: auto; min-height: 52px; flex-direction: column; gap: 3px; padding: 5px 2px; font-size: 11px; } -.app-user-workspace .creation-chat-composer.is-thread .creation-composer-mode-row .creation-mode-indicator { top: 4px; left: 4px; } +.app-user-workspace .creation-chat-composer.is-thread .creation-composer-mode-row .creation-mode-indicator { inset: 0; width: auto; height: auto; } .app-user-workspace .creation-chat-composer.is-thread .creation-chat-writing-surface, .app-user-workspace .creation-chat-composer.is-thread .creation-chat-dock { grid-column: 2; min-width: 0; } .app-user-workspace .creation-composer-stage { display: flex; min-width: 0; align-items: stretch; gap: 14px; } .app-user-workspace .creation-composer-stage.is-thread-mode { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 14px; } .app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs { display: grid; width: 76px; grid-template-columns: 1fr; grid-template-rows: repeat(3, minmax(54px, 1fr)); align-self: stretch; margin: 0; padding: 4px; border-radius: 18px; } .app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs .creation-mode-button { min-width: 0; min-height: 0; flex-direction: column; gap: 3px; padding: 7px 3px; font-size: 11px; } -.app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs .creation-mode-indicator { inset: 4px 4px auto; width: calc(100% - 8px); height: calc((100% - 8px) / 3); transition: transform 360ms var(--product-ease); } -.app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs[data-active-mode="image"] .creation-mode-indicator { transform: translateY(100%); } -.app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs[data-active-mode="text"] .creation-mode-indicator { transform: translateY(200%); } +.app-user-workspace .creation-composer-stage.is-thread-mode > .creation-mode-tabs .creation-mode-indicator { inset: 0; width: auto; height: auto; transform: none; } .app-user-workspace .creation-composer-stage.is-home-mode { display: block; } .app-user-workspace .creation-composer-stage.is-home-mode > .creation-mode-tabs { display: grid; width: min(456px, 100%); grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: 1fr; margin: 0 auto 20px; padding: 4px; } .app-user-workspace .creation-composer-stage.is-home-mode > .creation-mode-tabs .creation-mode-button { min-height: 46px; flex-direction: row; gap: 6px; padding: 0 8px; font-size: 13px; } diff --git a/web/test/welcome-contributors.test.ts b/web/test/welcome-contributors.test.ts new file mode 100644 index 000000000..ef34a2e6a --- /dev/null +++ b/web/test/welcome-contributors.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { parseContributors } from "../src/pages/welcome/contributors-parse"; + +const identityAvatar = (path: string) => path; + +test("welcome contributors parse the README HTML table", () => { + const readme = readFileSync(resolve(import.meta.dir, "../../README.md"), "utf8"); + const people = parseContributors(readme, identityAvatar); + + expect(people.length).toBeGreaterThanOrEqual(20); + expect(people[0]).toMatchObject({ + name: "ddCat", + avatar: "assets/user-ddcat.jpg", + email: "ddcat666@126.com", + wechat: "ddcat0829", + isFounder: true, + }); + expect(people[0]?.signature).toContain("没有任何黑魔法"); + expect(people.some((person) => person.name === "爱笑的毛毛虫")).toBe(true); + expect(people.filter((person) => person.isFounder)).toHaveLength(1); +}); + +test("welcome contributors still parse markdown pipe tables", () => { + const markdown = `## 贡献者与团队 + +| 头像 | 昵称 | 邮箱 | 签名 | +| --- | --- | --- | --- | +| ddCat | ddCat
项目发起者 · 微信:ddcat0829 | founder@example.com | 把故事搬上银幕 | +| ken | ken | ken@example.com | 走自己的路 | + +## 下一章 +`; + expect(parseContributors(markdown, identityAvatar)).toEqual([ + { + name: "ddCat", + avatar: "assets/user-ddcat.jpg", + signature: "把故事搬上银幕", + email: "founder@example.com", + wechat: "ddcat0829", + isFounder: true, + }, + { + name: "ken", + avatar: "assets/user-ken.jpg", + signature: "走自己的路", + email: "ken@example.com", + wechat: undefined, + isFounder: false, + }, + ]); +});